まえがき
Coincheck(コインチェック) の XEM盗難騒動で 技術的に難しかった というお話があったようだ。
今回はコールドウォレットを自作してみて、本当に難しいのかを検証してみた。
設計
ネットワークにつながっていない端末で PrivateKey を保有する(または入力する)
上記端末で署名済みのトランザクションを生成する
署名済みのトランザクションを ネットワークにつながっている端末でNEMネットワークに送信する
こうすることで PrivateKey がネットワーク上に露出することがなくなる
ここで検討しないといけないのが 署名済みのトランザクション をどのようにネットワークにつながっている端末 に受け渡すか
最も単純な方法として、 QRコード
に変換して画像として渡してしまえば、 カメラが有る端末で容易に送金できる
まとめるとこんな感じ
-
ネットワークにつながっていない端末
- オンラインウォレット宛の
署名済みトランザクション
を生成する -
署名済みトランザクション
を QRコードへ変換
- オンラインウォレット宛の
-
スマートフォン等
- QRコードを読み込み、署名済みトランザクションをNEMネットワークに送信する
実装
実装は nodejs
+ nem-sdk
を利用するのが良いようだ
環境
nodejs: v8.9.1
"nem-sdk": "^1.6.2",
"qrcode": "^1.2.0",
"readline-sync": "^1.4.7"
自作コールドウォレットのコード
const readlineSync = require('readline-sync');
const nem = require("nem-sdk").default;
const QRCode = require('qrcode');
class NemOfflineTransaction {
constructor(networkId) {
this.networkId = networkId;
}
start() {
// [1] 入力
this.keyIn();
// [2] 確認
this.confirm();
// [3] 署名付きのトランザクション生成
const tx = this.generateTransaction();
// [4] QRコードへ変換
this.generateQRCode(tx);
}
keyIn() {
this.recipient = readlineSync.question('Recipient: ');
this.amount = readlineSync.question('Amount: ');
// 便宜上 標準入力で privateKey を渡す
this.privateKey = readlineSync.question('PrivateKey: ', { hideEchoBack: true });
this.keyPair = nem.crypto.keyPair.create(this.privateKey);
this.address = nem.model.address.toAddress(this.keyPair.publicKey.toString(), this.networkId);
}
confirm() {
console.log(`\n---------------------------------------------------------------`);
console.log(`[1] Your address: ${this.address}`);
console.log(`[2] Recipient address: ${this.recipient}`);
console.log(`[3] amount: ${this.amount} XEM`);
console.log(`---------------------------------------------------------------`);
if (!readlineSync.keyInYN('Are you sure?:')) {
process.exit(0);
}
}
generateTransaction() {
// [1] 未署名のトランザクション生成
const txEntity = this.generateUnsignedTransaction();
const serializeTx = nem.utils.serialization.serializeTransaction(txEntity);
// [2] トランザクションに署名
const signature = this.generateSignature(this.keyPair, serializeTx);
// [3] announce で送信できる形式に変換
return JSON.stringify({
'data': nem.utils.convert.ua2hex(serializeTx),
'signature': signature.toString()
});
}
// トランザクションへの署名
generateSignature(keyPair, unsignedTx) {
return keyPair.sign(unsignedTx);
}
// 未署名のTransactionの生成
generateUnsignedTransaction() {
const tx = nem.model.objects.create("transferTransaction")(this.recipient, this.amount, null);
const common = nem.model.objects.create("common")("", this.privateKey);
return nem.model.transactions.prepare("transferTransaction")(common, tx, this.networkId);
}
// QRコードをファイルとして書き出す
generateQRCode(tx) {
QRCode.toFile(`${this.address}_${this.recipient}_${this.amount}.png`, tx);
}
}
// 本番系でやるには `testnet` => `mainnet` に変換
const network = nem.model.network.data.testnet.id;
offlineTx = new NemOfflineTransaction(network);
offlineTx.start();
実行例
$ node transaction.js
Recipient: TXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Amount: 1
PrivateKey: ****************************************************************
---------------------------------------------------------------
[1] Your address: TXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
[2] Recipient address: TXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
[3] amount: 1 XEM
---------------------------------------------------------------
Are you sure? [y/n]: y
これで実行したディレクトリ直下に以下のようなQRコードのファイルが出来上がる
ちなみに中身は以下の形式
data
が送金トランザクション本体(hex)
signature
が 署名(hex)
{
"data":"010100000100...",
"signature":"c87c7443730d8..."
}
署名済みトランザクションの送信部分
const tx = "{ data: .... }"; // QRコードの中身
const nem = require("nem-sdk").default;
const endpoint = nem.model.objects.create("endpoint")(nem.model.nodes.defaultTestnet, nem.model.nodes.defaultPort);
nem.com.requests.transaction.announce(endpoint, tx).then((res) => {
console.log("Transaction announced!");
});
コード中に PrivateKey
がない状態なのでオンラインの端末でも安全に利用できる
あとがき
nem-sdk
の存在は知っていたが、使ったことはなかった
仮想通貨の基本的な知識はあった
nodejs はぼちぼち触れる
こんな状態で、プロトタイプの実装は4時間程度
Bitcoin-Core 等をいれて、同期して~みたいなことをしなくて良いので、
結構簡単にいじれるイメージとなった
補足
プライベートキーの作り方
const networkId = nem.model.network.data.testnet.id;
const nem = require("nem-sdk").default;
const rBytes = nem.crypto.nacl.randomBytes(32);
const privateKey = nem.utils.convert.ua2hex(rBytes);
const keyPair = nem.crypto.keyPair.create(privateKey);
const address = nem.model.address.toAddress(keyPair.publicKey.toString(), networkId);
console.log(`Address : ${address}`);
console.log(`PrivateKey: ${privateKey}`);
console.log(`PublicKey : ${keyPair.publicKey.toString()}`);
アドレスの残高確認
const nem = require("nem-sdk").default;
const address = 'NC3BI3DNMR2PGEOOMP2NKXQGSAKMS7GYRKVA5CSZ';
const endpoint = nem.model.objects.create("endpoint")(nem.model.nodes.defaultMainnet, nem.model.nodes.defaultPort);
nem.com.requests.account.data(endpoint, address).then((res) => {
console.log(res);
});
{ meta:
{ cosignatories: [],
cosignatoryOf: [],
status: 'LOCKED',
remoteStatus: 'INACTIVE' },
account:
{ address: 'NC3BI3DNMR2PGEOOMP2NKXQGSAKMS7GYRKVA5CSZ',
harvestedBlocks: 0,
balance: 9211155333,
importance: 0.06940358968348674,
vestedBalance: 921764202,
publicKey: 'e40d7a1c74f173cde9a1d19369da59d53017e799143f969a4e788a6f972016af',
label: null,
multisigInfo: {} } }