Rand Protocol
Documentação
Conceitos fundamentais

Notes e a árvore de compromissos

O modelo de notes sobre o qual toda transferência é construída - compromissos, nullifiers, envelopes, a árvore de profundidade 32, âncoras e as janelas de âncora e de tempo - e como uma carteira varre a cadeia em busca das suas próprias notes.

Use esta página para construir ou auditar qualquer coisa que leia o pool: uma carteira que varre as suas notas, um indexador que acompanha compromissos e nulificadores, ou uma ferramenta que verifica o que uma transação publica. O valor nesta chain é um conjunto de notas; a chain guarda os seus compromissos, nunca o seu conteúdo.

Uma nota

Uma nota é { pk, from, amount, asset, time, r }: 28 palavras de 32 bits, 112 bytes, dispostas como pk(8) from(8) amount_lo amount_hi asset time r(8). pk é o valor público de nota do proprietário (derivado da viewing key), from o do remetente, amount um u64 em unidades (1 RAND = 10⁹ unidades), asset um u32 (0 para RAND; caso contrário, o índice denso do registro de tokens para um token RPL, com ponte ou nativo; veja tokens RPL), time uma altura de bloco visada pelo remetente, e r oito palavras aleatórias de cegamento.

O texto em claro nunca aparece na chain. O que aparece é:

  • o seu compromisso cm = H(CM, words), um hash Poseidon2 das 28 palavras sob a etiqueta de domínio 4, acrescentado como folha da árvore;
  • o seu envelope, o texto em claro selado para o endereço do proprietário, de modo que apenas o proprietário, ou o remetente por meio da sua outgoing viewing key, possa lê-lo. Desde a chain 18 (chain 20 incluída), o envelope também carrega um memo cifrado. Algumas notas que a chain calcula a partir de campos públicos não têm envelope algum (abaixo).

Gastar a nota publica o seu nulificador H(NF, nk, cm), um hash Poseidon2 sob a etiqueta de domínio 3 da viewing key do proprietário e do compromisso. Sem nk, um nulificador não pode ser associado ao seu compromisso. O conjunto de nulificadores é o que impede o gasto duplo.

O envelope

Envelope { kem_ct, to_receiver, to_sender, body }
partebyteso que é
kem_ct1.088uma encapsulação ML-KEM-768 para o endereço do destinatário
to_receiver60a chave por transação TxKey envolvida sob o segredo encapsulado
to_sender60a mesma TxKey envolvida sob a outgoing viewing key do remetente, ovk
body652a nota de 112 bytes e um campo de memo de 512 bytes sob TxKey (140 bytes, apenas a nota, até a chain 17)

Cada parte é ChaCha20-Poly1305 com um nonce aleatório de 12 bytes anteposto e uma tag de 16 bytes, e os dados associados são uma etiqueta (rand-envelope-receiver, rand-envelope-sender, rand-envelope-body) seguida do compromisso da nota, de modo que uma chave não consegue abrir um envelope que não selou e uma nota aberta fica vinculada à folha sob a qual se encontra. São 1.860 bytes de texto cifrado; os prefixos de comprimento do bincode elevam-no a 1.892 na rede. O genesis da chain 20 define envelope_bytes: 1860, e o seu ledger recusa um envelope de nota de qualquer outro comprimento (envelope is 1348 bytes, this chain requires exactly 1860); nada mais sobre ele é verificado. Até a chain 17, o envelope não tinha campo de memo e tinha 1.348 bytes (1.380 na rede), e o ledger apenas o limitava a 2.048 bytes, o que continua a ser a regra numa chain cujo genesis não tem envelope_bytes.

