Rand Protocol
Documentation
Core concepts

Transactions and actions

The transaction on the wire, its id, every Action the ledger knows, which ones chain 21 admits, and the order a node checks a submission in.

Use this page to construct, submit or decode a transaction: the wire shape rand_sendTransaction takes, how the id is computed, the full list of actions with their bundle and fee rules, and the admission order that decides which error you get first.

The transaction

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>,     // postcard(Proof) of the hidden-asset guest
  auth_commit: Word8, auth_proof: Vec<u8>,      // since chain 17: split authorisation's c and its auth proof
                                                // (since chain 21 either the single-key auth guest's proof or the private-multisig one's)
}
Envelope { kem_ct: Vec<u8>, to_receiver: Vec<u8>, to_sender: Vec<u8>, body: Vec<u8> }

Word8 is eight little-endian u32 words, rendered as 64 lowercase hex characters. The wire form is bincode(Transaction) with bincode’s defaults (fixed-width integers, 8-byte length prefixes, u32 enum tags), and rand_sendTransaction takes it as hex. There is no signature over the transaction and no sender field: a bundle authorises itself by its proofs (the bundle proof and, since chain 17, the auth proof beside it), and an action that is signed carries the signature inside itself. Replay protection is the nullifier set for bundles, a per-validator nonce in the register for the signed staking actions, a per-token mint_nonce for TokenMint and SetAuthority, and the bridge’s pause_nonce and list_nonce for its governance actions.

A transaction carries at most one bundle, and every action that is not bundle-less carries exactly one. Slots 0–1 of the bundle carry a private asset and slots 2–3 RAND, so one proof moves a token and pays its RAND fee; the two-bundle bridge burn of the earlier design is gone: a burn is one bundle (Notes).

The rand wallet builds all of this through randprotocol_client::wallet::{send, submit}: select inputs, fetch the anchor, compute the witnesses from the wallet’s own copy of the tree, build the outputs and seal all four envelopes, take the transaction’s binding, prove the bundle and the auth proof, check the bundle proof’s published digest, submit.

The transaction binding

A transaction has no signature, and a bundle’s proof commits only to the bundle’s own digest: anchor, nullifiers, commitments, fee, the burn fields, time. Through chain 13 nothing tied a proved bundle to the action it rode with or to its envelopes, so anyone who saw a transaction could keep every proof byte and change the rest: a BridgeBurn’s destination, a Bond’s validator, an envelope. Since chain 14 every bundle proof is made over, and verified against, the transaction’s binding:

binding(tx) = blake3("rand-tx-bind-1" || bincode(chain_id, bundle', action'))            chains 14 to 19
binding(tx) = blake3("rand-tx-bind-2" || bincode(genesis_hash, chain_id, bundle', action'))  since chain 20
                                                                                  as 8 LE u32 words

where ' means every bundle proof byte string is replaced by the empty vector: bundle.proof, since chain 17 bundle.auth_proof, and an Aggregate’s proof. Everything else is inside: auth_commit, all four envelopes, every burn field, a destination, a validator, a recipient, signatures, the guardian attestation and its Dilithium2 co-signatures, and a Call’s own proof. The eight words ride in the proof’s public input segment, whose digest H_PUB the circuit already binds, so the fix needed no guest or circuit change; the node runs Machine::verify_public(hc_bundle, binding, proof) and refuses a bundle proof that declares any other public height (public height not the transaction binding's). Transaction::binding is the one function the wallet calls before proving and the ledger before verifying. Since chain 17 the auth proof is made over and verified against the same eight words, so it cannot be moved onto another transaction either.

