21
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

nemAdvent Calendar 2021

Day 16

AggregateBondedTransactionをもうちょっと確実にアナウンスする

Posted at

はじめに

Symbolのメインネット上でAggregateBondedTransactionをアナウンスしようとすると、AggregateBondedTransactionをうまくアナウンスされず、HashLockTransactionで差し出した10xymを取られてしまうという話題がありました。
そんな思わぬ悲劇を避けるべく、どうすればAggregateBondedTransactionをより確実にアナウンスすることができるかを考えてみたいと思います。

AggregateBondedTransactionとは

  • AggregateTransactionは複数のトランザクションをひとまとめにして処理することができるSymbolの機能なのですが、その中には事前に必要な署名を全て集めてからネットワークにアナウンスする AggregateCompleteTransaction と必要な署名をネットワークにアナウンスしてから集める AggregateBondedTransaction の2種類があります。
  • AggregateBondedTransactionをアナウンスするには先にHashLockTransactionで10xymを預ける必要があります。
    • これはスパム対策の意味合いがあり、HashLockTransactionで指定したトランザクションハッシュのAggregateBondedTransactionで必要が署名がそろい、トランザクションが成立した場合は10xymは返ってきますが、そろわずにトランザクションが成立しなかった場合は、そのブロックを収穫したハーベスターに渡り、失われます。

AggregateBondedTransactionのアナウンス手順

AggregateBondedTransactionのアナウンス方法は以下の通りです。前提として symbol-sdk-typescript-javascript を使った場合の手順です。

  1. AggregateBondedTransaction を構成し、署名する
  2. 1.で作成した署名済みのトランザクションを使って、 HashLockTransaction を構成し、署名する。
  3. 2.で作成した、署名済みの HashLockTransaction をネットワークにアナウンスする
  4. 3.でアナウンスしたトランザクションがブロックに取り込まれるのを待つ
  5. 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諦めずにすむかも!!

21
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
21
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?