Update 2023/3/30

2023-02-27 事後報告

strapi-blog-api-image

観測結果

ブロック 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へ進みます。
strapi-blog-api-image

修正

修正につきましては、一意のハッシュトランザクションバリデーターのどちらもを、外部トランザクションと内部トランザクションの両方をチェックするようにすることです。このことは、単に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;
}

News
Community
Docs
Contact