Rand Protocol
Documentation
Guides

Staking

Registering a validator, admission by the set's vote, bonding stake from a wallet, unbonding and withdrawing, the payout address, epochs, the stake-entry budget, and why there is no slashing on the live chain.

Use this page to join the validator set on a running chain, to leave it, and to read the register back. Consensus weight must be public, so the validator register is the one table on this chain that holds amounts in the clear; the stake still comes out of a shielded note and goes back into one.

The register

One row per validator that has ever bonded, keyed by its base58 address:

ValidatorEntry {
    public_key,                  // Dilithium2: signs blocks, and this validator's staking actions
    stake:    u64,               // bonded: the weight an epoch's set gives it
    pending:  Vec<(u64, u64)>,   // unbonding, as (release_epoch, amount), oldest first
    rewards:  u64,               // fees earned as a block proposer, unpaid
    payout:   ShieldedAddress,   // rand1… — where a withdraw pays
    nonce:    u64,               // replay protection for its signed actions
    activation_epoch: u64,       // under a `staking` section: the first epoch this row may be in a set
}

Every field is public and the register is hashed into every block’s state root. A row is never deleted: a validator that unbonds everything keeps a row with stake = 0.

constantvaluemeaning
MIN_STAKE1,000 RANDstake a row needs to be in an epoch’s set at all
MAX_VALIDATORS100largest set an epoch can have
UNBONDING_EPOCHS2epochs an unbonded amount waits before it can be withdrawn
epoch_blocks1,000 on chain 21, as on chain 20blocks per epoch; a genesis parameter

Epochs

epoch(h) = h / epoch_blocks. The set for epoch e ≥ 1 is derived from the register as of the last block of epoch e − 1: every row with stake ≥ MIN_STAKE, the top MAX_VALIDATORS by (stake descending, address ascending), sorted by address. The leader of a view, the voters of a block and the verification of a certificate all use the set of the epoch the block belongs to. Two consequences:

  • A bond is not weight until a later boundary. It is in the register at once, but the set changes only at a boundary — the next one on a chain without a staking section, later on chains 20 and 21 (below).
  • An unbond costs weight at the next boundary, while the amount waits two epochs to become withdrawable. A validator stops proposing long before it can withdraw.

If a derivation is empty, the previous set is carried forward. A node started with --validator whose key is in no current set observes until an epoch admits it, so a validator that bonds in after genesis does not need a restart; rand_status reports is_validator (the key is here) and active_validator (it is in the current set) separately.

The staking section

A genesis may carry a staking section (fullnode v0.5.4, audit v4 STAKE-2). Without it the rules above are the whole story, as on chain 14. Every chain since chain 15 carries it. Chain 21’s, unchanged from chain 20’s, switches on:

"staking": {
  "faucet_budget_per_epoch": "10000000000000", "bond_activation_epochs": 2,
  "max_weight_bps": 3333, "max_stake_entry_bps_per_epoch": 2500,
  "admission_by_vote": true, "registration_v2": true,
  "faucet_recipients": ["rand1…", "…16 wallets…"],
  "faucet_minters": ["<validator address>", "…18 validators…"]
}
fieldchain 21 (as chain 20)rule
bond_activation_epochs2a bond applied in epoch e is queued for epoch e + 1 + 2; a registration and a top-up alike. Its amount is in stake at once but is not weight until then, and queued stake cannot be unbonded
max_stake_entry_bps_per_epoch2,500 (25 %)the most stake, registrations and top-ups together, that becomes weight at one boundary, as a fraction of the weight already active there: ⌊active · bps / 10,000⌋. Due rows past it wait for the next boundary, in bond order. On chain 21’s 26,000 RAND of active stake that is 6,500 RAND a boundary. An entrant’s share after k boundaries is at most 1 − 1.25^−k at any scale, about a third after two
admission_by_votetruea bond that registers a new key is refused (NotAdmitted) until validators holding more than two thirds of the voting weight have voted the key in (below); a top-up needs no vote
max_weight_bps3,333no validator’s weight exceeds 33.33 % of its set’s total; the excess is clamped in the set, the register’s stake is unchanged
registration_v2truea registration signs rand-register-2 over the genesis hash, the chain id, the validator address and the payout; a v1 registration is refused
faucet_budget_per_epoch10,000 RANDthe faucet mints at most this per epoch (FaucetBudgetExhausted past it; the counter restarts at the next epoch)
faucet_recipients16 walletsa faucet Mint pays only these wallets (FaucetRecipientNotAllowed); this list is what lets a faucet sit beside the bridge
faucet_minters18 validatorsa faucet Mint may be signed only by these validator keys (MinterNotAllowed), checked at admission and when a block applies, however long another key has been bonded. On chain 21, as on chain 20, they are 18 of the 26 genesis validators. The list has been in every genesis since chain 16
slashingabsentno slashing section, so no validator is slashed or jailed (What is public)

