Use esta página para construir o auditar cualquier cosa que lea el pool: una wallet que busca sus notas, un indexador que sigue compromisos y anuladores, o una herramienta que comprueba lo que publica una transacción. El valor en esta cadena es un conjunto de notas; la cadena guarda sus compromisos, nunca su contenido.
Una nota
Una nota es { pk, from, amount, asset, time, r }: 28 palabras de 32 bits, 112 bytes, dispuestas como pk(8) from(8) amount_lo amount_hi asset time r(8). pk es el valor público de nota del propietario (derivado de la clave de visualización), from el del remitente, amount un u64 en unidades (1 RAND = 10⁹ unidades), asset un u32 (0 para RAND; en otro caso, el índice denso del registro de tokens para un token RPL, puenteado o nativo; véase Tokens RPL), time una altura de bloque a la que apuntó el remitente, y r ocho palabras aleatorias de cegado.
El texto en claro nunca aparece en la cadena. Lo que aparece es:
- su compromiso
cm = H(CM, words), un hash Poseidon2 de las 28 palabras con la etiqueta de dominio 4, añadido como hoja del árbol; - su sobre, el texto en claro sellado para la dirección del propietario de modo que solo el propietario, o el remitente mediante su clave de visualización de salida, pueda leerlo. Desde la cadena 18 (la cadena 20 incluida) el sobre lleva además un memo cifrado. Algunas notas que la cadena calcula a partir de campos públicos no llevan sobre alguno (más abajo).
Gastar la nota publica su anulador H(NF, nk, cm), un hash Poseidon2 con la etiqueta de dominio 3 de la clave de visualización del propietario y del compromiso. Sin nk, un anulador no puede vincularse a su compromiso. El conjunto de anuladores es lo que impide el doble gasto.
El sobre
Envelope { kem_ct, to_receiver, to_sender, body }
| parte | bytes | qué es |
|---|---|---|
kem_ct | 1.088 | una encapsulación ML-KEM-768 hacia la dirección del receptor |
to_receiver | 60 | la clave por transacción TxKey envuelta bajo el secreto encapsulado |
to_sender | 60 | la misma TxKey envuelta bajo la clave de visualización de salida del remitente, ovk |
body | 652 | la nota de 112 bytes y un campo de memo de 512 bytes bajo TxKey (140 bytes, solo la nota, hasta la cadena 17) |
Cada parte es ChaCha20-Poly1305 con un nonce aleatorio de 12 bytes antepuesto y una etiqueta de 16 bytes, y los datos asociados son una etiqueta (rand-envelope-receiver, rand-envelope-sender, rand-envelope-body) seguida del compromiso de la nota, de modo que una clave no puede abrir un sobre que no selló y una nota abierta queda ligada a la hoja bajo la que se encuentra. Son 1.860 bytes de texto cifrado; los prefijos de longitud de bincode lo dejan en 1.892 en la red. El génesis de la cadena 20 fija envelope_bytes: 1860, y su libro mayor rechaza un sobre de nota de cualquier otra longitud (envelope is 1348 bytes, this chain requires exactly 1860); no comprueba nada más sobre él. Hasta la cadena 17 el sobre no tenía campo de memo y medía 1.348 bytes (1.380 en la red), y el libro mayor solo lo limitaba a 2.048 bytes, que sigue siendo la regla en una cadena cuyo génesis no tiene envelope_bytes.
El campo de memo es len (u16 LE) ‖ texto UTF-8 ‖ relleno de ceros, de 512 bytes, es decir, como máximo 510 bytes de texto. Todo sobre de nota en la cadena 20 lo tiene, haya memo o no: una salida de cambio, una salida ficticia y un pago sin memo se rellenan igual, y nada en la cadena distingue un memo, su longitud o su ausencia. El memo no está en la nota, ni en el compromiso, ni en ninguna prueba. Lo puede leer exactamente quien ya puede leer la nota: el propietario, el remitente mediante ovk, o cualquiera a quien se le entregue la TxKey de esa salida. Un campo de memo mal formado (una longitud errónea, UTF-8 inválido, relleno distinto de cero) se abre como si no hubiera memo y nunca le cuesta al propietario la nota. Un memo es texto de cualquiera, ya que cualquiera puede pagar una nota de polvo a una dirección pública: muéstrelo como no confiable, con los caracteres de control y de formato Unicode sustituidos, como hace la wallet rand. rand send --memo sella uno en la salida del pago; rand notes --memo y rand history --memo lo muestran.
El árbol, las anclas y las ventanas
Los compromisos se añaden a un único árbol Poseidon2 de profundidad 32, de más antiguo a más reciente, y nunca se eliminan. La hoja i es la i-ésima nota que la cadena ha creado; las notas de depósito del génesis son las primeras hojas. Un bundle añade cuatro hojas y cuatro anuladores sea cual sea lo que mueva, incluidas las ficticias. La hoja vacía es todo ceros y un nodo es H(NODE, left, right) (etiqueta de dominio 7). Un nodo conserva solo la frontera, de modo que el estado de consenso nunca contiene las hojas; rand_getWitness reconstruye el árbol completo a partir de las hojas almacenadas bajo demanda, y por eso es la lectura más costosa que sirve un nodo.
Un bundle prueba la pertenencia de sus entradas frente a un ancla, una raíz del árbol. Dos reglas mantienen honesta un ancla:
| constante | valor | regla |
|---|---|---|
ANCHOR_WINDOW | 256 bloques; 1.024 en la cadena 20 | el ancla debe ser una de las últimas window raíces de fin de bloque; una raíz por la que el árbol pasa a mitad de bloque nunca es un ancla |
TIME_WINDOW | 256 bloques; 1.024 en la cadena 20 | el time del bundle debe estar dentro de [height − window, height] |
Ambas ventanas son de 256 bloques salvo que el génesis fije proof_window_blocks (v0.6.8, de 256 a 4.096), que sustituye a las dos a la vez. La cadena 20 fija 1.024; las cadenas 14 a 19 no tenían el campo.
Las ventanas existen porque probar lleva tiempo real: una prueba de bundle de nivel 14 mide unos 100 segundos en un portátil (y desde la cadena 17 la wallet genera una segunda prueba de autorización, de nivel 10, de unos 7 segundos). 256 bloques eran unos cinco minutos con los bloques de 1,17 s de la cadena 18, y una prueba delegada en un probador lento fue rechazada a los 294 s; los 1.024 bloques de la cadena 20 son unos 23 minutos a los 1,34 s que el explorador registró allí. Una wallet que pierde la carrera recibe anchor is not one of the last 1024 roots y vuelve a probar. rand_getAnchor sin parámetro devuelve la raíz de fin de bloque de la cabeza, la única ancla contra la que debería construir un probador.
El bundle
Toda transacción que mueve valor lleva exactamente un bundle de activo oculto de 4 entradas y 4 salidas fijo (desde la cadena 14; era de 2 entradas y 2 salidas hasta la cadena 13), probado por el guest zkVM de activo oculto cuyo digest hc_bundle está fijado en el archivo de génesis (60af094acfe65d85fdb18fb3d06cf9085dcf28c96e59e87f1ee527226e6e3fce, guest de bundle v3, en las cadenas 17 a 20; era 83d3a3704a1fcdb9bae7136c0a947ffa34bd53f055388395ed705fe8cacd0ef8, guest v1, en las cadenas 14 y 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
Las ranuras 0–1, de entrada y de salida, llevan un activo privado A; las ranuras 2–3 llevan siempre RAND. En un pago solo en RAND las ranuras 0–1 son ficticias y el valor y la comisión se mueven por las ranuras 2–3. En una transferencia de token, el token se mueve por las ranuras 0–1 y la comisión en RAND se paga desde las ranuras 2–3 de la misma prueba. A es una palabra testigo de la prueba y no figura en ningún campo público, así que una transferencia de cualquier token RPL es Action::None con burn_a = burn_r = burn_asset = 0, campo por campo la misma forma que un pago en RAND.
La prueba establece que cada entrada real es una hoja bajo anchor y lleva el activo de su ranura, que los cuatro anuladores son los publicados, que cada grupo cuadra (in0 + in1 = out0 + out1 + burn_a en el activo privado, in2 + in3 = out2 + out3 + fee + burn_r en RAND), y que quien gasta posee las claves, sin revelar qué hojas, qué importes, qué activo ni quién. Los tres campos de quema son la única declaración pública del bundle sobre valor que sale del pool: burn_a y burn_asset son distintos de cero solo en un TokenBurn o un BridgeBurn, donde igualan el importe y el activo de la acción; burn_r es RAND quemado, distinto de cero solo en un Bond (la participación) y un RegisterAggregator (la fianza). La comisión es siempre en RAND.
Desde la cadena 17 la autoridad para gastar se reparte en dos pruebas (autorización dividida). El guest de bundle v3 toma la clave de visualización nk y una sal allí donde los guests anteriores tomaban la clave de gasto: deriva pk_self = H(PK, nk), exige que todas las entradas pertenezcan a ella e incorpora c = H(AUTH, nk ‖ salt) al digest que publica. Un segundo guest, el guest de autorización (génesis hc_auth, 1e4e347f44cf86750b30a9a4bdf9ec9256efe353d4ff8017451eca7d195639c1), toma la clave de gasto sk y la misma sal, deriva nk = H(NK, sk) y publica la misma c. El bundle lleva c como auth_commit y la prueba de nivel 10 del guest de autorización como auth_proof, y el libro mayor exige que ambas pruebas se verifiquen frente al mismo vínculo de transacción y que los dos valores de c coincidan. Un probador que posea nk y la sal puede por tanto generar la prueba del bundle pero no la de autorización, y en eso se apoya la prueba delegada. La sal son 256 bits aleatorios nuevos por bundle, de modo que auth_commit es público y no vincula nada. Una wallet que prueba por sí misma genera también ambas pruebas, así que una transacción probada por uno mismo y una delegada se ven igual en la cadena.
Las ranuras tienen una forma fija, así que a menudo son ficticias: una entrada ficticia es una nota de valor cero cuyo propietario es quien gasta (su nullifier se publica igualmente), una salida ficticia es una nota de importe cero, y la salida de cambio está presente incluso cuando el cambio es cero. Una transacción que se viera distinta cuando no hay cambio revelaría que no lo hubo. Una consecuencia para las wallets: cada uno de los dos grupos de un bundle gasta como máximo dos notas. Un pago en RAND, un bono, un despliegue o una llamada gasta como máximo dos notas de RAND; una transferencia o quema de tokens gasta como máximo dos notas del token y como máximo dos notas de RAND para la comisión. El valor repartido en tres o más notas dentro de un mismo grupo tiene que consolidarse antes, mediante un envío a la propia dirección.
Desde la chain 14 la prueba también queda ligada a su transacción: se genera sobre un hash de la transacción completa con las pruebas en blanco, y se verifica contra él, de modo que no puede copiarse a otra acción, a otros envelopes ni a otra chain; en la chain 20 el hash cubre además el hash de génesis, de modo que tampoco puede copiarse a una chain que comparta el mismo chain id (Transacciones).
Qué es público y qué permanece oculto
| público en la chain | oculto | |
|---|---|---|
| una transferencia, de RAND o de cualquier token RPL | anchor, los cuatro nullifiers, los cuatro compromisos, comisión, burn_a = burn_r = burn_asset = 0, time, cuatro cifrados de envelope, la prueba, auth_commit y la prueba de autorización | quién la envió, a quién se paga, el importe, el memo, qué activo se movió, el cambio, qué hojas se gastaron, qué ranuras eran ficticias |
| una quema de tokens o una quema de bridge | todo lo que publica una transferencia, con burn_asset y burn_a iguales al activo y al importe de la acción (y, en una quema de bridge, la comisión del relayer, el token de respaldo y el destino) | qué notas se quemaron y quién las quemó |
| un mint del faucet | el compromiso, el envelope, el importe, el validator que acuñó y, desde la chain 14, la apertura de la nota (pk, time, r), a partir de la cual la admisión recalcula el compromiso | cada gasto posterior de la nota |
| un depósito de génesis | { cm, envelope, amount, opening: { pk, time, r } } en el archivo de génesis, con asset en la apertura para una nota de un token listado (la génesis de la chain 20 incluye dos notas de zUSD): la pk del propietario, el importe y el activo son públicos por diseño | cuándo y en qué se gasta después |
| un withdraw, un depósito de bridge, un mint de tokens o el mint inicial de un registro | el importe, el destinatario, el time de la nota y el blinding r (la chain calcula el compromiso por sí misma) | cada gasto posterior de la nota |
| el pago de un invoke o el mint de un programa (chain 20) | el importe, el activo, el destinatario y r; el time de la nota es el del bundle | cada gasto posterior de la nota |
| una nota de comisión de bridge (chain 20) | el importe, el activo y time, un blinding derivado, la tesorería de génesis como propietaria, y ningún envelope | cada gasto posterior de la nota |
El valor entra en el pool con un importe público —un mint del faucet, un depósito de génesis, un withdraw de un validator, una atestación de bridge y su nota de comisión, un mint de tokens, el pago de una bóveda de programa— y sale de él como comisión o quema públicas. Todo lo que ocurre entre ambos extremos es una nota.
La tabla recoge lo que ocultan las pruebas de la chain 20, y no siempre fue así. En las chains 14 y 15 la propia prueba del bundle revelaba más (la revisión del zkVM del 2026-09-27, INT-2 / GV-1): el total acumulado de LogUp de cada tabla se publicaba sin blinding, y el guest v1 omitía las comprobaciones de Merkle y de activo para una entrada ficticia y bifurcaba según cada bit del camino de Merkle, de modo que cualquiera podía leer en la prueba de un bundle cuáles de las cuatro ranuras de entrada eran reales y, por tanto, si movía un token o solo RAND, así como el número de bits en 1 de cada índice de hoja gastada. Dos cambios lo cerraron. La chain 16 pasó al conjunto de restricciones 7, que aplica blinding a cada terminal de LogUp y fija un mínimo de 2^7 filas para cada tabla declarada, y al guest de bundle v2, sin bifurcaciones, en el que cada ranura, ficticia o real, ejecuta la comprobación completa de pertenencia y de activo con las mismas instrucciones. El guest v3, que ejecutan las chains 17 a 20, es la relación de v2 con nk y la sal en lugar de la clave de gasto, y tampoco tiene bifurcaciones. Los bundles confirmados en las chains 14 y 15 conservan lo que filtraron. Lo que una prueba sigue mostrando en la chain 20 es su cabecera: para un bundle, una forma fija (tier 14, un límite de gas declarado de 20,479 en cada bundle) y, para un Call o un Invoke, el tier, el límite de gas declarado y si la llamada usó el precompilado de keccak o de sha256 y, aproximadamente, con qué frecuencia (Pruebas).
Notas sin envelope
La mayoría de las notas se encuentran abriendo su envelope. Algunas las calcula la chain a partir de campos públicos y, para ellas, el envelope o bien no existe o bien no es de fiar.
- Notas de comisión de bridge (chain 20). Con
bridge.fees, la chain añade una nota de zUSD a la tesorería de génesis en cada depósito y en cada quema (Tokens RPL). No tiene envelope: el almacenamiento la indexa con uno vacío, sellado para nadie, yrand_getCompactBlocksyrand_getCommitmentsla muestran así, la de un depósito después de la nota del depósito y la de una quema después de las cuatro del bundle. Cada palabra de la nota es pública: el importe, a partir de la transacción y de la tasa de génesis; el activo ytime, a partir de la transacción;from = 0; el blindingblake3("rand-bridge-fee-r-1" ‖ mu)para un depósito oblake3("rand-bridge-burn-fee-r-1" ‖ txid)para una quema; y como propietario, elrecipientde la génesis. - Notas cuyo envelope no liga nada. Un depósito de bridge, un mint de tokens, el mint inicial de un registro y, en la chain 20, los pagos y los mints de programa de un invoke llevan un envelope sellado por quien envió la transacción, pero la chain calcula la nota a partir de los campos públicos de la acción y nunca comprueba el envelope contra ella. Un remitente descuidado u hostil puede sellar datos basura, por lo que un destinatario no debe depender de él.
Por ello, la wallet rand reconstruye estas notas por sí misma (wallet::rebuilt_notes_with), en una pasada pública junto al descifrado de prueba: para cada transacción de este tipo que nombre su propia pk, y para las notas de comisión cuando su dirección es la de la tesorería, calcula la nota y la registra en la hoja cuyo compromiso resulta de su hash. Una nota reconstruida es solo una candidata: un nodo que mienta sobre una transacción, la tasa de comisión o un hash hace que una nota no se encuentre, nunca que se acredite una. Desde v0.6.8, las cabeceras que devuelve rand_getBlocks incluyen en public_notes las transacciones de depósito, mint y registro y, en una chain con comisiones de bridge, también las quemas, sin pruebas y con su id real, de modo que la pasada lee únicamente páginas de cabeceras. Los invokes no están en public_notes en v0.6.8, así que, frente a un nodo v0.6.8, la wallet solo encuentra el pago de un invoke a través de su envelope, que sella quien invoca y que nada comprueba. El main del fullnode también los incluye allí, sin pruebas, desde d0bb73fb; es un cambio que afecta solo al nodo, aún no desplegado en la flota de la chain 20, y la wallet no necesita ningún cambio para ello.
Escaneo: cómo encuentra una wallet sus notas
Un nodo responde «aquí está el árbol completo», nunca «aquí está tu saldo». Una wallet:
- recorre las hojas por páginas con
rand_getCommitments(from_index, limit)(como máximo 1,000 por llamada) o con el flujo por bloquerand_getCompactBlocks(from_height, to_height)(como máximo 128 bloques por llamada); cada transacción que lleva un bundle lista allí cuatro notas y cuatro nullifiers, y ningún lector puede distinguir una ranura ficticia de una real; - descifra de prueba cada envelope: como receptora, mediante su clave de decapsulación ML-KEM; como emisora, mediante
ovk; conserva las notas cuyapkes la suya; y reconstruye las notas de importe público a partir de sus transacciones (arriba); - calcula el nullifier de cada nota conservada y marca las que aparecen en
rand_getNullifiers(from_height, limit); - suma las restantes.
rand_getTreeInfo indica hasta dónde hay que escanear: { "next_index", "root", "nullifiers" }. Para un gasto, la wallet necesita el camino de Merkle de 32 niveles de cada entrada real. La wallet rand mantiene su propia copia del árbol de compromisos, construida durante el escaneo a partir de las mismas páginas de rand_getCommitments, y calcula ella misma todos los caminos, de modo que la única consulta sobre el árbol que un envío hace al nodo es rand_getAnchor, que no nombra ninguna hoja. rand_getWitness(index) permanece en el nodo para otros clientes. Le dice al nodo qué hoja está a punto de gastar quien llama, que era la mayor fuga del pool antes del árbol local: un cliente que tenga que usarlo debería ejecutar su propio nodo, o pedir también testigos que no necesita junto a los que sí necesita.
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
Una nota está pending entre rand send --no-wait y el siguiente sync que vea su nullifier o bloques posteriores a time + window (1,024 en chain 20, leído de rand_getLimits.proof_window_blocks; 256 si no está disponible), tras lo cual el bundle ya no podrá ser admitido y la nota vuelve a poder gastarse.