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"
```

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

```js: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](https://qiita-image-store.s3.amazonaws.com/0/14258/df2ff702-f6b0-f37e-761d-941c39276001.png)

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

`data` が送金トランザクション本体(hex)
`signature` が 署名(hex)

```json
{
  "data":"010100000100...",
  "signature":"c87c7443730d8..."
}
```

### 署名済みトランザクションの送信部分

```js
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 等をいれて、同期して~みたいなことをしなくて良いので、
結構簡単にいじれるイメージとなった

## 補足

### プライベートキーの作り方

```js
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()}`);
```

### アドレスの残高確認

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