Under the section the faucet counter and the bond queue are appended to the state root, under the domain rand-state-5, and the validator leaf is rand-validator-leaf-4. Chains 20 and 21 also have dynamic gas prices and RPL-2 program state, appended after them, which move the domain to rand-state-8 (chain 21’s perps state moves it on to rand-state-9, and its multisig register is wrapped in under rand-state-multisig-1; Consensus), and admission by vote wraps that root once more as H("rand-state-admitted-1", root ‖ admitted_root). Chain 15 had no faucet_minters list: there, every node pooled and forwarded a faucet Mint only when a genesis validator signed it (MinterNotAllowed, as node policy), so that a validator bonded after genesis could not drain the budget through the pool. With the list, the same refusal is a validity rule.

History: chains 15 to 19 had no admission_by_vote, so any key holding 1,000 RAND could register, and they set the entry budget as a fixed max_stake_entry_per_epoch of 10,000 RAND a boundary. A fixed figure is sized to one genesis; the fraction holds an entrant to the same share whatever the stake. The two fields cannot both be set.

Admission by vote

New in fullnode v0.6.8 (audit v6, STAKE-2) and on for chains 20 and 21. With 26 keys of 1,000 RAND each, a blocking third of the set is about 13,000 RAND of bought stake, and nothing is slashed. Under this rule the set decides which new keys may register at all.

  • The vote is an action, AdmitValidator { candidate, signatures }, bundle-less and fee-less. Each signature is a validator’s Dilithium2 signature over blake3("rand-admit-validator-1" ‖ genesis hash ‖ candidate address), in ascending order of the voters’ addresses. The voters must hold strictly more than two thirds of the voting set’s weight, the arithmetic of a quorum certificate: on chain 21, as on chain 20, 18 of the 26 validators. A vote is bound to one genesis and needs no nonce: an admission is consumed by the registration it permits.
  • The voting set is the register’s own derivation for the current epoch. A queued bond is not weight in it, and an unbond leaves it at once. A candidate cannot vote itself in.
  • The admitted set is consensus state, at most 256 keys waiting to register (AdmissionSetFull past it). rand_getAdmitted serves it; rand_getLimits.admission_by_vote says whether a chain needs the vote.
# each voting validator, offline
rand-node admit sign --candidate <registration hex or public key> --key node.key.json --genesis-hash <hash>
# whoever collects the lines
rand-node admit submit --candidate <…> --signature <line> --signature <line> … --rpc http://127.0.0.1:8545

Then the bond goes ahead as below. rand_getGenesisHash returns the hash admit sign needs.

The four commands

Which binary owns which action follows from which key signs it: a bond spends notes, so it is a wallet command; an unbond and a withdraw are signed by the validator’s Dilithium2 key, which only the node holds.

actioncommandrides onpays
registerrand-node registernothing — prints a blobnothing
Bondrand bonda bundle whose burn_r is the stake0.001 RAND out of the wallet’s notes
Unbondrand-node unbondnothing (bundle: null)nothing
Withdrawrand-node withdrawnothing (bundle: null)0.001 RAND out of the amount withdrawn

Register

