Rand Protocol
Documentação
Conceitos fundamentais

Mempool

O que um nó faz com uma transação entre recebê-la e incluí-la em um bloco: a ordem de admissão, o rastreamento de conflitos no lugar de nonces, os quatro workers de verificação, o medidor de peers e o cache de veredictos, como um proposer escolhe um bloco e por que um gasto duplo perdedor desaparece por conta própria.

Use esta página para entender o que acontece com uma transação depois que rand_sendTransaction a aceita e antes que um bloco a inclua: por que ela foi recusada, se foi; por que pode desaparecer do pool sem nunca ter falhado; em que ordem um bloco é preenchido; e o que uma enxurrada de provas inválidas pode e não pode fazer a um validator. O mempool é crates/randprotocol-node/src/mempool.rs; as proteções à sua frente estão em admission.rs.

Sem remetentes, sem nonces

Em uma chain de contas, o mempool ordena as transações de cada remetente por nonce. Uma chain shielded não tem remetentes: um bundle não carrega endereço nem assinatura, e dois bundles só se relacionam quando tocam a mesma note. Não resta, portanto, nenhuma ordenação por remetente a fazer. O que substitui a contabilidade de nonces é o rastreamento de conflitos: o pool indexa os nullifiers e os commitments de toda transação pendente e recusa um recém-chegado que reivindique qualquer um deles. O mesmo vale para os poucos contadores em que o ledger aceita exatamente uma transação por valor: o nonce de registro de um validator (um Unbond ou um Withdraw), o mint_nonce de um token (compartilhado por TokenMint e SetAuthority), o índice de registro a que um RegisterToken está preso, e o pause_nonce e o list_nonce da bridge. Cada um é um slot que no máximo uma transação do pool pode ocupar. No máximo uma de duas dessas transações poderia ser incluída, e carregar a outra custaria ao proposer espaço de bloco e a cada nó uma verificação de prova por rodada de gossip.

O pool comporta no máximo 10.000 transações (Mempool::new(10_000) em node.rs) e, desde a v0.6.8, no máximo o equivalente em bytes a oito blocos: MAX_POOL_BLOCKS = 8 vezes o max_block_bytes da chain, 160 MiB na chain 20 (auditoria v6, CH-7). Antes disso só a contagem tinha limite, embora uma transação com prova ocupe de cerca de 1,3 MB a mais de 12 MB. Cada uma é validada contra o ledger na ponta da árvore de consenso, e não no último bloco confirmado: o estado sobre o qual um proposer realmente construiria.

Admissão, o barato antes do caro

