![post-mortem.png](../images/post_mortem_7efc587c07.png)

## 観測結果

ブロック 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に追加

    ```java
    public void notify(final TransactionHashesNotification notification, final BlockNotificationContext context) {
	if (NotificationTrigger.Execute == context.getTrigger()) {
		this.transactionHashCache.putAll(notification.getPairs());
	} 
    ...
    ```
    重要な点は、`TransactionsHashesNotification` が作成された時、 `streamDefault` が使用され、 内部及び外部トランザクションに拡張されたことです:
    ```java
	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に追加されます ✅ 
    ```java
	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'のハッシュのみがチェックされます。 その結果、トランザクションはハーベストされたブロックに含まれます。 ✅ 
    ```java
	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へ進みます。

![2023 02 27 NEM Stall Post Mortem](../images/2023_02_27_NEM_Stall_Post_Mortem_544cb72628.png)

## 修正

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

### BatchUniqueHashTransactionValidator

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

```java
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' (または類似のもの) が新たにハーベストされるブロックへの追加を防ぐことができます。

```java
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;
}
```
