nonce 공백 뒤에서 사라진 트랜잭션을 어떻게 찾아내고 고쳤는가

검증자 네 개로 구성된 네트워크에 지속적인 부하를 걸었을 때, 트랜잭션을 연달아 제출하는 송신자가 그 대부분을 잃을 수 있다는 사실을 발견했습니다. 90초짜리 한 실행에서는 전송 291건 중 14건만 확정되었습니다. 합의는 한 번도 위험에 처하지 않았습니다. 블록, state root, 잔액은 모든 노드에서 동일했습니다. 사라진 것은 사용자의 트랜잭션이었습니다.

어떻게 찾았는가

네트워크 테스트를 저장소(tools/quorum-net)로 옮겼습니다. 모든 실행은 제네시스에서 검증자 네 개를 시작하고, 파라미터, 모든 노드의 로그, 요약을 보관합니다. 깨끗한 빌드에서의 첫 실행은 오류 없이 통과했습니다. 반복 실행 시리즈를 돌리고 나서야 결함이 간헐적이며 진행 중인 작업보다 오래되었다는 것이 드러났습니다. 로그는 정확한 그림을 보여 주었습니다. nonce N의 트랜잭션이 아직 블록 제안자에게 도달하지 않았는데, N+1, N+2…는 이미 블록에 들어가 nonce에서 실패하고 풀에서 제거되었습니다.

첫 번째 원인: 블록에 넣을 트랜잭션을 고르는 방식

멤풀은 송신자의 트랜잭션을 nonce 순으로 정렬했지만, 그것이 커밋된 nonce에서 이어지는지는 확인하지 않았습니다. 거의 같은 버그를 겪은 go-ethereum(pending 및 queued 집합), Cosmos EVM, Aptos, Polygon Bor가 이를 어떻게 처리하는지 살펴보았습니다. 이들은 하나의 규칙을 공유하며, 저희도 그것을 채택했습니다. 연속된 nonce 구간만 블록에 들어가고, 공백 뒤의 트랜잭션은 풀에서 기다립니다. 이후 모든 실행에서 nonce 오류는 0건이었습니다.

수정이 드러낸 두 번째 원인: 전달 과정의 손실

이제 트랜잭션 N을 받지 못한 검증자는 정직하게 기다렸고, 그 검증자의 블록은 비어서 나왔습니다. 측정 결과 각 피어는 실행마다 트랜잭션 2–5건을 놓치고 있었습니다. 모든 메시지가 각자 TCP 연결을 열었고, 주소당 연결 수 제한이라는 저희의 DoS 방어가 그중 일부를 끊었습니다. 수정은 전달될 때까지 다시 알리는 Bitcoin과 2초마다 다시 보내는 Solana를 따릅니다. 노드는 대기 중인 트랜잭션이 블록에 포함될 때까지 한 번의 배치로 다시 보냅니다.

세 번째 단계: 피어 간 지속 연결

같은 제한이 합의 메시지도 끊고 있었기 때문에, 높이의 절반가량이 두 번째 라운드에서야 결정되었고 최대 12초의 정체가 있었습니다. CometBFT와 libp2p를 따라, 이제 각 노드는 피어당 하나의 장기 연결을 유지합니다. DoS 방어는 그대로입니다. 정직한 피어는 그저 더 이상 수백 개의 연결을 열 필요가 없을 뿐입니다.

결과

같은 제한 아래에서: 전송 296건 중 296건 확정, 가장 긴 정체는 12초가 아니라 1.8초, 연결 끊김 없음. 다음은 피어 간 ML-DSA로 서명된 핸드셰이크로, 제한이 IP 주소가 아니라 검증자의 신원에 적용되도록 하는 것입니다.

전체 뉴스