Liskのトランザクションをソースコードから読み解く

Liskのトランザクションをソースコードから読み解く

はじめに

今回は、分散型ブロックチェーンプラットフォームである「Lisk」のトランザクションへの理解を深めるために、Liskのソースコードを読み解いていきます。

なお本記事では、Liskの公式ウォレットであるLiskHubを利用してトランザクションを作成する前提とします。

前提知識

  • 基本的なブロックチェーン、P2Pネットワークの知識
  • Javascript(Node.js)のコードが読めること

Liskのトランザクションの流れ

LiskHubはブロックチェーン自体のデータを保持していないため、ウォレット自体がトランザクションを行うことはありません。
その代わりにLiskの開発チームが運営しているメインネットのノードのAPIを利用してトランザクションを実行します。

Lisk HubとLiskメインネット接続イメージ

Lisk API について

Liskは開発者向けに様々なAPIを公開しています。これらのAPIはメインネットやテストネットのノードのアドレスに対してPUTやGET形式でリクエストを送ることで利用できます。
 
トランザクションを実行する際の例を見てみると、

トランザクション実行の例

curl -k -H "Content-Type: application/json" \
-X PUT -d '{"secret":"<INSERT SECRET HERE>","amount":<INSERT AMOUNT HERE>,"recipientId":"<INSERT WALLET ADDRESS HERE>"}' \
http://ノードのアドレス:ノードのポート/api/transactions

このようにノードのアドレスとポートを指定してINSERT SECRET HEREのように太字になっている箇所の値を埋めてPUTすることで、LiskのノードのAPIを利用してトランザクションを実行することができます。

Liskが運営しているノードのアドレスとポートは下記になります。

MainNet BetaNet TestNet
https://node01.lisk.io:443 http://94.237.42.109:5000 http://testnet.lisk.io:7000
http://83.136.252.99:5000
https://node08.lisk.io:443

メインネットは01番から08番の8つのノードが使えるようになっています。いずれかのノードがダウンしていても問題が無いよう、冗長性を持たせていると考えられます。

ソースコードの解説

それでは、APIから命令を受け取って実際にトランザクションを実行するLiskのフルノードのソースコードから実際のトランザクションの流れを確認していきましょう。

まず、下記のトランザクションの流れを図示したものをご覧いただき、全体像を掴んでください。

Liskトランザクションの流れ図

githubからソースコードをダウンロード

LiskのソースコードはGithub上に公開されているので、下記のリンクよりまずソースコードをダウンロードしましょう。
今回読んでいくソースコードはメインネットのノードのソースコードです。

https://github.com/LiskHQ/lisk

上記のGitHubリポジトリからDownload ZIPするか

git clone https://github.com/LiskHQ/lisk.git

してください。

ダウンロードしたソースは次のようなフォルダ構成になっています。
それでは、APIの入力からトランザクションがメインネットにブロードキャストされるまでの過程を順番に見ていきます。

APIからのトランザクションの受取

/api/controllers/transactions.js のTransactionsController.postTransaction でAPIから送られてきたトランザクションのデータを受け取り、/modules/transactions.js のpostTransactionに渡します。