O campo de memo é len (u16 LE) ‖ texto UTF-8 ‖ preenchimento de zeros, 512 bytes, ou seja, no máximo 510 bytes de texto. Todo envelope de nota na chain 20 o tem, haja ou não memo: uma saída de troco, uma saída fictícia e um pagamento sem memo são preenchidos da mesma forma, e nada na chain distingue um memo, o seu comprimento ou a sua ausência. O memo não faz parte da nota, do compromisso nem de qualquer prova. É legível exatamente por quem já pode ler a nota: o proprietário, o remetente por meio de ovk, ou qualquer pessoa a quem tenha sido entregue a TxKey dessa saída. Um campo de memo malformado (comprimento inválido, UTF-8 inválido, preenchimento não nulo) abre como sem memo e nunca custa a nota ao proprietário. Um memo é texto de qualquer pessoa, já que qualquer um pode enviar uma nota de poeira a um endereço público: mostre-o como não confiável, com os caracteres de controle e de formatação Unicode substituídos, como faz a carteira rand. rand send --memo sela um na saída do pagamento; rand notes --memo e rand history --memo o mostram.

A árvore, âncoras e janelas

Os compromissos são acrescentados a uma única árvore Poseidon2 de profundidade 32, dos mais antigos para os mais recentes, e nunca são removidos. A folha i é a i-ésima nota que a chain já criou; as notas de depósito do genesis são as primeiras folhas. Um pacote acrescenta quatro folhas e quatro nulificadores, qualquer que seja o que movimente, fictícios incluídos. A folha vazia é só zeros e um nó é H(NODE, left, right) (etiqueta de domínio 7). Um nó guarda apenas a fronteira, de modo que o estado de consenso nunca contém as folhas; rand_getWitness reconstrói a árvore completa a partir das folhas armazenadas sob demanda, razão pela qual é a leitura mais cara que um nó serve.

Um pacote prova a pertinência das suas entradas contra uma âncora, uma raiz da árvore. Duas regras mantêm uma âncora honesta:

constantevalorregra
ANCHOR_WINDOW256 blocos; 1.024 na chain 20a âncora deve ser uma das últimas window raízes de fim de bloco; uma raiz pela qual a árvore passa no meio de um bloco nunca é âncora
TIME_WINDOW256 blocos; 1.024 na chain 20o time do pacote deve estar dentro de [height − window, height]

Ambas as janelas são de 256 blocos, a menos que o genesis defina proof_window_blocks (v0.6.8, 256 a 4.096), que substitui as duas em conjunto. A chain 20 define 1.024; as chains 14 a 19 não tinham o campo.

As janelas existem porque provar leva tempo real: uma prova de pacote de tier 14 mede cerca de 100 segundos em um laptop (e, desde a chain 17, a carteira faz uma segunda prova de autorização, de tier 10, de cerca de 7 segundos). 256 blocos eram cerca de cinco minutos com os blocos de 1,17 s da chain 18, e uma prova delegada em um provador lento foi recusada aos 294 s; os 1.024 blocos da chain 20 são cerca de 23 minutos aos 1,34 s que o explorador reportou ali. Uma carteira que perde a corrida recebe anchor is not one of the last 1024 roots e prova de novo. rand_getAnchor sem parâmetro devolve a raiz de fim de bloco da cabeça da chain, a única âncora contra a qual um provador deve construir.

O pacote

Toda transação que movimenta valor carrega exatamente um pacote de ativo oculto 4-entradas-4-saídas fixo (desde a chain 14; era 2-entradas-2-saídas até a chain 13), provado pelo guest zkVM de ativo oculto cujo digest hc_bundle está fixado no arquivo genesis (60af094acfe65d85fdb18fb3d06cf9085dcf28c96e59e87f1ee527226e6e3fce, bundle guest v3, nas chains 17 a 20; era 83d3a3704a1fcdb9bae7136c0a947ffa34bd53f055388395ed705fe8cacd0ef8, guest v1, nas chains 14 e 15):

Bundle { anchor, nullifiers[4], commitments[4], fee, burn_a, burn_r, burn_asset, time, envelopes[4], proof,
         auth_commit, auth_proof }     // the last two since chain 17

Os slots 0–1, de entrada e de saída, carregam um ativo privado A; os slots 2–3 carregam sempre RAND. Num pagamento apenas em RAND, os slots 0–1 são fictícios e o valor e a taxa circulam pelos slots 2–3. Numa transferência de token, o token circula pelos slots 0–1 e a taxa em RAND é paga a partir dos slots 2–3 da mesma prova. A é uma palavra de witness da prova e não consta de nenhum campo público, de modo que uma transferência de qualquer token RPL é Action::None com burn_a = burn_r = burn_asset = 0, campo a campo com a mesma forma de um pagamento em RAND.

