はじめに
Symbolのメインネット上でAggregateBondedTransactionをアナウンスしようとすると、AggregateBondedTransactionをうまくアナウンスされず、HashLockTransactionで差し出した10xymを取られてしまうという話題がありました。
そんな思わぬ悲劇を避けるべく、どうすればAggregateBondedTransactionをより確実にアナウンスすることができるかを考えてみたいと思います。
AggregateBondedTransactionとは
- AggregateTransactionは複数のトランザクションをひとまとめにして処理することができるSymbolの機能なのですが、その中には事前に必要な署名を全て集めてからネットワークにアナウンスする
AggregateCompleteTransaction
と必要な署名をネットワークにアナウンスしてから集めるAggregateBondedTransaction
の2種類があります。 - AggregateBondedTransactionをアナウンスするには先に
HashLockTransaction
で10xymを預ける必要があります。- これはスパム対策の意味合いがあり、HashLockTransactionで指定したトランザクションハッシュのAggregateBondedTransactionで必要が署名がそろい、トランザクションが成立した場合は10xymは返ってきますが、そろわずにトランザクションが成立しなかった場合は、そのブロックを収穫したハーベスターに渡り、失われます。
AggregateBondedTransactionのアナウンス手順
AggregateBondedTransactionのアナウンス方法は以下の通りです。前提として symbol-sdk-typescript-javascript を使った場合の手順です。
- AggregateBondedTransaction を構成し、署名する
- 1.で作成した署名済みのトランザクションを使って、 HashLockTransaction を構成し、署名する。
- 2.で作成した、署名済みの HashLockTransaction をネットワークにアナウンスする
- 3.でアナウンスしたトランザクションがブロックに取り込まれるのを待つ
- 1.で作成した AggregateBondedTransaction をネットワークにアナウンスする
ここでのポイントは、先に HashLockTransaction をアナウンスし、それがブロックに取り込まれネットワークに認識されてから、AggregateBondedTransactionを投げる必要があることです。
もし、HashLockTransactionを投げないもしくは承認されないうちにAggregateBondedTransactionをアナウンスしようすると Failure_LockHash_Unknown_Hash
と言うエラーが返ってきて、アナウンスすることができません。
よくやるAggregateBondedTransactionのアナウンス例
Symbolの公式ドキュメント を参考にすると以下の様なコードになると思います。
async/awaitを使っているのは個人の好みです。また、一部省略している部分があります。
const aggregateTx = AggregateTransaction.createBonded(
Deadline.create(epochAdjustment),
[
tx1.toAggregate(account1.publicAccount),
tx2.toAggregate(account2PublicAccount)
],
networkType,
).setMaxFeeForAggregate(100, 2);
const signedTx = account.sign(aggregateTx, generationHash);
const hashLockTx = HashLockTransaction.create(
Deadline.create(epochAdjustment),
NetworkCurrencies.PUBLIC.currency.createRelative(10),
UInt64.fromUint(5760),
signedTx,
networkType
).setMaxFee(100);
const signedHashLockTx = account.sign(hashLockTx, generationHash);
const transactionRepo = repoFactory.createTransactionRepository();
const receiptRepo = repoFactory.createReceiptRepository();
const listener = repoFactory.createListener();
const transactionService = new TransactionService(transactionRepo, receiptRepo);
try {
await listener.open();
await transactionService.
announceHashLockAggregateBonded(signedHashLockTx, signedTx, listener).
toPromise();
} catch(err) {
console.log(err);
} finally {
listener.close();
}
先に書いたアナウンス手順に従ったコードですが、このコードでは transactionService.announceHashLockAggregateBonded()
を使うことで、HashLockTransactionをアナウンスし、ブロックに取り込まれてからAggregateBondedTransactionをアナウンスするということをひとまとめにしています。
ところが、この方法ではまれに Failure_LockHash_Unknown_Hash
が発生し、AggregateBondedTransactionのアナウンスに失敗する場合があります。
原因としては、このメソッドではHashLockTransactionの承認が確認された後に間髪入れずにAggregateBondedTransactionをアナウンスしているのですが、それが先にアナウンスしたHashLockTransactionによる状態変更が伝搬する前に来てしまい、失敗するのではないかとのことです。
実際、symbol-sdkの実装をみるとこのようなコードになっています
public announceHashLockAggregateBonded(
signedHashLockTransaction: SignedTransaction,
signedAggregateTransaction: SignedTransaction,
listener: IListener,
): Observable<AggregateTransaction> {
return this.announce(signedHashLockTransaction, listener).pipe(
mergeMap(() => this.announceAggregateBonded(signedAggregateTransaction, listener)),
);
}
もうちょっと確実にアナウンスしたい
announceHashLockAggregateBonded()
は、HashLockTransactionとAggregateBondedTransactionのアナウンス処理を一括して行ってくれる便利なメソッドですが、Failure_LockHash_Unknown_Hash
エラーになるリスクがあります。
なので、もう少し安全にアナウンスしたい場合は、別な方法を取る必要があります。
以下は代わりのアナウンス方法です。
try {
await listener.open();
await transactionService.announce(signedHashLockTx, listener).toPromise();
await sleep(3000);
await transactionService.announceAggregateBonded(signedTx, listener).toPromise();
} catch(err) {
console.log(err);
} finally {
listener.close();
}
announceHashLockAggregateBonded()
の代わりに annouce()
でHashLockTransactionをアナウンスし、ブロックに取り込まれるのを待ち、3秒のsleepを入れてからannounceAggregateBonded()
でAggregateBondedTransactionをアナウンスしています。なぜ、AggregateBondedTransactionはannouce()
ではなくannounceAggregateBonded()
を使っているかというと、AggregateBondedTransactionをアナウンスする際には専用のエンドポイントでアナウンスしないといけないためです。
ちなみにsleepメソッドは以下の様に定義します。
async function sleep(ms) {
return new Promise(r => setTimeout(r, ms));
}
それでもアナウンスできなかったら
これでも何らかの原因でHashLockTransactionは通って、ネットワーク上に10xymを担保に差し出したのに、AggregateBondedTransactionのアナウンスに失敗してしまう可能性は考慮した方がよいでしょう。
署名済みのアグリゲートトランザクションはそのトランザクションのペイロードとトランザクションハッシュ、署名者の公開鍵があれば再現できます。これをもともと定義していたトランザクションの有効期限内に再送すれば、AggregateBondedTransactionのアナウンスに成功します。
// 署名済みのトランザクションを再生成
const announceTx = new SignedTransaction(
// 署名済みのトランザクションのペイロード
signedTx.payload,
// 署名済みのトランザクションのトランザクションハッシュ
signedTx.hash,
// 署名済みのトランザクションの署名者の公開鍵
signedTx.signerPublicKey,
// トランザクションの種類
TransactionType.AGGREGATE_BONDED,
// ネットワークタイプ(NetworkType.MAIN_NET or NetworkType.TEST_NET)
networkType
);
// 再生成した署名済みトランザクションをアナウンスする
await transactionService.announceAggregateBonded(announceTx, listener).toPromise();
AggregateBondedTransactionを扱う場合は、アナウンス失敗時に備えて、署名済みトランザクションのペイロードとハッシュ、署名者の公開鍵は控えておきましょう。それがあればなんとかなる可能性が高くなります。
まとめ
気をつければその10xym諦めずにすむかも!!