Rand Protocol
Documentation
Guides

Bridge

The guardian bridge as it is built - a source-chain lock, a guardian attestation with a Dilithium2 co-signature, a BridgeAttest that deposits a note, a one-bundle BridgeBurn that withdraws - with the wire format, zUSD's seven backings, the zUSD fees on mint and burn, the mint caps and pause, the rules-v2 rotations with their possession proof and delay, and what is live on chain 21.

Use this page to integrate with the bridge: to build a deposit on a source chain, to submit an attestation on Rand, to burn a bridged asset back out, and to read the bridge’s public state. Chain 21, the current testnet (live since 2026-10-12 10:31 UTC, fullnode v0.8.0), carries chain 20’s bridge section unchanged: eight guardians starting at guardian set index 1, eight post-quantum guardians, a pause key, emitters for Ethereum, BSC, Tron and Solana (the first three the endpoints redeployed on 2026-09-30), the rules-v2 group, whose global mint cap is 4,000 zUSD per rolling 24 hours, an inbound replay floor, and two groups new in v0.6.8 (chain 20): fees of 10 bps on every mint and every burn, taken in zUSD, and rotation rules that make a post-quantum-set or pause-key rotation prove possession of the new keys and wait a day. Unlike chain 14’s, its genesis lists zUSD with all seven backings and carries over the custody that has come down from chain 14 through each cut since (Carried over from chain 14). rand_getAssets and the explorer’s /bridge/assets give each backing’s current locked.

The flow

source chain (Ethereum / BSC / Tron / Solana)                 Rand
┌────────────────────────────────┐                           ┌────────────────────────────────┐
│ lock tokens, emit              │  guardians observe,       │ BridgeAttest transaction        │
│ (amount, token, to, fee)       │  sign mu = keccak²(body)  │   admission → one deposit note  │
└────────────────────────────────┘  threshold n·2/3 + 1      │   computed by the chain          │
              ▲                     + Dilithium2 co-signers  │                                  │
   release after guardian         guardian committee         │ BridgeBurn transaction           │
   signatures on the burn  ◄───── secp256k1 keys ◄─────────  │   one bundle: slots 0–1 burn the │
   message                        read the burn log          │   asset, 2–3 pay the RAND fee;   │
                                                             │   emits a sequenced burn message │
                                                             └────────────────────────────────┘

Inbound: value is locked on the source chain, guardians attest, anyone submits the attestation to Rand, and the ledger appends one deposit note for the recipient — a note whose asset word is the bridged token’s registry index. Outbound: a Rand transaction burns notes of that asset and emits a message; guardians attest that message; the source-chain contract releases. Rand never verifies a source-chain header or state proof: the guardian committee’s signatures are the entire trust model.

Trust model and chain ids

Bridge-local chain ids, shared with the contracts (bridge-codec):

chainid
Rand1
Ethereum2
BSC3
Tron4
Solana5

Guardians are a committee of secp256k1 keys; a guardian’s identity is the last 20 bytes of keccak256(uncompressed_pubkey[1..]), Ethereum’s address derivation, so one key works on the EVM contracts, the Solana program and Rand. Quorum for a set of size n is n·2/3 + 1. Every signature must be low-s (s ≤ n/2), foreclosing malleability. The digest guardians sign is mu = keccak256(keccak256(body_bytes)). A guardian-set rotation is an attestation from the governance emitter (chain 1, keccak256("rand-bridge-governance")), must carry new_index == current + 1, and must be signed by the current set, not a superseded one; the outgoing set keeps a 86,400-second grace window for in-flight transfers. (That is the ECDSA set. The post-quantum set and the pause key rotate by their own actions, which since chain 20 wait out a one-day delay: below.) If n·2/3 + 1 guardians collude they can mint arbitrary value; Bridge hardening bounds how much and how fast, and does not remove that.

Transaction binding. A Rand transaction carries no signature: a burn is authorised by the STARK proof of its bundle. Since chain 14 every bundle proof is made over, and verified against, Transaction::binding — a hash of the whole transaction (chain id, the one bundle with its four envelopes, the action) with only the proof bytes blanked. Since chain 17 a bundle carries two proofs, the bundle proof and the spend-authorisation proof: the binding blanks both, keeps the bundle’s auth_commit, and both are verified against it. A copy of a pending burn with to, relayer_fee or (to_chain, token) changed, or of an attest with its time or envelope changed, fails the proof (TxError::InvalidBundleProof). Since chain 20 the binding also carries the genesis hash (genesis binding_domain: 1: rand-tx-bind-2 over the genesis hash, the chain id, the bundle and the action), so a proof made for another chain that shares the chain id does not verify either. Wallets and relayers must run a binary built for the chain they submit to: v0.8.0 on chain 21 (v0.6.8 on chain 20, v0.6.7 on chains 18 and 19); an older rand makes proofs and signatures chain 21 refuses — and on chain 21 an older wallet’s proofs do not even decode, since constraint set 9 changed every verifier key and the proof encoding.

The guardian and relayer daemons are not part of the full node; the full node carries the shared codec and the receiving half.

Wire format

Every integer is big-endian; the wire version is 1.

Attestation envelope

offsetlenfield
01version = 1
14guardian_set_index
51n_sigs
666·n_sigssignatures: index (1), r (32), s (32), v (1)
6 + 66·n_sigs…body

Body header — 51 bytes, then the payload

offsetlenfield
04timestamp
44nonce
82emitter_chain
1032emitter_address
428sequence
501consistency_level
51…payload