A prova afirma que toda entrada real é uma folha sob anchor e carrega o ativo do seu slot, que os quatro nulificadores são os publicados, que cada grupo se equilibra (in0 + in1 = out0 + out1 + burn_a no ativo privado, in2 + in3 = out2 + out3 + fee + burn_r em RAND), e que o gastador detém as chaves — sem revelar quais folhas, quais valores, qual ativo, nem quem. Os três campos de queima são a única declaração pública do pacote sobre valor que sai do pool: burn_a e burn_asset só são diferentes de zero em um TokenBurn ou um BridgeBurn, onde igualam o valor e o ativo da ação; burn_r é RAND queimado, diferente de zero apenas em um Bond (o stake) e um RegisterAggregator (a caução). A taxa é sempre em RAND.

Desde a chain 17, a autoridade para gastar é dividida em duas provas (autorização dividida). O bundle guest v3 recebe a viewing key nk e um salt onde os guests anteriores recebiam a spend key: deriva pk_self = H(PK, nk), exige que toda entrada pertença a ele e incorpora c = H(AUTH, nk ‖ salt) no digest que publica. Um segundo guest, o auth guest (genesis hc_auth, 1e4e347f44cf86750b30a9a4bdf9ec9256efe353d4ff8017451eca7d195639c1), recebe a spend key sk e o mesmo salt, deriva nk = H(NK, sk) e publica o mesmo c. O pacote carrega c como auth_commit e a prova de tier 10 do auth guest como auth_proof, e o ledger exige que ambas as provas sejam verificadas contra a mesma vinculação da transação e que os dois valores de c coincidam. Um provador que detenha nk e o salt pode, portanto, produzir a prova do pacote, mas não a prova de autorização, o que é a base da prova delegada. O salt são 256 bits aleatórios novos por pacote, de modo que auth_commit é público e não vincula nada. Uma carteira que prova por conta própria também produz ambas as provas, de modo que uma transação provada pela própria carteira e uma delegada têm o mesmo aspecto na chain.

Os espaços são frequentemente fictícios, pois o formato é fixo: uma entrada fictícia é uma nota de valor zero pertencente a quem gasta (o seu nullifier continua a ser publicado), uma saída fictícia é uma nota de valor zero, e a saída de troco existe mesmo quando o troco é zero. Uma transação que tivesse outro aspecto quando não há troco revelaria que não houve troco. Uma consequência para as carteiras: cada um dos dois grupos de um bundle gasta no máximo duas notas. Um pagamento em RAND, uma bond, um deploy ou uma chamada gasta no máximo duas notas RAND; uma transferência ou um burn de token gasta no máximo duas notas do token e no máximo duas notas RAND para a taxa. O valor distribuído por três ou mais notas num mesmo grupo tem de ser consolidado antes, por um envio ao próprio endereço.

Desde a chain 14, a prova também fica vinculada à sua transação: é produzida e verificada sobre um hash da transação inteira com as provas em branco, de modo que não pode ser copiada para outra ação, outros envelopes ou outra chain; na chain 20, o hash cobre também o hash de gênese, de modo que também não pode ser copiada para uma chain que partilhe o mesmo chain id (Transações).

O que é público e o que é oculto

