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

## 관찰

블록 4129631이 생성된 후, NEM 네트워크의 블록 생성 속도가 느려졌습니다. 이러한 상황은 미확인 트랜잭션 캐시에 예기치 않은 트랜잭션이 들어와서 발생했습니다. 이 트랜잭션이 만료되자 블록 생성은 정상으로 돌아갔습니다.

저희는 예기치 않은 트랜잭션의 구성과 미확인 트랜잭션 캐시에 들어간 경위를 확인했습니다.

2대1 다중 서명으로 구성된 다중 서명 계정이 두 개의 상충되는 트랜잭션을 보냈습니다. 두 명의 공동 서명자의 이름은 A와 B이고 다중 서명 계정의 이름은 M이라고 가정합니다.

## 근본 원인 분석

다음과 같은 일련의 이벤트가 발생했습니다:

1. M이 A가 서명한 내부 트랜잭션 X가 포함된 다중서명 트랜잭션 MT를 보냅니다.

1. 다중서명 트랜잭션이 체인에 커밋되고 (블록트랜잭션팩토리의 일부로 실행되는) TransactionHashesObserver가 H(MT), H(X)를 모두 해시트랜잭션캐시에 추가합니다.

   ```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(새로운 트랜잭션해시알림(쌍));
   }
   
   ```

1. M은 B가 서명한 **SAME** 내부 트랜잭션 X와 함께 다중서명 트랜잭션 MT'를 보냅니다.

1. `BatchUniqueHashTransactionValidator`는 H(MT')가 해시트랜잭션캐시에 없는지 확인하여 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)는 확인되지 않습니다.

1. 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;
   }
   ```

1. 새로 수집된 블록(MT'를 포함)이 처리될 때, `TransactionHashesObserver`는 상태를 변경하는 블록 처리의 일부에서 `HashTransactionsCache`에 H(MT'와 H(X)를 모두 추가하려고 시도합니다. H(X)가 이미 `UnconfirmedTransactionsCache`에 존재하기 때문에 실패하고, 수집된 블록은 거부됩니다 🛑.

1. 5단계로 이동하여 `UnconfirmedTransactionsCache`에서 H(MT')가 만료될 때까지 기다립니다.

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

## 수정

수정 사항은 외부 및 내부 트랜잭션 해시를 모두 확인하도록 두 고유 해시 트랜잭션 유효성 검사기를 모두 업데이트하는 것입니다. 트랜잭션 유효성 검사기는 `TransactionExtensions.streamDefault()`를 사용하여 간단히 수행할 수 있습니다.

### 배치유니크해시 트랜잭션 검증자

트랜잭션 익스텐션에 `TransactionExtensions.streamDefault`를 추가하면 MT'(또는 이와 유사한)가 `UnconfirmedTransactionsCache`에 추가되는 것을 방지할 수 있습니다.

```java
public ValidationResult validate(final List<TransactionsContextPair> groupedTransactions) {
    if (groupedTransactions.isEmpty()) {
        ValidationResult.SUCCESS를 반환합니다;
    }

    final List<Hash> hashes = groupedTransactions.stream().flatMap(pair -> pair.getTransactions().stream())
            .flatMap(트랜잭션 -> 트랜잭션익스텐션.streamDefault(트랜잭션)).map(해시유틸스::계산해시)
            .collect(Collectors.toList());
    return this.validate(hashes);
}
```

### 블록유니크해시 트랜잭션 검증자

TransactionExtensions.streamDefault`를 추가하면 새로 수집된 블록에 MT(또는 이와 유사한)가 추가되는 것을 방지할 수 있습니다.

```java
public ValidationResult validate(final Block 블록) {
    if (block.getTransactions().isEmpty()) {
        ValidationResult.SUCCESS를 반환합니다;
    }

    final List<Hash> hashes = block.getTransactions().stream().flatMap(transaction -> TransactionExtensions.streamDefault(transaction))
            .map(HashUtils::calculateHash).collect(Collect

Translated with www.DeepL.com/Translator (free version)
