即時ファイナリティ。
分散化を徹底したブロックチェーンほど、縁の遠い存在と考えがちな言葉です。
例えば取引所の多くは、ブロックが覆らないと思われる時間を待って入金を確定させます。
ブロックチェーンを従来システムに組み込むためには、待たなければいけない時間がある。
と思っているなら、あなたの頭はまだWeb2.0を彷徨っています。
少し立ち止まって考えてみましょう。ブロックチェーンは第三者の仲介無しに送金を実現します。送金指示はあなたに直接投げられたメッセージです。たとえそれが未承認状態だったとしても、情報送信者の意思表示を証明するために必要な情報はすべて世界中に公開されているのです。
他チェーン同士が自律分散的にトークン交換するならまだしも、特定の組織によって管理されたシステム系への送信であれば、利益を享受する情報の受け手がトランザクションの信頼性向上に歩み寄ることができます。
何が言いたいかというと、ブロックチェーンがロールバックしようが、送金メッセージを受信して利益を得る権利を得た人が、そのチャンスが確信に変わるまで何度もアナウンスすればいいのです。
というわけで、今回はSymbolブロックチェーンのロールバックとファイナライズを検知し再アナウンスするための方法を紹介します。
latestBlock = 0;
lastFinalizedBlock = 0;
unfinalizedTxes = [];
listener.open().then(() => {
listener.newBlock()
.pipe(
op.mergeMap(x=>blockRepo.getBlockByHeight(x.height)),
)
.subscribe(b=>{
console.log("------------------");
if(latestBlock >= b.height.compact()){
console.log("■■■ roll back!! ■■■" + latestBlock + "->" + b.height.compact());
rollbackBlocks = Array.from(Array(latestBlock - b.height.compact() + 1), (v, k) => k + b.height.compact());
console.log(rollbackBlocks);
checkTxes = unfinalizedTxes.filter(tx=>rollbackBlocks.indexOf(tx.transactionInfo.height.compact()) >= 0);
console.log(checkTxes);
for(checkTx of checkTxes){
rxjs.of({
existTxes:txRepo.getTransactionsById([checkTx.transactionInfo.hash],sym.TransactionGroup.Confirmed),
checkTx:checkTx
})
.subscribe(async obs=>{
if(obs.existTxes.length > 0){
console.log("on chain");
}else{
console.log("==>■DROP TRANSACTION!!!!■");
signedTx = new sym.SignedTransaction(
obs.checkTx.serialize(),
obs.checkTx.transactionInfo.hash,
obs.checkTx.signer.publicKey,
obs.checkTx.type,
obs.checkTx.networkType
)
clog(signedTx);
//ネットワークへアナウンス
try {
res = await transactionService.announce(signedTx, listener).toPromise();
console.log(res);
} catch(err) {
console.log(err);
} finally {
}
}
});
}
}
console.log("prevHash:" + b.previousBlockHash);
console.log("timestamp:" + new Date(Number(b.timestamp.toString()) + epochAdjustment * 1000));
console.log(b.height.compact() + ":("+ b.transactionsCount +"txs):"+ b.hash);
latestBlock = b.height.compact();
txRepo.search({group:sym.TransactionGroup.Confirmed,height:b.height.compact(),pageSize:100 })
.subscribe(tx=>{
unfinalizedTxes = unfinalizedTxes.concat(tx.data);
console.log(unfinalizedTxes)
})
});
listener.finalizedBlock()
.subscribe(fb=>{
diff_max = latestBlock - lastFinalizedBlock;
diff_min = latestBlock - fb.height.compact();
if(lastFinalizedBlock != 0){
console.log("================");
console.log("FinalizedBlock:" + fb.height.compact() + ":" + diff_min.toString() +"<" + diff_max.toString() + ":" + fb.hash);
finalizedBlocks = Array.from(Array(fb.height.compact() - lastFinalizedBlock), (v, k) => k + lastFinalizedBlock + 1);
console.log(finalizedBlocks);
unfinalizedTxes = unfinalizedTxes.filter(tx=>finalizedBlocks.indexOf(tx.transactionInfo.height.compact()) ==-1);
console.log(unfinalizedTxes);
}
lastFinalizedBlock = fb.height.compact();
})
});
解説
基本構成はこちらです。
listener.newBlock()
.pipe(
op.mergeMap(x=>blockRepo.getBlockByHeight(x.height)),
)
.subscribe(b=>{
console.log("------------------");
if(latestBlock >= b.height.compact()){
console.log("■■■ roll back!! ■■■" + latestBlock + "->" + b.height.compact());
}
latestBlock = b.height.compact();
});
listener.finalizedBlock()
.subscribe(fb=>{
diff_max = latestBlock - lastFinalizedBlock;
diff_min = latestBlock - fb.height.compact();
if(lastFinalizedBlock != 0){
console.log("================");
console.log("FinalizedBlock:" + fb.height.compact() + ":" + diff_min.toString() +"<" + diff_max.toString() + ":" + fb.hash);
}
lastFinalizedBlock = fb.height.compact();
})
listener.newBlock()で新規に生成されたblockを検知します。
listener.finalizedBlock()でファイナライズブロックを検知します。
今回は新規ブロックで承認された全トランザクションをキャッシュし、ブロックがファイナライズされたらキャッシュから解放します。途中でロールバックが発生した場合、キャッシュしていたトランザクション情報を用いてノードに再アナウンスを行います。
キャッシュへのトランザクション追加
txRepo.search({group:sym.TransactionGroup.Confirmed,height:b.height.compact(),pageSize:100 })
.subscribe(tx=>{
unfinalizedTxes = unfinalizedTxes.concat(tx.data);
console.log(unfinalizedTxes)
})
新規に生成されたブロックに含まれるトランザクションをunfinalizedTxesに格納します。
ファイナライズされたトランザクションの解放
finalizedBlocks = Array.from(Array(fb.height.compact() - lastFinalizedBlock), (v, k) => k + lastFinalizedBlock + 1);
unfinalizedTxes = unfinalizedTxes.filter(tx=>finalizedBlocks.indexOf(tx.transactionInfo.height.compact()) ==-1);
キャッシュしたトランザクション情報のブロックがファイナライズブロックだった場合にキャッシュから解放します。
ロールバック内に含まれるトランザクションを抽出
rollbackBlocks = Array.from(Array(latestBlock - b.height.compact() + 1), (v, k) => k + b.height.compact());
checkTxes = unfinalizedTxes.filter(tx=>rollbackBlocks.indexOf(tx.transactionInfo.height.compact()) >= 0);
ロールバックしたブロックの配列を生成します。
キャッシュからそのブロック高を含むトランザクションをcheckTxesに代入します。
##### キャッシュからロールバックしたチェーンに含まれないトランザクションを抽出
for(checkTx of checkTxes){
rxjs.of({
existTxes:txRepo.getTransactionsById([checkTx.transactionInfo.hash],sym.TransactionGroup.Confirmed),
checkTx:checkTx
})
.subscribe(async obs=>{
if(obs.existTxes.length > 0){
console.log("on chain");
}else{
console.log("==>■DROP TRANSACTION!!!!■");
}
});
}
checkTxes内のトランザクションの存在をノードに問い合わせ存在しない場合は再アナウンスの必要なトランザクションとして把握します。
キャッシュしたトランザクションから署名済みトランザクションを構築して再アナウンス
signedTx = new sym.SignedTransaction(
obs.checkTx.serialize(),
obs.checkTx.transactionInfo.hash,
obs.checkTx.signer.publicKey,
obs.checkTx.type,
obs.checkTx.networkType
)
clog(signedTx);
//ネットワークへアナウンス
try {
res = await transactionService.announce(signedTx, listener).toPromise();
console.log(res);
} catch(err) {
console.log(err);
} finally {
}
これで、ロールバック発生時に消失の可能性があるトランザクションを、再アナウンスすることができました。
留意点
上記サンプルプログラムはアグリゲートトランザクションには対応しておりません。内部トランザクションまでキャッシュするにはノードに対して別途抽出作業が必要です。
また、Symbolの場合、ロールバックで覆されたブロックにしか含まれなかったトランザクションは未承認トランザクションに差し戻されます。
したがって、上記のような手続きを踏む必要もなく次のブロックで承認される場合がほとんどで、再アナウンスしたトランザクションは二重支払いのチェックにて無効になります。
ただ、ノードが保持するキャッシュサイズを超えるような大幅なロールバックが発生した場合は未承認トランザクションに戻らないトランザクションが含まれるため、万全を期したい場合は有効な手段となりうるでしょう。また、期限切れを迎えてしまったトランザクションについても再アナウンスができないことにもご注意ください(その前に確定的ファイナライズを迎えるとは思いますが)。
さいごに
今後、様々なコンソーシアムがパブリックチェーンの採用を検討するでしょう。ブロックチェーンはファイナライズ機構が弱いから使えない、というのは非常に受け身な考え方です。コンソーシアム間で流通するトークンに関するトランザクションのみを監視して万が一でもドロップしたトランザクションを拾い上げればほとんどの懸念は解消されます。万が一の危険にたじろいで多大なビジネス変革のチャンスを逃してはいけません。
ファイナライズとは、
「つかみかけたチャンスを逃がさないために努力することを止めてもいい時間」
という意味に読み替えましょう。