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":"","amount":,"recipientId":""}' \
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)

参考資料:

     

免責事項

本記事に掲載されている記事の内容につきましては、正しい情報を提供することに務めてはおりますが、提供している記事の内容及び参考資料からいかなる損失や損害などの被害が発生したとしても、弊社では責任を負いかねます。実施される際には、法律事務所にご相談ください。

技術・サービス・実装方法等のレビュー、その他解説・分析・意見につきましてはblock-chani.jp運営者の個人的見解です。正確性・正当性を保証するものではありません。本記事掲載の記事内容のご利用は読者様個人の判断により自己責任でお願いいたします。

     

コンセンサス・ベイス(株)とブロックチェーン事業を行なってみませんか?

当サイトを運営するコンセンサス・ベイス株式会社は、2015年設立の国内で最も古いブロックチェーン専門企業です。これまでに、大手企業の顧客を中心に、日本トップクラスのブロックチェーンの開発・コンサルティング実績があります。

ブロックチェーンに関わるビジネスコンサル・システム開発・教育・講演などご希望でしたら、お気軽にお問い合わせください。

     
     

ブロックチェーン学習に最適の書籍の紹介

図解即戦力 ブロックチェーンのしくみと開発がこれ1冊でしっかりわかる教科書

ブロックチェーン イーサリアムへの入り口 第二版 (ブロックチェーン技術書籍)

本書は、ブロックチェーン技術に興味を持ったエンジニアや、その仕組みを学び、自分の仕事に活かしたいビジネスパーソンを対象にして、ブロックチェーンのコア技術とネットワーク維持の仕組みを平易な言葉で解説しています。この本を読んだうえで、実際にコードを書くような専門書、ブロックチェーンビジネスの解説書を読むことで、理解度が飛躍的に高まるでしょう。(はじめにより)

Blockchainカテゴリの最新記事