Update 2023. 3. 30.

2023년 2월 27일 NEM 스톨 포스트모템

strapi-blog-api-image

관찰

블록 4129631이 생성된 후, NEM 네트워크의 블록 생성 속도가 느려졌습니다. 이러한 상황은 미확인 트랜잭션 캐시에 예기치 않은 트랜잭션이 들어와서 발생했습니다. 이 트랜잭션이 만료되자 블록 생성은 정상으로 돌아갔습니다.
저희는 예기치 않은 트랜잭션의 구성과 미확인 트랜잭션 캐시에 들어간 경위를 확인했습니다.
2대1 다중 서명으로 구성된 다중 서명 계정이 두 개의 상충되는 트랜잭션을 보냈습니다. 두 명의 공동 서명자의 이름은 A와 B이고 다중 서명 계정의 이름은 M이라고 가정합니다.

근본 원인 분석

다음과 같은 일련의 이벤트가 발생했습니다:
  1. M이 A가 서명한 내부 트랜잭션 X가 포함된 다중서명 트랜잭션 MT를 보냅니다.
  2. 다중서명 트랜잭션이 체인에 커밋되고 (블록트랜잭션팩토리의 일부로 실행되는) TransactionHashesObserver가 H(MT), H(X)를 모두 해시트랜잭션캐시에 추가합니다.
    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(새로운 트랜잭션해시알림());
    }
    
  3. M은 B가 서명한 SAME 내부 트랜잭션 X와 함께 다중서명 트랜잭션 MT'를 보냅니다.
  4. BatchUniqueHashTransactionValidator는 H(MT')가 해시트랜잭션캐시에 없는지 확인하여 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'가 하베스터에 의해 블록으로 선택됩니다. BlockUniqueHashTransactionValidatorBatchUniqueHashTransactionValidator`와 동일한 로직을 가지고 있기 때문에 외부의 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는 상태를 변경하는 블록 처리의 일부에서 HashTransactionsCache에 H(MT'와 H(X)를 모두 추가하려고 시도합니다. H(X)가 이미 UnconfirmedTransactionsCache에 존재하기 때문에 실패하고, 수집된 블록은 거부됩니다 🛑.
  7. 5단계로 이동하여 UnconfirmedTransactionsCache에서 H(MT')가 만료될 때까지 기다립니다.
strapi-blog-api-image

수정

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

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

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

News
Community
Docs
Contact