Transfer payload — id 1, 133 bytes

offsetlenfield
01id = 1
132amount (u256; Rand uses the low 128 bits, and a deposit must also fit a note’s u64)
3332token_address
652token_chain
6732to — the recipient hash, see below
992to_chain
10132fee (u256): the relayer’s portion of amount

GuardianSetUpgrade payload — id 2: id (1), new_index (4), n (1), then n 20-byte keys.

Ethereum, BSC and Tron addresses are 20 bytes left-padded with 12 zero bytes in the 32-byte slots; a Solana address fills the slot. A Transfer must come from the chain’s registered emitter for emitter_chain, and token_chain must equal the emitting chain. An attestation is at most 16 KiB. None of bridge hardening changed the wire format, the codec or the deployed source-chain endpoints: the post-quantum co-signatures and the governance messages travel in the Rand transaction, not in the attestation.

The 32-byte recipient

A shielded address is 1,216 bytes and the wire has room for 32. The to field carries a hash of it:

to = blake3("rand-shielded-recipient" || pk_bytes || kem_ek)     // ShieldedAddress::recipient_hash

The depositor names the hash on the source chain, the guardians sign it, the submitter puts the full address in the BridgeAttest action, and the ledger recomputes the hash and refuses a mismatch — otherwise the submitter could choose who receives someone else’s deposit. Source-chain front ends must accept this form; the wallet prints it. Chain 11 briefly made the slot the receiver id directly; that address form was reverted with chain 12 (Accounts and addresses).

One token, many backings: zUSD

The asset registry is no longer the bridge’s. It moved into the ledger-level RPL token registry, shared by native and bridged tokens, and the chain’s state root commits to it as tokens_root. A bridged token is a registry entry whose mint authority is Bridge { backings }, and it no longer corresponds to one (chain, token) pair. The first one, zUSD, is backed by USDT and USDC locked on four chains — Ethereum, Binance Smart Chain and Solana carry both coins, Tron only USDT (Circle discontinued USDC on Tron):

chain  token (32-byte wire form)                                                  coin  decimals
2      000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7           USDT  6
2      000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48           USDC  6
3      00000000000000000000000055d398326f99059ff775485246999027b3197955           USDT  18
3      0000000000000000000000008ac76a51cc950d9822d68b83fe1ad97b32cd580d           USDC  18
4      000000000000000000000000a614f803b6fd780986a42c78ec9c7f77e6ded13c           USDT  6
5      ce010e60afedb22717bd63192f54145a3f965a33bb82d2c7029eb2ce1e208264           USDT  6
5      c6fa7af3bedbad3a3d65f36aabc97431b1bbe4c2d2f6e0e47ca60203452f5d61           USDC  6

On chain 21 zUSD is index 1, rpl1xtj6k2x8strx8c2d5fjs50ltztck5ykms4ve7nmzmstf6fhn0v0spelqtx, name “Shielded USD”, 8 decimals, listed in the genesis file (the only token chain 21’s genesis lists), as on chains 15 to 20, with the name, symbol and salt it had on chain 14, so its asset id (32e5ab28c782c663e14da2650a3feb12f16a12db85599f4f62dc169d26f37b1f) and rpl1… id carried over unchanged (explorer: GET /tokens/1). On chain 14 it was registered at height 256 by a register_bridged_token transaction.

Backing { chain: u16, token: [u8; 32], decimals: u8, locked: u64, minted_today: u64, mint_day: u32 }

A (chain, token) pair backs at most one token in the whole registry (TokenError::BackingTaken); a token has 1 to 32 backings; decimals is the source coin’s own count, 0 to 18, never the bridged token’s 8. The pooled backing is a stated risk: the seven coins are fungible into and out of zUSD at par, so a depeg on any one of them dilutes every zUSD holder, and there is no Rand-side cap on one backing’s share.

zUSD is a basket. A holder bears the risk of every listed backing, not of the coin they deposited: one zUSD is a claim on the pool, on any backing with enough locked. Nothing prices a backing. A lock of 1 unit of any listed coin mints 1 zUSD, and a burn may name any backing whose locked covers it, so a coin that has lost its peg can be deposited at par and the zUSD burned toward a coin that kept its peg, until the sound backings are drained. The loss is bounded by the rolling mint windows and by the custody actually locked: on chains 20 and 21 at most 4,000 zUSD across all backings, and 100,000 per backing, can be minted in any 24 hours (rules v2), and no more can leave through a sound backing than that backing holds. The window limits the rate, not the total: it buys the time to pause mints, which stops new deposits of the weak coin while burns stay open. There is no per-backing redemption limit and no per-backing pause. On chain 14, before rules v2, the source endpoints’ own pauser and per-token rate caps were the only limit.

The invariant: total_supply == Σ backings.locked. TokenRegistry::lock (a BridgeAttest) adds the attested amount to the backing’s locked and to the token’s total_supply in one call; TokenRegistry::release (a BridgeBurn) subtracts the released amount from both. Nothing else writes either field, and the token leaf commits every backing’s locked into tokens_root, so two nodes that disagree about one diverge at the state root. The source endpoints take no fee on lock and deduct the whole attested amount on release, so each backing’s locked equals that endpoint’s custody, modulo messages in flight. The fees since chain 20 do not break this: a fee is a zUSD note that stays issued and backed, so the deposit still locks the gross and the burn releases only amount − fee. At chain 20’s cut the endpoints’ own 10 bps USDT skim on release was set to 0; the fee is now taken on Rand.