público na chainoculto
uma transferência, de RAND ou de qualquer token RPLâncora, os quatro nullifiers, os quatro compromissos, taxa, burn_a = burn_r = burn_asset = 0, time, quatro textos cifrados de envelope, a prova, auth_commit e a prova de autorizaçãoquem enviou, quem recebe, o valor, o memo, qual ativo se moveu, o troco, quais folhas foram gastas, quais espaços eram fictícios
um burn de token ou um burn de bridgetudo o que uma transferência publica, com burn_asset e burn_a iguais ao ativo e ao valor da ação (e, num burn de bridge, a taxa do relayer, o token de lastro e o destino)quais notas foram queimadas e quem as queimou
um mint do fauceto compromisso, o envelope, o valor, o validator que fez o mint e, desde a chain 14, a abertura da nota (pk, time, r), a partir da qual a admissão recalcula o compromissotodos os gastos posteriores da nota
um depósito de gênese{ cm, envelope, amount, opening: { pk, time, r } } no arquivo de gênese, com asset na abertura para uma nota de um token listado (a gênese da chain 20 traz duas notas zUSD): o pk do titular, o valor e o ativo são públicos por desenhoquando e em quê é gasta depois
um withdraw, um depósito de bridge, um mint de token ou o mint inicial de um registroo valor, o destinatário, o time da nota e o fator de cegamento r (a chain calcula ela própria o compromisso)todos os gastos posteriores da nota
um pagamento de invoke ou um mint de programa (chain 20)o valor, o ativo, o destinatário e r; o time da nota é o do bundletodos os gastos posteriores da nota
uma nota de taxa de bridge (chain 20)o valor, o ativo e o time, um fator de cegamento derivado, a tesouraria de gênese como titular, e nenhum envelopetodos os gastos posteriores da nota

O valor entra no pool com um valor público — um mint do faucet, um depósito de gênese, um withdraw de validator, uma atestação de bridge e a sua nota de taxa, um mint de token, um pagamento do cofre de um programa — e sai dele como taxa ou burn públicos. Tudo o que fica entre os dois é uma nota.

A tabela é o que as provas da chain 20 ocultam, e nem sempre foi assim. Nas chains 14 e 15, a própria prova do bundle revelava mais (a revisão do zkVM de 2026-09-27, INT-2 / GV-1): o total acumulado LogUp de cada tabela era publicado sem cegamento, e o guest v1 omitia as verificações de Merkle e de ativo para uma entrada fictícia e ramificava em cada bit do caminho de Merkle, de modo que qualquer pessoa podia ler numa prova de bundle quais dos quatro espaços de entrada eram reais, e portanto se movia um token ou apenas RAND, e o número de bits 1 no índice de cada folha gasta. Duas mudanças fecharam a brecha. A chain 16 passou ao conjunto de restrições 7, que cega todos os terminais LogUp e fixa cada tabela declarada em pelo menos 2^7 linhas, e ao guest de bundle v2, sem ramificações, no qual todo espaço, fictício ou real, executa a verificação completa de pertença e de ativo pelas mesmas instruções. O guest v3, que as chains 17 a 20 executam, é a relação do v2 com nk e o salt no lugar da chave de gasto, e também não tem ramificações. Os bundles confirmados nas chains 14 e 15 mantêm o que revelaram. O que uma prova ainda mostra na chain 20 é o seu cabeçalho: para um bundle, uma forma única e fixa (tier 14, um limite de gas declarado de 20.479 em todos os bundles) e, para um Call ou um Invoke, o tier, o limite de gas declarado e se a chamada usou o precompile keccak ou sha256 e, aproximadamente, com que frequência (Provas).

Notas sem envelope

A maioria das notas é encontrada abrindo o seu envelope. Algumas são calculadas pela chain a partir de campos públicos e, para essas, o envelope ou está ausente ou não merece confiança.

  • Notas de taxa de bridge (chain 20). Com bridge.fees, a chain acrescenta uma nota zUSD à tesouraria de gênese em cada depósito e em cada burn (Tokens RPL). Não tem envelope: o armazenamento a indexa com um envelope vazio, selado para ninguém, e rand_getCompactBlocks e rand_getCommitments a mostram assim, a de um depósito depois da nota do depósito e a de um burn depois das quatro do bundle. Cada palavra da nota é pública: o valor, a partir da transação e da taxa de gênese, o ativo e o time, a partir da transação, from = 0, o fator de cegamento blake3("rand-bridge-fee-r-1" ‖ mu) num depósito ou blake3("rand-bridge-burn-fee-r-1" ‖ txid) num burn, e o titular, o recipient da gênese.
  • Notas cujo envelope não vincula nada. Um depósito de bridge, um mint de token, o mint inicial de um registro e, na chain 20, os pagamentos e os mints de programa de um invoke trazem um envelope selado por quem submeteu a transação, mas a chain calcula a nota a partir dos campos públicos da ação e nunca confere o envelope com ela. Um submissor descuidado ou hostil pode selar lixo, de modo que um destinatário não deve depender dele.