Since chain 20 the binding covers the genesis hash. Through chain 19 the binding, and the signed messages of the bundle-less actions, covered the chain id alone, so two chains sharing a chain id (a re-cut, a private copy) would have accepted each other’s proofs and signatures. Chain 20’s genesis sets binding_domain: 1 (v0.6.8, audit v6 BIND-1; crates/randprotocol-core/src/types/binding.rs). The genesis hash then leads every preimage under a tag one version up: rand-tx-bind-2 and rand-call-bind-2 for the two bindings, and the same for the faucet mint (rand-mint-3), Unbond and Withdraw (rand-unbond-2, rand-withdraw-2), the RPL mint and authority messages (rand-rpl-mint-2, rand-rpl-authority-2), the aggregator actions and the bridge’s governance messages. A validator registration on such a chain is always the v2 message (genesis, chain, address, payout). Nothing proved or signed for one chain verifies on another, whatever its chain id. The binding words are a public input, so no guest or verifier key changed. A wallet older than v0.6.8 still proves against the chain-id form and is refused on chain 20. Chain 21 (fullnode v0.8.0, live since 2026-10-12) keeps the same binding over its own genesis hash, ca76c276…ca9a; what changes there is the proof system underneath — constraint set 9 (rVM phase 4 and the MODQ chip) gives every guest a new verifier key and a new proof encoding, so a proof made by a wallet older than v0.8.0 does not decode on chain 21 at all, before any binding is checked. rand_getLimits and rand_status serve binding_domain (1 on chains 20 and 21, 0 without the field).

Under genesis hardening_v6 (every chain since 16, chain 21 included) a Call’s own proof is bound too, by a second digest of the same construction under its own domain, with the call proof blanked as well as the bundle’s proofs:

call_binding(tx) = blake3("rand-call-bind-1" || bincode(chain_id, bundle', action''))   as 8 LE u32 words

Since chain 20 the tag is rand-call-bind-2 and the genesis hash leads the preimage, as above. An Invoke’s proof is bound the same way, with its declared transition kept in the preimage, so the proof commits to exactly the state change, payouts and recipients the transaction carries.

The call proof carries these words in its public input segment, after the program’s deploy-time public input when it has one, so a copy of it attached to another fee bundle does not verify. The two bindings nest: the wallet chooses the fee bundle’s notes and builds the transaction with the proofs empty, proves the call against the call binding, puts that proof in, and only then takes the transaction’s binding and proves the bundle.

The consequence for a client: build the whole transaction — outputs, envelopes, action — before proving. A wallet built before the fork proves against the empty public segment and is refused. The bundle-less actions have no proof to bind and rest on their own signatures. The binding is not the transaction id, which is unchanged.

The transaction id

txid = blake3("rand-txid-3" || bincode(view))

where view is the transaction with the bundle’s proof bytes replaced by blake3(proof) (proof_hash) and its auth_proof bytes by blake3(auth_proof) (auth_proof_hash, after auth_commit, which enters as it is), every other field verbatim. The domain was rand-txid-2, without the two auth fields, through chain 16; every transaction id changed with chain 17. Hashing the proof by digest is what lets a pruned bundle (block aggregation, not active on chain 21) keep the same id as its raw form; a pruned bundle keeps its auth proof. A bundle-less transaction hashes its bytes as they are. The same id keys the mempool, receipts and every RPC lookup.

Every action

Action is a bincode enum, tags in declaration order:

tagactionfieldsbundlefee floor
0None—requiredBUNDLE_BASE = 0.001 RAND
1Mintcm, pk, time, r, envelope, amount, minter: PublicKey, signaturemust be absent0 (validator-signed; faucet chains only, amount ≤ 100 RAND)
2Deploybase_pc: u32, words: Vec<u32>, public: Vec<u32>requiredbase + 100,000 units per program word and public word
3Callprogram, proof, input_envelope: Option<CallEnvelope>requiredon a gas-metered chain (chain 21, as chains 18 to 20), base + gas_price × the proof’s declared GAS_LIMIT + byte_price per KiB of proof and input envelope; base + call_fee(tier) on a chain without a gas section
4Bondvalidator, amount, registration: Option<Registration>required, and burn_r == amountbase
5Unbondvalidator, amount, nonce, signaturemust be absent0
6Withdrawvalidator, amount, nonce, time, r, envelope, signaturemust be absent0 on the wire; the base is taken out of amount
7BridgeAttestattestation, recipient: ShieldedAddress, r, time, asset, envelope, pq_signatures: Vec<PqSignature>requiredbase
8BridgeBurnasset, amount, relayer_fee, to_chain: u16, token: [u8; 32], to: [u8; 32]required, with burn_asset == asset and burn_a == amountBRIDGE_BURN_FEE = 0.01 RAND
9RegisterAggregatorregistrationrequired, burn_r = the bondbase
10UnbondAggregatoraggregator, nonce, signaturemust be absent0
11WithdrawAggregatoraggregator, nonce, time, r, envelope, signaturemust be absent0
12SlashAggregatortwo signed aggregate headersmust be absent0
13Aggregatecovers, proof, aggregator, nonce, time, r, envelope, signature, and since v0.8.0 payout_total and layout (Flat or Tree), so its encoding and txid moved; no chain has carried onemust be absent0
14RegisterTokenname, symbol, decimals: u8, authority: MintAuthority, initial: Option<InitialMint>, salt: [u8; 32], index: u32requiredbase + the registry’s registration_fee (1 RAND on chain 21, as on chain 20, burned)
15TokenMintasset, amount, recipient: ShieldedAddress, r, time, envelope, nonce, signaturerequiredbase
16SetAuthorityasset, new: Option<PublicKey>, nonce, signaturerequiredbase
17TokenBurnasset, amountrequired, with burn_asset == asset and burn_a == amountbase
18PauseMintsnonce, signaturemust be absent0
19UnpauseMintsnonce, pq_signaturesmust be absent0
20RegisterBridgedTokenname, symbol, salt, chain: u16, token: [u8; 32], decimals: u8, nonce, pq_signaturesrequiredbase + registration_fee
21ListBackingtoken_index, chain: u16, token: [u8; 32], decimals: u8, nonce, pq_signaturesrequiredbase
22RotatePqGuardiansnew_pq_guardians: Vec<PublicKey>, nonce, pq_signaturesmust be absent0
23RotatePauseKeynew_pause_key: PublicKey, nonce, pq_signaturesmust be absent0
24ClaimVestedentry: [u8; 32], amount, nonce, to: ShieldedAddress, time, r, envelope, signaturemust be absent0 on the wire; the base is taken out of amount
25RevokeVestingentry: [u8; 32], unvested, nonce, to: ShieldedAddress, time, r, envelope, signatures (since v0.6.8 a threshold of the entry’s revokers, paying the treasury its genesis entry names; one signature before)must be absent0 on the wire; the base is taken out of unvested
26BondVestedentry: [u8; 32], validator, amount, registration: Option<Registration>, nonce, signaturemust be absent0
27UnbondVestedentry: [u8; 32], amount, nonce, signaturemust be absent0
28AdmitValidatorcandidate: PublicKey, signatures: Vec<(PublicKey, Signature)>must be absent0
29SlashEquivocationfirst, second: two signed block headers of one leader for one viewmust be absent0
30RotatePqGuardiansV2new_pq_guardians: Vec<PublicKey>, possession: Vec<Vec<u8>>, nonce, pq_signaturesmust be absent0
31RotatePauseKeyV2new_pause_key: PublicKey, possession: Vec<u8>, nonce, pq_signaturesmust be absent0
32CancelRotationkind: u8, nonce, signaturemust be absent0
33Invokeprogram, proof, input_envelope: Option<CallEnvelope>, transition: Transitionrequired; burn_r, and burn_a of burn_asset, are what comes into the programa call’s floor + cell_fee per created cell
34–39the six RPL-3 perps actions (v0.8.0): PerpDeposit, PerpOrder, PerpCancel, PerpWithdraw, PerpOracle, PerpStateProof (tx_json kinds perp_deposit … perp_state_proof)a deposit’s bundle burn funds a trading account; an order, a cancel, a withdrawal and an oracle update are signed, bundle-less and fee-less; a state proof is a STARK of the engine over a window of blocks, unsigned, so anyone may prove ita deposit: required, burn_a of the collateral asset; the others: must be absentbase for a deposit; 0 otherwise
40–43the four multisig-account actions (v0.8.0): CreateMultisig, MultisigDeposit, MultisigPay, MultisigRotate (tx_json kinds create_multisig, multisig_deposit, multisig_pay, multisig_rotate)a creation names the salt, the threshold and the signers; a deposit the account; a payment the account, its nonce, up to max_payouts notes and the indices of the signers that signed; a rotation the new set, the new threshold and the indices that signedcreate and deposit: required, burn_r is what goes in; pay and rotate: must be absentcreate: base + the genesis create_fee (1 RAND on chain 21); deposit: base; a payment pays the 0.001 RAND base out of the account’s RAND row; a rotation 0
44SetRandPrice (v0.8.0)price_micros_per_rand, nonce, votes — the validator set’s vote on a RAND/USD price, AdmitValidator’s quorum, at most a factor of two per updatemust be absent0
MintAuthority = None | Key(PublicKey) | Bridge { backings: Vec<Backing> } | Program(Hash)
InitialMint   { amount: u64, recipient: ShieldedAddress, r: Word8, time: u32, envelope: Envelope }
PqSignature   { index: u8, signature: Vec<u8> }   // one guardian's Dilithium2 co-signature, by set index

On chain 21 (fullnode v0.8.0, live since 2026-10-12), tags 0 to 8, 14 to 21, 28, 30 to 33 and 34 to 43 are usable. Its genesis has everything chain 20’s had — a bridge section with rules_v2, a tokens section, staking.admission_by_vote (tag 28), bridge.rotation (tags 30 to 32) and program_state (tag 33) — and three sections new with v0.8.0: perps (tags 34 to 39: one market, BTC-PERP on zUSD; Tokens), multisig (tags 40 to 43: M-of-N Dilithium2 accounts with public balances, seeded with the foundation treasury, 100,000 RAND under 2 of 3 signers, id d5fd1883…1958, create_fee 1 RAND, at most 10 signers and 4 payouts a payment) and fees (no action of its own: it burns every bundle’s floor, Fees). Tag 44 exists in the build but is refused on chain 21 (UnsupportedAction): it needs fees.usd_subsidy, which prices an aggregate’s subsidy in dollars, and chain 21 has neither that nor aggregation; rand_getRandPrice answers null there. Tags 22 and 23 are refused, as on chain 20: under bridge.rotation.needs_possession a rotation must be the …V2 form that carries each new key’s own signature. On chain 20, tags 0 to 8, 14 to 21, 28 and 30 to 33 were usable; on chain 19, which had none of those sections, tags 0 to 8 and 14 to 23 (chain 14, without rules_v2, refused 22 and 23). SlashEquivocation needs a staking.slashing section, and chain 21’s is null. The five aggregation actions need an aggregation section, which chain 21 does not have (a build refuses to start on a genesis that carries one until aggregation is re-measured). A node refuses all of these on the gate before any other check, but they still occupy their bincode tags, so an encoder that hard-codes tag numbers must count them. Aggregation is documented as upcoming in Proofs. The four genesis-vesting actions (tags 24 to 27, in the build since v0.5.11) need a vesting section, which no chain has had, chain 21 included, and are refused at admission without one. Tags 28 to 33 are new in v0.6.8 and tags 34 to 44 in v0.8.0, each batch appended last, so every earlier chain’s history still decodes.

The second auth guest (chain 21). Chain 21’s genesis also pins hc_auth_multisig, 9705f8e2f1bc8561848b29cb8efd57c37e3e39788fa58a5f03304d8019668247: a tier-18 guest that proves an M-of-N Dilithium2 policy was satisfied without naming the policy or the signers. A bundle it authorises is an ordinary v3 bundle on the wire — the same fields, the same txid, the same binding — and only its auth_proof’s declared shape (its header’s table heights) differs; admission decodes the auth proof at one of the two pinned shapes, verifies the single-key shape against hc_auth and the multisig shape against hc_auth_multisig, and refuses anything else as InvalidAuthProof. The chain learns only “multisig-authorised”; on a chain without the field a multisig-shaped proof is refused permanently. The wallet side is rand multisig-private (Command line); the proof is made locally at tier 18, which needs a machine of about 96 GB. No private-multisig spend had been proved on chain 21 at its cut.

The four RPL actions (tags 14 to 17) register a token, mint more of a Key-authorised one, hand over or renounce its mint authority, and burn a holding; a RegisterToken naming a Bridge or Program authority is refused. There is no token-transfer action: a token transfer is None, indistinguishable from a RAND payment. See RPL tokens. The four bridge governance actions (tags 18 to 21) are the mint pause, its lifting, and the listing of a bridged token or of a further backing after genesis. PauseMints is signed by the genesis pause key, which can only pause; the other three need a quorum of the Dilithium2 (PQ) guardians. A BridgeAttest carries such a quorum too, beside its ECDSA one. Tags 22 and 23, under bridge rules_v2, replace the PQ guardian set or the pause key, each under a PQ quorum of the current set and the bridge’s rotation_nonce. Tags 30 and 31 do the same under bridge.rotation and also carry each new key’s own Dilithium2 signature over the rotation message, a proof that the key is held; since chain 20 a rotation is recorded as pending and takes effect after delay_secs, 86,400 s. CancelRotation (tag 32), signed by the current pause key, drops a pending rotation before it takes effect. See Bridge.

AdmitValidator (tag 28) is the validator set’s vote to admit a new key: signatures from validators holding more than two thirds of the stake, in ascending address order. Under staking.admission_by_vote (chains 20 and 21) a Bond that would register a new validator is refused unless its address has been admitted this way (Staking). Invoke (tag 33, RPL-2) is a call whose proof vouches for one declared state transition of the program: cells read and written, what the bundle’s burn fields bring into the program’s vault, and the notes the vault pays out or the program mints. The ledger applies the transition if every declared read still holds and both proofs verify (Programs).

A Deploy and a Call are ordinary shielded transactions whose bundle is a self-transfer of zero: it exists to pay the floor out of the caller’s own notes, so the chain never learns whose they were. RAND leaves the pool only through burn_r — a Bond’s stake, a RegisterAggregator’s bond, an Invoke’s deposit into a program vault — and a token only through burn_a with burn_asset naming it, on a TokenBurn, a BridgeBurn or an Invoke. Every other action burns nothing, and a bundle with burn_asset = 0 and burn_a non-zero is refused everywhere.

Admission order

Ledger::validate_inner checks cheap things before expensive ones, in this order; the mempool runs the same check before gossiping, so a bad transaction is refused once, at the edge.

  1. Size caps — the whole transaction ≤ the chain’s max_block_bytes; each of the bundle’s four envelopes, and any note envelope the action carries, exactly the genesis envelope_bytes (1,860 on chains 20 and 21; at most 2,048 bytes on a chain without the field); the bundle proof, the auth proof and a call proof each ≤ max_proof_bytes (2 MiB by default; 6 MiB in chain 21’s genesis, room for the tier-18 multisig auth proof; 4 MiB on chains 18 to 20; 8 MiB on chains 13 to 16), and on a genesis with hc_auth_multisig the request body may hold three proofs at that cap; a program ≤ the chain’s program cap (4,096 words by default, 65,535 in chain 21’s genesis, as chain 20’s); a call envelope ≤ max_call_envelope_bytes (18,432 bytes by default, 65,536 on chains 20 and 21); an attestation ≤ 16 KiB. A bundle proof in the sealed (pruned) marker form is refused here outside sealed-form sync (PrunedFormOutsideSync), before any proof work.
  2. Chain id matches.
  3. Shape and fee floor — a bundle-less action carries no bundle and everything else must carry exactly one; check_burn_shape fixes which of the three burn fields the action may set: a TokenBurn or BridgeBurn burns its own asset and amount through burn_asset and burn_a, a Bond or RegisterAggregator burns its amount through burn_r, and every other action burns nothing; fee ≥ fee_floor(action). On a chain with a gas section (chain 21, as chains 18 to 20) a Call’s or an Invoke’s floor at this step is the gas rule at a declared limit of 1, BUNDLE_BASE + gas_price + byte_price × ⌈bytes / 1024⌉ over its proof and input envelope, which are known before any verification.
  4. Anchor — one of the last 1,024 block-end roots on chain 21, as on chain 20, whose genesis sets proof_window_blocks: 1024 (v0.6.8); one of the last 256 on a chain without the field, as through chain 19.
  5. Time — within [height − 1024, height] on chains 20 and 21; [height − 256, height] without the field. The one genesis value sets both windows.
  6. Nullifiers and commitments — all four nullifiers differ and none is spent; all four commitments differ and none is already a leaf.
  7. Action checks — the faucet is enabled, the mint is under the cap and validator-signed, and since chain 20 names a recipient on the genesis staking.faucet_recipients allowlist, fits the epoch’s faucet_budget_per_epoch and is signed by a validator on the genesis staking.faucet_minters list (a validity rule since chain 16); a mint or deposit at or above 2⁶³ is refused on every chain; the program decodes; the program exists; the staking rules (registration present exactly when the validator is unknown, the nonce, the signature, enough stake); the bridge’s own rules for BridgeAttest and BridgeBurn and for the pause and listing actions; RPL’s rules for its four actions, including the registration fee; since chain 20 the admission vote’s quorum, and for an Invoke the transition’s shape, the declared reads against the stored cells, the vault’s cover for every payout and the cell fee. On chain 21 the perps rules (the market, the account, the window), the multisig rules (the signer set, the threshold of valid signatures, the nonce, the vault’s cover) and, for a CreateMultisig, the create_fee sit here too. All of it runs before the bundle’s proof, so a transaction that cannot apply costs no STARK verification. Under hardening_v6 (every chain since 16) two more refusals sit here: a Deploy of a program no call could hold (ProgramUncallable), and a bundle or call proof whose header or transcript carries a field the honest prover would not write (non-canonical proof: …), refused before either proof is verified. Since v0.6.8 a proof whose header declares a degree past the tallest table’s ceiling is refused first under the same verdict (audit v6 ZKV-4), before the check walks it.
  8. Bundle digest — on a split-authorisation chain (every chain since 17) the auth fields are checked first: a bundle without an auth proof is refused, the c the auth proof publishes (read, not yet verified) must equal auth_commit, and since chain 20 a single-key auth proof must declare a GAS_LIMIT of exactly 1,279. On chain 21 the auth proof’s declared shape decides which guest it is checked against (above); a multisig-shaped one declares 436,159, its own header’s ceiling. Then the ledger recomputes the digest of the bundle’s published fields, auth_commit among them, and it must equal what the proof published. Since chain 20 the bundle proof must also declare a GAS_LIMIT of exactly the genesis bundle_gas_limit, 20,479.
  9. Bundle proof — Machine::verify_public against the genesis-pinned hc_bundle and the transaction’s binding. On a split-authorisation chain the auth proof is verified next, against the genesis-pinned hc_auth — or, on chain 21, hc_auth_multisig for a multisig-shaped proof — and the same binding. Since v0.8.0 a node keeps the Ok verdict of each verification under a digest of its inputs (the verify cache, 32,768 entries), so a proof admission has seen is verified once per node, not again at apply.
  10. Call proof and its tier fee — for a Call or an Invoke, the proof’s header is pinned first (tier at most 14, a keccak table at most 2¹², a sha256 table at most 2¹³, the deployed program’s height, floored at 2⁷ rows under hardening_v6, and since chain 18 a declared GAS_LIMIT no greater than the header’s ceiling gas_max), then the call’s STARK against the program’s code_hash and, under hardening_v6, the transaction’s call binding. The fee is checked last, because it is only knowable once the proof is decoded: on a gas-metered chain fee ≥ BUNDLE_BASE + gas_price × GAS_LIMIT + byte_price × ⌈bytes / 1024⌉ at the prices in force, plus cell_fee per created cell for an Invoke (Fees); on a chain without a gas section fee ≥ BUNDLE_BASE + call_fee(tier).

Errors come back from rand_sendTransaction as code -32000 with the reason: nullifier already spent, anchor is not one of the last 1024 roots (256 on a chain without proof_window_blocks), time 12 is outside [476, 1500], 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, and since chain 14 the bundle burns asset 2 on an action that burns no token, a burn of 5 is not allowed on this action, the bundle burns 399, not the 400 the action declares, wrong mint nonce: expected 1, got 0, wrong token index: expected 3, got 2, since chain 17 this chain requires an auth proof on every bundle (genesis hc_auth) and the auth proof publishes a different commitment than the bundle's auth_commit, and since chain 20 envelope is 1348 bytes, this chain requires exactly 1860 and bundle proof declares gas limit …, this chain requires exactly 20479. Acceptance is not commitment: poll rand_getTransaction until it returns a block.

A block containing any invalid transaction is invalid. The mempool indexes every pending nullifier and commitment and refuses a conflict outright, so two bundles spending the same note never share a block.

Sizes

partbytes
Word832
one Envelope1,892 encoded (1,860 of ciphertext) since chain 18; 1,380 (1,348) through chain 17
Bundle minus its two proofs7,936 (four nullifiers, four commitments, four envelopes, auth_commit)
transfer transaction minus its two proofs7,949
deploy transaction minus its two proofs, 100-word program, no public input8,369
call transaction minus its three proofs, no input envelope7,990
mint transaction (no bundle)5,761 (a 1,312-byte Dilithium2 key and a 2,420-byte signature)
a bundle proof, tier 14, production profile1,497,544 and 1,504,260 read on chain 18 (a deploy at height 97,821 and a call at 98,335, 2026-09-30); 1,422,116 and 1,423,170 measured on chain 14 (the zUSD registration and a backing listing, 2026-09-20)
an auth proof, tier 10, production profile1,370,827 and 1,367,303 in the same two chain 18 transactions

The first six rows are the encoding since chain 18: the memo field added 512 bytes to every envelope, and split authorisation added 40 bytes to the bundle before the auth proof’s own bytes. So a shielded transfer on the wire was about 2.88 MB from chain 18 to chain 20, essentially all proof, whichever asset it moves: about 1.5 MB of bundle proof and 1.37 MB of auth proof. It was about 1.43 MB through chain 15, with one proof. Chain 21’s proofs are constraint set 9’s, whose sizes are not measured here yet; a private-multisig authorisation adds a tier-18 auth proof in place of the tier-10 one (estimated at over 3.4 MB, which is why chain 21’s proof cap is 6 MiB). Blocks are capped at 2,000 transactions (MAX_BLOCK_TXS) and at the chain’s max_block_bytes of transaction bytes, both consensus rules: 4 MiB by default (MAX_BLOCK_BYTES), which would hold one such transfer; 20 MiB in chain 20’s genesis, which had room for seven; 19 MiB (19,922,944 bytes) in chain 21’s. A genesis with hc_auth must set max_block_bytes to at least three times max_proof_bytes plus 1 MiB, since a transaction can carry three proofs (bundle, auth, call), so the 4 MiB default is refused there. Block-level aggregation is the queued remedy; see Proofs. Since v0.8.0 a leader gossips a compact block — the header, its signature and the transaction hashes — and replicas rebuild the block from their mempool, fetching only the bodies they lack (Consensus); the cap above is on the block, not on the frame.

Reading one back

rand_getTransaction(hash) returns null until the transaction is committed, then its block position and the public fields only:

{
  "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": 1497544, "envelope_len": [1860, 1860, 1860, 1860],
      "auth_commit": "0bab…e8a1", "auth_proof_bytes": 1370827
    },
    "action": { "kind": "none" }
  }
}