Amounts are 8-decimal units on the wire, whatever the source coin’s own decimals: a deposit of 1 USDT from Ethereum (6 decimals) locks as 100_000_000 units. Only index 0, RAND, has this chain’s nine decimals.

A token must be listed before its first deposit. There is no first-sighting registration any more: an attestation naming a (chain, token) nobody has listed is refused BridgeError::UnlistedToken (token … of chain … is not listed on this chain). The funds stay in the source endpoint’s custody until a post-quantum guardian quorum lists the coin (Listing after genesis).

asset_id = blake3("rand-bridge-asset" || token_chain (2 bytes BE) || token_address (32 bytes))

rand_bridgeAssetId(token_chain, token_address) still computes this on any chain, and it is what a Transfer payload’s (token_chain, token_address) resolves to, but it is no longer a registry key. What a note and a wallet carry is the token’s index, a dense u32: index 0 is RAND and is never in the registry. rand_getAssets has one row per backing, every row of one token under the same index:

[{ "index": 1, "chain": 2, "token": "000000000000000000000000dac17f…", "asset_id": "…", "decimals": 6,
   "locked": "0", "mint_cap_per_day": "10000000000000", "minted_today": "0", "mint_day": 20716 }]

locked, mint_cap_per_day and minted_today are decimal strings in 8-decimal units, like every u64 amount the RPC serves since chain 14; index, chain, decimals and mint_day stay numbers. The token-level methods — rand_getTokens, rand_getToken, rand_getTokenSupply — return a bridged token as one row carrying all its backings (RPL tokens). The explorer’s GET /bridge/tokens is the static allowlist of coins the bridge accepts.

Inbound: BridgeAttest

Action::BridgeAttest { attestation: Vec<u8>, recipient: ShieldedAddress, r: Word8, time: u32,
                       asset: u32, envelope: Envelope, pq_signatures: Vec<PqSignature> }

PqSignature { index: u8, signature: Vec<u8> }        // Dilithium2, 2,420 bytes

Carried by an ordinary shielded transaction whose one hidden-asset bundle pays the 0.001 RAND base and nothing more: its relayer is paying for a depositor who holds no RAND yet. The chain, not the submitter, computes the deposit note { pk: recipient.pk, from: 0, amount, asset: <index>, time, r } from the amount the guardians signed, so a relayer cannot inflate or redirect a mint; its commitment is the one leaf a transaction does not carry on the wire. Since chain 20 the note’s amount is the gross less the mint fee, and the chain appends a second note, the fee, to the treasury (Fees on mint and burn).

The blinding is derived, not chosen. r must be

r = blake3("rand-deposit-r-1" || mu)        // read as eight little-endian words; mu = keccak256(keccak256(body))

the digest the guardian quorum signed; anything else is BridgeError::WrongDepositBlinding, refused before any signature work. The field stays on the wire — the wallet’s public-field rebuild reads it there — it is only constrained, and the rule covers a rotation’s attest too. Before it, anyone who saw a relayer’s pending transaction could submit the same attestation under a blinding of its own: the recipient still got a note, but the relayer’s transaction died after it had paid for a proof. With r fixed, two submitters of one attestation at one time name one note and the pool conflicts them on the commitment.

time and asset are the depositor’s two predictions, made so the envelope can be sealed before the transaction exists. time is held to the same window a bundle’s is (1,024 blocks since chain 20, from genesis proof_window_blocks; 256 before), and is deliberately not derived: no rule gives a time every submitter would agree on, so a front-runner may still pick another time in the window — a different note of the same recipient, amount and asset, which the recipient’s wallet finds the same way — and the two submissions collide on the attestation digest instead. asset must equal the index the registry has already given (chain, token), else TxError::AttestAssetMismatch.

The Transfer payload’s fee is carried and paid to nobody: there is no submitter identity to pay, so the deposit is the gross amount, less only the chain’s own mint fee where the genesis sets one (chains 20 and 21). Replay protection is by digest: every consumed mu goes into the bridge’s spent set, and the mempool refuses a second relayer racing the same attestation. A deposit is also held to the backing’s mint cap, since chain 20 to the global cap as well, and refused while mints are paused (B1, rules v2). Both caps count the gross. On a chain whose genesis sets a replay floor, chains 20 and 21 included, a transfer whose sequence is below its source chain’s floor is refused BelowReplayFloor before any signature is checked (Carried over from chain 14).

rand bridge-mint <attestation hex or @file> --pq <JSON or @file> [--to rand1…] [--fee <RAND>] [--no-wait]
rand bridge-rotate <rotation hex or @file> --pq <JSON or @file> [--fee <RAND>] [--no-wait]

--pq is required: the guardians’ Dilithium2 co-signature quorum as [{"index":0,"signature":"<4840 hex>"},…]. Before any proving the wallet decodes the attestation, checks the quorum against the node’s pq_guardians and chain id, checks that the recipient hashes to the attested one, and reads the listed index; then it derives r, seals the recipient’s envelope (for the net amount on a chain with fees) and pays with one bundle of its own RAND. bridge-rotate submits a guardian-set rotation (payload 2), which deposits nothing, and refuses anything that does not step Rand’s set by one. Because every field of a deposit note is public in that one transaction, a recipient’s wallet rebuilds deposit notes addressed to it by walking committed bridge_attest transactions, with no envelope opened — a hostile relayer can seal garbage but cannot hide the note.

