はじめに
今回は、分散型ブロックチェーンプラットフォームである「Lisk」のトランザクションへの理解を深めるために、Liskのソースコードを読み解いていきます。
なお本記事では、Liskの公式ウォレットであるLiskHubを利用してトランザクションを作成する前提とします。
前提知識
- 基本的なブロックチェーン、P2Pネットワークの知識
- Javascript(Node.js)のコードが読めること
Liskのトランザクションの流れ
LiskHubはブロックチェーン自体のデータを保持していないため、ウォレット自体がトランザクションを行うことはありません。
その代わりにLiskの開発チームが運営しているメインネットのノードのAPIを利用してトランザクションを実行します。
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のフルノードのソースコードから実際のトランザクションの流れを確認していきましょう。
まず、下記のトランザクションの流れを図示したものをご覧いただき、全体像を掴んでください。
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) => {
....省略
トランザクションの受け渡し
/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/transport.js のpostTransactionは第1引数で受け取ったtransactionのデータをクエリとして、クエリに含まれているtransaction,nonce,extraLogMessageの3つのプロパティを同じtransport.jsにあるreceiveTransactionに渡します。
postTransaction(query, cb) {
__private.receiveTransaction(
query.transaction,
query.nonce,
query.extraLogMessage,
...省略
トランザクションの正規化、不正のチェック
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/transactions.js のprocessUnconfirmedTransactionにトランザクションなどのデータを渡します。
modules.transactions.processUnconfirmedTransaction(
transaction,
...省略
トランザクションをトランザクションプールに渡す
/modules/transactions.js のprocessUnconfirmedTransactionはすべての引数をそのまま/logic/transaction_pool.js のprocessUnconfirmedTransactionに渡します。
Transactions.prototype.processUnconfirmedTransaction = function(
transaction,
broadcast,
cb
) {
return __private.transactionPool.processUnconfirmedTransaction(
transaction,
broadcast,
cb
);
};
トランザクションプールの制御
/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;
}
次にトランザクションを同一ファイル内にある__private.processVerifyTransactionに渡します。
__private.processVerifyTransaction(
transaction,
...省略
トランザクションの承認
__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) {...},
],
関数名 | 機能 |
---|---|
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);
}
ジョブリストへトランザクションを追加
__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);
}
};
ブロードキャスト
ブロードキャストではまず、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);
}
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;
};
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 });
}
}
);
}
まとめ
本記事では、Liskのトランザクションが実行されていく過程をソースコードから読み解いて行きました。同じようなトランザクションの検証作業を何度も繰り返すなど、かなり厳重なチェックを行っていることがソースコードからわかりました。トランザクションは実際にトークンをやり取りするとても重要なプロセスなのでこのような実装になっているのでしょう。(文・シンヨシアキ:@ShinYoshiaki)