Por isso, a carteira rand reconstrói essas notas por si própria (wallet::rebuilt_notes_with), numa passagem pública ao lado da tentativa de decifração: para cada transação desse tipo que nomeie o seu pk, e para as notas de taxa quando o seu endereço é o da tesouraria, calcula a nota e a regista na folha cujo compromisso ela produz por hash. Uma nota reconstruída é apenas uma candidata: um nó que minta sobre uma transação, a taxa ou um hash faz com que uma nota não seja encontrada, nunca que uma seja creditada. Desde a v0.6.8, os cabeçalhos que rand_getBlocks devolve trazem em public_notes as transações de depósito, de mint e de registro e, numa chain com taxas de bridge, também os burns, sem as provas, sob o seu id real, de modo que a passagem lê apenas páginas de cabeçalhos. Os invokes não estão em public_notes na v0.6.8, de modo que, contra um nó v0.6.8, a carteira só encontra o pagamento de um invoke pelo seu envelope, que o invocador sela e que nada verifica. O main do fullnode também traz lá os invokes, sem as provas, desde d0bb73fb; é uma mudança apenas no nó, ainda não aplicada à frota da chain 20, e a carteira não precisa de nenhuma mudança para ela.

Varredura: como uma carteira encontra as suas notas

Um nó responde “aqui está a árvore inteira”, nunca “aqui está o seu saldo”. Uma carteira:

  1. percorre as folhas por páginas com rand_getCommitments(from_index, limit) (no máximo 1.000 por chamada) ou com o fluxo por bloco rand_getCompactBlocks(from_height, to_height) (no máximo 128 blocos por chamada); toda transação que carrega um bundle lista aí quatro notas e quatro nullifiers, e nenhum leitor consegue distinguir um espaço fictício de um real;
  2. tenta decifrar cada envelope: como destinatária, pela sua chave de decapsulação ML-KEM, como remetente, por ovk; guarda as notas cujo pk é o seu; e reconstrói as notas com valor público a partir das suas transações (acima);
  3. calcula o nullifier de cada nota guardada e marca as que aparecem em rand_getNullifiers(from_height, limit);
  4. soma as restantes.

rand_getTreeInfo diz até onde há para varrer: { "next_index", "root", "nullifiers" }. Para um gasto, a carteira precisa do caminho de Merkle de 32 níveis de cada entrada real. A carteira rand mantém a sua própria cópia da árvore de compromissos, construída durante a varredura a partir das mesmas páginas de rand_getCommitments, e calcula ela própria todos os caminhos, de modo que a única pergunta sobre a árvore que um envio faz ao nó é rand_getAnchor, que não nomeia nenhuma folha. rand_getWitness(index) continua no nó para outros clientes. Ela diz ao nó qual folha o chamador está prestes a gastar, o que era o maior vazamento do pool antes da árvore local: um cliente que tenha de usá-la deve executar o seu próprio nó, ou pedir testemunhos de que não precisa junto com aqueles de que precisa.

rand sync        # scanned 41 leaves and 37 blocks; 1 notes, 1 unspent
rand notes       # index, asset, amount, height, spent, pending, memo
rand history     # notes this wallet created for others, opened through ovk

Uma nota fica pending entre rand send --no-wait e o próximo sync que veja o seu nullifier ou blocos além de time + window (1.024 na chain 20, lido de rand_getLimits.proof_window_blocks; 256 sem ele), após o que o pacote nunca mais poderá ser admitido e a nota volta a poder ser gasta.

Última verificação em relação ao commit c0878c3f do fullnode (chain 21) em 2026-10-12. Onde o código e esta página divergirem, o código está certo; avise-nos.