Outbound: BridgeBurn

Transaction {
  chain_id,
  bundle: <hidden-asset bundle: slots 0–1 spend the token, slots 2–3 pay the RAND fee;
           burn_asset = asset, burn_a = amount, burn_r = 0, fee >= BRIDGE_BURN_FEE>,
  action: BridgeBurn { asset, amount, relayer_fee, to_chain, token, to }
}

Since chain 14’s hidden-asset bundle a burn is one bundle (it was two before): slots 0–1 prove the burner owned notes of the asset summing to at least amount, with burn_a the value leaving the pool, and slots 2–3 pay the fee in RAND. The fee is BRIDGE_BURN_FEE, 0.01 RAND, which covers the bundle’s base. relayer_fee is a portion of amount (fee ≤ amount) carved out on the destination chain by the release contract, not an extra charge, so burn_a is exactly amount. Since chain 20 the chain also keeps a burn fee of 10 bps of amount as a zUSD note to the treasury, and the release is amount − fee; the relayer’s portion must fit inside that release (Fees on mint and burn).

A burn names its coin. (to_chain, token) must be a backing of the burned asset (TokenError::NotABacking otherwise), and two rules are checked at the action step, before the bundle’s proof:

  • The release unit. A backing with d < 8 source decimals releases in units of 10^(8−d) (100 for a 6-decimal coin; 1 for an 18-decimal one). The released amount and relayer_fee must each be a whole multiple, else TokenError::NotReleasable (… is not a multiple of the release unit …): a fraction below the source coin’s smallest unit would be stranded in custody.
  • The backing’s own custody. The released amount ≤ locked for that backing, else TokenError::InsufficientBacking (only … is locked in that backing, not the … asked for). The token’s total supply may cover the release; this one backing’s custody is what has to.

The released amount is amount on a chain without fees and amount − fee on chains 20 and 21.

to must have the right shape (zero-padded 20 bytes on chains 2, 3 and 4; never all zero). The ledger records a BridgeBurnRecord under a strictly increasing burn_sequence, with the transaction hash in the sender slot, and guardians read the burn log:

→ {"method":"rand_getBridgeBurn","params":[13]}
← { "sequence": 13, "amount": "99900000", "fee": "99900", "release_amount": "99800100",
    "body_hex": "…", "digest": "a4bc1c7d…63f4", "tx": "ceb9e3c2…0be7", "height": 2899 }

body_hex is the outbound message exactly as guardians must hash and sign it; its Transfer amount is release_amount. The record above is chain 20’s first burn: 0.999 zUSD burned toward BSC USDT at height 2,899, of which 0.000999 went to the treasury and 0.998001 was released.

rand bridge-burn <ASSET> <AMOUNT> <TO_CHAIN> <TOKEN> <TO> [--relayer-fee N] [--fee <RAND>] [--no-wait] [--yes]
rand bridge-message <SEQUENCE>       # one outbound message, verbatim, for a guardian to sign

The wallet checks first — off one read of the bridge state and the token registry, before any proving — that the chain has a bridge, that ASSET is in the registry, that TOKEN on TO_CHAIN backs it and has at least AMOUNT locked, and that AMOUNT and the relayer fee are whole release units. On a chain with fees it then prints the bridge fee and the amount that will be released, and asks before proving (--yes skips the question). A burn then proves one bundle: about 100 s on a laptop and about 1.5 MB of proof, and, as for every bundle since chain 17, a spend-authorisation proof beside it (a few seconds, about 1.37 MB).

Fees on mint and burn

New in fullnode v0.6.8, set on chain 20 and carried unchanged to chain 21 by the genesis group

"bridge": { …, "fees": { "mint_bps": 10, "burn_bps": 10, "recipient": "rand1…" } }

the chain takes a fee on every bridged mint and every bridged burn, in the bridged token, on Rand, as a shielded note to the treasury address recipient. Each rate is at most 100 bps (1 %); on chains 20 and 21 both are 10 bps, 0.1 %. The fee is ⌊amount · bps / 10⁴⌋ rounded down to a whole release unit of the backing (10^(8 − source decimals): 100 units for a 6-decimal USDT, 1 for an 18-decimal coin), so the fee and the rest are both whole units, and a fee that rounds to zero makes no note (ledger/bridge_notes.rs, bridge_fee).

deposit (BridgeAttest) of gross gburn (BridgeBurn) of amount
fee⌊g · mint_bps / 10⁴⌋, down to a release unit⌊amount · burn_bps / 10⁴⌋, down to a release unit
the user’s sidea deposit note of g − feeamount burned (burn_a == amount, no guest change); the outbound body attests release_amount = amount − fee, and the endpoint releases exactly that
the treasury’s notefee, blinding blake3("rand-bridge-fee-r-1" ‖ mu), at the deposit’s time and assetfee, blinding blake3("rand-bridge-burn-fee-r-1" ‖ txid), at the bundle’s time
locked and total_supplyrise by gfall by release_amount
checked againstthe per-backing and global mint caps, on gInsufficientBacking and NotReleasable on release_amount; the relayer’s portion must fit inside it (FeeExceedsAmount)

Both notes are issued and backed, so total_supply == Σ locked == custody still holds: the fee note is zUSD like any other, and its value stays locked on the source chain until someone burns it. BRIDGE_BURN_FEE, the 0.01 RAND a burn pays in its RAND slots, is unchanged and separate.