rand-node register --key node.key.json --payout "$(rand --key payout.key.json address)" --v2 [--rpc http://127.0.0.1:8545]

Prints the validator’s address and a hex Registration: its public key, the payout address and a signature. With --v2, which chains 20 and 21 require, the signature is under rand-register-2 over the chain’s genesis hash, the chain id, the validator’s address and the payout; without it, the v1 form, blake3("rand-register" ‖ bincode(chain_id, payout)), which they refuse (on a binding_domain: 1 chain such as chain 20 the registration message is the v2 one in any case). The RPC is read for the chain id and, with --v2, the genesis hash, so a registration signed for one chain is refused on another. The payout address is a wallet address, not a node key, and it is the one field a later top-up cannot change. Hand the hex to whoever holds the stake.

Bond

rand bond <validator base58> <amount in RAND> [--registration <hex>] [--fee <RAND>] [--no-wait] [--cuda]

An ordinary shielded transaction: the wallet proves the 4-in-4-out hidden-asset bundle every transaction has carried since chain 14, with the staked amount in its RAND burn, burn_r, and the ledger admits a bond only when burn_r == amount. On chain 21 the bundle carries two proofs, as every bundle has since chain 17: the tier-14 bundle proof and a tier-10 authorisation proof over the spend key. Both are proved locally by default, in about two minutes on an Apple Silicon laptop (Proving memory). With the wallet’s --prover option the bundle proof is made by a paired prover instead, and the authorisation proof stays on the wallet’s machine. --registration is required exactly when the validator is not in the register yet, and refused when it is; the wallet asks the register first. On chain 20 a registering bond is refused NotAdmitted unless the set has voted the key in first (Admission by vote); a top-up needs no vote. Registering bonds at least 1,000 RAND; a top-up can be any amount above zero. What the chain learns is that this validator’s stake grew by this much; which notes paid, and whose they were, is hidden as in a transfer — anyone can stake onto a validator without revealing anything but the amount.

submitted bond 8c3f…e1
  0 RAND out, 1000 RAND burned, 0.999 RAND change, fee 0.001 RAND, anchored at height 412
2nRdFC…: stake 1000 RAND, counting as consensus weight from epoch 69

The epoch the wallet prints is the next boundary. It does not count the activation delay: on chain 21, as on chain 20, a bond committed in epoch 68 is weight from epoch 71 at the earliest, later if the entry budget, a quarter of the active weight per boundary, is spent.

Unbond

rand-node unbond <amount in RAND> --key node.key.json [--rpc …] [--no-wait]

Moves amount from stake into pending with release_epoch = epoch + 2 and increments the nonce. Free, bundle-less, signed over blake3("rand-unbond" ‖ bincode(chain_id, validator, amount, nonce)) on chains 14 to 19 and over blake3("rand-unbond-2" ‖ bincode(genesis_hash, chain_id, validator, amount, nonce)) since chain 20 (genesis binding_domain: 1); it commits in a block’s time. rand-node v0.6.8 picks the form for the chain it talks to. Unbonding more than the stake, or zero, is refused; several unbonds in one epoch merge into one pending row.

Withdraw

rand-node withdraw <amount in RAND> --key node.key.json [--rpc …] [--no-wait]

Pays released value — rewards plus the pending rows whose release_epoch has arrived, oldest first — into a deposit note at the register’s payout address. It is validator-signed (since chain 20 under rand-withdraw-2, over the genesis hash first, like the unbond) and bundle-less but not free: the 0.001 RAND base goes to the proposer of the block that applies it, out of the amount, so a withdrawal of 1,000 RAND creates a note worth 999.999 and an amount that cannot cover the base is refused. The chain computes the note itself:

cm = H_CM(payout.pk, from = 0, amount − 0.001 RAND, asset 0, time, r)

time is the head height when the command ran (printed, and bound by the signature), admitted anywhere inside the bundle window (1,024 blocks on chains 20 and 21, whose genesis sets proof_window_blocks; 256 on earlier chains); r is published so the chain never takes a commitment from the wire. Afterwards the payout wallet finds the note by scanning, with nothing told to it:

rand --key payout.key.json balance      # balance: 999.999 RAND

Joining and leaving

# on the joining validator's machine
rand-node register --key node.key.json --payout "$(rand --key payout.key.json address)" --v2
rand-node run --datadir ./data --key node.key.json --validator --bootstrap /ip4/…/p2p/…
#   it syncs and observes; rand_status says is_validator true, active_validator false

# since chain 20: validators holding more than two thirds of the weight vote the key in
rand-node admit sign --candidate <hex from above> --key <their node.key.json> --genesis-hash <hash>
rand-node admit submit --candidate <hex from above> --signature <line> … --rpc http://127.0.0.1:8545

# on the machine holding the stake
rand bond <validator base58> 1000 --registration <hex>
rand validators                       # the new row, active false until it activates

# from its activation epoch on (on chain 21, as on chain 20, three boundaries after the bond at the earliest)
rand-node status                      # active_validator: true
rand-node unbond 1000 --key node.key.json     # out of the set at the next boundary
#   wait two epochs
rand-node withdraw 1000 --key node.key.json    # a note worth 999.999 RAND at the payout address

Check the quorum arithmetic before unbonding: a set of n needs more than 2/3 of its stake online. On chain 21, as on chain 20, all 26 validators sit exactly at MIN_STAKE, so any unbond at all drops that node out of the next epoch’s set, and 18 of the 26 must stay up. A validator that is up but does not vote counts against the quorum too: since chain 14 a validator withholds its vote from a block more than 15 s ahead of its own clock, so a validator whose clock runs slow stops voting without an error. Run NTP on it (Running a validator).

Genesis seeds the register directly — one entry per --validator <key or hex pubkey>,<stake>,<payout> — and refuses a stake below the minimum. Chain 21’s genesis seeds 26 rows of 1,000 RAND each, the same keys as chains 17 to 20’s.

Since v0.5.11 the build also carries genesis vesting: a public register of timelocked allocations, from which an irrevocable entry can bond locked RAND as a validator’s stake (rand-node vesting bond) and take it back (rand-node vesting unbond). It needs a vesting section in the genesis. No chain has one, chain 21 included, and without it those actions are refused at admission. (v0.6.8 also lets a revocable vesting entry be revoked only by a threshold of signers to a fixed treasury, again only under a vesting section.)

What is public

publichidden
the registerevery row: address, stake, unbonding queue, unpaid rewards, payout address, noncenothing
Bondthe validator, the amount, whether a registration was attached, the bundle’s burn_rwhich notes paid, who owns them, the change
AdmitValidator (since chain 20)the candidate and every voter with its signaturenothing else exists
Unbondthe validator, the amount, the noncenothing else exists
Withdrawthe validator, the amount, the nonce, the note’s time and rwho can open the note, and every later spend of it

Rewards are the fees of the blocks a validator proposed — through chain 20 the whole fee; on chain 21 (fullnode v0.8.0, live since 2026-10-12) only the tip above each bundle’s floor, since the genesis fees section burns the floor itself (Fees). There are no block rewards and no inflation, and on chain 21, as on chain 20, there is no slashing and no jailing of validators: a validator that misbehaves costs its stake nothing, and the remedy is the operators’. Fullnode v0.6.8 does carry a slashing rule for leader equivocation (audit v6, STAKE-1: two signed block headers for one view by one key destroy equivocation_bps of the stake and jail the key), but only under a genesis staking.slashing section, and chain 20 has none (rand_getLimits.slashing is null); the fullnode recommends it only once stake is held by more than one operator. (The block-aggregation code also carries a SlashAggregator action for aggregators who sign two different aggregates at one nonce, and it is not active on chain 20, which has no aggregation section.)

Reading it back

rand validators        # the whole register
rand status            # is_validator / active_validator for the node asked
→ {"method":"rand_getValidators","params":[]}
← [{ "address": "2nRdFC…", "stake": "1000000000000",
     "pending": [{ "release_epoch": 71, "amount": "1000000000000" }],
     "rewards": "4000000", "payout": "rand1…", "nonce": 3, "active": true, "jailed_until": null }]
→ {"method":"rand_getAdmitted","params":[]}
← { "admission_by_vote": true, "admitted": [], "max": 256 }
→ {"method":"rand_getEpoch","params":[]}
← { "epoch": 69, "epoch_blocks": 1000, "next_set": ["2nRdFC…", "ByDkxs…"] }

Amounts are decimal strings of units. active says whether the row is in the set running the current epoch; jailed_until is null except under a slashing section; admitted lists keys voted in that have not registered yet; next_set is a projection every bond and unbond before the boundary moves. The explorer’s GET /validators adds share_percent, blocks_proposed and the last proposed height.

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.