Uma transação que chega por RPC ou por gossip passa por estas etapas, em ordem, e a primeira recusa é o erro que você vê:

  1. Duplicada. O hash já está no pool: already in mempool.
  2. Conflito. Um nullifier, um commitment ou um dos slots de nonce e de índice acima já foi reivindicado por uma transação pendente: conflicts with a pending transaction over <hex>. Para um slot de token, o hex é uma chave de exibição: uma palavra de etiqueta (tokn para um mint_nonce, tidx para um índice de registro), depois o índice e o nonce. Uma atestação da bridge disputada por dois relayers é distinguida pelo seu digest: conflicts with a pending transaction over bridge attestation <hash>.
  3. Cheio. mempool full. A verificação de capacidade é a última das três. No limite de bytes, um recém-chegado pode tomar o lugar de entradas do pool que estejam estritamente abaixo dele na classificação — pela taxa acima do piso atual de cada uma por KiB, depois pela taxa — removendo primeiro a mais barata até caber; se isso não bastar, ou se apenas empatar, é ele o recusado. As ações de governança ficam fora de ambos os limites e nunca são removidas: as quatro ações de governança da bridge PauseMints, UnpauseMints, RegisterBridgedToken, ListBacking e, desde a v0.6.8, RotatePqGuardians, RotatePauseKey, suas formas …V2, CancelRotation, AdmitValidator e SlashEquivocation. Seus bytes e suas entradas continuam contando para os limites. PauseMints é o freio para o comprometimento da chave de um guardian: sem bundle, sem taxa, e a única transação que precisa chegar a um bloco enquanto um atacante está emitindo, de modo que um pool cheio não deve barrá-la. A isenção é limitada pela etapa de conflito: o pause_nonce e o list_nonce admitem, cada um, uma transação no pool por vez, de modo que as quatro ações da bridge podem acrescentar no máximo duas entradas além do limite; as demais gastam, cada uma, um nonce ou um quórum próprio.
  4. A metade barata de Ledger::validate. Tudo o que pode ficar obsoleto entre a admissão e a inclusão e não custa nenhuma prova: a âncora é uma das últimas 1.024 raízes na chain 20 (proof_window_blocks do genesis; 256 sem ele), time está dentro da janela, os nullifiers não foram gastos, os commitments estão ausentes, o digest de uma atestação não foi reivindicado, o nonce de registro de uma ação de staking está atual, o mint_nonce ou o índice de registro de uma ação de token é aquele em que o registro está (wrong mint nonce, wrong token index quando a chain já passou dele), o nonce de uma pausa ou de uma listagem é o atual da bridge. Os commitments que uma transação cria também são calculados aqui, inclusive a note que o próprio ledger deriva para um Withdraw, uma BridgeAttest ou um pagamento de aggregator, que não está na rede, mas também não pode colidir. Os limites de tamanho são verificados antes de qualquer decodificação: uma prova acima do max_proof_bytes da chain (2 MiB por padrão, 4 MiB na chain 20), uma atestação acima de 16 KiB, ou uma transação maior que um bloco (max_block_bytes: 4 MiB por padrão, 20 MiB na chain 20) é recusada de imediato, de modo que um blob de tamanho excessivo não compra trabalho algum. Uma prova de bundle na forma de marcador selado (podado) é recusada nesta mesma etapa fora da sincronização em forma selada, como PrunedFormOutsideSync. Em seguida, ainda sem verificar nada, o pool lê os cabeçalhos das provas. Três políticas vêm primeiro: uma chamada cuja tabela de input, keccak ou sha256 tenha menos de 2^7 linhas não entra no pool (CallRevealsPrivateInputs), nem um deploy que nenhuma chamada poderia conter (ProgramUncallable), nem uma transação cuja prova de bundle ou de chamada tenha um campo de cabeçalho que o prover honesto não escreveria (NonCanonicalProof). As duas últimas também são regras do ledger na chain 20, sob hardening_v6. Desde a v0.6.8, um cabeçalho cujo grau declarado esteja acima do teto da maior tabela é recusado como NonCanonicalProof antes que a verificação o percorra (auditoria v6, ZKV-4), e um panic em qualquer ponto desta pré-triagem é a recusa daquela transação (VerifierPanicked), e não um nó morto. Depois, o piso de taxa: na chain 20 o pool decodifica o limite de gas que uma prova de chamada ou de invoke declara e recusa uma taxa inferior a BUNDLE_BASE + gas_price × limit + byte_price × ⌈bytes/1024⌉ aos preços da ponta, mais a taxa de célula de um invoke, como FeeTooLow (Taxas). Essa é a regra do próprio bloco, de modo que o pool exige exatamente o que um bloco exige.
  5. A metade cara, fora do loop de consenso. A transação é enfileirada, 64 de profundidade no máximo, para um dos quatro workers de verificação. Um worker executa o restante de Ledger::validate em uma tarefa bloqueante: o digest do bundle, o limite de gas que a prova de bundle declara (na chain 20, exatamente 20.479, caso contrário BundleGasLimit), a prova de bundle e, desde a chain 17, sua prova de autorização contra o binding da transação, e, para uma chamada, a própria prova e seu piso de taxa. Ele valida contra um snapshot da ponta que é clonado de forma preguiçosa, no máximo uma vez por mudança de ponta e apenas enquanto algo estiver esperando. Uma transação recebida por gossip que encontra a fila cheia é descartada, sem ser verificada nem repassada, em vez de bloquear o loop que vota; a próxima rodada de gossip do peer a traz de novo.
  6. Nova verificação e entrada no pool. insert_verified executa a etapa 4 outra vez contra a ponta em que a transação de fato entra no pool, porque a ponta pode ter se movido enquanto a prova era verificada, depois a admite e, no caso de tráfego por gossip, a repassa.