Chain 20’s first round trip, read from the node: a 1 zUSD deposit from BSC at height 1,664 (4f6e6c54…1a64) gave the depositor 0.999 zUSD and the treasury 0.001; a burn of 0.999 zUSD back to BSC USDT at height 2,899 (sequence 13) kept 0.000999 zUSD and released 0.998001. After it (read 2026-10-01) the BSC USDT backing held 22.001999 zUSD, and the token’s supply, 99.001999 zUSD, equalled the sum of its backings.

How the treasury finds its notes. A fee note has no envelope: every word of it is public (the amount from the transaction and the genesis rate, the asset and time from the transaction, the blinding derived, the owner the genesis recipient). The wallet whose address is recipient rebuilds fee notes in the same public pass that rebuilds deposit notes, reading rand_getBridgeState.fees and the backings’ decimals once per sync. A node that lies about the fees can only make a note go unfound, never credit one. A depositor’s wallet values its own deposit the same way, gross less fee. A v0.6.8 or later wallet does this; on chain 21 that wallet is v0.8.0, and an older one cannot send there in any case.

Without the group, chains 14 to 19, there is no fee: a deposit credits the gross and a burn releases amount. Until chain 20 the only bridge fee was the source endpoints’ own 10 bps skim of USDT on release, on the host chain; it was set to 0 at chain 20’s cut. The group is committed to the genesis hash and folded into the bridge root under rand-bridge-fees-1. Fullnode docs/bridge.md §25 has the encoding.

Bridge hardening

The per-backing locked and InsufficientBacking guard redemption. They do not bound minting: an attestation with a valid guardian quorum mints what it says. Three layers close that gap and a fourth lets a token be listed without a chain cut. None changes the source-chain endpoints, the attestation wire format or bridge-codec.

B1: a mint cap per backing, and a mint pause

Genesis tokens.mint_cap_per_day applies per backing: chain 21 sets 10000000000000, 100,000 zUSD per backing per day, as every chain since 14 has. Without rules v2 (chain 14) a day is timestamp_ms / 86_400_000 of the block timestamp; under rules v2 (chains 15 to 21) the same cap is measured over a rolling window of cap_window_secs of block time, and the registry-wide global cap — 4,000 zUSD per 86,400 s on chains 20 and 21 — is the tighter bound (rules v2). A deposit past the cap is refused TokenError::MintCapExceeded before the guardian signature work, and is admissible once the window has room again.

Genesis bridge.pause_key is one Dilithium2 key, distinct from every post-quantum guardian. Two bundle-less, fee-less actions:

  • PauseMints { nonce, signature } — signed by the pause key alone. It can only pause.
  • UnpauseMints { nonce, pq_signatures } — needs a post-quantum guardian quorum, never the pause key.

Both carry and bump pause_nonce, so neither replays. While paused every transfer BridgeAttest is refused BridgeError::MintsPaused; burns and guardian-set rotations stay open, so a pause never traps redemption. Under rules v2 (chains 15 to 20) listing is also refused while paused. The pause key is meant to be held on a different machine from the guardian keys.

rand bridge-pause --sig <HEX or @file> [--no-wait]        # no key file and no RAND needed
rand bridge-unpause --pq <JSON or @file> [--no-wait]

What the pause covers: guardian daemons tricked or compromised into co-signing a forged mint while the holders of the post-quantum quorum are not. It is not a defence against theft of that quorum, which can unpause, list up to 32 backings on one token, each with its own daily cap, and under rules v2 replace the post-quantum set and the pause key.

B2: bounded block timestamps

A bridged chain’s block timestamp decides guardian-set expiry and the mint-cap day, so a leader must not be able to jump it. On a chain with a bridge:

  • A validity rule, checked on replay too: timestamp_ms ≤ parent.timestamp_ms + 60_000, else BlockError::TimestampLeap. With the existing rewind rule (BlockError::TimestampRewind), block time only moves forward, by at most a minute a block.
  • A vote rule, never a replay rule: a validator withholds its vote from a block more than 15,000 ms ahead of its own clock.

Every validator on a bridged chain therefore needs NTP: one more than 15 seconds off is a faulty leader and a faulty voter. While a chain catches up after a halt, block time can advance up to 60 s per block, so a mint-cap day can roll over faster than a real day — at most one extra day’s cap per full day of accumulated lag.

B3: a Dilithium2 co-signature on every mint

Every BridgeAttest, a deposit and a rotation alike, must carry beside its ECDSA quorum a quorum of Dilithium2 signatures over

M = "rand-bridge-pq-cosign-1" || rand_chain_id (u64 BE) || mu        // 63 bytes

mu is the digest the ECDSA quorum signs; the chain id keeps a testnet co-signature from verifying on another Rand chain. The signatures ride in the action’s pq_signatures, inside the transaction binding, so they cannot be stripped or swapped on a copy. Checked cheap before expensive: quorum(n) ≤ len ≤ n (PqNoQuorum), indices strictly increasing (PqIndexOrder), every index < n (PqIndexOutOfRange), every signature exactly 2,420 bytes (PqBadSignatureLength), and last that each verifies under pq_guardians[index] (PqBadSignature). The two quorums are counted independently. Genesis bridge.pq_guardians is index-aligned with the genesis ECDSA set (set 1 on chains 20 and 21). A payload-2 rotation moves the ECDSA set only; without rules v2 the post-quantum set is fixed for the chain’s life, and under rules v2 only RotatePqGuardians replaces it (since chain 20 its V2 form, with a possession proof and a one-day delay). Chain 21 has eight, the same eight keys as chains 15 to 20; a mint needs six of them (chain 14 had six and needed five).