There is no from, no to, no nonce, no amount and no asset field in a transfer, and nothing in the stored block either: the node has nothing more to redact. auth_commit and auth_proof_bytes (the auth proof’s length) are there since chain 17. The bundle always has four slots, dummies included, and a transfer of RAND and a transfer of any RPL token are both "kind": "none". Every u64 amount is a decimal string since chain 14 (fee, burn_a, burn_r, an action’s amount); indices, nonces, heights and lengths stay JSON numbers. The other action kinds render their public fields — mint, deploy, call, bond, unbond, withdraw, bridge_attest, bridge_burn, register_token, token_mint, set_authority, token_burn, pause_mints, unpause_mints, register_bridged_token, list_backing, rotate_pq_guardians, rotate_pause_key, the four vesting kinds claim_vested, revoke_vesting, bond_vested, unbond_vested, since v0.6.8 admit_validator, slash_equivocation, rotate_pq_guardians_v2, rotate_pause_key_v2, cancel_rotation and invoke, and since v0.8.0 the six perp_* kinds, create_multisig, multisig_deposit, multisig_pay, multisig_rotate and set_rand_price — as listed in the JSON-RPC reference.

Last verified against fullnode commit c0878c3f (chain 21) on 2026-10-12. Where the code and this page disagree, the code is right; tell us.