이 페이지는 rand_sendTransaction이 트랜잭션을 수락한 이후부터 블록에 담기기 전까지 그 트랜잭션에 무슨 일이 일어나는지 이해하기 위한 것입니다. 거부되었다면 그 이유, 실패하지 않았는데도 풀에서 사라질 수 있는 이유, 블록이 채워지는 순서, 그리고 잘못된 증명이 대량으로 유입될 때 검증자(validator)에게 가능한 일과 불가능한 일을 다룹니다. 멤풀은 crates/randprotocol-node/src/mempool.rs이며, 그 앞단의 방어 장치는 admission.rs에 있습니다.
발신자가 없으므로 nonce도 없습니다
계정 기반 체인의 멤풀은 각 발신자의 트랜잭션을 nonce 순으로 정렬합니다. 차폐(shielded) 체인에는 발신자가 없습니다. 번들(bundle)에는 주소도 서명도 없으며, 두 번들은 같은 노트(note)를 건드릴 때에만 서로 관련됩니다. 따라서 발신자별로 정렬할 대상이 남아 있지 않습니다. nonce 관리를 대신하는 것은 충돌 추적입니다. 풀은 대기 중인 모든 트랜잭션의 nullifier와 commitment를 색인하고, 그중 하나라도 주장하는 새 트랜잭션은 거부합니다. 원장이 값 하나당 정확히 하나의 트랜잭션만 받아들이는 몇몇 카운터도 마찬가지입니다. 검증자의 등록 nonce(Unbond 또는 Withdraw), 토큰의 mint_nonce(TokenMint와 SetAuthority가 공유), RegisterToken이 따라야 하는 레지스트리 인덱스, 그리고 브리지의 pause_nonce와 list_nonce가 그것입니다. 각각은 풀에 있는 트랜잭션 최대 하나만 차지할 수 있는 슬롯입니다. 그런 두 트랜잭션 중 포함될 수 있는 것은 많아야 하나이며, 나머지를 들고 있으면 제안자는 블록 공간을, 모든 노드는 gossip 라운드마다 증명 검증 한 번을 낭비하게 됩니다.
풀은 최대 10,000개의 트랜잭션(node.rs의 Mempool::new(10_000))을 보유하며, v0.6.8부터는 최대 블록 8개 분량의 바이트도 상한으로 둡니다. MAX_POOL_BLOCKS = 8에 체인의 max_block_bytes를 곱한 값으로, chain 20에서는 160 MiB입니다(audit v6, CH-7). 그 이전에는 개수만 제한되었는데, 증명을 담은 트랜잭션 하나가 약 1.3 MB에서 12 MB 이상에 이르기 때문입니다. 각 트랜잭션은 마지막으로 커밋된 블록이 아니라 컨센서스 트리의 팁(tip), 즉 제안자가 실제로 이어서 만들 상태의 원장을 기준으로 검증됩니다.
승인, 저렴한 검사가 먼저입니다
RPC나 gossip으로 도착한 트랜잭션은 다음 단계를 순서대로 거치며, 처음 거부된 지점의 오류가 사용자에게 보이는 오류입니다.
- 중복. 해시가 이미 풀에 있는 경우:
already in mempool. - 충돌. nullifier, commitment 또는 위의 nonce 및 인덱스 슬롯 중 하나를 대기 중인 트랜잭션이 이미 차지하고 있는 경우:
conflicts with a pending transaction over <hex>. 토큰 슬롯의 경우 hex는 표시용 키입니다. 태그 단어(mint_nonce에는tokn, 등록 인덱스에는tidx) 다음에 인덱스와 nonce가 이어집니다. 두 릴레이어가 경합한 브리지 attestation은 다이제스트로 구분됩니다:conflicts with a pending transaction over bridge attestation <hash>. - 가득 참.
mempool full. 용량 검사는 세 가지 중 마지막입니다. 바이트 상한에 도달하면, 새 트랜잭션은 자신보다 엄격히 낮게 평가되는 풀 내 항목을 대체할 수 있습니다. 평가 기준은 각 항목의 현재 KiB당 하한을 넘는 수수료, 그다음으로 수수료이며, 새 트랜잭션이 들어갈 때까지 가장 저렴한 것부터 축출합니다. 이것으로 부족하거나 동률일 뿐이라면 거부되는 쪽은 새 트랜잭션입니다. 거버넌스 액션은 두 상한 모두의 바깥에 있어 축출되지 않습니다. 브리지 거버넌스 액션 네 가지인PauseMints,UnpauseMints,RegisterBridgedToken,ListBacking, 그리고 v0.6.8부터는RotatePqGuardians,RotatePauseKey, 이들의…V2형태,CancelRotation,AdmitValidator,SlashEquivocation이 해당합니다. 이들의 바이트와 항목 수는 여전히 상한에 산입됩니다.PauseMints는 guardian 키가 탈취되었을 때의 제동 장치입니다. 번들도 수수료도 없고, 공격자가 민팅하는 동안 반드시 블록에 도달해야 하는 유일한 트랜잭션이므로, 풀이 가득 찼다고 해서 막혀서는 안 됩니다. 이 예외는 충돌 단계에 의해 한정됩니다.pause_nonce와list_nonce는 각각 한 번에 풀의 트랜잭션 하나만 허용하므로, 브리지 액션 네 가지는 상한을 넘어 최대 두 항목만 추가할 수 있으며, 나머지는 각자 자신의 nonce나 정족수를 소모합니다. Ledger::validate의 저렴한 절반. 승인과 포함 사이에 낡을 수 있으면서 증명이 필요 없는 모든 검사입니다. 앵커가 chain 20에서 최근 1,024개 루트 중 하나여야 하고(제네시스의proof_window_blocks, 없으면 256),time이 윈도 안에 있어야 하며, nullifier가 사용되지 않았고, commitment가 존재하지 않아야 합니다. attestation의 다이제스트가 점유되지 않았고, 스테이킹 액션의 등록 nonce가 최신이며, 토큰 액션의mint_nonce또는 등록 인덱스가 레지스트리의 현재 값과 같아야 하고(체인이 그보다 앞서 나아갔다면wrong mint nonce,wrong token index), 정지 또는 상장의 nonce가 브리지의 현재 값과 같아야 합니다. 트랜잭션이 생성하는 commitment도 여기서 계산됩니다. 여기에는Withdraw,BridgeAttest또는 aggregator 지급을 위해 원장이 직접 도출하는 노트도 포함되는데, 이는 와이어에는 없지만 마찬가지로 충돌해서는 안 됩니다. 크기 상한은 무엇이든 디코딩하기 전에 검사합니다. 체인의max_proof_bytes(기본 2 MiB, chain 20에서는 4 MiB)를 넘는 증명, 16 KiB를 넘는 attestation, 블록보다 큰 트랜잭션(max_block_bytes: 기본 4 MiB, chain 20에서는 20 MiB)은 즉시 거부되므로, 크기가 과도한 blob으로는 연산 비용을 유발할 수 없습니다. 봉인된(가지치기된) 마커 형태의 번들 증명은 봉인 형태 동기화 밖에서는 같은 단계에서PrunedFormOutsideSync로 거부됩니다. 그다음 풀은 아직 아무것도 검증하지 않은 채 증명 헤더를 읽습니다. 먼저 세 가지 정책이 적용됩니다. 입력, keccak 또는 sha256 테이블이 2^7행 미만인 call은 풀에 들어가지 않으며(CallRevealsPrivateInputs), 어떤 call도 담을 수 없는 deploy(ProgramUncallable)나, 번들 또는 call 증명에 정직한 prover라면 기록하지 않을 헤더 필드가 있는 트랜잭션(NonCanonicalProof)도 마찬가지입니다. 마지막 두 가지는 chain 20에서hardening_v6아래의 원장 규칙이기도 합니다. v0.6.8부터는 선언된 차수(degree)가 가장 높은 테이블의 상한을 넘는 헤더가 검사가 이를 순회하기 전에NonCanonicalProof로 거부되며(audit v6, ZKV-4), 이 사전 선별 어디에서든 발생한 panic은 노드가 죽는 대신 해당 트랜잭션에 대한 거부(VerifierPanicked)로 처리됩니다. 그다음은 수수료 하한입니다. chain 20에서 풀은 call 또는 invoke 증명이 선언한 가스 한도를 디코딩하여, 팁의 가격 기준BUNDLE_BASE + gas_price × limit + byte_price × ⌈bytes/1024⌉에 invoke의 셀 수수료를 더한 값보다 낮은 수수료를FeeTooLow로 거부합니다(수수료). 이것은 블록 자체의 규칙이므로, 풀은 블록이 요구하는 것과 정확히 같은 것을 요구합니다.- 비싼 절반, 컨센서스 루프 바깥에서. 트랜잭션은 최대 64개 깊이의 큐에 들어가 네 개의 검증 워커 중 하나를 기다립니다. 워커는 블로킹 태스크에서
Ledger::validate의 나머지를 실행합니다. 번들 다이제스트, 번들 증명이 선언한 가스 한도(chain 20에서는 정확히 20,479여야 하며 아니면BundleGasLimit), 번들 증명과 chain 17부터는 트랜잭션의 바인딩에 대한 인가 증명, 그리고 call의 경우 그 자체의 증명과 수수료 하한을 검사합니다. 검증은 팁의 스냅숏에 대해 수행되며, 이 스냅숏은 팁이 바뀔 때마다 많아야 한 번, 그리고 대기 중인 항목이 있을 때에만 지연 복제됩니다. 큐가 가득 찬 상태에서 도착한 gossip 트랜잭션은 투표를 수행하는 루프를 막는 대신, 검증도 전달도 하지 않고 폐기됩니다. 해당 피어의 다음 gossip 라운드가 다시 전달합니다. - 재검사 및 풀 등록.
insert_verified는 증명이 검증되는 동안 팁이 바뀌었을 수 있으므로, 트랜잭션이 실제로 풀에 들어가는 팁을 기준으로 4단계를 다시 실행한 뒤 승인하고, gossip으로 받은 트래픽이면 전달합니다.
1~4단계는 Mempool::precheck이며, STARK를 검증하는 것은 5단계뿐입니다. 이 비용은 워커가 부담하므로, 잘못된 증명이 대량으로 유입되어도 스레드 네 개가 점유될 뿐 컨센서스 루프는 영향을 받지 않습니다. 트랜잭션과 액션에는 원장 자체의 승인 순서가 나와 있으며, 4단계와 5단계가 이를 합쳐서 실행합니다.
gossip을 위한 두 개의 방어 장치와 하나의 캐시
승인 앞단에서는 두 가지가 피어로부터 도착한 트랜잭션에만 적용됩니다. RPC 제출은 이를 건너뛰는데, 그 포트는 운영자 자신의 것이며 이미 요청 본문 크기 제한으로 제한되기 때문입니다. 계량되는 유일한 RPC 경로는 아래의 faucet입니다.
- 피어별 바이트 미터. v0.6.8부터 모든 가십 프레임은 디코딩되기도 전에 전달한 피어의 바이트 예산에서 차감됩니다. 버스트는 최대 전송 크기 프레임 8개이고 보충은 초당 2개 프레임입니다. 예산을 넘은 프레임은 디코딩되지 않고 폐기됩니다(audit v6, GOSSIP-1). 증명 전체를 해시하는 트랜잭션 id는 전달 피어의 허용량 안에서만 계산됩니다.
- 피어별 트랜잭션 미터. 전달하는 피어마다 토큰 버킷을 두며, 버스트 16, 초당 4 보충입니다(
PEER_TX_BURST,PEER_TX_PER_SEC). 미터는 트랜잭션을 전달한 피어에 적용되며, 차폐 체인에서는 지목할 수 없는 작성자에게는 적용되지 않습니다. - 판정 캐시. 이 노드가 이미 거부한 트랜잭션 중 거부 사유가 바이트만의 함수인 경우, 예를 들어 잘못된 증명, 잘못된 서명, 크기를 초과한 부분이 사유인 경우에는 다시 검증하지 않고 8,192개 항목의 FIFO 캐시(
REFUSED_CACHE_ENTRIES)에서 곧바로 거부합니다. 이 노드의 상태에 관한 판정, 즉 알 수 없는 앵커나 이미 사용된 널리파이어는 캐시하지 않습니다. 그렇지 않으면 한 블록 뒤처진 노드가 곧 유효해질 트랜잭션을 스스로 막게 됩니다. 이 목록은 허용 목록이므로, 새로운 오류는 누군가 달리 결정하기 전까지 상태로 취급됩니다. 다음 항목들은 그 경계가 어디에 있는지 보여 줍니다.BadPauseSignature(the pause signature does not verify)는 v0.6.8부터 영구적이지 않습니다(audit v6, BRG-18). 이 오류는 체인의 현재 일시 중지 키로 서명을 확인하는데, 브리지 규칙 v2가 이 키를 교체할 수 있으므로 상태에 해당합니다. 교체 직전 한 블록 뒤처진 노드는 재시작할 때까지 새 키 보유자의PauseMints를 거부하게 됩니다. 범위를 벗어난 PQ 공동 서명 인덱스도 같은 이유로 캐시에서 빠졌습니다. v0.6.7까지는 체인 18과 19에서 둘 다 영구적인 것으로 캐시되었습니다. 이제 잘못된 일시 중지가 반복되면 전달될 때마다 Dilithium2 검증 한 번의 비용이 들며, 이는 피어별 미터로 제한됩니다.PrunedFormOutsideSync는 의도적으로 영구적이지 않습니다. 봉인된 번들의 마커 형식은 원본 트랜잭션 자체의 id로 해시되고 캐시는 그 id를 키로 사용하므로, 이 거부를 캐시하면 어떤 가십 피어든 정상 트랜잭션을 네트워크 전체에서 검열할 수 있게 됩니다.- 증명이 선언한 가스 한도는 그 자신의 바이트를 제네시스 상수와 비교하는 것이므로
BundleGasLimit와AuthGasLimit는 영구적입니다.FeeTooLow는 그렇지 않습니다. 체인 20에서는 하한이 가격에 따라 움직이기 때문입니다. 4단계의 세 가지 헤더 정책도 캐시되지 않습니다. 각각은 이 빌드가 아는 내용이며, 이후 빌드에서 바뀔 수 있습니다. - 페이싯 허용 목록이 있는 체인(체인 20)에서
FaucetRecipientNotAllowed는 영구적입니다. 민트 자체의 수신자를 제네시스 상수와 비교하기 때문입니다.FaucetBudgetExhausted는 그렇지 않습니다. 상태에 해당하며, 같은 민트가 다음 에포크에는 다시 수락됩니다. - 토큰 액션의
BadSignature는 스테이킹 액션과 달리 영구적이지 않습니다. 토큰의 민트 권한은 상태이고,SetAuthority로 다른 키에 넘길 수 있으므로, 같은 바이트가 그 교체가 커밋되기 전에는 거부되고 커밋된 뒤에는 수락될 수 있습니다. 바이트 수준의 토큰 거부, 즉 잘못된 이름이나 심볼, 지나치게 많은 소수 자릿수, 0 수량, 토큰 인덱스 0은 캐시됩니다. 액션이 설정해서는 안 되는 소각 필드, 커밋먼트가 자신의 수량으로 열리지 않는 민트, 블라인딩이 유도된 값이 아닌 브리지 입금도 마찬가지입니다.
페이싯 한도
rand_mint는 수수료가 없고 호출 한 번당 풀 트랜잭션 하나를 차지하며, 체인 20에서는 페이싯이 켜져 있고 그 뒤에 가치를 보유한 브리지가 있습니다. 제네시스는 이 호출이 할 수 있는 일을 제한합니다. 민트는 허용 목록에 오른 16개 지갑에만 지급하며 1,000블록 에포크당 최대 10,000 RAND까지이고(Staking), 제네시스의 staking.faucet_minters 목록에 있는 18개 키 중 하나만 서명할 수 있습니다(MinterNotAllowed, 캐시되지 않음. 체인 16부터는 원장 규칙이고, 목록이 제네시스 밸리데이터였던 체인 15에서는 노드 정책입니다). 이에 더해 각 노드는 자체 페이싯에 미터를 둡니다. 연속 민트 8회, 초당 1회 보충입니다(FAUCET_MINT_BURST, FAUCET_MINT_PER_SEC). 이를 넘으면 호출은 faucet is rate limited on this node (8 mints back to back, refilling at 1/s); try again shortly라고 응답합니다. 이 한도는 호출자별이 아니라 노드 프로세스별입니다. RPC 포트에는 미터를 적용할 피어 신원이 없고, 보호 대상 자원이 이 노드의 풀이기 때문입니다. 위의 용량 면제와 함께, 페이싯 폭주로 풀이 가득 차서 PauseMints가 밀리는 일은 없습니다.
Gossipsub는 애플리케이션 수준 검증으로 실행되므로, 트랜잭션은 여기서 검증을 통과한 뒤에야 다른 피어로 전달됩니다. 유효하지 않은 트랜잭션을 쏟아내는 피어는 미터로 제한되고 캐시에서 응답을 받으며, 합의 루프에는 어떤 비용도 지우지 않습니다.
블록 구성
밸리데이터가 뷰를 이끌 때는 풀에 후보를 요청합니다. candidates_within은 구성 중인 원장에 여전히 적용되는 트랜잭션만 남기며, 이를 위해 수락 검사의 가벼운 절반을 다시 실행합니다. 앵커가 아직 윈도 안에 있는지, time이 아직 윈도 안에 있는지, 널리파이어가 아직 사용되지 않았는지, 커밋먼트가 아직 존재하지 않는지를 확인합니다. 또한 체인이 mint_nonce나 등록 인덱스를 이미 지나친 토큰 액션과, 오래된 nonce의 일시 중지나 상장도 제외합니다. v0.6.8부터는 수락 시점에 기억해 둔 가스 한도를 바탕으로, 구성 중인 팁에서 유효한 가격으로 모든 call과 invoke의 요금을 다시 계산하고, 수수료가 이제 하한에 못 미치는 것은 제외합니다. 제외된 트랜잭션은 풀에 남아 있다가 가격이 내려가면 다시 제안됩니다(audit v6, CH-9). 체인 20에서는 그다음 나머지를 거버넌스 먼저, 그다음 현재 하한을 넘는 수수료를 트랜잭션 KiB당으로 환산한 값, 그다음 수수료가 높은 순, 동률은 해시로 결정하는 순서로 정렬하므로, 같은 풀을 기반으로 블록을 구성하는 정직한 제안자는 모두 같은 블록을 고르게 됩니다. 그리고 트랜잭션 2,000개와 체인의 블록 상한(MAX_BLOCK_TXS, max_block_bytes. 기본값은 4 MiB이며 체인 20에서는 20 MiB)이라는 예산까지 채웁니다. 블록에서 부족한 것은 바이트이므로 초과분을 KiB당으로 계산합니다. gas 섹션이 없는 체인과 가스 정책이 없는 노드는 이 키를 건너뛰고, 체인 15가 그랬듯 거버넌스, 수수료, 해시 순으로 정렬합니다. 거버넌스를 먼저 두는 것은 일시 중지 보장의 나머지 절반입니다. PauseMints는 RAND를 보유하지 않은 지갑에서도 작동해야 하므로 설계상 수수료를 내지 않으며, 단순한 수수료 순서로는 비상 브레이크가 수수료를 내는 모든 트랜잭션 뒤로 밀리게 됩니다.
call이나 invoke가 준비되어 있는 동안에는 규칙이 하나 더 적용됩니다(audit v6, POOL-2. 모든 체인에서 노드 정책이며 유효성 규칙이 아닙니다). 바이트 가격을 내는 것은 이들뿐이고, 평액 기본 요금을 조금 넘게 내는 전송이 KiB당 기준으로 이들을 앞서므로, 전송이 계속 이어지면 준비된 call이 모든 블록에서 빠질 수 있습니다. 그래서 정액 수수료 트랜잭션은 첫 번째 패스에서 블록 바이트의 최대 4분의 3까지만 차지합니다. call이 배치된 뒤 남은 공간은 보류해 둔 전송으로 그 순서대로 채워집니다. 블록 공간은 낭비되지 않으며, 준비된 call이 없으면 아무것도 달라지지 않습니다(Fees).
남은 공간에 비해 너무 큰 트랜잭션은 건너뛰며, 블록이 거기서 끝나지는 않습니다. 그 뒤에 있는 더 작은 트랜잭션은 그대로 포함됩니다. 체인 18의 전송은 약 2.85 MB로, 번들 증명 1.49 MB와 승인 증명 1.36 MB로 이루어져 있으므로 20 MiB 블록에는 약 7개, 기본 4 MiB 블록에는 1개가 들어갑니다. 체인 15에서는 전송당 약 1.43 MB였으므로 같은 20 MiB에 14개가 들어갔습니다. 부하가 걸리면 어느 쪽이 들어갈지는 수수료가 결정합니다(Fees).
정리
매 커밋 후 prune은 candidates_within이 건너뛰었을 트랜잭션을 정확히 풀에서 제거합니다. 즉 노트가 다른 누군가에 의해 사용된 트랜잭션, 커밋먼트가 이제 존재하는 트랜잭션, 체인이 nonce나 등록 인덱스를 이미 지나친 트랜잭션, 앵커나 time이 윈도 밖으로 밀려난 트랜잭션입니다(체인 20에서는 1,024블록, 체인 19까지는 256블록). 따라서 이중 지불 경쟁에서 진 번들은 누구에게도 오류를 내지 않고 저절로 사라집니다. rand_sendTransaction의 수락은 커밋이 아닙니다. rand_getTransaction이 블록을 반환할 때까지 폴링하고, 그러지 못한 채 앵커 윈도가 지나가면 새 앵커로 번들을 다시 만들어 제출하십시오.
모니터링
rand_status는 mempool_size, verify_queue(워커를 기다리는 트랜잭션), refused_cache(판정 캐시의 항목 수)를 보고합니다. verify_queue가 64에 머문다면 증명이 네 개의 워커가 검증하는 속도, 즉 키가 준비된 상태에서 건당 약 16 ms보다 빠르게 도착하고 있다는 뜻입니다. 블록이 계속 가득 찬 상태에서 mempool_size가 늘어난다면, 도착 순서가 아니라 수수료가 포함 여부를 결정하고 있다는 뜻입니다.
아직 없는 것
- 대체 불가. Chain 20에서 gas 가격과 바이트 가격은 매 블록 상한까지 움직이며 호출의 최저 수수료(floor)도 함께 움직이지만, 전송의 최저 수수료는 고정되어 있습니다. 동일한 note를 대상으로 더 높은 수수료를 낸 트랜잭션이 풀에 있는 트랜잭션을 대체하는 방식은 지원되지 않으며, 충돌 규칙이 이를 거부합니다. 가격 상승으로 요건을 충족하지 못하게 된 호출은 가격이 내려가거나 앵커 구간이 지날 때까지 제안 대상에 오르지 못한 채 풀에서 대기합니다. v0.6.7(chain 18 및 19)에서는 풀에 있는 호출이 승인될 당시의 최저 수수료를 그대로 유지했기 때문에, 가격이 오른 뒤에도 계속 제안 대상에 올랐고 제안자의 시험 적용 단계에서
FeeTooLow로 실패했습니다. fullnode의 블록 공간 문서는 수수료 순서 mempool을 더 큰 블록에 앞서는 단계로, 블록 단위 집계를 블록당 상한을 높이는 수단으로 제시합니다. - 나이에 따른 만료 없음. 풀에 있는 트랜잭션은 블록 포함, 바이트 상한에 따른 퇴출, 또는 앵커 및 시간 구간 경과로만 풀을 떠납니다. 구간은 chain 20에서 1,024블록이며, 탐색기가 보고한 블록당 1.34초 기준으로 약 23분입니다. Chain 19까지는 구간이 256블록, 약 5분이었습니다.
- mempool RPC 없음. 대기 중인 트랜잭션을 나열하는 메서드는 없습니다.
rand_status의mempool_size와rand_getMempoolInfo의 개수, 바이트, 가장 오래된 항목의 나이만이 확인 수단이며, 탐색기는 커밋된 블록만 색인합니다.