This co-signature still binds the chain id only, on chains 20 and 21 too: unlike the governance messages below, it was not moved behind the genesis hash, because the guardian daemons that make it live in another repository and have not changed. A same-id copy of a chain that also copied the guardian set would accept a deposit’s co-signatures made for the original; the deposit transaction’s own fee bundle is genesis-bound since chain 20, the attestation inside it is not.

B4: listing a bridged token after genesis

An ordinary address can never create a Bridge-authority token. Two actions, each on an ordinary RAND fee bundle and each under the post-quantum quorum:

  • RegisterBridgedToken { name, symbol, salt, chain, token, decimals, nonce, pq_signatures } — a new bridged token at the next index, 8 decimals on Rand, under the genesis mint cap, with its first backing. Fee: the bundle base plus the registry’s registration_fee (1 RAND on chains 20 and 21, so 1.001 RAND; since chain 20 the registration fee is burned instead of paid to the proposer, and on chain 21 the base is burned too, under the genesis fees.burn_floor).
  • ListBacking { token_index, chain, token, decimals, nonce, pq_signatures } — a further backing on a registered bridged token, starting at locked = 0. Fee: the bundle base.

Both carry and bump list_nonce (BadListNonce { expected, got } otherwise) and run the same checks: the chain has a registered emitter (BridgeError::NoEmitter), (chain, token) backs nothing yet, fewer than 32 backings, decimals ≤ 18.

rand token register-bridged --name … --symbol … --salt <hex32> --chain N --token <hex32> --decimals N --pq <JSON or @file>
rand token list-backing --asset <index> --chain N --token <hex32> --decimals N --pq <JSON or @file>

Two ordering rules. The guardians sign the registration first, wait for it to commit, read back the assigned index, and only then sign the listings that name it — registering a native token is permissionless, so a listing signed ahead could find its index taken. And a coin is listed on Rand before setToken enables it on the source endpoint: the other way round, a deposit could lock before Rand has a backing to credit, and its attestation would be refused UnlistedToken until Rand caught up.

Governance messages

Fixed bytes, never bincode; all integers big-endian:

M_pause    = "rand-bridge-pause-1"        || chain_id u64 || nonce u64
M_unpause  = "rand-bridge-pq-unpause-1"   || chain_id u64 || nonce u64
M_list     = "rand-bridge-pq-list-1"      || chain_id u64 || nonce u64 || token_index u32 || chain u16 || token [32] || decimals u8
M_register = "rand-bridge-pq-register-1"  || chain_id u64 || nonce u64 || u8 len || name || u8 len || symbol || salt [32] || chain u16 || token [32] || decimals u8
M_rotate_pq    = "rand-bridge-pq-rotate-pq-1"    || chain_id u64 || rotation_nonce u64 || count u32 || key [1312] …   // rules v2
M_rotate_pause = "rand-bridge-pq-rotate-pause-1" || chain_id u64 || rotation_nonce u64 || key [1312]            // rules v2

M_pause/M_unpause carry pause_nonce, M_list/M_register carry list_nonce, and M_rotate_pq/M_rotate_pause carry rotation_nonce. count is the number of keys, not a byte length; each key is a Dilithium2 public key’s raw 1,312 bytes, in the order the new set will have.

Since chain 20 each message is bound to the genesis hash (genesis binding_domain: 1): the tag’s version digit goes to 2 and the 32-byte genesis hash sits between the tag and the chain id, the tail unchanged — M'_pause = "rand-bridge-pause-2" || genesis [32] || chain_id u64 || nonce u64, and likewise rand-bridge-pq-unpause-2, rand-bridge-pq-list-2, rand-bridge-pq-register-2, rand-bridge-pq-rotate-pq-2 and rand-bridge-pq-rotate-pause-2 (bridge/gov.rs). A pause key or a quorum that signs for one chain then signs for no other chain that shares its chain id and its keys. A file signed in the …-1 form is refused on chains 20 and 21 (BadPauseSignature, PqBadSignature). Chains 14 to 19 use the …-1 layouts above, byte for byte.

Rules v2: rotations, rolling caps, no listing while paused

An optional group inside the genesis bridge section. Chain 14 did not have it; every chain since 15 does, chains 20 and 21 with the same values:

"bridge": { …, "rules_v2": { "global_mint_cap_per_window": 400000000000, "cap_window_secs": 86400 } }
  • Two rotations under the post-quantum quorum. RotatePqGuardians { new_pq_guardians, nonce, pq_signatures } replaces the whole post-quantum set; RotatePauseKey { new_pause_key, nonce, pq_signatures } replaces the pause key. Both are bundle-less and fee-less like PauseMints, both are signed by the current post-quantum set, and both carry and bump rotation_nonce, so one signed quorum authorises exactly one rotation. The new set must have the current ECDSA set’s length, 1,312-byte keys, no repeats and not the pause key. There is no grace window: the superseded set or key signs nothing further. Since chain 20 these v1 actions are refused, and a rotation must prove possession of the new keys and wait a day (below).
  • Rolling-window caps. The per-backing mint_cap_per_day is measured over a rolling window of cap_window_secs instead of the calendar day, and a second, registry-wide cap, global_mint_cap_per_window, covers every backing of every token together (TokenError::GlobalMintCapExceeded). On chains 20 and 21 that is 400,000,000,000 units, 4,000 zUSD per 86,400 s. Under the day rule, the cap at 23:59:59 and again at 00:00:01 was twice the cap in two seconds; under the window the second is refused.
  • No listing while paused. RegisterBridgedToken and ListBacking are refused MintsPaused while mint_paused. Burns and both rotations stay open.

