LoginSignup
450
300

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-01-27

まえがき

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: {} } }
450
300
27

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
450
300