Try   HackMD

2023年02月27日 NEM ブロック生成遅延についての事後報告 “2023 02 27 NEM Stall Post Mortem Japanese Translation”

See original document 原文を見る

観測結果

ブロック 4129631 の生成後に、NEMネットワークにおけるブロック生成は鈍化し、遅延しました。この状況は、予期せぬトランザクションがUnconfirmed Transactionsキャッシュに入ったことをきっかけに発生いたしました。その後、このトランザクションが期限切れになると、ブロックの生成は通常どおりへと戻りました。

確認された予期せぬトランザクションの構成と、Unconfirmed Transactionsキャッシュに入った経緯は次の通りです。

1-of-2マルチシグで設定されたマルチシグアカウントが、2つの矛盾するトランザクションを送信しました。ここでは、その連署者をA、Bとし、マルチシグアカウントをMとします。

根本的な原因の解析

発生の流れは次の通りです:

  1. アカウントMが、 署名者Aによって署名された内部トランザクション- X -を内包するマルチシグトランザクションMTを送信

  2. そのマルチシグトランザクションはチェーンにコミットされ、(BlockTransactionFactoryの一部として実行される)TransactionHashesObserverが、H(MT)と H(X) をHashTransactionsCacheに追加

    ​​​​public void notify(final TransactionHashesNotification notification, final BlockNotificationContext context) {if (NotificationTrigger.Execute == context.getTrigger()) {this.transactionHashCache.putAll(notification.getPairs());} 
    ​​​​...
    

    重要な点は、TransactionsHashesNotification が作成された時、 streamDefault が使用され、 内部及び外部トランザクションに拡張されたことです:

    protected void notifyTransactionHashes() {final List<HashMetaDataPair> pairs = BlockExtensions.streamDefault(this.block).map(t -> new HashMetaDataPair(HashUtils.calculateHash(t), new HashMetaData(this.block.getHeight(), t.getTimeStamp()))).collect(Collectors.toList());this.observer.notify(new TransactionHashesNotification(pairs));}
    
    
  3. アカウントMは、連署者Bが署名した同じ内部トランザクションXを持つマルチシグトランザクションMT'を送信します

  4. BatchUniqueHashTransactionValidator は、H(MT')がHashTransactionsCacheにないことをチェックするので、それはUnconfirmedTransactionsCacheに追加されます ✅

    public ValidationResult validate(final List<TransactionsContextPair> groupedTransactions) {
    ​​​​        ...final List<Hash> hashes = groupedTransactions.stream().flatMap(pair -> pair.getTransactions().stream()).map(HashUtils::calculateHash).collect(Collectors.toList());return this.validate(hashes);}
    
    

    重要な点は、 pair.getTransactions() が 外部トランザクションしか含まれていない Collection<Transaction>を返すことです。 結果として、 H(MT') != H(MT) はチェックされますが、 H(X) はチェックされません。

  5. MT' はハーベスターによってブロックに選択されます。BlockUniqueHashTransactionValidator は、BatchUniqueHashTransactionValidatorと同じロジックを持つため、外部トランザクション MT'のハッシュのみがチェックされます。 その結果、トランザクションはハーベストされたブロックに含まれます。 ✅

    public ValidationResult validate(final Block 
    ​​​​        ...final List<Hash> hashes = block.getTransactions().stream().map(HashUtils::calculateHash).collect(Collectors.toList());return this.transactionHashCache.anyHashExists(hashes) ? ValidationResult.NEUTRAL : ValidationResult.SUCCESS;}
    
  6. MT'を含む新しくハーベストされたブロックが処理される時、TransactionHashesObserver は、状態の変更を行うブロック処理の一部で、H(MT') H(X)の両方をHashTransactionsCacheに登録しようとします。ですが既に H(X) は UnconfirmedTransactionsCacheに存在するために失敗し、ハーベストされたブロックは拒否されます。 🛑

  7. H(MT') が失効するまで、UnconfirmedTransactionsCacheステップ5へ進みます。

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

修正

修正につきましては、一意のハッシュトランザクションバリデーターのどちらもを、外部トランザクションと内部トランザクションの両方をチェックするようにすることです。このことは、単にTransactionExtensions.streamDefault()を用いることで解決されます。

BatchUniqueHashTransactionValidator

TransactionExtensions.streamDefault を追加することで、MT' (または類似のもの) が `UnconfirmedTransactionsCache への追加を防ぐことができます。

public ValidationResult validate(final List<TransactionsContextPair> groupedTransactions) {
    if (groupedTransactions.isEmpty()) {
        return ValidationResult.SUCCESS;
    }

    final List<Hash> hashes = groupedTransactions.stream().flatMap(pair -> pair.getTransactions().stream())
            .flatMap(transaction -> TransactionExtensions.streamDefault(transaction)).map(HashUtils::calculateHash)
            .collect(Collectors.toList());
    return this.validate(hashes);
}

BlockUniqueHashTransactionValidator

TransactionExtensions.streamDefault を追加することで、MT' (または類似のもの) が新たにハーベストされるブロックへの追加を防ぐことができます。

public ValidationResult validate(final Block block) {
    if (block.getTransactions().isEmpty()) {
        return ValidationResult.SUCCESS;
    }

    final List<Hash> hashes = block.getTransactions().stream().flatMap(transaction -> TransactionExtensions.streamDefault(transaction))
            .map(HashUtils::calculateHash).collect(Collectors.toList());
    return this.transactionHashCache.anyHashExists(hashes) ? ValidationResult.NEUTRAL : ValidationResult.SUCCESS;
}