Rotation: possession and a one-day delay

Rules v2 left three things open: nothing proved the new keys were held by anyone (a typo, or a key nobody holds, would brick the post-quantum quorum for good), a rotation took effect in the block it landed in, and nothing could stop a hostile one. A second optional group beside rules_v2 (which it requires), new in v0.6.8 (audit v6, BRG-14), closes them. Chain 20 set it and chain 21 carries it:

"bridge": { …, "rotation": { "delay_secs": 86400, "needs_possession": true } }
  • Possession. A rotation must be RotatePqGuardiansV2 { new_pq_guardians, possession, nonce, pq_signatures } or RotatePauseKeyV2 { new_pause_key, possession, nonce, pq_signatures }: possession[i] is new key i’s own Dilithium2 signature over the very message the quorum signs (M_rotate_pq or M_rotate_pause, in their …-2 form since chain 20), one per key in key order, 2,420 bytes each. The v1 actions are refused PossessionRequired. A wrong count or length is refused before the quorum is verified; a bad possession signature (BadPossession { index }) after it.
  • A delay. An accepted rotation is recorded as pending, with effective_at_secs = block time + delay_secs, and takes effect at the end of the first block whose timestamp reaches it. On chains 20 and 21 that is 86,400 s, one day. Until then the old set co-signs every mint and the old pause key pauses. Accepting a rotation spends one rotation_nonce and activating it spends another, so a mint attestation pooled under the old set leaves the pool when the set changes. One rotation of each kind may pend at a time (RotationPending { effective_at_secs }).
  • The cancel. CancelRotation { kind, nonce, signature } (bundle-less, fee-less) drops the pending rotation of kind (0 = the post-quantum set, 1 = the pause key). It is signed by the current pause key, the one key held apart from the quorum, over "rand-bridge-cancel-rotation-2" || genesis [32] || chain_id u64 || rotation_nonce u64 || kind u8 since chain 20, and spends the nonce. A captured quorum therefore cannot rotate the set in under a day, and the pause-key holder can stop it in that day; the incoming pause key cannot cancel its own arrival.

The signer is rand-node bridge-gov (rotate-pq, rotate-pause, cancel-rotation, each with message, sign and submit steps; submit re-verifies every possession line before sending). rand_getBridgeState serves rotation_rules and pending_rotations; both are null on a chain without the group (chains 14 to 19). The ECDSA guardian-set rotation (payload 2) is not affected: it keeps its own rules and grace window. No rotation has been run on chain 20 or chain 21.

Carried over from chain 14

Chain 15 was cut from chain 14’s bridge position (the same emitters and source endpoints), and chains 16, 17 and 18 were each cut from their predecessor’s the same way. Chain 19 was cut from chain 18’s position with one change: its Ethereum, BSC and Tron emitters are the endpoints redeployed on 2026-09-30 (0x7af6b17047c1db6cb54347fdea45cf9179075bfa for chains 2 and 3, 0x6410797df959987a5baf65b5fab97edeb34d5163 for chain 4). Chain 18 never trusted them, so nothing from them was minted there. Chain 20 was cut from a snapshot of chain 19 at height 7,258, with the bridge stopped first, and kept chain 19’s emitters, guardians, post-quantum keys and pause key unchanged; what it added was the fees and rotation groups. Chain 21 was cut on 2026-10-12 from chain 20’s bridge state at height 814,702 (the bridge stopped 07:40–07:41 UTC, the snapshot at 07:41), and keeps every one of them — emitters, guardians, post-quantum keys, pause key, both groups — unchanged; the relayer and the guardians were restarted for chain 21 with the v0.8.0 rand. Solana’s emitter and the Rand-side emitter have not changed since chain 14. What chain 21’s genesis carries:

  • Custody. 100.001999 zUSD, as three genesis zUSD notes (10, 89.002999 and 0.999 zUSD), against 22 USDT locked on Ethereum, 23.001999 USDT on BSC, 30 USDT on Tron and 25 USDT on Solana, and 0 on every USDC backing: chain 20’s locked at the snapshot, which matched the endpoints’ custody. total_supply == Σ locked equals the endpoints’ custody at block 0. History: the 10 zUSD left on chain 14 were one genesis note on chains 15 to 20; chain 18’s genesis held them as 9 USDT locked on Tron and 1 on Solana, and chain 19’s as 10 on Solana after a rebalancing on chain 18. Chain 20’s genesis carried 99 zUSD as two notes (10 and 89, chain 19’s net deposits less its burns); chain 20’s two BSC deposits and one burn, and the 10 bps fees the treasury kept on them, make up the rest.
  • Guardian set index 1, where the source endpoints already are; the next ECDSA rotation must carry index 2. The eight post-quantum keys and the pause key are chain 15’s, unchanged.
  • burn_sequence 14: chain 14’s burns ended at 6, the genesis of chains 15 to 18 started at 7, chain 18 made one burn (7) and chain 19 started at 8. Chain 19 made five burns, sequences 8 to 12, chain 20 started at 13 and made one burn (13), so chain 21 starts at 14, and no endpoint or daemon keyed by sequence sees one twice.
  • Replay. An earlier chain’s mint would still pass the ECDSA quorum, but its post-quantum co-signature binds that chain’s id, so it is refused on chain 21. Chain 15’s genesis had no inbound replay floor (min_inbound_sequence was null), so there this rested on the co-signature and on guardians never co-signing an old lock for the new chain id. Since chain 16 the genesis sets one: per source chain, the lowest sequence a transfer attestation from that chain’s emitter may carry. Chain 21’s is { "2": 3, "3": 5, "4": 3, "5": 7 }: each endpoint’s next outbound sequence when chain 20 was snapshotted, every earlier lock having minted on chain 19 or chain 20. (Chain 20’s was { "2": 3, "3": 3, "4": 3, "5": 7 }; chain 18’s was { "2": 2, "3": 2, "4": 2, "5": 2 }; chain 19’s was { "2": 1, "3": 1, "4": 1, "5": 4 }, where sequence 0 on each redeployed endpoint was the operator’s own lock, which must never mint.) A transfer below the floor is refused BelowReplayFloor, whatever quorum and co-signature it carries. Guardian-set rotations are not judged by it. The floor is fixed at genesis, committed to the genesis hash and wrapped into the bridge root.

