Phase S2 gives the shielded chain its stake. Consensus weight has to be public — every replica must agree on who may propose a block and whose votes make a quorum — so the register is the one table on this chain that holds amounts in the clear. Everything else stays in the pool: the stake a validator bonds comes out of somebody's note, and the stake it withdraws goes back into a note, and neither end says whose.
This page is the operator's guide to that: the register, the epochs, the four commands, what is
public, and how to read it back. The supply audit that ties the register and the pool together is
docs/supply.md; the design spec is
docs/superpowers/specs/2026-09-11-shielded-pool-design.md §8; the wire-level reference is
docs/rpc.md.
One row per validator that has ever bonded, keyed by its base58 address:
ValidatorEntry {
public_key, // Dilithium2: signs blocks, and signs 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, // the replay protection for its signed actions
activation_epoch: u64, // v0.5.4: the first epoch this row may be in the set of (§2)
}Every field of every row is public, and the register is hashed into every block's state root
(leaf domain rand-validator-leaf-2; rand-validator-leaf-4, with activation_epoch appended,
on a chain whose genesis has the staking section of §2), so a node that disagrees about one of
them disagrees about the chain. A row is created by genesis or by the bond that registers the validator, and is never
deleted: a validator that unbonds everything keeps a row with stake = 0, which is simply in no
epoch's set.
| constant | value | meaning |
|---|---|---|
MIN_STAKE |
1000 RAND | stake a row needs to be in an epoch's set at all |
MAX_VALIDATORS |
100 | largest set an epoch can have |
UNBONDING_EPOCHS |
2 | epochs an unbonded amount waits before it can be withdrawn |
epoch_blocks |
1000 (genesis) | blocks per epoch; rand-node genesis --epoch-blocks |
epoch(h) = h / epoch_blocks, so genesis (height 0) is in epoch 0 and the set for epoch 0 is the
genesis validators. 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_VALIDATORSof those by(stake descending, address ascending), - sorted by address.
That rule is written once, in staking::derive_set, and it is a pure function of the register — which
is what lets every replica derive the same set from the same parent block without agreeing on
anything else first. Consensus then uses it in three places: the leader of a view comes from the set
of the epoch the next block belongs to, a block is voted by the set of its own epoch, and a quorum
certificate is verified against the set of the epoch of the block it certifies. Each epoch's set is
persisted when its first block commits, so replay and sync verify old certificates without holding
old registers.
Two consequences worth knowing:
- A bond is not weight until the next epoch. It is in the register the moment it commits, and
rand_getEpoch'snext_setshows it immediately, but the set only changes at a boundary. - An unbond costs weight at the next boundary, not two epochs later. The
UNBONDING_EPOCHSwait is about the amount becoming withdrawable; the set is re-derived at the very next boundary, so a validator stops proposing long before it can withdraw.
If an epoch's derivation is empty — every validator below the minimum — the previous epoch's set is carried forward, because an epoch with no leader is a halt nothing could end.
Audit v4 pointed out that on a faucet chain the register was free to buy: Mint hands any
validator key 100 RAND per transaction with no counter, so ~91 mints and one Bond reached a
third of chain 14's genesis stake, active at the next boundary. Three rules close that. All three
are switched on by one optional genesis section and are off — behaviour and hashes
byte-for-byte unchanged — on any chain whose genesis lacks it, chain 14 included:
"staking": { "faucet_budget_per_epoch": "100000000000", "bond_activation_epochs": 2 }- Activation delay. A row created by a
Bondin epocherecordsactivation_epoch = e + 1 + bond_activation_epochs, andderive_setskips a row whose activation epoch is past the epoch it is deriving for: the bond is in no set for epochse + 1 ..= e + Nand joins ate + N + 1. Genesis validators carry 0 and are in every set from epoch 0; a top-up of an existing row never moves its activation epoch — but since the bond queue below, the top-up's own amount waits exactly as long as a registration does. Withbond_activation_epochs: 0the rule is exactly today's ("weight at the next boundary"). The field is hashed into the validator leaf (rand-validator-leaf-4) only under the section. - The bond queue (the v4 re-review's "delay" gap). Under the section every
Bond— a registration and a top-up of an active row alike — adds its amount to the row'sstakeat once (the supply audit,rand_getValidatorsand the overflow bound see bonded value where it is) and appends(validator, amount, epoch = e + 1 + bond_activation_epochs)to a ledger-wide queue, in the order the bonds are applied (a validator's consecutive rows for one epoch merge). A row's set weight is itsstakeless what of it is still queued for a later epoch, so before the queue an active validator could top up and be weight at the very next boundary while a fresh key served the whole delay; now neither can. Queued stake cannot be unbonded (InsufficientStakenames the active part). The last block of every epoch admits the rows due at the next one (below), and admitted rows leave the queue. The queue is consensus state: its root (rand-bond-queue-1, length then every row in order) is appended to therand-state-5root, it is persisted beside the supply counters (META_BOND_QUEUE) andrand-node verifyreplays it. - The faucet budget. The ledger keeps
(faucet_epoch, faucet_minted_in_epoch); aMintthat would push the epoch's total overfaucet_budget_per_epochis refused (FaucetBudgetExhausted), and the counter starts from zero in the first block of the next epoch — the epoch is the ledger's own,height / epoch_blocks, never wall time. The refusal is state, not bytes: the node never caches it as permanent, and the same transaction is admitted again an epoch later. Both counters are consensus state under the section: the state root is re-domainedrand-state-5with them appended, they are persisted beside the supply counters (META_FAUCET_EPOCH) andrand-node verifyreplays them.rand_getSupplyreports them asfaucet_epoch/faucet_minted_in_epoch. - A bridged chain has no faucet.
faucet: truebeside abridgesection is refused at genesis (GenesisError::FaucetWithBridge) once the section is present: free RAND against a chain holding bridged custody is what the finding is about. Chain 14's genesis has both and no section, so it still loads — and a testnet that wants both names its testers instead (faucet_recipients, below).
Three further fields close what the v4 re-review still found open in the gated rules ("admission, weight cap, proof of possession"). Each is optional inside the section, omitted from the file when absent, and committed to the genesis hash by name only when present, so a section without them hashes exactly as v0.5.4's did:
"staking": {
"faucet_budget_per_epoch": "100000000000", "bond_activation_epochs": 2,
"max_weight_bps": 3333, "max_stake_entry_per_epoch": "10000000000000", "registration_v2": true
}max_weight_bps— the weight cap. No validator's voting weight exceeds this fraction of its set's total, in basis points (3333 = a third;1..=10000, 10 000 = no cap). The cap is applied once, instaking::cap_weights, to every setderive_set_withderives and to the genesis set, so the quorum and third checks, QC verification and everything else that reads aValidatorSetsee the capped weights and nothing else; the register'sstakeis unchanged. Clamping lowers the total, so a one-pass cap at a third of the unclamped total would leave the clamped validator above a third of the new one. Instead one levelCis computed: with the weights sorted descendingw_0 ≥ … ≥ w_{n−1}andR_k = w_k + … + w_{n−1}, clamping exactly the topkneedsC ≤ b·(k·C + R_k)/10⁴, whose largest integer solution isC_k = ⌊b·R_k / (10⁴ − b·k)⌋; the smallestkwithC_k ≥ w_kis taken, every weight aboveC_kis lowered to it and nothing else moves. A set of fewer than ⌈10⁴ / b⌉ validators cannot meet the cap at all (three equal validators hold a third each) and is levelled to its smallest weight. A faucet-bought whale with 96 % of the stake ends with a third of the weight — no quorum alone, and not even a blocking third. The cap binds one key, not one operator: many keys are what the entry budget and the delay are for.max_stake_entry_per_epoch— the admission (churn) limit. The most stake, registrations and top-ups together, that may become weight at one epoch boundary, in RAND's base unit as a decimal string (> 0). The last block of epoche − 1walks the bond queue front to back and admits the rows due byewhile the budget lasts — a row in part when it runs out mid-row — and moves whatever of the due rows is left to epoche + 1, keeping its place. The set foreis derived from that very ledger, so it counts exactly what was admitted. Absent, every due row is admitted (the delay rule alone). A large bond at the head of the queue holds everyone behind it for as many epochs as it needs — the price of an order nobody can jump.max_stake_entry_bps_per_epoch— the same limit as a fraction (audit v6, STAKE-2). A fixed RAND figure is sized to one genesis: 10 000 RAND an epoch holds an entrant to a third of chain 18's 26 000 RAND for two epochs, and of a 26 000 000-RAND genesis for thirteen hundred — so the safety it buys scales with the stake the genesis happens to carry. As basis points (1..=10000) of the active weight at the boundary — the register as the set derivation reads it for the epoch being opened, with every queued row still waiting, uncapped, the top 100 — the budget is⌊active · bps / 10 000⌋and an entrant's share afterkboundaries is1 − (1 + b)^−kat every scale (a quarter an epoch: a third in two epochs, whatever the unit). What a boundary admits never counts towards its own budget; a budget under one registration admits it in parts across boundaries, as the fixed figure does. Refused besidemax_stake_entry_per_epoch(one budget), committed to the genesis hash under its own tag only when set. Chain 18 keeps its fixed figure.registration_v2— proof of possession, bound to the chain. The v1 registration a new row carries is the key's signature over(chain_id, payout)underrand-register: it proves the key, but binds neither the chain's genesis (any chain sharing the id accepts it) nor the address it registers. Withtruethe registration isrand-register-2over(genesis hash, chain_id, validator address, payout)— the genesis-hash binding the consensus domain's v1 tags use — and a v1 registration is refused (BadSignature).rand-node register --v2signs it (the genesis hash is read from--rpc). Absent orfalseis the v1 rule and commits nothing.
One more optional field lets a testnet keep a faucet beside a bridge (chain 15):
"staking": {
"faucet_budget_per_epoch": "100000000000", "bond_activation_epochs": 2,
"faucet_recipients": ["rand1…the operator's address…", "rand1…a second tester's…"]
}faucet_recipients— the faucet allowlist. AMintpublishes its note's opening, and the ledger recomputescmfrom it (POOL-1), so the note's ownerpkis on the wire; under this list aMintwhosepkis not on it is refused (FaucetRecipientNotAllowed) at admission and at apply, before any signature work. The faucet then feeds the named wallets and cannot buy the register for anyone else, which is why the list — and only the list — letsfaucet: truesit beside abridgesection. The budget still applies on top. An entry is either a wholerand1…address, exactly whatrand --key <wallet.key.json> addressprints (itspkis taken, its ML-KEM key dropped), or thatpk's 64 hex characters; the file is written back as hex, and only the 32pkbytes are committed to the genesis hash, key by key in file order, so both spellings are the same chain. An empty or duplicated list is refused atinit(BadStaking). The refusal is about the transaction's own bytes against a genesis constant, so the node caches it as permanent. A tester outside the list is sent coins by a listed wallet, like anyone else.
Who may sign a faucet Mint (RESCAN-LEDGER-1). The ledger's rule is a row in the validator
register. A permissionless Bond writes that row at once, and the bonded key is in the active set
bond_activation_epochs + 1 epochs later, so neither bounds who mints: anyone with MIN_STAKE
and one allowlisted wallet could register a key, mint each epoch's whole faucet budget to that
wallet ahead of the operator (FaucetBudgetExhausted) and bond the proceeds. Every node since
this fix pools and forwards a Mint only from a genesis validator (MinterNotAllowed, node
policy, never cached); a validator bonded after genesis — the operator's included — cannot mint
through the pool, and the operator mints through a genesis key.
The validity rule is the genesis list staking.faucet_minters (next cut): a Mint signed by a
key not on it is refused (MinterNotAllowed) at admission and at apply, after the register check
and before any signature work, however long the key has been bonded. An entry is a validator
address as rand-node prints it (base58), those 32 bytes in hex, or the validator's whole public
key in hex as validators[].public_key carries it; the file is written back as the base58
address, only the 32 address bytes are committed, key by key in file order, and an empty or
duplicated list is refused at init (BadStaking). Where a genesis lists it, every node's pool
admits exactly that list. Absent, it commits nothing — chain 15 runs without it.
Every rule above bounds how fast bought stake becomes weight; none asks whether the set wants
the key at all, and Bond stays permissionless — anyone holding MIN_STAKE (1 000 RAND) and a
key registers a validator. On chain 18's 26 keys of 1 000 RAND that makes a blocking third about
13 000 RAND and two thirds about 52 000: at the seed-sale price, a purchase, not a vote. Until
slashing exists the register is therefore permissioned where it holds value, and one optional
field says so:
"staking": { …, "admission_by_vote": true }Only true switches it on; absent or false is every chain through 18, byte for byte (the field
is committed to the genesis hash under its own tag, after every earlier one, only when true).
Under it:
- A registration needs the set's vote. A
Bondthat would register a new key (one with aregistration) is refusedNotAdmittedunless the key's address is in the ledger's admitted set; a top-up of a row already in the register is not a registration and needs no vote. ABondVestedthat registers goes through the same check. - The vote is an action,
AdmitValidator { candidate, signatures }— bundle-less and fee-less like the bridge's governance actions, pooled past a full pool and offered first, one pooled vote per candidate.candidateis the key that will register;signaturesis one(validator key, Dilithium2 signature)per voter overblake3("rand-admit-validator-1" ‖ genesis hash ‖ candidate address), in strictly ascending order of the voters' addresses. Every listed vote must count: each voter must be in the voting set, each signature must verify, a voter listed twice or out of order refuses the action, and the voters' combined weight must be strictly more than two thirds of the set's — the arithmetic of a quorum certificate (ValidatorSet::has_quorum). A vote is bound to one genesis, so a chain that shares the validators' keys cannot reuse it, and it needs no nonce: an admission is consumed by the registration it permits, and a registered key can never be admitted again. - The voting set is the register's own derivation for the ledger's current epoch —
derive_set_withover the register, the bond queue andmax_weight_bps, the function every epoch's consensus set is derived by. The ledger does not keep the set consensus is running the epoch with (that one was derived from the register as of the last block of the epoch before), so the two differ only by what happened since the boundary, and only downwards: under the section every bond is queued and is weight in neither, while anUnbondleaves the voting set at once and the consensus set at the next boundary. A key that unbonded below the minimum stops voting on admissions a little before it stops voting on blocks; nothing that is not weight in consensus is weight here. A lone validator of four cannot admit (one of four is not a quorum), and a candidate cannot vote itself in. - The admitted set is consensus state, at most
MAX_ADMITTED= 256 keys waiting to register (AdmissionSetFullpast it, until a registration consumes a row): folded into the state root asH("rand-state-admitted-1", root ‖ admitted_root)around the root the section already commits, persisted beside the bond queue (META_ADMITTED, written only while non-empty), restored on restart and replayed byrand-node verify.rand_getAdmittedserves it;rand_getLimits.admission_by_votetells a wallet whether a registration needs the vote. - The commands. Each voter runs
rand-node admit sign --candidate <key or registration> --key <its validator key> --genesis-hash <hash>(offline) and hands back one line; whoever collects them runsrand-node admit submit --candidate … --signature <line>… --rpc …, which checks each vote against the node's genesis hash, orders them and sends the action. Then the bond as before:rand bond <validator> 1000 --registration <hex>.
The dollar-indexed sealing subsidy (docs/fees.md §1.3) converts a dollar target at a RAND/USD
price, and the chain takes that price from the validator set, not from a market oracle. The vote
is AdmitValidator's shape with a nonce:
- The action,
SetRandPrice { price_micros_per_rand, nonce, votes }(wire tag 44) — bundle-less and fee-less, pooled past a full pool and offered first, one pooled update per nonce.votesis one(validator key, Dilithium2 signature)per voter overblake3("rand-set-price-1" ‖ genesis hash ‖ be64(price) ‖ be64(nonce)), strictly ascending by voter address. - The quorum is the admission's: the same voting set (
staking::voting_set, the register's own derivation for the current epoch), every listed vote counting, the voters holding strictly more than two thirds of the weight (ValidatorSet::has_quorum). The check is one function,staking::check_votes, shared by both actions.SetRandPricevotes over the current validator set and does not needstaking.admission_by_vote(or anystakingsection): it is gated onfees.usd_subsidyalone. - A pooled vote is re-judged at every tip without signatures — membership and quorum in the
current voting set (
staking::check_vote_weight), the nonce and the band — so a vote whose quorum was lost after pooling (a voter unbonded, an epoch moved the weights) leaves the pool and frees the price-nonce slot for a replacement. - The nonce is the ledger's plus one (the genesis price is nonce 0, so the first update is 1),
so a vote for update
nnever counts forn + 1, even at the same price; a pooled update leaves once another spends its nonce. - The band: a positive price within a factor of two of the current one (
2·new ≥ old,new ≤ 2·old). Moving further takes several updates, each a fresh quorum. There is no minimum gap between updates: the band and the quorum are the governance bound, and the genesismax_subsidy_per_blockcaps what any price, voted or stale, can mint. - The state is
RandPrice { price_micros_per_rand, set_at_height, nonce }, consensus state under the section (rand-state-price-1,META_RAND_PRICE), served byrand_getRandPrice. RefusedUnsupportedActionon a chain withoutfees.usd_subsidy. - The commands.
rand-node price statusprints the price and the next nonce; each voter runsrand-node price sign --price 0.15 --nonce <n> --key <its validator key> --genesis-hash <hash>(offline) and hands back the line as a vote file; whoever collects them runsrand-node price submit --price 0.15 --nonce <n> --signature <file or line>… --rpc …(docs/cli.md).
Until audit v6 an equivocating leader's second block was refused and logged with both hashes and nothing was at stake. Under one optional sub-section
"staking": { …, "slashing": { "equivocation_bps": 1000, "jail_epochs": 4 } }(absent on every chain through 18; committed to the genesis hash under its own tag only when set;
it needs consensus_domain: 1 and is refused beside a vesting section — stake bonded from a lock
returns to the lock's unbonding rows, which name no validator, and would escape a slash):
- What is evidence. Two block headers for one view, signed by one proposer key under
this chain's consensus signing domain, with different hashes —
SlashEquivocation { first, second }, the lower hash first. Each header is checked exactly as a replica checks a proposal's signature (SigningDomain::block_message); nothing else about the headers matters. What is not: the same header twice, two views, two keys, a pair signed for another chain, a tampered header (its signature covers something else), and — not slashed by this change at all — vote equivocation (two votes by one key for one view, still only refused and logged byon_vote), a wrong-view or non-leader proposal, or anything a leader did not sign twice. An honest leader cannot be slashed: nobody can make its second signature, and a leader that restarts mid-view does not sign one either (SafetyState::last_proposed_viewis persisted before every proposal leaves, andproposerefuses a view at or under it). - The action. Bundle-less, fee-less, signer-less (the two signatures authenticate it),
pooled like a governance action with one pooled pair per
(offender, view), the whole transaction capped at 1 MiB (two headers with their certificates of up to 100 Dilithium2 votes each, ~380 KB a header). Every replica that sees the second proposal keeps both signed headers and the node pools the evidence itself; anyone may also submit it. - The window. Both headers at most one block above the ledger's height and at most
EVIDENCE_EPOCHS= 1 epoch old. The inequality the rule rests on isEVIDENCE_EPOCHS < UNBONDING_EPOCHS(1 < 2, asserted at compile time): a leader of epochEthat unbonds everything inEhas it inpendinguntilE + 2, and the evidence lands byE + 1, so the stake is always there to slash. - The effect.
equivocation_bpsof the offender's bonded stake and of each unbonding row is destroyed (floor-rounded per amount) and counted in the supply'sslashed—total_supply == issued − slashedholds through it. The key is jailed: out of every set from the next boundary forjail_epochswhole epochs (0: for good; otherwise more than the evidence window, which genesis enforces, so one offence can never be replayed after its jail), aBondtop-up is refused (Jailed), and so is any further evidence while jailed — ten equivocations in one epoch cost the stake fraction once, and the seat. A registered key never leaves the register, so it cannot re-register either. Rewards (fees earned) are not touched. - State. The jail is consensus state:
H("rand-state-slashing-1", root ‖ jailed_root)around the root, persisted beside the bond queue (META_JAILED), restored on restart, replayed byrand-node verify;rand_getValidatorsshows each key'sjailed_until,rand_getLimits.slashingthe section.
Not in v0.5.4: slashing (audit decision D8 — "it means nothing while stake is free"); audit v6 built the leader-equivocation half above, behind its own section.
A node runs with --validator when it holds a validator key at all; being in the current set is a
separate thing, and rand_status reports the two separately as is_validator and
active_validator. A key in no current set neither proposes nor votes, but keeps following the
chain, so a validator that bonds in after genesis does not need a restart: start it with
--validator and it begins proposing when its epoch arrives.
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.
| action | command | rides on | pays |
|---|---|---|---|
| register | rand-node register |
nothing — it prints a blob | nothing (offline) |
Bond |
rand bond |
a bundle, which burns the stake | the 0.001 RAND bundle base, out of the wallet's notes |
Unbond |
rand-node unbond |
nothing (bundle: null) |
nothing |
Withdraw |
rand-node withdraw |
nothing (bundle: null) |
the 0.001 RAND base, out of the amount withdrawn |
rand-node register --key node.key.json --payout rand1<payout address> [--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 over (chain_id, payout). The RPC is only read for the chain id: a registration signed
for one chain is refused on another. The payout address comes from a wallet key
(rand --key payout.key.json address), not from a node key, and it is the one field a later
top-up cannot change.
Hand the hex to whoever holds the stake.
rand bond <validator base58> <amount in RAND> \
[--registration <hex>] [--fee <RAND>] [--no-wait] [--cuda]A bond is an ordinary shielded transaction: the wallet proves the one 4-in/4-out hidden-asset bundle
(since chain 14) whose RAND burn, burn_r, is the staked amount — burn_a and burn_asset stay
zero, so a bond can only ever burn RAND — and the ledger admits a bond only when burn_r == amount.
It takes about a minute and a half of local proving, like any transfer.
--registrationis required exactly when the validator is not in the register yet, and refused when it is. The wallet asks the register first, so the wrong shape is an answer rather than a wasted proof.- Registering bonds at least
MIN_STAKE(1000 RAND). A top-up afterwards can be any amount above zero — the wallet refuses zero, which would pay a fee and a proof to move nothing. - What the chain learns is that this validator's stake grew by this much. Which notes paid for it, and who holds them, is hidden exactly as in a transfer — so anyone can stake onto a validator without revealing anything but the amount.
The wallet prints the new stake and the epoch the weight starts counting from:
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
balance: 0.999 RAND
rand-node unbond <amount in RAND> --key node.key.json [--rpc …] [--no-wait]Moves amount from stake into pending with release_epoch = epoch + UNBONDING_EPOCHS, and
increments the nonce. It is free and carries no bundle, exactly as a faucet mint does — there is
nothing to prove and a validator key owns no notes to pay from — so it commits in a block's time.
The command reads the register for its nonce, signs (chain_id, validator, amount, nonce) with the
node's key, and submits the action alone.
Unbonding more than the bonded stake is refused, and so is unbonding zero. Several unbonds in one
epoch merge into one pending row, because that row is hashed into the state root.
rand-node withdraw <amount in RAND> --key node.key.json [--rpc …] [--no-wait]Pays released value into a deposit note at the register's payout address. Released means
rewards plus the pending rows whose release_epoch has arrived; the command takes the released
rows oldest first and then the rewards.
Like an unbond it is validator-signed and bundle-less, but it is not free: it pays the 0.001 RAND bundle base to the proposer of the block that applies it, out of the amount withdrawn. So a withdrawal of 1000 RAND leaves the register entirely and creates a note worth 999.999 RAND, and an amount that cannot cover the base is refused rather than buying a note worth nothing.
The note is the one the chain computes for itself, from the register's payout address, the amount
less the base, the blinding r the action publishes, and the action's own time:
cm = H_CM(payout.pk, from = 0, amount − 0.001 RAND, asset 0, time, r)
time is the head height when the command ran, which it prints and which the signature binds — not
the height of the block that applies the transaction, because the envelope only the payout wallet
can open is sealed against this exact note before that block exists. Admission accepts any time
inside the 256-block window, so a withdraw that waits a few blocks for inclusion still pays a note
the payout wallet finds by scanning. Publishing r is what closes the "declare one amount, mint
another" gap: the chain never takes the note's commitment from the transaction.
withdrawing 1000 RAND: a note worth 999.999 RAND to rand1x7Qk…, the 0.001 RAND base to the block's proposer
note blinding r 3f9a…7c at time 4131
submitted withdraw of 1000 RAND 1b7e…
committed in block 4133
Afterwards the payout wallet finds the note like any other — by scanning, with nothing told to it:
rand --key payout.key.json balance # balance: 999.999 RAND
rand --key payout.key.json send rand1… 1Joining, from the two sides:
# on the joining validator's machine
rand-node register --key node.key.json --payout "$(rand --key payout.key.json address)"
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
# on a chain with staking.admission_by_vote: each of enough validators signs the key in first,
# and someone submits the votes (docs/staking.md §2, "Admission by vote")
rand-node admit sign --candidate <hex from above> --key 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, with the hex from above
rand bond <validator base58> 1000 --registration <hex>
rand validators # the new row, active false until the boundary
# back on the validator's machine, from the next epoch on
rand-node status # active_validator: true — it is proposing nowLeaving:
rand-node unbond 1000 --key node.key.json # out of the set at the next boundary
rand validators # stake 0, pending [{release_epoch, amount}]
# … wait two epochs …
rand-node withdraw 1000 --key node.key.json # a note worth 999.999 RAND at the payout addressA set of n validators needs more than 2/3 of its stake online, so check the arithmetic before
unbonding: dropping one of four leaves three, which is a quorum only if all three are up.
Genesis seeds the register directly, one entry per --validator:
rand-node genesis --chain-id 6 \
--validator node-a.key.json,1000,rand1<a's payout> \
--validator <hex public key of b>,1000,rand1<b's payout> \
--epoch-blocks 1000 --alloc rand1<address>=1000 --faucet --out genesis.jsonThe three fields of a --validator travel together because they are one register entry, and all
three are part of the genesis hash — the payout address included, because it is register state.
Genesis refuses a stake below MIN_STAKE: such a validator would be in the register but in no
epoch's set, and a chain seeded entirely from those would have nobody to pick a leader from.
| public | hidden | |
|---|---|---|
| the register | every row: address, stake, unbonding queue with its release epochs, unpaid rewards, payout address, nonce | nothing |
Bond |
the validator, the amount, whether a registration was attached, and the bundle's burn_r |
which notes paid, who owns them, the bonder's change |
Unbond |
the validator, the amount, the nonce | nothing — there is nothing else in it |
Withdraw |
the validator, the amount, the nonce, the note's time |
who can open the note, and every later spend of it |
So a watcher learns that 1000 RAND was bonded to this validator and, later, that the validator withdrew 1000 RAND into a note at its published payout address. What the note is then worth to whom, and where the value goes next, is a shielded transfer like any other. The payout address is public in the register from the day the validator registers, so publishing the withdraw's blinding leaks nothing the register did not already say.
Rewards are the fees of the blocks a validator proposed: every bundle's fee, and the base a withdraw
pays. They accrue in rewards and are paid out by Withdraw — there are no block rewards and no
inflation. On a chain without staking.slashing — every chain through 18 — there is no slashing
and no jailing (spec §13): a validator that misbehaves costs its stake nothing, and the remedy is
the operators'. Under the section a leader that signs two headers for one view is slashed and
jailed (§2, "Slashing leader equivocation").
rand validators # the whole register, one row per entry
rand status # is_validator / active_validator for the node you askedrand_getValidators (docs/rpc.md) returns one row per register entry, in address order, with
amounts as decimal strings — a stake is 10⁹ units per RAND and a JSON number is not an exact
integer past 2⁵³:
{ "address": "2nRdFC…", "stake": "1000000000000",
"pending": [{ "release_epoch": 71, "amount": "1000000000000" }],
"rewards": "4000000", "payout": "rand1…", "nonce": 3, "active": true }active is whether this row is in the set running the current epoch — that, and not the presence of
a row, is what says who is producing blocks. rand_getEpoch answers where the chain is in its
schedule and what the next boundary would derive today:
{ "epoch": 69, "epoch_blocks": 1000, "next_set": ["2nRdFC…", "ByDkxs…"] }next_set is a projection, not a commitment: every bond and unbond before the boundary moves it.
For where the staked value came from and went to, read rand_getSupply and docs/supply.md: the
register's total and the pool's value have to add up to everything the chain ever issued, and a node
checks it.