The node serves JSON-RPC 2.0 over HTTP on --rpc (default 127.0.0.1:8545), and a WebSocket on the same port for subscriptions. Since v0.6.8 it can also open a second, public listener (--public-rpc, below). randprotocol_client::RpcClient (Rust) wraps every method below. This page is the method list as v0.8.0 (c0878c3f), the build chain 21 runs since 2026-10-12, serves it; where a method exists in the code but is inert on chain 21, it says so. What v0.8.0 adds over v0.6.8 (chain 20’s method list) is gathered at the end, What chain 21 and v0.8.0 add.
curl -s http://127.0.0.1:8545 -H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"rand_getHead","params":[]}'
The public endpoint
https://rpc.randprotocol.org (the same proxy is also at https://randprotocol.org/api/rpc) forwards to a chain-21 archive node that keeps every block, for wallets and pages without a node of their own (rand --rpc https://rpc.randprotocol.org …). It is HTTP POST only, with open CORS and no WebSocket, and it is narrower than a node:
- One request per call. A batch is refused with
-32600. - An allowlist of methods: since 2026-10-01 the same set as the node’s own public listener (
rpc::PUBLIC_METHODS), so the program-state readsrand program invokeneeds,rand_getAdmitted, the aggregation reads and the vesting reads are on it, withrand_checkTransactionandrand_sendTransaction; since 2026-10-12 it follows v0.8.0’s set, which addsrand_getScanLeaves(a wallet’s scan by envelope heads),rand_getRandPrice,rand_getMultisigand the four perps reads (rand_getPerps,rand_getPerpAccount,rand_getPerpAccounts,rand_getPerpInputs).rand_mint,rand_importViewingKey,rand_getViewingNotes,rand_removeViewingKeyandrand_getPeersare not, and answer-32601method not allowed herewithout reaching the node: a public faucet is an abuse surface, and a viewing key should only go to a node you run. - A request body of at most 12 MiB, which holds any transfer, token action or deploy and a call whose proof is up to about 3.4 MB (the transaction is hex-encoded, and its bundle and auth proofs take about 2.85 MB before the call’s). A larger call goes to your own node. Over the cap: HTTP 413 and
-32600. - Per address, 1,200 requests a minute, and 6 a minute for
rand_sendTransaction. Over it: HTTP 429 and-32005. The read budget is sized for a wallet’s first sync, which is several hundred requests in one burst (645 at chain 18’s height 99,000). -32000upstream unavailablewhen the node behind it does not answer.
Everything else on this page describes the node itself.
The node’s two listeners
Since v0.6.8 (audit v6) a node has two JSON-RPC listeners.
--rpc <ADDR>, the operator’s listener. Every method, batches and the WebSocket. It identifies a caller by its socket address and trusts loopback: the viewing-key methods answer loopback only, and loopback is not metered. Keep it on127.0.0.1. A reverse proxy or an SSH forward arrives on loopback, so behind one every visitor is the operator: never point a public endpoint at this port.--public-rpc <ADDR>, the public listener, for exactly that hop. It serves a fixed method set, the reads a wallet, an explorer or a page needs andrand_sendTransaction(rpc::PUBLIC_METHODS).rand_importViewingKey,rand_getViewingNotes,rand_removeViewingKey,rand_mintandrand_getPeersare never served there, nor is any method added later until it is listed; such a call answers-32000<method> is not served on this node's public listener. A batch is-32600, and there is no WebSocket route. One meter covers every caller together, 600 requests in a burst and 150 a second; past it, HTTP 429 with a-32000body. Per-visitor limits belong to the proxy, which can see the visitors. Its callers hold at most half of the node’s blocking-read slots, so the operator’s listener is never starved.
Both listeners share two read limits. A blocking read that has not returned within 30 s is answered -32000 this read took longer than 30 s on this node; retry with a smaller range. rand_getBlocks ends its page once it has read 64 MiB of blocks.
--rpc-viewing-token-file <PATH> puts a bearer token on the viewing-key methods of --rpc: the first line of the file, at least 32 characters, sent as Authorization: Bearer <token>. With it, loopback alone is not enough, and a caller anywhere that presents it is served.
Conventions
- Validator addresses are base58 strings (32 bytes). Hashes are 64 hex characters, with or without
0x. Shielded values — commitments, nullifiers, anchors, tree roots, witness levels — areWord8: eight little-endianu32words as 64 lowercase hex characters. - Shielded addresses are
rand1+ base58, 1,667 characters. A parameter longer than 2,000 characters is refused on its length before it is parsed. - Every
u64amount is a decimal string ("1500000000"= 1.5 RAND; 1 RAND = 10⁹ units): RAND units or token units, chain state or a decoded transaction’s own field. A JSON number is not an exact integer past 2⁵³, and a 9-decimal token’s supply passes that at a few tens of millions of units. See the dated note below for what this changed. - Indices, nonces, heights, leaf indices, views, lengths, decimals and day counters (
mint_day) are never amounts and are JSON integers throughout. - Every request must carry an
idmember. An object without one is a notification and is refused with-32600; an explicit"id": nullis a normal request. - There is no balance method and no account method. A wallet computes its balance by scanning
rand_getCommitmentsandrand_getNullifierswith its viewing key.rand_getBalance,rand_getAccountandrand_getAssetBalanceanswer-32601.
Breaking change, 2026-09-20 (chain 14): every amount is a decimal string
Before chain 14, amounts inside a decoded transaction were JSON integers. That exception is gone. These fields changed from JSON numbers to decimal strings, with no other change to their meaning or position:
bundle.fee, inrand_getTransaction’stxand inrand_getBlockByHeight/rand_getBlockByHash’stransactions[];action.amountforkind=mint,bond,unbond,withdraw,bridge_attest(the same three methods);action.amountandaction.relayer_feeforkind=bridge_burn(the same three methods);lockedin the rows ofrand_getAssetsand ofrand_getBridgeState.assets[].
A client that reads any of these as a number must parse a string instead. rand_status’s aggregation.subsidy_base (aggregation chains only) became a string in the same change. Every other amount was already a string: rand_getSupply, rand_getEmission, rand_getValidators, viewing notes, and every field introduced with chain 14 — the bundle’s burn_a and burn_r, the token supplies, each backing’s locked, mint_cap_per_day and minted_today, registration_fee, and the amount of token_mint, token_burn and register_token (initial_amount, initial.amount).
Batches
A POST body may be an array of at most 20 request objects, answered by an array of the same length in request order, errors included. Requests run one after another. An empty array, an array over 20, or a body that is neither an object nor an array is a single -32600 with a null id. The whole body is still bounded by the node’s request-body limit, sized for one proof-carrying transaction: a batch of two proof-carrying rand_sendTransaction calls is refused with 413. Batch reads, not proved submissions.
Chain and node
rand_chainId
Params []. Result: the chain id (integer); 21 on the testnet since 2026-10-12 (20 from 2026-10-01 to then).
rand_tokenInfo
Params []. Result { "symbol": "RAND", "decimals": 9 }.
rand_getHead
Params []. Result { "height": 1998, "hash": "…", "view": 2251 }. view is the node’s current consensus view, which runs ahead of the height when views time out.
rand_status (alias rand_syncStatus)
Params []. Result:
{
"height": 1998, "head_hash": "…", "view": 2251, "high_qc_view": 2250,
"syncing": false, "sync_target": 1998,
"sync_inflight_age_ms": null, "sync_failures": 0, "sync_late_batches": 0, "compact_fetched": 0,
"peer_count": 26, "connected_peers": 26, "reserved_peers": 25, "ws_clients": 0, "refused_cache": 0,
"verify_queue": 0, "mempool_size": 0,
"is_validator": true, "active_validator": true, "faucet": true, "confidential": true,
"testnet": true, "fri_profile": "production", "programs": 10, "viewing_keys": 0,
"notes": 197, "nullifiers": 160, "tree_root": "72b9…1197", "hc_bundle": "60af…3fce", "hc_auth": "1e4e…39c1",
"hc_auth_multisig": "9705…8247", "msauth_key": "loaded", "binding_domain": 1,
"address": "5TAofR…", "peer_id": "12D3KooW…",
"gas_prices": { "gas_price": "100", "byte_price": "800" }
}
is_validator: this node holds a validator key; active_validator: that key is in the set running the current epoch. reserved_peers (v0.6.8) counts the peers admitted past the inbound connection cap: the bootstraps, any --reserved-peer, and every validator identity learned from a signed peer binding, each once; on a fleet node it should reach the validator count within a minute of start. viewing_keys is how many viewing keys the node holds in memory for node-side scanning. verify_queue is the number of transactions waiting for one of the four proof-verification workers (64 deep at most). hc_bundle is the bundle guest this chain’s proofs are against and hc_auth the auth guest its genesis pins (split authorisation, since chain 17), or null on a chain without one; a node whose build disagrees with either refuses to start. hc_auth_multisig (v0.8.0) is the second auth guest, the private-multisig policy (9705f8e2…8247 on chain 21, tier 18), or null, and msauth_key says where the node’s verifier key for it came from: "loaded" (the pinned msauth-vk-production.bin beside the binary, which every chain-21 validator must have), "built" (a development build) or "absent" (a chain without the guest). compact_fetched (v0.8.0, compact blocks) counts the transaction bodies this node has fetched by hash because a leader’s compact proposal named one its mempool did not hold; a count that climbs with every block says the node is missing gossip. testnet and binding_domain are the genesis fields of the same names, as rand_getLimits serves them. gas_prices is the tip ledger’s current gas prices, refreshed at every commit, as decimal strings; null on a chain without a gas section. Chain 21’s prices, like chain 20’s, are dynamic and move per block by fullness, never below the floors of 100 and 800 and never above the ceilings of 10,000 and 80,000, so read them here or in rand_getLimits and do not hard-code them. The object also carries an aggregation object (its subsidy_base a decimal string, the rest integers), which reads all zeros on a chain without an aggregation section, as chain 21 is.
The object also carries disk_free_bytes (a number) and disk_low, and the retention pair: prune_floor, the lowest height this node still holds (0 on an archive), and prune_history_secs, the window a node started with --prune-history keeps (null when it keeps everything). On chain 21, as on chains 19 and 20, 24 of the 26 validators keep 24 hours of blocks; the two archives keep all of them. See Pruned nodes.
rand_getPeers
Params []. Result: an array of { "peer_id": "12D3KooW…", "addrs": ["/ip4/…/tcp/30303"], "connected_secs": 1241 }.
rand_getBlockByHeight / rand_getBlockByHash
Params [height] or [hash]. Result null if unknown, else:
{
"hash": "647b…", "height": 50, "view": 92, "parent": "2d41…",
"proposer": "3v3VBJ…", "timestamp_ms": 1788000123456,
"tx_root": "0000…", "state_root": "a1b2…", "justify_view": 91,
"tx_count": 0, "transactions": [ ]
}
Only committed blocks are served. transactions carries the same shape as rand_getTransaction’s tx. A block also carries sealed, and each of its transactions sealed_by; they read false and null on a chain without aggregation.
rand_getBlocks
Params [from_height, to_height]. Result: block headers, oldest first, at most 1,024 from from_height (128 on a node before 2026-09-24): the fields of rand_getBlockByHeight without transactions. A range wider than the cap, or past the head, is truncated, not refused, and since v0.6.8 a page also ends once it has read 64 MiB of blocks; advance from the last height returned. -32602 for to_height below from_height.
Since v0.6.8 every header also carries public_notes: the block’s bridge_attest, token_mint and register_token transactions, and on a chain with bridge fees its bridge_burn transactions, as [{ "hash", "raw" }] (the encoding rand_getRawTransaction serves); empty for every other block. A burn is served with its proofs emptied and "proofs_stripped": true; its hash is the real transaction id. A wallet’s first sync reads these pages and needs nothing else to rebuild the public notes.
rand_getFinality
Params [height] or [hash]. Result, one of { "status": "committed", "height", "hash" }, { "status": "certified", "height", "hash", "qc_view" }, { "status": "proposed", "height", "hash" }, { "status": "unknown" }. A height is only ever checked against the committed chain, so it answers committed or unknown; a hash can also read certified (in the replica’s uncommitted tree with a quorum certificate) or proposed (in the tree without one).
rand_getProposer
Params [view] or [from_view, to_view], at most 64 views. Result { "epoch": 3, "proposers": [ { "view": 2251, "proposer": "2nRdFC…" } ] }: the leader of each view under the current validator set, not the set that ran at that view historically. -32602 for a backwards range or more than 64 views.
rand_getVersion
Params []. Result { "version", "git_sha", "chain_id", "hc_bundle", "hc_auth", "fri_profile" }. version is the workspace crate version; git_sha the commit hash captured at build time, with -dirty appended when tracked files differed, or "unknown". hc_auth is the genesis auth guest, as in rand_status, or null. The public endpoint on 2026-10-12, on chain 21:
{ "version": "0.8.0", "git_sha": "c0878c3fbe5659cf91415d27b0e74d1282f0efef", "chain_id": 21,
"hc_bundle": "60af094acfe65d85fdb18fb3d06cf9085dcf28c96e59e87f1ee527226e6e3fce",
"hc_auth": "1e4e347f44cf86750b30a9a4bdf9ec9256efe353d4ff8017451eca7d195639c1",
"fri_profile": "production" }
rand_getGenesisHash
Params []. Result: the genesis hash as hex; "ca76c2766ed6090ac6ba5dad86b724dd5d2b80c0edfaed287cd8b129e663ca9a" on chain 21 (chain 20’s was 6210cf07…5135, chain 19’s a3defc93…228a). A chain id alone does not tell two chain cuts apart; this does. Since chain 20 it is also part of every transaction binding and every signed action message (binding_domain: 1, see rand_getLimits).
rand_getHealth
Params []. Result, one of { "status": "disk_low", "free_bytes": "3221225472" }, { "status": "ok" }, { "status": "syncing", "behind": 412 }, { "status": "behind", "behind": 30 }. disk_low comes ahead of everything else, when free space on the data directory’s filesystem is under four times the node’s --min-free-disk-mb (default 1024, so under 4 GB); free_bytes is a decimal string. ok when sync_target - height is at most 2 and no sync batch request is outstanding; syncing while one is; behind when the node is not syncing and the lag is still above 2.
rand_getMempoolInfo
Params []. Result { "count": 2, "bytes": 2611200, "oldest_ms": 1450, "max_count": 10000 }. bytes sums every pooled transaction’s encoded length; oldest_ms is null when the pool is empty. Since v0.6.8 the pool is also capped in bytes, at eight times the genesis max_block_bytes (160 MiB on chain 20, 152 MiB on chain 21): past it, a transaction paying more above its floor per KiB displaces the cheapest, and one paying less or the same is refused mempool full. Governance actions pass both caps.
Submitting
rand_sendTransaction
Params [hex], where hex is bincode(Transaction). Result: the transaction hash. The node validates in the admission order (Transactions), pools and gossips it. Errors are -32000 with the reason: nullifier already spent, anchor is not one of the last 1024 roots, time 12 is outside [1500, 2524] (both windows are the genesis proof_window_blocks, 1,024 on chains 20 and 21; 256 on chains through 19), fee 1000000 below minimum 2000000, the bundle's digest is not what its proof published, invalid bundle proof: …, unknown program …, already in mempool, conflicts with a pending transaction over <nullifier>, faucet is disabled on this chain; for an invoke, cell <key> is no longer what this transition read (a state verdict: re-read the cell and prove again) or the vault holds … of asset … and the transition pays …; for a registering Bond under admission by vote, validator … has not been admitted by the validator set's vote (staking.admission_by_vote); ask the validators for an AdmitValidator first; for a bundle that does not fit the genesis (chains 20 and 21), envelope is 1348 bytes, this chain requires exactly 1860, this chain requires an auth proof on every bundle (genesis hc_auth), the auth proof publishes a different commitment than the bundle's auth_commit, auth proof: …, bundle proof declares gas limit …, this chain requires exactly 20479, non-canonical proof: … — re-prove with an up-to-date wallet; for the bridge actions bridge: attestation already consumed, the attestation names a different recipient, the attestation deposits under asset 2, and the transaction names 1, the bundle burns 399, not the 400 the action declares; for a bundle carrying a burn its action may not carry, the bundle burns asset 2 on an action that burns no token, a burn of 5 is not allowed on this action or RAND burned through burn_a (5); a RAND burn goes through burn_r; for the bridge’s state, bridge minting is paused, mint cap … per backing per day, already paused, not paused, wrong pause nonce, wrong list nonce, chain N has no registered emitter; for the token registry, wrong mint nonce and wrong token index. Acceptance is not commitment: poll rand_getTransaction or rand_getTransactionStatus.
A transaction larger than the chain’s block cap (max_block_bytes from rand_getLimits) is refused with -32000, naming both sizes, before it reaches the mempool. The request body is capped at 2 × (2 × max_proof_bytes + 4 × 2048 + max_call_envelope_bytes + 16,384 + 64 KiB) + 256 KiB, computed from the genesis at startup: 8,867,840 bytes on a default chain and 17,350,656 at chain 20’s limits, the same as chains 17 to 19 (34,127,872 on chains 13 to 15, whose proof cap was 8 MiB). On a genesis with hc_auth_multisig, as chain 21’s, the first term counts three proofs at the cap (the bundle proof, the auth proof and the larger multisig auth proof), so with chain 21’s 6 MiB max_proof_bytes the body cap is wider again. 4 × 2048 is the bundle’s four envelopes at the envelope cap. A body over it is -32600, naming the limit.
Only -32000 (admission refused it) and -32602 (it could not be decoded) are a verdict on the transaction. Any other failure, -32603 included, leaves its fate unknown: the node pools and gossips a transaction before it replies, so a node that stops in that window can have admitted what it never answered for. randprotocol_client treats a send as refused on those two codes only.
rand_getTransactionStatus
Params [[hash, …]], 1 to 64 hashes. Result: one entry per hash, in order:
[ { "hash": "…", "status": "committed", "height": 1998, "index": 0 },
{ "hash": "…", "status": "pending" },
{ "hash": "…", "status": "rejected", "reason": "bad mint signature" },
{ "hash": "…", "status": "unknown" } ]
Lookup order per hash: committed (storage), pending (the mempool), rejected (the refused cache that rand_status’s refused_cache counts), else unknown. rejected covers only refusals about the transaction’s own bytes, which no later block can change: an invalid proof, a bundle digest that is not what its proof published, a bad mint signature, the wrong chain id, an oversized part, a repeated nullifier or commitment inside the transaction, an envelope of the wrong size, a missing or mismatched auth proof, a bundle or auth proof that declares the wrong gas limit. A refusal that depends on the node’s state at that moment — a spent nullifier, an anchor or time outside the window, a fee below the floor (which since chain 20 moves with the gas prices), an unknown program, a register nonce, a stale invoke read — is not remembered and reads unknown. -32602 for an empty list or more than 64 hashes.
rand_mint (testnet faucet)
Params [address] or [address, amount]; address a rand1… address, amount a string of units at most 100000000000 (100 RAND, the default). Result: the mint transaction hash. Only on a chain whose genesis has "faucet": true; otherwise -32000 faucet is disabled on this chain. An observer answers faucet mints are signed by validators; ask a validator node.
On a chain whose genesis sets staking.faucet_recipients, as chain 20’s did and chain 21’s does (16 wallets), only the listed wallets can be paid: a mint to any other address is refused with the faucet may not mint to this recipient (not in staking.faucet_recipients). The genesis (chain 20’s and chain 21’s alike) also sets staking.faucet_minters (18 validator keys): only a listed key may sign a mint. The faucet also has a per-epoch budget, staking.faucet_budget_per_epoch (10,000 RAND per 1,000-block epoch on chains 20 and 21); a mint past it is refused FaucetBudgetExhausted, a state verdict that reads unknown once it leaves the pool.
The faucet is rate limited per node process: eight mints back to back, refilling at one a second. Past that the call answers faucet is rate limited on this node (8 mints back to back, refilling at 1/s); try again shortly. A faucet flood therefore cannot fill the pool ahead of a bridge PauseMints, which, like the other three bridge-governance actions, is also exempt from the pool’s mempool full refusal and ordered first in a block.
Since chain 14 a mint is opened: the Mint action carries the note’s pk, time and r beside cm and amount, admission recomputes the commitment and refuses a cm that does not open to them (the mint's commitment does not open to its published note and amount), and the minter signs all seven fields under the domain rand-mint-2; on chain 20 (binding_domain: 1) under rand-mint-3, with the genesis hash first. A faucet note’s owner and amount are therefore public, as a withdraw’s payout is. rand_mint’s parameters are unchanged.
rand_estimateFee
Params [spec], one of {"kind":"bundle"}, {"kind":"deploy","words":n,"public_words":m}, {"kind":"call","tier":t,"bytes":b,"gas":g} with t in 10, 12, 14, 16, 18, 20. Result: the minimum fee in units, as a string. {"kind":"bundle"} is "1000000". public_words (optional, default 0) is paid per word like code, so the fee is the deploy fee of n + m words. bytes (optional, default 0) is the call’s proof length plus its input envelope’s length.
How a call is priced depends on rand_getLimits’s gas_metering:
"circuit", a chain with its owngassection, as chains 20 and 21 are.gasis the gas limit the call’s proof declares, and it is required: without it the call answers-32602under the gas section a call estimate needs its gas. The answer is1,000,000 + gas_price × gas + byte_price × ⌈bytes / 1024⌉at the tip’s current prices, the onesrand_getLimitsserves. At the floor prices of 100 and 800 (chains 20 and 21), a call declaring 20,479 gas with 1,300,000 bytes of proof and envelope is"4063900". The prices move per block, so estimate close to sending."header", a node-level gas policy on a chain without the section. The call spec takeskeccak_log_heightandsha256_log_height(optional, default 0, each in 0 to 40, else-32602), the hash-table heights the proof header declares, and the answer is the greater of the ledger’s tier floor and the gas floor of that header. Under agassection the two heights are accepted and unused.null, neither. The ledger’s tier schedule alone: at or under the free allowance (2,097,152 + 18,432 bytes) a call costs its tier’s fee; each KiB over it, a partial KiB counting as whole, adds 1,000 units.
Since v0.6.8, {"kind":"invoke", …, "created_cells": c} prices an RPL-2 invoke: every field of a call spec, plus created_cells (optional, default 0, at most max_writes, else -32602), the cells the transition would create. Each adds the chain’s cell_fee (rand_getLimits.program_state). The caller counts them: a write of a non-zero value to a cell that rand_getProgramCell reads as zeros. On chain 20, the call above with two created cells is "24063900". On a chain without a program_state section the kind is -32602.
Fees has the schedule.
The pool
rand_getTreeInfo
Params []. Result { "next_index": 41, "root": "6b1d…c4", "nullifiers": 12 }: the leaf count, the current root, how many notes have been spent.
rand_getCommitments
Params [from_index] or [from_index, limit]. Result: a page of leaves from from_index, oldest first, at most 1,000 rows however large limit is.
[ { "index": 40, "cm": "2a9f…07", "height": 37,
"envelope": { "kem_ct": "b41c…", "to_receiver": "77e0…", "to_sender": "0c31…", "body": "9dd2…" } } ]
rand_getScanLeaves
v0.8.0. Params [from_index] or [from_index, limit]. Result: the same page as rand_getCommitments, same order and the same 1,000-row cap, with each envelope cut to its head: scan is kem_ct ‖ to_receiver ‖ to_sender, standard base64 with padding, 1,208 bytes for every envelope a wallet seals and empty for a chain-made note with an empty envelope.
[ { "index": 40, "cm": "2a9f…07", "height": 37, "scan": "tBzc…" } ]
The head is all a scan needs to decide ownership: decapsulating kem_ct and opening to_receiver (or to_sender under the outgoing viewing key) succeeds only for the right key. The v0.8.0 rand wallet pages heads, then reads with rand_getCommitments only the leaves one of its keys opened, and any head that is not 1,208 bytes: on chain 20’s first 1,000 leaves a head row was 1,718 bytes of JSON against a full row’s 3,876. A node too old to have the method answers -32601, and the wallet pages rand_getCommitments as before. On the public listener, and forwarded by the public endpoint since 2026-10-12.
rand_getNullifiers
Params [from_height] or [from_height, limit]. Result: every nullifier published from that height onwards, same 1,000-row cap. A page can stop inside a height; page back to the highest height seen.
[ { "height": 37, "nullifier": "8c04…d1" }, { "height": 41, "nullifier": "12be…9a" } ]
rand_getCompactBlocks
Params [from_height, to_height], both required. Result: one row per block, oldest first, with what a light wallet needs and nothing else:
[ { "height": 192, "hash": "63f6…08", "timestamp_ms": 1788000123456,
"commitments": [],
"transactions": [
{ "hash": "4f2c…e7",
"commitments": [ { "index": 40, "cm": "2a9f…07", "envelope": { "kem_ct": "…", "to_receiver": "…", "to_sender": "…", "body": "…" } } ],
"nullifiers": ["8c04…d1", "5e77…20", "03aa…6f", "e19b…42"] } ] } ]
Every bundle-carrying transaction owns four notes and four nullifiers here — its four slots, the dummy slots included, which no reader can tell from real ones (only the example’s first commitment row is shown). Since v0.6.8 some own more notes, after those four: an invoke owns one note per payout, pays then mints, each with the payout’s own envelope, so a payee finds it by trial decryption; on a chain with bridge fees, as chains 20 and 21 are, a bridge_burn owns its treasury fee note, and a bridge_attest owns its fee note after its deposit. A fee note’s envelope is empty, sealed to nobody: the treasury wallet rebuilds the note from the transaction’s public fields.
At most 128 blocks per call (a wider range is clamped) and, past the first block — always served whole — 1,000 notes; resume from the last returned height plus one. The block-level commitments array holds the genesis deposits at height 0 and is empty elsewhere; a Withdraw’s and a BridgeAttest’s derived notes appear under their transaction. -32602 for a backwards range or a missing bound.
rand_getAnchor
Params [] for the head, or [height]. Result { "height": 192, "root": "6b1d…c4" }, or -32001 for a height with no recorded anchor. Only block-end roots are anchors. The node may answer for an old height, but a bundle’s anchor is admissible only if it is one of the last proof_window_blocks roots: 1,024 on chains 20 and 21, 256 on chains through 19. Build against the head.
rand_getWitness
Params [index]. Result null past the end of the tree, else the Merkle path of that leaf, leaf-first, exactly 32 levels, with the tree’s current root:
{ "index": 40, "root": "6b1d…c4", "path": ["0000…00", "f2a1…3b", "…"] }
The most expensive read the node serves (it rebuilds the full depth-32 tree) and the one request that discloses something about the caller. A node runs at most two tree rebuilds at a time, rand_getWitness and rand_getWitnesses together; a request that waits more than 10 s for a slot is refused with -32000 witness builds are busy on this node; retry shortly.
rand_getWitnesses
Params [[index, …]], 1 to 32 leaf indices. Result { "root": "…", "witnesses": [ { "index": 7, "path": ["…"] }, { "index": 9, "path": null } ] }: rand_getWitness folded over many leaves in one tree build, so a wallet proving several notes pays for the rebuild once. An index past the end of the tree reads path: null for that entry. -32602 for an empty list or more than 32 indices.
Viewing keys and disclosure
rand_importViewingKey
Params [viewing_key] or [viewing_key, rescan_from_height], the key as 64 hex characters. Result { "imported": true, "rescan_from_height": 0, "viewing_keys": 3 }. The node holds the key in memory only: at most 64 per node, cleared at restart; a key already held is a no-op ("imported": false). Import never scans; the scan is driven by the next method. -32602 for a malformed key; -32000 at the 65th key.
The three viewing-key methods answer a loopback caller only and refuse anyone else with -32000, unless the node runs with --rpc-viewing-open behind something that authenticates. rand_removeViewingKey ([viewing_key] → { "removed": bool, "viewing_keys": n }) frees a slot and zeroises the key. The registry is keyed by a one-way id of the key, blake3("rand-viewing-registry-id-1" ‖ nk), never by the key itself, so the import is the only long-lived copy and removal wipes it. The request’s own JSON text and the transient copies trial decryption makes are not wiped.
rand_getViewingNotes
Params [viewing_key] or [viewing_key, from_index, limit] (limit default and maximum 1,000). Each call advances the key’s scan by up to 10,000 leaves, then serves the page:
{ "scanned_index": 10041, "next_index": 10041, "complete": true,
"notes": [
{ "index": 40, "cm": "2a9f…07", "height": 37, "role": "received",
"note": { "pk": "…", "from": "…", "amount": "1500000000", "asset": 0, "time": 5, "memo": "coffee" },
"nullifier": "8c04…d1", "spent": false },
{ "index": 43, "cm": "b310…88", "height": 39, "role": "sent",
"note": { "pk": "…", "from": "…", "amount": "25000000", "asset": 0, "time": 9, "memo": null },
"nullifier": null, "spent": null } ] }
role is received (the envelope opened as receiver and the note names the key’s pk) or sent (opened through the outgoing viewing key). The note’s memo is the sender’s encrypted memo, UTF-8 text of at most 510 bytes, or null when there is none or its field opened malformed; a malformed memo never costs the payee the note. A memo is anyone’s text: show it as untrusted. -32001 for a key this node has not imported.
rand_checkTransaction
Params [hash, key], key a per-transaction TxKey as 64 hex characters. Result null for an unknown hash, else what the key discloses:
{ "tx": "4f2c…e7", "height": 192,
"disclosed": [ { "output": "bundle:0", "cm": "2a9f…07", "index": 40,
"note": { "pk": "…", "from": "…", "amount": "1500000000", "asset": 0, "time": 5, "memo": "invoice 7" } } ] }
Stateless: the key is dropped with the call. output is bundle:0 … bundle:3 for the bundle’s four output slots (slots 0–1 the private-asset outputs, slots 2–3 the RAND outputs, dummies included), deposit for a bridge attestation’s deposit envelope, token_mint for a TokenMint’s note, initial_mint for a RegisterToken’s initial mint, mint:0 for a faucet mint’s one envelope, or (v0.6.8) payout:0 … payout:3 for the notes an invoke paid out, numbered by their place in the transition, pays then mints; asset_bundle:* is gone with the second bundle. On a chain with bridge fees, deposit opens the net deposit, the gross less the fee. The disclosed note carries its asset: this call, with the key the sender sealed under, is the one place the RPC reveals which asset a transfer moved. It carries the memo too, as rand_getViewingNotes does: the key that opens the note opens the memo, from the same sealed body. A wrong key returns { "disclosed": [] }, indistinguishable from a transaction that discloses nothing. A withdraw’s and a genesis alloc’s envelopes are not tried: they are sealed inside the node under keys dropped at once, so no TxKey for them exists. A faucet mint’s is tried: its recipient recovers the key through the envelope’s KEM half (rand tx-key).
Transactions
rand_getTransaction
Params [hash]. Result null until committed, then:
{
"height": 192, "index": 0, "block_hash": "63f6…08",
"tx": {
"hash": "4f2c…e7", "chain_id": 21,
"bundle": { "anchor": "6b1d…c4",
"nullifiers": ["8c04…d1", "5e77…20", "03aa…6f", "e19b…42"],
"commitments": ["2a9f…07", "b310…88", "77c1…0e", "5d20…b3"],
"fee": "1000000", "burn_a": "0", "burn_r": "0", "burn_asset": 0, "time": 5,
"proof_len": 1490435, "envelope_len": [1860, 1860, 1860, 1860],
"auth_commit": "9217…ee9e", "auth_proof_bytes": 1361161 },
"action": { "kind": "none" }
}
}
bundle is null on the bundle-less actions. The proofs and envelopes are reported by length only. envelope_len is 1,860 for every slot on chains 20 and 21, whose genesis fixes the note envelope at that size (envelope_bytes in rand_getLimits); it was 1,348 on chains 14 to 17. auth_commit and auth_proof_bytes are split authorisation’s (since chain 17): the public commitment c = H(AUTH, nk, salt) that the bundle proof and the auth proof both publish, and the auth proof’s length. On a chain without hc_auth they are zeros and 0.
bundle is the hidden-asset bundle (since chain 14): four input slots and four output slots, so always four nullifiers, four commitments and four envelope_len, dummies included. Every transaction carries at most one; there is no second bundle. It has no asset field: slots 0–1 carry a private asset and slots 2–3 RAND, and nothing public says which asset slots 0–1 moved. A transfer of RAND and a transfer of any RPL token are both "kind": "none" with burn_a, burn_r and burn_asset all 0 — the same shape, field for field. There is no token_transfer kind. The three burn fields are the bundle’s only public statement about value leaving the pool:
burn_a/burn_asset— an amount of the private asset burned, and which asset. Non-zero on atoken_burnor abridge_burn, where they equal the action’samountandasset, and on aninvokewhoseinflowisdepositorburn.burn_r— RAND burned. Non-zero on abond(equal to itsamount), aregister_aggregator, and aninvokethat deposits RAND into the program’s vault.fee— the RAND fee, always from slots 2–3.
Action renderings (every amount, relayer_fee and initial_amount a decimal string; a token’s amounts are in its own units, not RAND’s):
| kind | fields |
|---|---|
none | — |
mint | cm, amount, minter (validator base58) |
deploy | program, words, public_words_len |
call | program, proof_len, input_envelope_len (null when the call carries none) |
bond | validator, amount, registered |
unbond | validator, amount, nonce |
withdraw | validator, amount, nonce, time |
bridge_attest | attestation_len, recipient, asset, asset_index, amount, time, r, commitment, pq_signers (the co-signing guardians’ indices into pq_guardians; the 2,420-byte signatures are not rendered). asset_index, amount and commitment are null for a guardian-set rotation. r is derived, blake3("rand-deposit-r-1" ‖ mu) over the digest the guardians signed. Since v0.6.8 also deposit_amount (the depositor’s note: amount, the gross the guardians signed, less the bridge fee) and fee_note ({ amount, asset, time, r, commitment }, the treasury’s note without its owner, or null without bridge fees); commitment is the net deposit’s leaf |
bridge_burn | asset, amount, relayer_fee, to_chain, token (the backing being redeemed), to. One bundle carries the whole burn: its burn_asset and burn_a are the action’s asset and amount. Since v0.6.8 also release_amount (what the signed body releases, amount less the bridge fee) and fee_note, as on an attest |
register_token | name, symbol, decimals, authority ("none", "key", "bridge" or "program"), index, initial_amount, initial ({ amount, recipient, time, r } or null) |
token_mint | asset, amount, recipient, time, r, nonce |
set_authority | asset, nonce, new_authority (base58, or null for a renunciation) |
token_burn | asset, amount; the bundle’s burn_asset / burn_a repeat them |
pause_mints | nonce; bundle-less and fee-less, signed by the genesis pause key |
unpause_mints | nonce, pq_signers; bundle-less and fee-less |
register_bridged_token | name, symbol, salt, chain, token, decimals (the first backing’s source decimals), nonce, asset_id, pq_signers |
list_backing | token_index, chain, token, decimals, nonce, pq_signers |
rotate_pq_guardians | new_pq_guardians, nonce (the rotation_nonce spent), pq_signers (indices into the PQ set before the rotation); bundle-less and fee-less, bridge rules_v2 chains only |
rotate_pause_key | new_pause_key, nonce, pq_signers; the same |
rotate_pq_guardians_v2, rotate_pause_key_v2 | v0.6.8: the v1 fields, plus possession_signatures (a count) on the PQ form. Bundle-less and fee-less; required on a chain whose genesis bridge.rotation sets needs_possession, as chain 20’s did and chain 21’s does. The rotation takes effect delay_secs later (86,400 s on both) |
cancel_rotation | v0.6.8: rotation_kind ("pq_guardians" or "pause_key"), nonce; signed by the current pause key, drops a pending rotation |
admit_validator | v0.6.8: candidate (address), candidate_key (hex), voters (addresses). Bundle-less and fee-less: the validator set’s vote to let a new key register, on a chain with staking.admission_by_vote, as chains 20 and 21 are |
slash_equivocation | v0.6.8: offender, view, first and second ({ hash, height }); chains with staking.slashing only, which chains 20 and 21 are not |
invoke | v0.6.8, RPL-2: a call’s fields (program, proof_len, input_envelope_len) and its transition (below). Chains with a program_state section only, as chains 20 and 21 are |
perp_deposit, perp_order, perp_cancel, perp_withdraw, perp_oracle, perp_state_proof | v0.8.0, RPL-3 (chain 21, whose genesis has a perps section; refused UnsupportedAction without one). perp_deposit: trading_key, account (the bundle’s burn_r, or burn_a of burn_asset, is the collateral — zUSD on chain 21). perp_order: account and a nested body (nonce, market, side 0 buy / 1 sell, kind 0 limit / 1 market, tif 0 GTC / 1 IOC, reduce_only, price, size). perp_cancel: account, nonce, target. perp_withdraw: account, nonce, amount, recipient, time. perp_oracle: validator, prices ({ market, price }), nonce. perp_state_proof: from_height, to_height, new_root, payouts ({ request, amount }), fees, proof_bytes. The orders, cancels, withdrawals and oracle prices are signed, bundle-less and fee-less; the state proof is unsigned, so anyone may prove it. Every field is public by design: the engine’s inputs are what its prover replays |
create_multisig, multisig_deposit, multisig_pay, multisig_rotate | v0.8.0, multisig accounts (chain 21, whose genesis has a multisig section; refused UnsupportedAction("multisig") without one). create_multisig: salt, threshold, signer_count (bundle-carried; its burn_r is the 1 RAND create_fee plus any first deposit). multisig_deposit: account (the bundle’s burn is the deposit). multisig_pay: account, nonce, time, pays (each note’s cm, as an invoke’s), signers (the indices that signed); bundle-less, pays the 0.001 RAND base from the account’s RAND row. multisig_rotate: account, nonce, threshold, signer_count, signed_by; bundle-less and fee-less. A block header carries a multisig_pay’s notes in public_notes |
set_rand_price | v0.8.0: price_micros_per_rand (a decimal string), nonce, voters (addresses, in listed order). The validator set’s RAND/USD vote for the dollar-indexed sealing subsidy, bundle-less and fee-less; chains with fees.usd_subsidy only, which chain 21 is not, so it is refused UnsupportedAction there |
register_aggregator, unbond_aggregator, withdraw_aggregator, slash_aggregator, aggregate | their public fields; aggregation chains only |
claim_vested, revoke_vesting, bond_vested, unbond_vested | entry (hex), amount (unvested on a revoke), nonce; time on a claim or a revoke; validator and registers on a bond; since v0.6.8 signers on a revoke (the positions of its signers in the entry’s revokers). Bundle-less; chains with a genesis vesting section only, which chains 20 and 21 are not |
A token’s registration and mints are public, as a bridge deposit is: every word of a minted note is in the action (its from is the chain’s fixed MINT_FROM), so a recipient rebuilds the note from these fields with nothing decrypted. The kind strings bridge_attest, token_mint, register_token and invoke are what the wallet’s scan keys on and are pinned by a test.
An invoke at height 3,079 of chain 20 (the chain-20 example programs are not yet redeployed on chain 21), which read one cell, deleted it, and paid two notes out of the vault:
{ "kind": "invoke", "program": "aaaf…4b21", "proof_len": 1433874, "input_envelope_len": null,
"transition": {
"reads": [{ "key": "0100…fa98", "value": "0000000004000000…00000000" }],
"writes": [{ "key": "0100…fa98", "value": "0000000000000000000000000000000000000000000000000000000000000000" }],
"inflow": "none",
"pays": [{ "asset": 0, "amount": "1000000000", "recipient": "rand1…", "time": 3027, "r": "87e6…9a67", "cm": "2555…6487" },
{ "asset": 4, "amount": "2000000000", "recipient": "rand1…", "time": 3027, "r": "b833…f390", "cm": "2b70…f204" }],
"mints": [] } }
All of it is public by design. reads are the cells the program read, with the values it read: the proof was made against them, and the ledger refuses the transaction unless they still hold. writes are the cells it wrote; a written value of 64 zeros deletes the cell. A cell key or value is a Word8. What came in is the bundle’s: burn_r is RAND deposited into the program’s vault, and burn_a of burn_asset is the token that inflow names, deposit into the vault or burn (the program’s own token), none when burn_a is 0. What went out is one chain-computed note per payout, pays out of the vault, then mints of a token whose mint authority is this program. Every word of each note is here under a token_mint’s field names; time is the bundle’s, cm the leaf the chain appended, and the note’s from is the chain’s fixed PROGRAM_FROM. The program’s eight output words are on the receipt, as a call’s are.
No transfer reply carries a sender, recipient, nonce, amount or asset: no such field exists in a stored transfer.
rand_getRawTransaction
Params [hash]. Result: the full committed transaction as bincode hex, proof bytes included — what an aggregator needs and rand_getTransaction never renders.
Programs and receipts
rand_getProgram
Params [program_id]. Result null or { "id", "base_pc", "words_len", "code_hash", "deployed_at", "public_words_len", "public_digest" }. No deployer field: a deploy is paid by a bundle. public_words_len is the length of the program’s deploy-time public input (0 without one); public_digest is its digest, the value every call’s proof is checked against, or null without one.
rand_getProgramCode
Params [program_id]. Result null or { "base_pc": 0, "words": [ … ] }.
rand_getProgramPublic
Params [program_id]. Result: the program’s deploy-time public words as one hex string, each word as its 4 little-endian bytes (8 hex digits a word): [1, 2, 0xdeadbeef] is "0100000002000000efbeadde". "" for a program deployed without a public input, null for an id no program has. A wallet proving a call passes these words to the prover; the ledger checks the proof’s commitment against the program’s public_digest.
rand_getProgramCell
v0.6.8, RPL-2. Params [program_id, key], the key a Word8 as 64 hex (with or without 0x). Result { "key": "<64 hex>", "value": "<64 hex>" }. A cell the program never wrote, or wrote zeros to (which deletes it), reads as 64 zeros, the one encoding of “absent”; so does any key of a program nobody deployed, since the program’s existence is not checked. { "enabled": false } on a chain without a program_state section. A cell is public by design: this is what a wallet reads to build an invoke’s reads.
rand_getProgramCells
v0.6.8. Params [program_id, { "after": "<64 hex>" | null, "limit": n }], the object optional. Result { "cells": [{ "key", "value" }, …], "next": "<64 hex>" | null }: the program’s cells in key order (an unsigned comparison of the eight words), starting after after, at most limit of them (clamped to 1 to 1,000). next is the last key served when more follow; pass it back as after. It is null on the last page. A program with no cell answers { "cells": [], "next": null }; { "enabled": false } without the section.
rand_getProgramVault
v0.6.8. Params [program_id]. Result [{ "asset": 0, "amount": "123" }, …], ascending by asset index (0 is RAND, every other index the token registry’s), amounts as decimal strings in the asset’s own units. A row at zero does not exist, so an empty vault, and a program nobody deployed, is []. { "enabled": false } without the section. Value enters a vault through an invoke’s bundle (burn_r, and burn_a of burn_asset when the inflow is deposit) and leaves it as the notes the transition’s pays name.
rand_getPerps
v0.8.0, RPL-3 (perps). Params []. Result: the exchange’s public state as of the head, or { "enabled": false } and nothing else on a chain without a perps section. On chain 21 (2026-10-12, before any account existed):
{ "enabled": true,
"collateral_asset": 1, "max_tier": 18, "max_window_blocks": 64,
"max_block_inputs": 24, "min_deposit": "100000000",
"engine_hc": "f286a61171e274278ec8d18d7853e5f36799ed8dc5a3e31e0077ad89ef1d1573",
"genesis_root": "da0e3a53d93e50ec03fa40c6d2bd3dad8b9c7b4f542175e86a1dea567cbb9846",
"markets": [{ "id": 0, "symbol": "BTC-PERP", "lot": 100000, "tick": 100000000,
"max_leverage": 20, "maintenance_bps": 250, "taker_fee_bps": 5, "maker_fee_bps": 2 }],
"proved_root": "…", "proved_height": 0, "head_height": 2178,
"pending_heights": [1, 2, …, 2178],
"medians": [{ "market": 0, "price": "0" }],
"accounts": 0, "pending_withdrawals": 0 }
engine_hc is the digest of the matching engine the state proofs run, genesis_root the state root the cut derived, and the three commitments are 64 lowercase hex characters. pending_heights are the closed heights no state proof has covered yet, ascending: the window the next proof is drawn from. A median is a decimal string in the collateral’s units (zUSD, eight decimals: the tick of 100,000,000 is one dollar a BTC), "0" while the last close had no quorum of fresh oracle stake behind a price. This is what the perp prover and the trading venue poll. On the public listener and the public endpoint.
rand_getPerpAccount
v0.8.0. Params [account], the id as 64 hex. Result one trading account, { "account", "trading_key" (the Dilithium2 public key, hex), "nonce_high", "used" (the 64-bit nonce bitmap, a decimal string), "withdrawals": [{ "request", "amount", "height" }] }: a wallet’s next nonce is nonce_high + 1, and withdrawals are the pending requests in request order with the height each was admitted in. The account’s collateral and positions are the engine’s state, not the ledger’s, and are not served. null for an id the exchange does not hold, and on a chain without the section.
rand_getPerpAccounts
v0.8.0. Params [{ "after": "<64 hex>" | null, "limit": n }], the object optional. Result { "accounts": [<as rand_getPerpAccount>, …], "next": "<64 hex>" | null }, the accounts in id order starting after after, at most limit (clamped to 1,000). The exchange holds at most 15 accounts, so one page is all of them; { "accounts": [], "next": null } without the section.
rand_getPerpInputs
v0.8.0. Params [height]. Result { "height", "digest", "words": [<u32>, …] }, a closed block’s perp input words — what a prover runs the engine over — and the digest the ledger recorded for the height; a prover refuses a window whose digests do not match its own. null for a height this node holds no row for: a state proof has covered it (the row is pruned with the proof), it is above the head, or the chain has no perps section.
rand_getLimits
Params []. Result: what a wallet needs from the chain’s genesis to build a transaction: the five call limits, the note-envelope size, the v0.6 switch, the auth guest, the gas terms, since v0.6.8 the audit-v6 genesis fields and the program-state limits, and since v0.8.0 the multisig auth guest, the fee rules, the perps limits and the multisig limits. On chain 21 (2026-10-12):
{ "max_program_words": 65535, "max_proof_bytes": 6291456, "max_block_bytes": 19922944,
"max_call_envelope_bytes": 65536, "max_program_public_words": 32768,
"envelope_bytes": 1860, "hardening_v6": true,
"hc_auth": "1e4e347f44cf86750b30a9a4bdf9ec9256efe353d4ff8017451eca7d195639c1",
"hc_auth_multisig": "9705f8e2f1bc8561848b29cb8efd57c37e3e39788fa58a5f03304d8019668247",
"gas_metering": "circuit", "gas_price": "100", "byte_price": "800",
"bundle_gas_limit": 20479, "adjust_bps": 1250,
"max_gas_price": "10000", "max_byte_price": "80000", "byte_load": "paying",
"admission_by_vote": true, "testnet": true, "slashing": null, "binding_domain": 1,
"proof_window_blocks": 1024,
"program_state": { "cell_fee": "10000000", "max_reads": 8, "max_writes": 8, "max_payouts": 4 },
"fee_rules": { "burn_base": true, "burn_floor": true, "subsidy_net_of_fees": false,
"proposer_share_bps": null, "prove_base": null, "usd_subsidy": null },
"perps": { "collateral_asset": 1, "max_tier": 18, "max_window_blocks": 64,
"max_payouts": 8, "max_block_inputs": 24, "min_deposit": "100000000" },
"multisig": { "create_fee": "1000000000", "max_signers": 10, "max_payouts": 4 } }
A genesis without the five limit fields (chain 12) gets 4096, 2097152, 4194304, 18432 and 0. max_proof_bytes was 8,388,608 on chains 13 to 15 and 4,194,304 on chains 16 to 20; chain 21 raised it to 6,291,456 (6 MiB) for the multisig auth proof and lowered max_block_bytes from 20,971,520 to 19,922,944 (19 MiB).
| field | meaning |
|---|---|
envelope_bytes | 1860 when the genesis fixes every note-creating envelope at exactly that length, the memo-carrying format; null on a genesis without the field (chains 14 to 17), where a wallet seals the 1,348-byte envelope with no memo. No other value exists |
hardening_v6 | true when the genesis runs the v0.6 validity rules. A wallet then proves a call over the program’s public input followed by the transaction’s call binding, not over the public input alone: the fee bundle’s notes are chosen first, the call proved second, the bundle last. A chain with the flag refuses the old proof, a chain without it the new one |
hc_auth | the auth guest the genesis pins, hex, or null. Set, hc_bundle is bundle guest v3 and every bundle carries auth_commit and an auth proof: the wallet gives the bundle guest nk and a fresh salt, and proves the auth guest over the spend key |
hc_auth_multisig | v0.8.0: the second auth guest the genesis pins, the private-multisig policy (an M-of-N of Dilithium2 keys proved at tier 18), hex, or null. Set, as on chain 21, the chain also admits auth proofs of that guest’s shape, checked against this digest; a wallet offers rand multisig-private only where it is set. A multisig-shaped auth proof on a chain without the field is refused InvalidAuthProof |
gas_metering | "circuit" on a chain whose genesis has a gas section (the in-circuit meter); "header" on a chain without one when the node runs its own gas policy (--gas-price, --byte-price); null with neither |
gas_price, byte_price | units per gas, and units per KiB of a call’s proof and input envelope, as decimal strings. Under a gas section they are consensus state and the same on every node: with gas.dynamic, as on chains 20 and 21, the tip’s current prices, which move per block by fullness, never fall below the genesis floors (100 and 800) and never rise above the ceilings; without it the section’s fixed prices. Without a section they are this node’s own policy, and two nodes may answer differently |
bundle_gas_limit | the flat gas every bundle proof must declare, from the genesis gas section; null without one |
adjust_bps | the dynamic controller’s largest one-block price move, in basis points; null on a chain without gas.dynamic |
max_gas_price, max_byte_price | v0.6.8: the ceilings the controller never lifts a price over, decimal strings; "10000" and "80000" on chains 20 and 21, null where the genesis sets none (chains 18 and 19) |
byte_load | v0.6.8: "paying" when only a call’s proof and input envelope move byte_price, so a block of transfers moves none, as on chains 20 and 21; null where every transaction’s bytes count (chains 18 and 19) |
admission_by_vote | v0.6.8: whether a bond that registers a new validator key needs the validator set’s vote first (rand_getAdmitted); true on chains 20 and 21 |
testnet | v0.6.8: the genesis testnet marker, which lets a faucet sit beside a bridge section; true on chains 20 and 21, false on every chain before them |
slashing | v0.6.8: { "equivocation_bps", "jail_epochs" }, what a leader equivocation costs, or null; null on chains 20 and 21 |
binding_domain | v0.6.8: 0 where a transaction’s binding and every signed action message bind the chain id alone (chains 14 to 19), 1 where they carry the genesis hash too, so a proof or signature made for one chain verifies on no other (chains 20 and 21). A wallet decides which form to prove by the chain id, never by this reply: the v0.6.8 rand wallet gives chain ids 14 to 19 the chain-id form and every other id the genesis-bound form, and reads this only to refuse early. A wallet older than v0.6.8 cannot send on chain 20, and one older than v0.8.0 cannot send on chain 21 (constraint set 9) |
proof_window_blocks | v0.6.8: how old, in blocks, a bundle’s anchor may be and how far behind the height its time may be, one window for both; 1024 on chains 20 and 21, null (256) on chains through 19. A wallet with a slow or delegated prover reads it for how long a proof may take |
program_state | v0.6.8, RPL-2: the genesis program_state section and its invoke limits, or null, in which case every invoke is refused and no program token can be registered. cell_fee is the RAND units (a decimal string) an invoke’s fee floor gains per cell it creates, a write of a non-zero value to a cell that reads as zeros; rewriting, deleting or writing zeros costs nothing. max_reads, max_writes and max_payouts (pays and mints together) are hard caps. In practice the segment rule binds first: the transition’s context words (11 + 16 per read or write + 3 per payout) must fit beside the program’s public input and the 8 binding words without growing the zkVM’s public table; for a program without a public input that table is 128 rows, which leaves 119 context words |
fee_rules | v0.8.0: the genesis fees section, or null on a chain without one (chains through 20). On chain 21 burn_base and burn_floor are true: a bundle’s whole settled floor (the 0.001 RAND base; base plus the per-word term of a deploy; a call’s tier-exact gas and byte terms; a bridge burn’s fee; an invoke’s cell fees) is destroyed rather than paid to the proposer, who keeps only the tip, and rand_getSupply.base_fees_burned counts it. What a sender pays does not change. subsidy_net_of_fees, proposer_share_bps, prove_base and usd_subsidy belong to aggregating chains and are false or null on chain 21; prove_base, where set, raises every bundle’s floor and rand_estimateFee includes it |
perps | v0.8.0, RPL-3: the genesis perps section’s limits, or null on a chain without one, where every perp action is refused. On chain 21 the collateral is asset 1 (zUSD), a state proof is capped at tier 18 over at most 64 blocks, a block records at most 24 perp inputs, a state proof pays at most 8 withdrawals and a deposit is at least 1 zUSD ("100000000" in its eight-decimal units). The market list is in rand_getPerps |
multisig | v0.8.0: the genesis multisig section’s limits, or null on a chain without one, where all four multisig actions are refused. On chain 21: create_fee "1000000000" (1 RAND, burned through the creating bundle), at most 10 signers an account, at most 4 payout notes a payment |
A node that predates a field answers without it. Nothing in this reply is authenticated, so a wallet does not take envelope_bytes on the node’s word alone: the rand wallet seals the legacy envelope on chain ids 14 to 17 whatever the node answers, and other clients should do the same.
rand_getReceipt
Params [tx_hash]. Result null until the call is committed, then { "tx", "program", "tier": 14, "outputs": [1, 0, 25, 0, 0, 0, 0, 0], "height": 17, "index": 0, "h_in": "9c0e…7f", "h_pub": null }. No effect field. h_pub is the digest of the program’s deploy-time public input the call’s proof was checked against, null for a program without one. An invoke (v0.6.8) has a call’s receipt, and rand_getReceipts and the receipts topic carry it unchanged.
rand_getReceipts
Params [program_id, from_height, to_height, limit?]. Result { "receipts": [ … ], "next_height": 2051 }: the receipts of program_id with from_height <= height <= to_height, by height then index, each exactly rand_getReceipt’s object. limit defaults to and is capped at 256 and never splits a height: once a page holds limit receipts it still serves the rest of that height. next_height is the first height not served, or null when to_height ended the page. -32602 for to_height below from_height.
rand_getCallEnvelope
Params [tx_hash]. Result null, or { "tx", "h_in", "kem_ct", "to_sender", "to_auditor", "body" } in hex — the call’s sealed input transcript, served verbatim; the node holds no key that opens it. null when the call published none (--no-envelope), the transaction is not a call, or there is no receipt.
Staking and supply
rand_getValidators
Params []. Result: one row per register entry, in address order, amounts as decimal strings:
[{ "address": "…", "stake": "1000000000000",
"pending": [{ "release_epoch": 41, "amount": "5000000000" }],
"rewards": "4000000", "payout": "rand1…", "nonce": 3, "active": true, "jailed_until": null }]
jailed_until (v0.6.8) is the first epoch a slashed key may be in a set again (18446744073709551615 for good), null when it is not jailed. Chains 20 and 21 have no staking.slashing, so it is always null there.
rand_getAdmitted
v0.6.8. Params []. Result { "admission_by_vote": true, "max": 256, "admitted": ["<address>", …] }: the keys the validator set has voted in with an AdmitValidator and that have not registered yet, in address order. On a chain whose genesis sets staking.admission_by_vote, as chain 20’s did and chain 21’s does, a Bond that registers a new key must be among them. max is how many the ledger holds at once. On a chain without the flag admission_by_vote is false, the list is always empty and a registration needs no vote. Chain 20 answered "admitted": [] on 2026-10-01.
rand_getMultisig
v0.8.0. Params [id], the account’s 64-hex id. Result { "enabled": true, "id", "signers": ["<Dilithium2 public key, hex>", …], "threshold", "nonce", "vault": [{ "asset", "amount" }, …] }: signers in the account’s order (a signature names its signer by position), nonce the one counter a payment and a rotation share, vault ascending by asset (0 is RAND, every other index the token registry’s) with amounts as decimal strings in the asset’s own units and no row at zero. null for an id the register does not hold; { "enabled": false } on a chain without a multisig section. On chain 21 the one genesis account is the foundation treasury, d5fd188308af5322b234b199ae6193fa686ae1453e6af39720638b1f441b1958: 2 of 3 signers, 100,000 RAND. Forwarded by the public endpoint since 2026-10-12.
rand_getRandPrice
v0.8.0. Params []. Result { "price_micros_per_rand", "set_at_height", "nonce", "fresh", "max_age_blocks" }, the validator set’s voted RAND/USD price for the dollar-indexed sealing subsidy (micro-dollars per RAND as a decimal string; fresh is judged for the next block), or null on a chain whose genesis has no fees.usd_subsidy, which chain 21 is: it answers null there, and SetRandPrice (tag 44) is refused. Forwarded by the public endpoint since 2026-10-12.
rand_getEpoch
Params []. Result { "epoch": 41, "epoch_blocks": 1000, "next_set": ["…", "…"] }; next_set is a projection.
rand_getSupply
Params []. Result:
{ "height": 1998,
"genesis_deposited": "…", "genesis_staked": "…", "faucet_minted": "…",
"faucet_epoch": "…", "faucet_minted_in_epoch": "…",
"withdraw_deposited": "…", "fees_paid": "…", "burned": "…",
"subsidised": "…", "sealed_blocks": "…", "aggregator_bonds": "…", "slashed": "…",
"registration_fees_burned": "…", "base_fees_burned": "…",
"vesting_issued": "…", "vesting_released": "…", "vesting_in_register": "…", "vesting_locked": "…",
"program_rand_out": "…", "program_rand_held": "…",
"perps_rand_out": "…", "perps_rand_held": "…",
"multisig_issued": "…", "multisig_rand_in": "…", "multisig_rand_out": "…",
"multisig_base_out": "…", "multisig_rand_held": "…",
"pool_value": "…", "register_total": "…", "total_supply": "…", "invariant_holds": true }
subsidised, sealed_blocks, aggregator_bonds and slashed move only on an aggregation chain. faucet_epoch and faucet_minted_in_epoch are the faucet’s per-epoch counter against staking.faucet_budget_per_epoch (consensus state since chain 20). The four vesting_* fields are genesis vesting’s counters, all "0" on a chain without a vesting section, which chains 20 and 21 are. program_rand_out and program_rand_held (v0.6.8, RPL-2; "0" without a program_state section) are the RAND that invokes have paid out of program vaults as notes (pool side, inside pool_value) and what the vaults still hold (register side, inside total_supply); it entered through an invoke’s burn_r, so it is inside burned on the pool side. registration_fees_burned is Σ of the token registration fees burned under the genesis tokens.burn_registration_fee, which chains 20 and 21 set; invariant_holds subtracts it, with slashed, from what the chain issued. Supply explains the identity.
Three groups are v0.8.0’s, each "0" on a chain whose genesis lacks the section. base_fees_burned is every bundle floor destroyed under fees.burn_base / fees.burn_floor (chain 21): inside burned on the pool side, subtracted on the identity’s right beside registration_fees_burned; under the flags fees_paid counts only the tips proposers kept. perps_rand_out and perps_rand_held are RAND that perp state proofs have paid out of the exchange as notes and what it still holds; on chain 21 the collateral is zUSD, a token, whose supply does not move on the way in or out, so both stay "0" there. multisig_issued is what the genesis seeded multisig accounts with (100,000 RAND on chain 21, the foundation treasury, beside genesis_staked), multisig_rand_in every bundle burn a create or deposit put in, multisig_rand_out the RAND payout notes, multisig_base_out the bundle bases payments paid into proposers’ rewards, and multisig_rand_held what the accounts still hold, inside total_supply. invariant_holds covers all of them. On chain 21 at its cut genesis_staked is 26,000 RAND, genesis_deposited 3,938.879078887 RAND and multisig_issued 100,000 RAND: 129,938.879078887 RAND before faucet mints.
rand_getEmission
Params []. Result { "inflation": "0", "subsidy": null, "faucet": true }. inflation is a fixed "0". subsidy is the block-aggregation schedule (current, base, halving_blocks, sealed_blocks, next_halving_at) and null on a chain without an aggregation section, which chains 20 and 21 are. faucet mirrors rand_status.
Bridge
All four answer on any chain; on a chain without a bridge section the state is { "enabled": false } and the registry is []. Chain 21’s genesis has one, carried from chain 20’s state at height 814,702: guardian set 1 (guardian_set_index 1) with eight guardians, eight PQ guardians, a pause key, burn_sequence 13 at genesis, a replay floor (min_inbound_sequence 3 for source chains 2, 3 and 4, and 7 for source chain 5), bridge rules_v2 with a global mint cap of 4,000 zUSD per 24-hour window across every backing, and two groups new in v0.6.8: rotation (a PQ-guardian or pause-key rotation needs proof of possession by the new keys and takes effect 86,400 s after it commits) and fees (10 bps of every deposit and 10 bps of every burn, kept on Rand as a zUSD note to a treasury address). Chain 19 had burn_sequence 8, floors of 1 and 4, and neither group. Its tokens section has a 1 RAND registration_fee (burned), a per-backing mint_cap_per_day of 10000000000000 (100,000 zUSD at eight decimals) and lists zUSD itself: carried over from chain 14 at genesis, same asset id, index 1, seven backings.
rand_getBridgeState
Params []. Result { "enabled": false }, or:
{
"enabled": true,
"emitter": "01…",
"emitters": { "2": "02…" },
"guardian_set_index": 0,
"guardians": ["aabb…"],
"pq_guardians": ["…"],
"mint_paused": false,
"pause_nonce": 0,
"list_nonce": 0,
"pause_key": "…",
"registration_fee": "1000000000",
"burn_sequence": 1,
"rotation_nonce": 0,
"rules_v2": { "global_mint_cap_per_window": "…", "cap_window_secs": 86400,
"global_minted_in_window": "…", "global_mint_headroom": "…" },
"min_inbound_sequence": { "2": 2, "3": 2, "4": 2, "5": 2 },
"rotation_rules": { "delay_secs": 86400, "needs_possession": true },
"pending_rotations": [],
"fees": { "mint_bps": 10, "burn_bps": 10, "recipient": "rand1…" },
"assets": [ … ]
}
| field | meaning |
|---|---|
emitter | this chain’s outbound emitter address, 32 bytes hex |
emitters | source chain id → the emitter address trusted there |
guardians | the current set’s 20-byte addresses, hex |
pq_guardians | the PQ set: Dilithium2 public keys (1,312 bytes), hex, index-aligned with the current guardian set (set 1 on chains 20 and 21). Every BridgeAttest carries a quorum of co-signatures by it. A guardian-set rotation never moves it; only a rotate_pq_guardians does, on a rules_v2 chain |
mint_paused | while true every transfer attest is refused bridge minting is paused; burns and rotations stay open |
pause_nonce | what the next pause or unpause must carry; both bump it |
list_nonce | what the next register_bridged_token or list_backing must carry; both bump it |
pause_key | the one Dilithium2 key that may pause minting, hex. It can only pause: an unpause needs the PQ guardian quorum |
registration_fee | what a RegisterBridgedToken owes past the bundle base, RAND units, a decimal string |
burn_sequence | outbound messages emitted so far |
rotation_nonce | what the next rotate_pq_guardians / rotate_pause_key must carry |
rules_v2 | the global mint cap and its rolling window, or null on a chain without bridge rules v2. Under it the per-backing cap is a rolling window too, and a listing is refused while minting is paused. Since v0.6.8 it also carries global_minted_in_window and global_mint_headroom, decimal strings |
min_inbound_sequence | the genesis replay floor: source chain id → the lowest sequence a transfer from that chain may carry, plain numbers; null on a chain without one (chain 15 and earlier). A bridge_attest below it is refused BelowReplayFloor, a permanent verdict. A relayer reads it to drop a lock that the chain this one was cut from already minted |
rotation_rules | v0.6.8: { "delay_secs", "needs_possession" } from the genesis bridge.rotation, or null. Under it a rotation is the _v2 action, carrying the new keys’ possession signatures, and waits delay_secs before it takes effect |
pending_rotations | v0.6.8: rotations committed and not yet in effect, each { "kind": "pq_guardians", "new_pq_guardians", "effective_at_secs" } or { "kind": "pause_key", "new_pause_key", "effective_at_secs" }; null without rotation_rules. A cancel_rotation signed by the current pause key drops one |
fees | v0.6.8: { "mint_bps", "burn_bps", "recipient" } from the genesis bridge.fees, or null (chains 14 to 19). The chain keeps that share of every deposit and every burn, rounded down to a whole release unit of the backing, as a zUSD note to recipient. A deposit credits the gross less the fee; a burn’s signed body releases its amount less the fee. A wallet reads it to value its own deposit and, when it is recipient, to rebuild its fee notes |
assets | the rows of rand_getAssets |
No balances: bridged value is notes. There is no next_index any more: a bridged token is listed, under an index its registration already fixed, before it can be deposited, so there is no index left to predict. Read a listed token’s index off assets or rand_getTokens.
rand_getAssets
Params []. Result: the bridge’s asset registry, ascending by index, or []:
[{ "index": 1, "chain": 2, "token": "aaaa…", "asset_id": "…", "decimals": 6, "locked": "600",
"mint_cap_per_day": "10000000000000", "minted_today": "1000", "mint_day": 20350,
"minted_in_window": "1000", "mint_window_secs": 86400, "mint_headroom": "390000000000" }]
Since v0.6.8 every backing row, here and wherever backings are served (rand_getBridgeState.assets, the backings of rand_getToken, rand_getTokens and rand_getTokenSupply), carries minted_in_window (a decimal string) and mint_window_secs, both null on a day-counter chain, and mint_headroom: the largest deposit to that backing the caps admit at the head’s block time, the per-backing cap less what it has minted and, under rules v2, no more than the registry-wide window has left. Read mint_headroom, not mint_cap_per_day − minted_today, which ignores the global cap. Under rules v2, as on chains 20 and 21, minted_today is the rolling-window count, the figure the per-backing cap is checked against; the day-counter description below is chain 14’s.
One row per backing (source coin), so a token with seven backings is seven rows under one index. index is the asset word a note of that asset carries; index 0 is RAND and is never in the registry. decimals is the backing’s source decimals. locked, mint_cap_per_day and minted_today are decimal strings in the token’s own eight-decimal units, never RAND’s; mint_day is a plain integer, a UTC day number. mint_cap_per_day is the genesis tokens.mint_cap_per_day, the most one backing may mint per UTC day of the block time. minted_today is what the backing has minted on mint_day, the UTC day (timestamp_ms / 86,400,000) of the head block, so a counter left from an earlier day reads "0" once the head crosses midnight. A deposit past the cap is refused MintCapExceeded and becomes admissible the next day. chain and token are the wire identity guardians sign about; asset_id is blake3 of the two, what rand_bridgeAssetId computes.
rand_bridgeAssetId
Params [token_chain, token_address] (integer, 32 bytes hex). Result: the asset id, 64 hex characters. Pure arithmetic.
rand_getBridgeBurn
Params [sequence]. Result null or { "sequence": 0, "body_hex": "…", "digest": "…", "tx": "…", "height": 2, "amount": "…", "release_amount": "…", "fee": "…" }.
The last three are v0.6.8, on every chain, as decimal strings in the token’s eight-decimal units. release_amount is the amount the signed body carries, read off body_hex itself: what the source contract releases, out of which it pays the body’s relayer fee. fee is the bridge fee the chain kept as a treasury note, and amount what the bundle burned, release_amount + fee. Without bridge.fees (chains 14 to 19) fee is "0". Sequence 13 on chain 20 (chain 21 carries chain 20’s burn_sequence, 14, so its first burn is sequence 14), a burn of 0.999 zUSD: "amount": "99900000", "fee": "99900", "release_amount": "99800100".
Tokens (RPL)
The RPL token registry: every token, bridged and native. Tokens explains the standard.
rand_getTokens
Params [from_index, limit], both optional (0 and 1000; limit is clamped to 1,000). Result: the registry ascending by index from from_index, read by range from that index:
{ "enabled": true, "registration_fee": "1000000000", "next_index": 3, "max_tokens": 4096,
"burn_registration_fee": true, "bound_note_value": true,
"tokens": [
{ "index": 2, "id": "<64 hex>", "id_text": "rpl1…", "name": "Test Coin", "symbol": "TST",
"decimals": 6,
"authority": { "kind": "key", "key": "<Dilithium2 key, hex>", "address": "<base58>" },
"mint_nonce": 1, "total_supply": "5700", "registered_at": 3 }
] }
On a chain without a tokens section: { "enabled": false, "tokens": [] }. index is the asset word a note of the token carries (0 is RAND and is never listed). id is the token’s asset id and id_text its checksummed text form: bech32m, HRP rpl, over the 32 id bytes, 62 characters. authority is one of:
kind | other fields |
|---|---|
none | — (fixed supply, or renounced) |
key | key, address |
bridge | backings: [{ "chain", "token", "decimals", "locked", "mint_cap_per_day", "minted_today", "mint_day" }], rand_getAssets’s fields, one row per backing |
program | program |
Supplies, locked, mint_cap_per_day, minted_today and registration_fee are decimal strings (RAND units for registration_fee, token units for the rest); next_index, mint_day and max_tokens are numbers. max_tokens is the genesis cap on the registry (4,096 on chains 20 and 21; null without one), past which a registration is refused RegistryFull. burn_registration_fee says whether a registration’s fee is burned rather than paid to the proposer; bound_note_value whether a mint or deposit at or above 2⁶³ is a validity rule (admission refuses one on every chain regardless). All three are set on chains 20 and 21. A page shorter than limit is the last.
A program authority ({ "kind": "program", "program": "<id>" }) is registrable only on a chain with a program_state section, as chains 20 and 21 are (v0.6.8): the token starts at zero supply, and only that program’s invokes mint or burn it.
A wallet resolves a token id through this listing, never through rand_getToken: a transfer’s asset is private on chain, and reading the whole registry costs the same whichever token is meant.
rand_getToken
Params [token]: a registry index (a number or a decimal string), the 64-hex id (with or without 0x, any case) or the rpl1… text form (all lower or all upper case). Result: that token’s rand_getTokens row, or null. A malformed id — a bad checksum, another HRP, a wrong length, mixed case, not hex — is -32602, so a typo is never some other token. A per-token lookup tells the node which token the caller cares about; it is for explorers and one-off reads.
rand_getTokenSupply
Params [token], the same forms. Result null, or:
{ "total_supply": "600",
"backings": [{ "chain": 2, "token": "<32 bytes hex>", "decimals": 6, "locked": "600",
"mint_cap_per_day": "10000000000000", "minted_today": "1000", "mint_day": 20350 }] }
backings is empty for a native token; for a bridged one the locked amounts sum to total_supply. The same privacy note as rand_getToken.
Block aggregation (not active on chain 21)
These exist in the node for chains whose genesis carries an aggregation section. On chain 21, as on chain 20, the register is empty and nothing is ever unsealed. v0.8.0 changed the Aggregate action’s wire form on every chain (it gained payout_total and layout, below), though no chain has carried one.
| method | params | result |
|---|---|---|
rand_getAggregate | [hash] | the sealing aggregate’s covers, aggregator, subsidy, proving_share, n, height; null for any other transaction |
rand_getAggregators | [] | the aggregator register: address, bond, payout, nonce, unbonding per row |
rand_getUnsealed | [from, limit] | { "bundles": [{ "hash", "height", "excess" }], "next_from" }, the bundles an aggregator may still cover |
An Aggregate is submitted through rand_sendTransaction like everything else.
Genesis vesting (not active on chain 21)
These exist in the node for chains whose genesis carries a vesting section. No chain has one yet: on chain 21, as on chain 20, each answers { "enabled": false }. The public endpoint forwards them since 2026-10-01.
| method | params | result |
|---|---|---|
rand_getVesting | [id] or [id, at_ms], the entry’s 64-hex id | the entry: its class, beneficiary, schedule (amount, start_ms, cliff_ms, linear_ms, step_ms), what has been claimed, revoked and bonded, and vested_now, claimable_now, unvested_now at as_of_ms (the head block’s time by default); null for an unknown id. Since v0.6.8 a revocable entry lists revokers, the threshold of them a revoke needs and the treasury a revoke pays (they replace the single revoker), and the entry carries revoke_nonce and locked_now |
rand_getVestingSummary | [] | { "enabled": true, "height", "as_of_ms", "entries", "issued", "released", "locked", "classes": [ … ] }: per-class totals, with no key or owner |
rand_getVestingSchedule | [from_ms, to_ms, step_ms], at most 1,000 points | { "enabled": true, "issued", "points": [{ "t_ms", "locked" }] }: the register’s locked total over time |
Amounts are decimal strings. The four vesting actions are submitted through rand_sendTransaction.
Subscriptions (WebSocket)
ws://127.0.0.1:8545/ or /ws. The socket serves only rand_subscribe and rand_unsubscribe and answers -32601 to anything else. Three topics, independent streams with no ordering promise between them:
newHeads—rand_getHead’s shape, sent once per committed block, in order, including during sync.viewhere is the block’s own view.receiptsorreceipts <program_id>— one notification per committed block that carries at least one matching call receipt:{ "height", "hash", "receipts": [ … ] }, each receipt inrand_getReceipt’s shape.transaction <hash>— one notification, then the node removes the subscription:{ "status": "committed", "height", "index" }, or{ "status": "rejected", "reason" }for a refusal about the transaction’s own bytes (the limitrand_getTransactionStatushas). A state-dependent refusal is never announced, so pair it with a timeout.
{ "jsonrpc": "2.0", "id": 1, "method": "rand_subscribe", "params": ["newHeads"] }
{ "jsonrpc": "2.0", "id": 2, "method": "rand_unsubscribe", "params": ["1"] }
{ "jsonrpc": "2.0", "id": 3, "method": "rand_subscribe", "params": ["receipts", "<program_id>"] }
{ "jsonrpc": "2.0", "id": 4, "method": "rand_subscribe", "params": ["transaction", "<hash>"] }
{ "jsonrpc": "2.0", "id": 1, "result": "1" }
{ "jsonrpc": "2.0", "id": 2, "result": true }
{ "jsonrpc": "2.0", "method": "rand_subscription",
"params": { "subscription": "1", "result": { "height": 1998, "hash": "…", "view": 2251 } } }
Bounds: 64 connections per node (the 65th is refused at the upgrade with 503), 8 subscriptions per connection, 64 KiB per client frame, a ping every 30 s answered within 5 s, and 256 entries of backlog per stream — a subscriber further behind on a stream it listens to is closed with code 1008, not buffered; reconnect and fill the gap with rand_getCompactBlocks (heads), rand_getReceipts (receipts) or rand_getTransactionStatus (a transaction). A plain GET / without upgrade headers answers 400.
Errors
| code | meaning |
|---|---|
-32601 | unknown method (including the retired account methods) |
-32602 | invalid or missing parameter |
-32000 | rejected: a transaction the mempool refused, a rate-limited faucet call, a witness build that waited over 10 s, a read over 30 s, a method the public listener does not serve, or a subscription over the cap |
-32001 | referenced object not found |
-32010 | pruned: a height below this node’s retention floor; data.floor names it (see below) |
-32603 | internal error |
-32600 | invalid request: body over the size limit, not JSON, a malformed batch, or a notification |
{ "jsonrpc": "2.0", "id": 1, "error": { "code": -32000, "message": "…" } }
For rand_sendTransaction, only -32000 and -32602 are a verdict on the transaction; after any other code its fate is unknown (see rand_sendTransaction).
Pruned nodes: error -32010
A node started with --prune-history <window> keeps the whole ledger (notes, nullifiers, anchors, the register) but only the last <window> of blocks, transactions and receipts; rand_status.prune_floor is the lowest height it still holds. On chain 21, as on chain 20, 24 of the 26 validators run --prune-history 24h; the two archives run without the flag and keep every block, and the public endpoint forwards to one.
- By height, below the floor (genesis excepted):
rand_getBlockByHeight,rand_getFinalityby height, and arand_getBlocks,rand_getCompactBlocksorrand_getReceiptsrange reaching below it answer-32010pruned: height h is below this node's retention floor fwithdata: { "floor": f }. Ask an archive.[0, 0]still serves genesis. - By hash:
rand_getBlockByHash,rand_getReceipt,rand_getRawTransaction,rand_getCallEnvelopeandrand_getAggregateanswernullfor a pruned hash, as for one that never existed; only an archive can tell the two apart.rand_getTransactionandrand_checkTransactionanswernulltoo, and-32010only when a location row survived naming a height below the floor.rand_getTransactionStatusaddsfloorto everyunknownentry on a pruned node. - Wallet scanning is unaffected:
rand_getCommitments,rand_getNullifiers,rand_getAnchorand the witness methods read families that are never pruned.
The transaction on the wire
Transaction { chain_id: u64, bundle: Option<Bundle>, action: Action }
Bundle { // the hidden-asset bundle (since chain 14)
anchor: Word8, nullifiers: [Word8; 4], commitments: [Word8; 4],
fee: u64, burn_a: u64, burn_r: u64, burn_asset: u32, time: u32,
envelopes: [Envelope; 4], proof: Vec<u8>,
auth_commit: Word8, auth_proof: Vec<u8>, // since chain 17 (v0.6.3): split authorisation's c and
} // its proof; zeros and empty without hc_auth
Envelope { kem_ct: Vec<u8>, to_receiver: Vec<u8>, to_sender: Vec<u8>, body: Vec<u8> }
Action::None
Action::Mint { cm: Word8, pk: Word8, time: u32, r: Word8, envelope: Envelope, amount: u64,
minter: PublicKey, signature: Signature }
Action::Deploy { base_pc: u32, words: Vec<u32>, public: Vec<u32> }
Action::Call { program: Hash, proof: Vec<u8>, input_envelope: Option<CallEnvelope> }
Action::Bond { validator: Address, amount: u64, registration: Option<Registration> }
Action::Unbond { validator: Address, amount: u64, nonce: u64, signature: Signature }
Action::Withdraw { validator: Address, amount: u64, nonce: u64, time: u32, r: Word8,
envelope: Envelope, signature: Signature }
Action::BridgeAttest { attestation: Vec<u8>, recipient: ShieldedAddress, r: Word8, time: u32,
asset: u32, envelope: Envelope, pq_signatures: Vec<PqSignature> }
Action::BridgeBurn { asset: u32, amount: u64, relayer_fee: u64, to_chain: u16,
token: [u8; 32], to: [u8; 32] }
// the five aggregation variants sit here in the enum's declared order
Action::RegisterToken { name: String, symbol: String, decimals: u8, authority: MintAuthority,
initial: Option<InitialMint>, salt: [u8; 32], index: u32 }
Action::TokenMint { asset: u32, amount: u64, recipient: ShieldedAddress, r: Word8, time: u32,
envelope: Envelope, nonce: u64, signature: Signature }
Action::SetAuthority { asset: u32, new: Option<PublicKey>, nonce: u64, signature: Signature }
Action::TokenBurn { asset: u32, amount: u64 }
Action::PauseMints { nonce: u64, signature: Signature } // bundle-less, fee-less
Action::UnpauseMints { nonce: u64, pq_signatures: Vec<PqSignature> } // bundle-less
Action::RegisterBridgedToken { name: String, symbol: String, salt: [u8; 32], chain: u16,
token: [u8; 32], decimals: u8, nonce: u64,
pq_signatures: Vec<PqSignature> }
Action::ListBacking { token_index: u32, chain: u16, token: [u8; 32], decimals: u8, nonce: u64,
pq_signatures: Vec<PqSignature> }
Action::RotatePqGuardians { new_pq_guardians: Vec<PublicKey>, nonce: u64,
pq_signatures: Vec<PqSignature> } // bundle-less, fee-less
Action::RotatePauseKey { new_pause_key: PublicKey, nonce: u64,
pq_signatures: Vec<PqSignature> } // bundle-less, fee-less
// the four vesting variants (ClaimVested, RevokeVesting, BondVested, UnbondVested) follow, tags 24 to 27;
// since v0.6.8 RevokeVesting carries `signatures: Vec<RevokerSignature>` in place of one `signature`
Action::AdmitValidator { candidate: PublicKey, signatures: Vec<(PublicKey, Signature)> } // tag 28, v0.6.8
Action::SlashEquivocation { first: Box<SignedHeader>, second: Box<SignedHeader> } // tag 29
Action::RotatePqGuardiansV2 { new_pq_guardians: Vec<PublicKey>, possession: Vec<Vec<u8>>,
nonce: u64, pq_signatures: Vec<PqSignature> } // tag 30
Action::RotatePauseKeyV2 { new_pause_key: PublicKey, possession: Vec<u8>, nonce: u64,
pq_signatures: Vec<PqSignature> } // tag 31
Action::CancelRotation { kind: u8, nonce: u64, signature: Signature } // tag 32
Action::Invoke { program: Hash, proof: Vec<u8>, input_envelope: Option<CallEnvelope>,
transition: Transition } // tag 33, RPL-2
Action::PerpDeposit { trading_key: PublicKey } // tag 34, RPL-3 (v0.8.0, chain 21): rides a bundle
Action::PerpOrder { account: Word8, body: PerpOrderBody, signature: Signature } // tag 35, bundle-less, fee-less
Action::PerpCancel { account: Word8, nonce: u64, target: u64, signature: Signature } // tag 36
Action::PerpWithdraw { account: Word8, nonce: u64, amount: u64, recipient: ShieldedAddress,
r: Word8, time: u32, envelope: Envelope, signature: Signature } // tag 37
Action::PerpOracle { validator: PublicKey, prices: Vec<PerpPrice>, nonce: u64,
signature: Signature } // tag 38
Action::PerpStateProof { from_height: u64, to_height: u64, new_root: Word8,
payouts: Vec<PerpPayout>, fees: u64, proof: Vec<u8> } // tag 39, unsigned
Action::CreateMultisig { salt: [u8; 32], signers: Vec<PublicKey>, threshold: u8 } // tag 40 (v0.8.0, chain 21): rides a bundle
Action::MultisigDeposit { account: [u8; 32] } // tag 41, rides a bundle
Action::MultisigPay { account: [u8; 32], nonce: u64, time: u32, pays: Vec<Payout>,
signatures: Vec<SignerSignature> } // tag 42, bundle-less
Action::MultisigRotate { account: [u8; 32], nonce: u64, signers: Vec<PublicKey>, threshold: u8,
signatures: Vec<SignerSignature> } // tag 43, bundle-less, fee-less
Action::SetRandPrice { … } // tag 44 (v0.8.0): the validators' RAND/USD vote; chains with fees.usd_subsidy only
MintAuthority = None | Key(PublicKey) | Bridge { backings: Vec<Backing> } | Program(Hash)
InitialMint { amount: u64, recipient: ShieldedAddress, r: Word8, time: u32, envelope: Envelope }
Transition { reads: Vec<Cell>, writes: Vec<Cell>, inflow: Inflow, pays: Vec<Payout>, mints: Vec<Payout> }
Cell { key: Word8, value: Word8 } // keys strictly ascending in each list
Inflow = None | Deposit | Burn // what the bundle's burn_a of burn_asset is to the program
Payout { asset: u32, amount: u64, recipient: ShieldedAddress, r: Word8, envelope: Envelope }
Registration { public_key: PublicKey, payout: ShieldedAddress, signature: Signature }
CallEnvelope { kem_ct: Vec<u8>, to_sender: Vec<u8>, to_auditor: Vec<u8>, body: Vec<u8> }
PqSignature { index: u8, signature: Vec<u8> } // one guardian's Dilithium2 co-signature, by set index
PerpOrderBody { nonce: u64, market: u32, side: u8, kind: u8, tif: u8, reduce_only: bool, price: u64, size: u64 }
PerpPrice { market: u32, price: u64 }
PerpPayout { request: Word8, amount: u64 }
SignerSignature { index: u8, signature: Signature } // a multisig signer, by position in the account's signers
bincode’s default configuration: fixed-width integers, 8-byte length prefixes, u32 enum tags in declaration order. The five aggregation variants follow BridgeBurn and precede RegisterToken in the enum (Transactions); chain 21, like chain 20, never admits them, nor the four vesting variants, SlashEquivocation or SetRandPrice, but an encoder that hard-codes tag numbers still has to count them.
The bundle’s last two fields arrived with v0.6.3 and chain 17. bincode is positional, so an encoder written for chain 16 or earlier does not produce a transaction this chain decodes, and every transaction id changed with them: the id’s domain is now rand-txid-3, which binds auth_commit and the auth proof’s digest. A client that recomputes ids locally must move with it. Since chain 20 every bundle carries an auth proof, self-proved or delegated alike. v0.6.8 changed nothing in the Transaction or Bundle layout; it appended actions 28 to 33. v0.8.0 likewise leaves Transaction and Bundle as they are — a multisig-authorised bundle has the same fields as any other, and only its auth proof’s declared shape differs — and appends actions 34 to 44: the six perp actions (34–39, v0.7.x’s RPL-3, inert until chain 21), the four multisig actions (40–43) and SetRandPrice (44). Two existing variants moved on every chain: Action::Aggregate gained a trailing payout_total: u64 (signed under rand-aggregate-4, genesis-bound -5) and a trailing layout (Flat or Tree, a 4-byte enum index), so its encoding and txid moved; no chain has ever carried one. Constraint set 9 (v0.8.0, chain 21) changes every proof’s encoding and every verifier key, not the transaction layout: a bundle, auth or call proof made by a build before v0.8.0 does not decode on chain 21, which is why v0.8.0 refuses chains 18 to 20 and a pre-v0.8.0 wallet cannot send on chain 21.
Every transaction carries at most one bundle. A Bond carries a bundle whose burn_r equals its amount, with burn_a and burn_asset 0. A BridgeBurn or a TokenBurn burns through the bundle: burn_asset must be the action’s asset (never 0), burn_a the action’s amount, and burn_r 0, while fee pays in RAND from slots 2–3. Every other action burns no token, and RAND is only ever burned through burn_r: a bundle with burn_asset 0 and burn_a non-zero is refused everywhere. A BridgeAttest’s r is derived, not chosen: admission requires blake3("rand-deposit-r-1" ‖ mu) over the digest the guardians signed and refuses any other value (WrongDepositBlinding, a permanent refusal). A bridged token is listed — at genesis, or by a RegisterBridgedToken / ListBacking — before any attestation of it is admissible; an attestation of an unlisted coin is refused UnlistedToken.
Every bundle proof is made over, and verified against, the transaction it rides in: the whole transaction with its proofs blanked, bound to the chain id and, since chain 20 (binding_domain: 1), to the genesis hash, under fresh tags. On chains 14 to 19 the binding was the chain id alone, so a proof made for chain 19 does not verify on chain 20, and a wallet older than v0.6.8 cannot send there; a proof made for chain 20 is bound to chain 20’s genesis and is in any case constraint set 8’s, so it does not verify on chain 21 either, and a wallet older than v0.8.0 cannot send there. On chains 18 to 21 a bundle without its two proofs encodes to 7,936 bytes and a transfer transaction without them to 7,949 (5,848 and 5,861 on chains 14 to 16, with 1,348-byte envelopes and no auth fields). A transfer on the wire is about 2.85 MB at the production profile, nearly all proof: a bundle proof of about 1.49 MB and an auth proof of about 1.36 MB. It was about 1.43 MB before split authorisation.
What chain 21 and v0.8.0 add
Chain 21 went live on 2026-10-12 (genesis cut 10:22 UTC, all 26 validators switched 10:31 UTC) on fullnode v0.8.0 (c0878c3f), a hard fork of chain 20. Against chain 20’s method list, as this page gave it for v0.6.8:
- Constraint set 9. Every proof’s encoding and every verifier key changed (rVM phase 4’s Merkle spans and the MODQ chip); v0.8.0 refuses chains 18 to 20 at startup and a proof made by an older build is refused on chain 21. No
TransactionorBundlefield moved. rand_getLimits:max_proof_bytes6,291,456 andmax_block_bytes19,922,944; newhc_auth_multisig,fee_rules,perpsandmultisig.rand_status:hc_auth_multisig,msauth_key,compact_fetched.rand_getSupply:base_fees_burned,perps_rand_out,perps_rand_heldand the fivemultisig_*counters.rand_sendTransaction: the body cap counts three proofs on a chain with the multisig guest.- New methods, all public:
rand_getScanLeaves,rand_getPerps,rand_getPerpAccount,rand_getPerpAccounts,rand_getPerpInputs,rand_getMultisig,rand_getRandPrice. The public endpoint forwards them since 2026-10-12. - New actions: tags 34 to 39 (RPL-3 perps), 40 to 43 (multisig accounts) and 44 (
SetRandPrice, inert on chain 21), rendered byrand_getTransactionas above;Aggregategainedpayout_totalandlayout. - Fees: under chain 21’s
fees.burn_baseandfees.burn_floorevery bundle’s settled floor is burned and the proposer keeps the tip (Fees). What a sender pays is unchanged. - Node-only, every chain: compact blocks (a leader gossips the header and the transaction hashes;
compact_fetched), a startup checkpoint, a verify cache (an aggregate’s STARKs verified once a node), the follower’s proposal apply off the consensus loop,rand_getBlockspages bounded at 16 MiB of reply (v0.7.0, audit v7), and--threads/--cpufor proving (v0.7.1). - Private multisig: an M-of-N policy proved inside the second auth guest at tier 18, made with
rand multisig-private(Command line); nothing on the wire or in this page’s shapes tells such a bundle apart. No chain-21 transaction has carried one yet.
The fullnode’s own docs/rpc.md at c0878c3f keeps the method-by-method changelog this page is read against.