Launch order on chain 14 (history)

How the bridge came up on chain 14, an earlier testnet, in September 2026:

  1. Cut the chain with the bridge section (pq_guardians and pause_key included) and a tokens section setting registration_fee and mint_cap_per_day, listing no token. Done 2026-09-20.
  2. Enable the faucet so a deployer wallet can obtain RAND. Done.
  3. The deployer registers zUSD with its first backing under the quorum’s signature over M_register at list_nonce = 0. Done at height 256.
  4. The guardians read back the index and sign the six remaining ListBacking messages, submitted one at a time. Done: list_nonce reads 7.
  5. For each backing: list on Rand before setToken on the source endpoint.
  6. The guardian-set rotation (set 0 → set 1, signed 5 of 6 by the current set) on the four source endpoints, then the same attestation on Rand. Rand read set 0 for chain 14’s life; chain 15 starts at set 1.
  7. A live round trip per source chain with small amounts — bridge in, mint zUSD, transfer, burn back — and a reconciliation of each backing’s locked (rand_getTokenSupply) against the endpoint’s custody.

Public state

rand_getBridgeState:

{
  "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": "400000000000", "cap_window_secs": 86400,
                "global_minted_in_window": "…", "global_mint_headroom": "…" },
  "min_inbound_sequence": null,
  "rotation_rules": null, "pending_rotations": null,
  "fees": null,
  "assets": [ … ]
}

pq_guardians are Dilithium2 public keys, 1,312 bytes each as hex. There is no next_index any more: a bridged token is listed, under an index its registration fixed, before it can be deposited, so there is no index left to predict. rotation_nonce is what the next M_rotate_pq/M_rotate_pause/M_cancel must carry; rules_v2 is null on a chain without the group (chain 14), and min_inbound_sequence is null on a chain without a replay floor (chain 15 and earlier); on chain 21 it is { "2": 3, "3": 5, "4": 3, "5": 7 } (chain 20’s was { "2": 3, "3": 3, "4": 3, "5": 7 }). rotation_rules, pending_rotations and fees are null on a chain without those groups (chains 14 to 19). The sample above shows the shape, not chain 21’s values: chain 21 started at guardian_set_index 1 and burn_sequence 14, and serves "rotation_rules": { "delay_secs": 86400, "needs_possession": true }, "pending_rotations": [] and "fees": { "mint_bps": 10, "burn_bps": 10, "recipient": "rand1…" }. A chain without a bridge section answers {"enabled": false}.

No balances: bridged value is notes, and a wallet reads what it holds with rand asset-balance [INDEX] from its own notes. The bridge root commits the emitters, the guardian sets, the spent digests, the burn sequence, the post-quantum guardian set, and the pause key, pause flag and governance nonces (two, and rotation_nonce under rules v2), the replay floor where the genesis sets one, and since chain 20 the rotation rules with any pending rotation and the fee group; it folds into the state root only on a chain that has a bridge. The token registry, with every backing’s locked, is committed separately as tokens_root. The explorer’s GET /bridge and GET /bridge/assets serve the same state, the second with per-backing deposit and burn totals as 8-decimal strings; GET /tokens and GET /tokens/:id serve the registry.

What is public and what is hidden

whatwho sees it
a deposit’s amount, asset index and recipient addresseveryone, in that one transaction
which note a recipient later spendsnobody
a burn’s amount, asset, coin, destination and relayer feeeveryone — the guardians need it
a bridge fee note’s amount, asset and owner, the treasury (since chain 20)everyone: it is rebuilt from public fields
which notes funded a burnnobody
bridged balancesonly the holder, or a viewing-key holder
guardian sets, emitters, the registry with each backing’s locked, the burn logeveryone

Known gaps

From the fullnode’s own list: no relayer is paid on Rand; a deposit’s envelope is bound to nothing, so a hostile relayer can seal garbage, which is why the wallet rebuilds deposit notes from public fields instead; the ledger-level shared test vectors have not been rebuilt on the note pool; bridge-burn still learns about the destination endpoint’s own policy only after paying for a proof; equal-to-parent block timestamps are allowed on a bridged chain, so colluding leaders could hold a superseded guardian set inside its grace window; the in-memory burn log is unbounded; no light client or on-chain verification of source-chain state exists or is planned.

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.