As etapas 1 a 4 são Mempool::precheck; a etapa 5 é a única que verifica um STARK. Como esse custo é pago em um worker, uma enxurrada de provas inválidas ocupa quatro threads e nunca o loop de consenso. Transações e ações lista a ordem de admissão do próprio ledger, que as etapas 4 e 5 executam juntas.

Duas proteções e um cache para o gossip

À frente da admissão, duas coisas se aplicam apenas a transações que chegam de peers; os envios por RPC as ignoram, porque essa porta é do próprio operador e já é limitada pelo limite de tamanho do corpo da requisição. O único caminho de RPC que é medido é o faucet, abaixo.

  • Um medidor de bytes por peer. Desde a v0.6.8, cada frame de gossip é cobrado do orçamento de bytes de quem o encaminhou antes mesmo de ser decodificado: uma rajada de oito e uma reposição de dois frames do tamanho máximo de transmissão por segundo. Acima disso, o frame é descartado sem ser decodificado (auditoria v6, GOSSIP-1). O id da transação, que é o hash da prova inteira, só é calculado dentro da cota de quem encaminha.
  • Um medidor de transações por peer. Um token bucket por peer que encaminha, com rajada de 16 e reposição de 4 por segundo (PEER_TX_BURST, PEER_TX_PER_SEC). Ele mede o peer que encaminhou a transação, não o seu autor, que uma cadeia blindada não consegue identificar.
  • Um cache de vereditos. Uma transação que este nó já recusou por um motivo que é função apenas dos seus bytes, como uma prova inválida, uma assinatura inválida ou uma parte grande demais, é recusada de novo a partir de um cache FIFO de 8.192 entradas (REFUSED_CACHE_ENTRIES), em vez de ser verificada uma segunda vez. Um veredito que diz algo sobre o estado deste nó, uma âncora desconhecida ou um nullifier já gasto, nunca é guardado em cache: um nó um bloco atrasado acabaria se envenenando contra transações que estão prestes a ser válidas. A lista é uma allowlist, de modo que um erro novo conta como estado até que alguém decida o contrário. Estas entradas mostram onde passa a linha:
    • BadPauseSignature (the pause signature does not verify) não é permanente desde a v0.6.8 (auditoria v6, BRG-18). Ele confere a assinatura sob a chave de pausa vigente da cadeia, que as regras de bridge v2 podem rotacionar; portanto é estado: um nó um bloco atrasado em relação a uma rotação recusaria o PauseMints do novo titular até reiniciar. Um índice de cossignatura PQ fora do intervalo saiu do cache pelo mesmo motivo. Até a v0.6.7, nas cadeias 18 e 19, ambos eram guardados em cache como permanentes. Uma pausa inválida repetida agora custa uma verificação Dilithium2 por entrega, limitada pelo medidor por peer.
    • PrunedFormOutsideSync não é permanente, deliberadamente. A forma marcadora de um bundle selado tem como hash o próprio id da transação bruta, e o cache usa esse id como chave; guardar a recusa permitiria que qualquer peer de gossip censurasse a transação honesta em toda a rede.
    • O limite de gas declarado por uma prova são seus próprios bytes comparados a uma constante do genesis, portanto BundleGasLimit e AuthGasLimit são permanentes. FeeTooLow não é: na cadeia 20, o piso se move com os preços. As três políticas de cabeçalho do passo 4 também nunca são guardadas em cache: cada uma é o conhecimento desta build, que uma build posterior pode alterar.
    • Em uma cadeia com allowlist de faucet (cadeia 20), FaucetRecipientNotAllowed é permanente: ele compara o destinatário do próprio mint com uma constante do genesis. FaucetBudgetExhausted não é: é estado, e o mesmo mint é admitido novamente na época seguinte.
    • O BadSignature de uma ação de token também não é permanente, ao contrário o de uma ação de staking: a autoridade de mint de um token é estado, um SetAuthority pode entregá-la a outra chave, e os mesmos bytes podem ser recusados antes de essa rotação ser confirmada e aceitos depois. As recusas de token em nível de bytes (um nome ou símbolo inválido, decimais demais, um valor zero, o índice de token 0) são guardadas em cache; o mesmo vale para um campo de burn que a ação não pode definir, um mint cujo compromisso não abre para o seu valor e um depósito de bridge cujo blinding não é o derivado.