TransactionsController.postTransaction = function(context, next) {
    var transaction = context.request.swagger.params.transaction.value;

    modules.transactions.shared.postTransaction(transaction, (err, data) => {
....省略
(/api/controllers/transactions.js #L113,L131)

トランザクションの受け渡し

/modules/transactions.js のpostTransactionは第1引数で受け取ったtransactionのデータをそのまま/modules/transport.jsのpostTransactionに渡します

postTransaction(transaction, cb) {
        return modules.transport.shared.postTransaction(
            { transaction },
            (err, res) => {
                __private.processPostResult(err, res, cb);
            }
        );
},
/modules/transaction.js #L873,L880

トランザクションのプロパティの取り出し

/modules/transport.js のpostTransactionは第1引数で受け取ったtransactionのデータをクエリとして、クエリに含まれているtransaction,nonce,extraLogMessageの3つのプロパティを同じtransport.jsにあるreceiveTransactionに渡します。

postTransaction(query, cb) {
        __private.receiveTransaction(
            query.transaction,
            query.nonce,
            query.extraLogMessage,
...省略
/modules/transport.js #L755,L770

トランザクションの正規化、不正のチェック

receiveTransactionでは受け取ったトランザクションが正しい形式であるか、不正な値が含まれているかを確認します。
はじめにあるtry/catch文はトランザクションの正規化を行い、トランザクションの形式に誤り、不正があればエラーを返すようになっています。

try {
        transaction = library.logic.transaction.objectNormalize(transaction);
    } catch (e) {
        library.logger.debug('Transaction normalization failed', {
            id,
            err: e.toString(),
            module: 'transport',
            transaction,
        });

        __private.removePeer({ nonce, code: 'ETRANSACTION' }, extraLogMessage);

        return setImmediate(cb, `Invalid transaction body - ${e.toString()}`);
    }
/modules/transport.js #L217,L234

その後 /modules/transactions.js のprocessUnconfirmedTransactionにトランザクションなどのデータを渡します。

 modules.transactions.processUnconfirmedTransaction(
            transaction,
...省略
/modules/transport.js #L252,L272

トランザクションをトランザクションプールに渡す

/modules/transactions.js のprocessUnconfirmedTransactionはすべての引数をそのまま/logic/transaction_pool.js のprocessUnconfirmedTransactionに渡します。

Transactions.prototype.processUnconfirmedTransaction = function(
    transaction,
    broadcast,
    cb
) {
    return __private.transactionPool.processUnconfirmedTransaction(
        transaction,
        broadcast,
        cb
    );
};
/modules/transactions.js #L542,L552

トランザクションプールの制御

/logic/transaction_pool.jsのprocessUnconfirmedTransactionの動作を説明していきます。
Liskではトランザクションを一旦キューをデータ構造として持っている、トランザクションプールに貯めて管理を行っているようです。

そこでまず、トランザクションプールにトランザクションが含まれているかどうかを調べます。
含まれていなければ、トランザクションプールのキューの番号を進めます。
もしキューの番号が1000を超えていれば、キューの再構築を行います。

    if (self.transactionInPool(transaction.id)) {
        return setImmediate(
            cb,
            `Transaction is already processed: ${transaction.id}`
        );
    }
    self.processed++;
    if (self.processed > 1000) {
        self.reindexQueues();
        self.processed = 1;
    }
/logic/transaction_pool.js #L566,L576

次にトランザクションを同一ファイル内にある__private.processVerifyTransactionに渡します。

__private.processVerifyTransaction(
        transaction,
...省略
/logic/transaction_pool.js #L582,L592

トランザクションの承認

__private.processVerifyTransactionはトランザクションを承認するプロセスです。
ここでは、asyncライブラリのwaterfallという配列内の関数を順番に(同期的に)実行する機能を用いています。実行する関数について見ていきましょう。

    async.waterfall(
        [
        function setAccountAndGet(waterCb) {...},
          function getRequester(sender, waterCb) {...},
    function processTransaction(sender, requester, waterCb) {...},
    function normalizeTransaction(sender, waterCb) {...},
    function verifyTransaction(sender, waterCb) {...},
    ],
/logic/transaction_pool.js #L822,L892
関数名 機能
setAccountAndGet アドレスやパブリックキーに異常がないか調べる
getRequester マルチシグネチャの確認。パブリックキーからアカウント情報を取得
processTransaction トランザクションの種類に応じた処理
normalizeTransaction トランザクションの正規化
verifyTransaction トランザクションの最終確認

これらの5段階のチェックで問題が発生しなければ、/modules/transport.js のonUnconfirmedTransactionが呼び出されて、トランザクションがメインネットにブロードキャストされます。

(err, sender) => {
            if (!err) {
                library.bus.message('unconfirmedTransaction', transaction, broadcast);
            }

            return setImmediate(cb, err, sender);
        }
/logic/transaction_pool.js #L893,L899

ジョブリストへトランザクションを追加

 __private.broadcaster.enqueueでブロードキャストのジョブリストのキューにトランザクションを追加します。websocketでLiskHub側へトランザクションの完了を通知します。

Transport.prototype.onUnconfirmedTransaction = function(
    transaction,
    broadcast
) {
    if (broadcast && !__private.broadcaster.maxRelays(transaction)) {
        __private.broadcaster.enqueue(
            {},
            { api: 'postTransactions', data: { transaction } }
        );
        library.network.io.sockets.emit('transactions/change', transaction);
    }
};
/modules/transport.js #L359,L370

ブロードキャスト

ブロードキャストではまず、getPeers(waterCb)でブロードキャスト先のピアを選択します。
getPeersは/logic/peers.jsのlistRandomConnectedでランダムなピアのリストを取得します。
最後にピアのリストからそれぞれのピアにデータを送信してトランザクションは完了です。

getPeers(params, cb) {
        params.limit = params.limit || this.config.peerLimit;
        const peers = library.logic.peers.listRandomConnected(params);
        library.logger.info(
            ['Broadhash consensus now', modules.peers.getLastConsensus(), '%'].join(
                ' '
            )
        );
        return setImmediate(cb, null, peers);
    }
/logic/broadcaster.js #L104,L113
Peers.prototype.listRandomConnected = function(options) {
    options = options || {};
    const peerList = Object.keys(self.peersManager.peers)
        .map(key => self.peersManager.peers[key])
        .filter(peer => peer.state === Peer.STATE.CONNECTED);
    const shuffledPeerList = _.shuffle(peerList);
    return options.limit ? shuffledPeerList.slice(0, options.limit)
        : shuffledPeerList;
};
/logic/peers.js #L264,L272
    broadcast(params, options, cb) {
        params.limit = params.limit || this.config.broadcastLimit;

        async.waterfall(
            [
                function getPeers(waterCb) {
                    if (!params.peers) {
                        return self.getPeers(params, waterCb);
                    }
                    return setImmediate(
                        waterCb,
                        null,
                        params.peers.slice(0, params.limit)
                    );
                },
                function sendToPeer(peers, waterCb) {
                    library.logger.debug('Begin broadcast', options);
                    peers.forEach(peer => peer.rpc[options.api](options.data));
                    library.logger.debug('End broadcast');
                    return setImmediate(waterCb, null, peers);
                },
            ],
            (err, peers) => {
                if (cb) {
                    return setImmediate(cb, err, { peers });
                }
            }
        );
    }
/logic/transport.js #L124,L153

まとめ

本記事では、Liskのトランザクションが実行されていく過程をソースコードから読み解いて行きました。同じようなトランザクションの検証作業を何度も繰り返すなど、かなり厳重なチェックを行っていることがソースコードからわかりました。トランザクションは実際にトークンをやり取りするとても重要なプロセスなのでこのような実装になっているのでしょう。(文・シンヨシアキ:@ShinYoshiaki)

参考資料:

ブロックチェーンの専門企業で働いてみませんか?

当サイトを運営するコンセンサス・ベイス株式会社では、エンジニア、プロジェクトマネージャー、ライターなど、様々なポジションで一緒に働いてくださる仲間を募集しています。

ブロックチェーン業界にチャレンジしてみたいあなたのご応募をお待ちしております!

Blockchainカテゴリの最新記事