Node.js
Blockchain
NEM

NEM のコールドウォレットの自作は難しいのか検証してみた

まえがき

Coincheck(コインチェック) の XEM盗難騒動で 技術的に難しかった というお話があったようだ。
今回はコールドウォレットを自作してみて、本当に難しいのかを検証してみた。

設計

ネットワークにつながっていない端末で PrivateKey を保有する(または入力する)
上記端末で署名済みのトランザクションを生成する
署名済みのトランザクションを ネットワークにつながっている端末でNEMネットワークに送信する

こうすることで PrivateKey がネットワーク上に露出することがなくなる

ここで検討しないといけないのが 署名済みのトランザクション をどのようにネットワークにつながっている端末 に受け渡すか

最も単純な方法として、 QRコード に変換して画像として渡してしまえば、 カメラが有る端末で容易に送金できる

まとめるとこんな感じ

  • ネットワークにつながっていない端末
    1. オンラインウォレット宛の 署名済みトランザクション を生成する
    2. 署名済みトランザクション を QRコードへ変換
  • スマートフォン等
    1. QRコードを読み込み、署名済みトランザクションをNEMネットワークに送信する

実装

実装は nodejs + nem-sdk を利用するのが良いようだ

環境

nodejs: v8.9.1
dependencies-package
"nem-sdk": "^1.6.2",
"qrcode": "^1.2.0",
"readline-sync": "^1.4.7"

自作コールドウォレットのコード

transaction.js
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コードのファイルが出来上がる

TB2SBFRLW7TVFDRUYY5T6J7MQX6POSEV3AYCWCFB_TDWWYDGQNBKSAJBSHZX7QWVX7WNVAWWB7HGPWRB2_1.png

ちなみに中身は以下の形式

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: {} } }