O limite do faucet

rand_mint não cobra taxa e custa uma transação no pool por chamada, e na cadeia 20 o faucet está ativo, com uma bridge que mantém valor por trás dele. O genesis limita o que ele pode fazer: um mint paga apenas as 16 carteiras da allowlist e no máximo 10.000 RAND por época de 1.000 blocos (Staking), e somente uma das 18 chaves da lista staking.faucet_minters do genesis pode assinar um (MinterNotAllowed, nunca guardado em cache: uma regra do ledger desde a cadeia 16, política do nó na cadeia 15, onde a lista eram os validadores do genesis). Além disso, cada nó mede o seu próprio faucet: oito mints seguidos, com reposição de um por segundo (FAUCET_MINT_BURST, FAUCET_MINT_PER_SEC). Acima disso, a chamada responde faucet is rate limited on this node (8 mints back to back, refilling at 1/s); try again shortly. O limite é por processo do nó, não por chamador, porque a porta RPC não tem identidade de peer para medir e o recurso protegido é o pool deste nó. Junto com a isenção de capacidade acima, uma inundação no faucet não consegue encher o pool antes de um PauseMints.

O Gossipsub opera com validação em nível de aplicação, de modo que uma transação só é encaminhada a outros peers depois de verificada aqui. Um peer que inunda a rede com transações inválidas é contido pelo medidor, atendido pelo cache e não cobra nada do laço de consenso.

Construindo um bloco

Quando um validador lidera uma visão, ele pede candidatas ao pool. candidates_within mantém apenas as transações que ainda se aplicam ao ledger sobre o qual está construindo, executando de novo a metade barata da admissão: âncora ainda na janela, time ainda nela, nullifiers ainda não gastos, compromissos ainda não presentes. Também descarta uma ação de token cujo mint_nonce ou índice de registro a cadeia já ultrapassou, e uma pausa ou uma listagem com nonce desatualizado. Desde a v0.6.8, ele reprecifica cada call e invoke pelos preços em vigor na ponta sobre a qual constrói, a partir do limite de gas registrado na admissão, e deixa de fora aquela cuja taxa agora está abaixo do piso; ela permanece no pool e é oferecida de novo quando os preços caem (auditoria v6, CH-9). Na cadeia 20, ele então ordena as demais primeiro as de governança, depois pela taxa acima do piso atual, por KiB de transação, depois pela taxa, da maior para a menor, com desempate pelo hash, de modo que todo proponente honesto que constrói sobre o mesmo pool escolhe o mesmo bloco, e preenche até o orçamento de 2.000 transações e o limite de bloco da cadeia (MAX_BLOCK_TXS; max_block_bytes, 4 MiB por padrão e 20 MiB na cadeia 20). O que falta a um bloco são bytes, e por isso o excedente é tomado por KiB. Uma cadeia sem seção gas e um nó sem política de gas ignoram essa chave e ordenam por governança, taxa e hash, como fazia a cadeia 15. Governança primeiro é a outra metade da garantia da pausa: um PauseMints não paga taxa por projeto, pois precisa funcionar a partir de uma carteira sem nenhum RAND, de modo que a ordenação simples por taxa colocaria o freio de emergência atrás de toda transação que paga.

Mais uma regra se aplica enquanto uma call ou um invoke está pronto (auditoria v6, POOL-2; política do nó em todas as cadeias, não uma regra de validade). Somente essas pagam o preço por byte, e uma transferência que paga um pouco acima da sua base fixa as supera por KiB, de modo que um fluxo de transferências poderia manter uma call pronta fora de todos os blocos. Por isso, as transações de taxa fixa ocupam no máximo três quartos dos bytes do bloco em uma primeira passagem; depois que as calls são alocadas, o espaço que sobra é preenchido com as transferências retidas, na ordem delas. Nenhum espaço de bloco é perdido, e, sem nenhuma call pronta, nada muda (Fees).

Uma transação grande demais para o espaço que resta é pulada, não encerra o bloco: uma menor logo atrás ainda segue. Uma transferência da cadeia 18 tem cerca de 2,85 MB, sendo 1,49 MB de prova de bundle e 1,36 MB de prova de autorização, de modo que seu bloco de 20 MiB comporta cerca de sete e um bloco padrão de 4 MiB comporta uma. Na cadeia 15, com cerca de 1,43 MB por transferência, os mesmos 20 MiB comportavam catorze. Sob carga, é a taxa que decide quais entram (Fees).

Poda

Após cada commit, prune remove do pool exatamente o que candidates_within teria pulado: transações cuja nota foi gasta por outra pessoa, cujo compromisso agora existe, cujo nonce ou índice de registro a cadeia já ultrapassou, ou cuja âncora ou time saiu da janela (1.024 blocos na cadeia 20, 256 até a cadeia 19). Um bundle que perde uma corrida de gasto duplo desaparece, portanto, por conta própria, sem nunca produzir um erro para ninguém. A aceitação por rand_sendTransaction não é commit: consulte rand_getTransaction até que ele devolva um bloco e, se a janela de âncora passar sem um, reconstrua o bundle sobre uma âncora nova e envie de novo.

Observando

rand_status informa mempool_size, verify_queue (transações aguardando um worker) e refused_cache (entradas no cache de vereditos). Uma verify_queue parada em 64 significa que as provas chegam mais rápido do que quatro workers conseguem verificar, cerca de 16 ms cada com uma chave aquecida; um mempool_size que cresce enquanto os blocos permanecem cheios significa que a taxa, e não a ordem de chegada, está decidindo a inclusão.

O que ainda não existe

  • Sem substituição. Os preços de gas e de bytes da chain 20 movem o piso de uma chamada a cada bloco, até os seus tetos; o piso de uma transferência é fixo. Não existe substituição de uma transação do pool por outra de taxa maior sobre as mesmas notes, o que a regra de conflito recusa. Uma chamada cujo preço ficou acima do que paga espera no pool, sem ser oferecida, até que os preços caiam ou que a sua janela de âncora termine. Na v0.6.7 (chains 18 e 19), uma chamada no pool mantinha o piso com que foi admitida, continuava a ser oferecida após uma alta e falhava na aplicação de teste do proponente como FeeTooLow. O documento de espaço em bloco do fullnode lista o mempool ordenado por taxa como o passo anterior a um bloco maior, e a agregação em nível de bloco como o que eleva o teto por bloco.
  • Sem expiração por idade. Uma transação do pool sai apenas por inclusão, por remoção no limite de bytes ou pelas janelas de âncora e de tempo: 1.024 blocos na chain 20, cerca de 23 minutos a 1,34 s por bloco, conforme o explorer reportou ali. Até a chain 19, as janelas eram de 256 blocos, cerca de cinco minutos.
  • Sem RPC de mempool. Não há método que liste transações pendentes; mempool_size em rand_status e a contagem, os bytes e a idade da mais antiga em rand_getMempoolInfo são as únicas vistas, e o explorer indexa apenas blocos confirmados.

Última verificação em relação ao commit c0878c3f do fullnode (chain 21) em 2026-10-12, e fullnode v0.6.8 `c9c9bd3c` (chain 20): `crates/randprotocol-node/src/mempool.rs` (`MAX_POOL_BLOCKS`, `make_room`, `is_governance`, the call reservation in `candidates_within`), the commits for CH-7, CH-9, POOL-2, BRG-18, GOSSIP-1 and ZKV-4, and `rand_getLimits` on chain 20, fullnode v0.6.7 `86941a1`: `crates/randprotocol-node/src/mempool.rs`, `admission.rs`, `crates/randprotocol-core/src/ledger/mod.rs` (the gas floor), fullnode `90e7716`: `docs/architecture.md` §8, `docs/rpc.md` (changelog, the 2026-09-28 gas entries), fullnode `ed88241`: `docs/confidential.md` (transaction binding). Onde o código e esta página divergirem, o código está certo; avise-nos.