Use this page to run your own rand-node against the testnet, which is also the recommended way to use a wallet privately: a node you run does not learn which leaf you are about to spend from anyone but you. An observer syncs, re-verifies every proof, and serves the JSON-RPC; a validator does the same and takes part in consensus once its key is in an epoch’s set.
Build
Requirements: Rust 1.98.1 (pinned in rust-toolchain.toml; rustup installs it), a C++ compiler and cmake for RocksDB. The zkVM crate has path dependencies into a sibling circuits/ checkout of the proof-system repository (randprotocol/zkp-circuits), which must sit beside fullnode/, at the commit the release was vendored from: the circuits: line of crates/randprotocol-zkvm/guests-compiled/PROVENANCE.md (aeacf31 for v0.6.8, as for v0.6.7). The first build takes 10 to 20 minutes.
git clone https://github.com/randprotocol/fullnode
cargo build --release # target/release/rand-node, target/release/rand, target/release/rand-prover
Run v0.8.0 (git checkout v0.8.0, commit c0878c3f, is the release the fleet runs since 2026-10-12; rand_getVersion reports a node’s sha). Its GitHub release carries rand-node, rand, rand-prover, msauth-vk-production.bin, SHA256SUMS and SHA256SUMS.sig, built by the release workflow from the tag and signed by the release key (the first signed releases were v0.7.0’s, on chain 20). Chain 21 needs it: v0.8.0 runs constraint set 9 (rVM phase 4’s Merkle spans and the MODQ chip), under which every proof’s encoding and every verifier key changed, so the build refuses chains 18 to 20 at startup and no earlier build decodes a chain-21 proof. Every bundle still carries an authorisation proof beside its bundle proof (split authorisation, since chain 17), and the genesis pins the same two guest digests as chain 20, bundle guest v3 (hc_bundle 60af094a…) and the auth guest (hc_auth 1e4e347f…), plus a third, the private-multisig auth guest (hc_auth_multisig 9705f8e2…), whose pinned verifier key msauth-vk-production.bin (23,754 bytes, SHA-256 68facb06…cd30) must sit beside rand-node: run and verify load it at start and refuse to start without it. The genesis also carries a gas section, hardening_v6, envelope_bytes, and the new fees, perps and multisig sections. Every node runs the fleet’s build.
324a4ab1029d39e67fc2de36b036341b0f88f13089997d7fe24c596363a58278 rand-node
e3a38e74ee28743a960fc1c3eb1ac6c538e18dc259411e10afff7f54e268bbf4 rand
da9df98aa4f9f1748a0be82934e12f0213e41b798daf0963a3023ff39e27c475 rand-prover
68facb06c7f321aaf1d4d541acf6b5c0e9d7f17788ed1c9add6c5a8a8f7acd30 msauth-vk-production.bin
Chain 21: an all-stop hard fork
Chain 21 went live on 2026-10-12: the genesis was cut at 10:22 UTC and all 26 validators switched at 10:31 UTC, all-stop, all-start, every node starting on chain 21 with v0.8.0. It was cut from chain 20: the bridge state from chain 20’s snapshot at height 814,702 (2026-10-12 07:41 UTC, the bridge stopped first), the operator wallets’ balances from a scan of chain 20 while it still ran. It was not a rolling update: chain 21 is a new genesis, so no block is shared with chain 20, and v0.8.0’s constraint set 9 cannot verify chain 20’s proofs at all. All 26 validators, their keys, peer ids and bootstraps carried over; a quorum is still 18 of 26. Chains 15 to 20 are retired. Chain 20 itself (2026-10-01, v0.6.8, a hard fork of chain 19 at height 7,258) brought the genesis binding, RPL-2 and audit v6’s CON-4 and CH-1; v0.7.0 and v0.7.1 then rolled onto it node by node.
Moving a node from chain 20 to chain 21 therefore takes the new binary, the verifier key beside it, the new genesis file and a fresh data directory, as below. Wallets need v0.8.0 to send on chain 21: an older wallet’s proofs are constraint set 8’s, which chain 21 does not decode, and since chain 20 every proof and signed message is bound to the genesis hash (binding_domain: 1) in any case.
Keys
rand-node keygen --out node.key.json # prints the address
rand-node address --key node.key.json # address, public key, libp2p peer id
The file is { "seed", "address", "public_key" }, mode 0600; only the seed is secret. An observer needs a key too — it is the p2p identity. A node key is not a wallet key: use rand keygen for a wallet.
Generate your own key and keep it outside the checkout. The fullnode repository tracks no key any more (audit v3, OPS-1): the deploy/node-*.key.json and deploy/payout/*.key.json files that were committed for chains 8 to 13 are published seeds and must not be used. Chain 21’s validator keys, chain 20’s carried over, were generated off-repo and have never been in the repository: its 26 genesis validators are the eighteen keys generated for chain 14 and the eight bonded on chain 15, carried through every cut since.
Since v0.6.8 rand-node keygen refuses an --out file that already exists; v0.6.7 overwrote it without asking. The file is the only copy of the seed. Back it up before a bond depends on it.
Initialise from the genesis
Every node on a chain needs the byte-identical genesis file; rand-node init copies it into the data directory and prints its hash, which must read ca76c2766ed6090ac6ba5dad86b724dd5d2b80c0edfaed287cd8b129e663ca9a for chain 21. The file is deploy/genesis-chain21.json (sha256 88d42b38…8ca1). It was committed to the fullnode’s main with the cut record (deploy/cut-records/chain21.record, commit 246cb5f9), after the tag, so a checkout of v0.8.0 does not have it: fetch it from main. init on a chain naming hc_auth_multisig needs the verifier key beside the binary too.
git -C fullnode show origin/main:deploy/genesis-chain20.json > genesis-chain20.json
rand-node init --datadir ./data-6210cf07 --genesis genesis-chain20.json
This creates <datadir>/db (RocksDB) with block 0, the nine genesis notes as the tree’s first leaves (seven RAND notes carrying 5,184.243719258 RAND of operator wallets from chain 19, and two zUSD notes of 10 and 89 zUSD, the 99 zUSD locked in the bridge’s custody), and the validator register. Re-running with the same genesis is a no-op; a different genesis is refused (already initialized with a different genesis means the datadir belongs to another chain). Name the data directory after the first eight characters of the genesis hash, as the fleet does (data-<name>-6210cf07), so it never collides with chain 19’s (-a3defc93).
The genesis file’s shape:
{
"chain_id": 20,
"timestamp_ms": 1790835251661,
"validators": [ { "public_key": "<hex>", "stake": 1000000000000, "payout": "rand1…" } ],
"alloc": [ { "cm": "<64 hex>", "envelope": { "kem_ct": "…", "to_receiver": "…", "to_sender": "…", "body": "…" }, "amount": 1000000000000,
"opening": { "pk": "<64 hex>", "time": 0, "r": "<64 hex>" } },
{ "cm": "…", "envelope": { … }, "amount": 1000000000,
"opening": { "pk": "<64 hex>", "time": 0, "r": "<64 hex>", "asset": 1 } } ],
"faucet": true,
"confidential": true,
"fri_profile": "production",
"hc_bundle": "60af094acfe65d85fdb18fb3d06cf9085dcf28c96e59e87f1ee527226e6e3fce",
"bridge": { "emitter": "<64 hex>", "guardians": [ "<40 hex>" ], "emitters": { "2": "<64 hex>", "3": "…", "4": "…", "5": "…" },
"pq_guardians": [ "<hex>" ], "pause_key": "<hex>",
"rules_v2": { "global_mint_cap_per_window": 400000000000, "cap_window_secs": 86400 },
"guardian_set_index": 1, "burn_sequence": 13,
"min_inbound_sequence": { "2": 3, "3": 3, "4": 3, "5": 7 },
"rotation": { "delay_secs": 86400, "needs_possession": true },
"fees": { "mint_bps": 10, "burn_bps": 10, "recipient": "rand1…" } },
"tokens": { "registration_fee": 1000000000, "mint_cap_per_day": 10000000000000,
"tokens": [ { "name": "Shielded USD", "symbol": "zUSD", "salt": "<64 hex>",
"backings": [ { "chain": 2, "token": "<64 hex>", "decimals": 6 }, … ] } ],
"max_tokens": 4096, "burn_registration_fee": true, "bound_note_value": true },
"consensus_domain": 1,
"staking": { "faucet_budget_per_epoch": "10000000000000", "bond_activation_epochs": 2,
"max_weight_bps": 3333, "registration_v2": true,
"faucet_recipients": [ "rand1…" ], "faucet_minters": [ "<validator address>" ],
"max_stake_entry_bps_per_epoch": 2500, "admission_by_vote": true },
"epoch_blocks": 1000,
"max_program_words": 65535,
"max_proof_bytes": 4194304,
"max_block_bytes": 20971520,
"max_call_envelope_bytes": 65536,
"max_program_public_words": 32768,
"envelope_bytes": 1860,
"hardening_v6": true,
"hc_auth": "1e4e347f44cf86750b30a9a4bdf9ec9256efe353d4ff8017451eca7d195639c1",
"gas": { "gas_price": "100", "byte_price": "800", "bundle_gas_limit": 20479, "metering": "circuit",
"dynamic": { "target_block_bytes": 10485760, "target_block_gas": 262144,
"adjust_bps": 1250, "min_gas_price": "100", "min_byte_price": "800",
"max_gas_price": "10000", "max_byte_price": "80000", "byte_load": "paying" } },
"testnet": true,
"binding_domain": 1,
"proof_window_blocks": 1024,
"program_state": { "cell_fee": 10000000 }
}
Every field is in the genesis hash. hc_bundle pins the bundle guest every bundle proof is checked against, and hc_auth the auth guest every authorisation proof is checked against; a node whose build assembles a different guest refuses to start and names both digests. The five max_* fields are optional and bound into the hash only when present; chain 21 sets all five. max_proof_bytes is 6 MiB on chain 21 (4 MiB on chains 17 to 20, 8 MiB on chains 13 to 16): since chain 17 a transaction can carry three proofs (bundle, authorisation, call), a genesis with hc_auth must have max_block_bytes of at least three times max_proof_bytes plus 1 MiB, and chain 21’s tier-18 multisig auth proof needs the wider cap — so max_block_bytes is 19 MiB (20 MiB on chain 20) and the byte target 9,961,472. Each alloc note carries its opening (pk, time, r, and asset for a token note), so a genesis note’s owner and amount are public by design. Chain 21 has:
- a
bridgesection, carried from chain 20’s snapshot at height 814,702: guardian set 1 at index 1 with eight guardians, emitters for source chains 2, 3, 4 and 5 (chain 19’s), eight Dilithium2pq_guardians, apause_key,burn_sequence14, amin_inbound_sequenceof 3 for source chains 2 and 4, 5 for source chain 3 and 7 for source chain 5, andrules_v2(a global mint cap of 4,000 zUSD per 24 h window). Since chain 20:rotation, under which a PQ-guardian or pause-key rotation needs proof of possession by the new keys and takes effect 86,400 s after it commits; andfees, under which the chain keeps 10 bps of every deposit and 10 bps of every burn as a zUSD note to a treasury address, so a deposit credits the gross less the fee and a burn releases its amount less the fee; - a
tokenssection: a registration fee of 1 RAND, burned rather than paid to the proposer; amint_cap_per_dayof 100,000 tokens per backing at eight decimals; a registry cap of 4,096 tokens; the 2^63 note-value bound; and zUSD listed at genesis with its seven backings, 99 zUSD locked (Ethereum USDT 22, BSC USDT 22, Tron USDT 30, Solana USDT 25) against the two genesis zUSD notes; consensus_domain: 1: every consensus signature is bound to this genesis (Consensus);- a
stakingsection: the faucet allowlisted to 16 wallets and to 18 minting validator keys, with a budget of 10,000 RAND per epoch, a two-epoch bond delay, a 3,333 bps weight cap and v2 registrations, as on chains 19 and 20; since chain 20,admission_by_vote(a new validator key registers only after the validator set votes it in) and an entry budget of 2,500 bps of the active weight per epoch boundary, where chain 19 had a fixed 10,000 RAND (Staking). There is noslashing; - a
gassection: a call pays for the gas limit its proof declares and for its bytes, at prices that start at 100 units per gas and 800 units per KiB and move after each block with its fullness against a target of 9,961,472 bytes (10,485,760 on chain 20) and 262,144 gas, by at most 1,250 bps a block, never below the starting prices and, since chain 20, never above 10,000 and 80,000. Only the bytes that pay a byte price, a call’s proof and input envelope, count toward the byte price’s load (byte_load: "paying"). Every bundle proof declares the same limit, 20,479 (Fees); envelope_bytes: 1860: every note envelope is exactly 1,860 bytes and carries an encrypted memo field;hardening_v6: true, the v0.6 hardening switch, set by every chain since chain 16;testnet: true, the marker that lets a faucet sit beside a bridge section;binding_domain: 1: every transaction’s proofs and every signed action message include the genesis hash, so nothing proved or signed for one chain verifies on another;proof_window_blocks: 1024: a bundle’s anchor may be one of the last 1,024 block-end roots and itstimeup to 1,024 blocks behind the height (256 on chain 19), which gives a slow or delegated prover about twenty minutes;program_state, RPL-2: program cells, program vaults and theInvokeaction, with a cell fee of 0.1 RAND per cell an invoke creates (JSON-RPC has the limits).
It has no aggregation section, and the build refuses to start on a genesis that carries one.
timestamp_ms is the chain’s clock zero. When you cut a genesis of your own, set it close to the actual launch time and never ahead of it. Too far in the past, and block time lags wall clock until the 60 s per block step bound lets it catch up. More than 15 seconds in the future, and block 1 does not commit at all: every vote, the leader’s included, is withheld until wall time reaches the genesis timestamp. Check it against the clock of the machine that launches the fleet, not the one that cut the file.
Run
rand-node run --datadir ./data --key node.key.json \
--listen /ip4/0.0.0.0/tcp/30303 --rpc 127.0.0.1:8545 --no-mdns \
--bootstrap /ip4/164.90.239.200/tcp/30303/p2p/12D3KooWEQEbUwZRgqhDPXhW7vNBmcRcnhUDGUADe81VFdv3ALYe \
--bootstrap /ip4/165.245.173.74/tcp/30303/p2p/12D3KooWJyc5oDHggAr89e9QrRFBy9Tg8TxXh18yzrb1SM6KKLYw \
--bootstrap /ip4/188.166.235.187/tcp/30303/p2p/12D3KooWSyX84P1DAbWByLHpRBMLRWD53r3Jj4aqVt6tHoF8B2ZQ
| argument | default | meaning |
|---|---|---|
--datadir | required | the initialised data directory |
--key | required | key file: validator identity, p2p identity, fee recipient |
--listen | /ip4/0.0.0.0/tcp/30303 | libp2p listen multiaddr, repeatable |
--bootstrap | none | peer to dial at start and every 30 s while disconnected, repeatable |
--rpc | 127.0.0.1:8545 | the operator’s JSON-RPC and WebSocket listener: every method, and it trusts loopback. Keep it on loopback, and never forward a public endpoint to it |
--public-rpc <ADDR> | off | v0.6.8: a second, public listener for a reverse proxy or an SSH forward to point at: a fixed method set without the viewing-key methods, rand_mint or rand_getPeers, no batches, no WebSocket, one meter for all callers (JSON-RPC) |
--rpc-viewing-token-file <PATH> | off | v0.6.8: require a bearer token (the file’s first line, 32 or more characters) on the viewing-key methods of --rpc |
--reserved-peer <PEER_ID> | none | v0.6.8, repeatable: a peer admitted past the inbound connection cap; see Reserved peers |
--strict-gossip | off | v0.6.8: accept only gossip with a valid author signature, sequence number and source; it rolls node by node |
--validator | off | this node holds a validator key and takes part in consensus when its key is in the current set |
--no-mdns | off | disable LAN discovery; recommended on servers |
--block-interval-ms | 1000 | minimum spacing between proposals |
--view-timeout-ms | 3000 | base view timeout, doubling per consecutive timeout up to 8× |
--verify-chain | quick | startup integrity check: off, quick (structure and ledger replay), full (also every signature and QC) |
--keep-raw-proofs | off | archive mode for block aggregation; no effect on chain 21 |
--verify-chain <MODE> | quick | v0.8.0: the startup check is skipped when this same clean build already verified the store through its head (the startup checkpoint); replay forces the walk, off skips it |
--apply-proposals-inline | off | v0.8.0: apply incoming proposals on the consensus loop as before; by default a follower applies them on a blocking thread |
--msauth-vk <PATH> | msauth-vk-<profile>.bin beside the binary ($RAND_MSAUTH_VK) | v0.8.0: the pinned multisig verifier key, required on a chain naming hc_auth_multisig, as chain 21 does |
--prune-history | off (keep everything) | keep the ledger and only this much block history: <n>m, <n>h or <n>d, at least 1h. The testnet’s validators run 24h, except the two archives; see History pruning |
--min-free-disk-mb | 1024 | refuse to start with less free space on the data directory’s filesystem; rand_getHealth answers disk_low under four times it. 0 disables the guard |
--prover <ADDR> | off | host a delegated prover on this address, a listener of its own and never the RPC; see below |
--gas-price, --byte-price | 100, 800 | the pool’s price floor for a call on a chain without a gas section. Chain 21’s genesis, like chain 20’s, sets its own prices, so the node ignores both flags there and logs a warning |
--prover-accept-spend-key is retired: a v0.6.8 node given it refuses to start, so take it out of any unit file carried over from v0.6.7.
The three bootstraps above are validators C, D and E, unchanged since chain 19. Chain 21 runs on the validator keys generated for chain 14, so their peer ids are the ones chain 14 used; a p2p identity is derived from the validator key. Most validators keep only a day of blocks, so a node syncing from genesis, or one more than a day behind, needs a full-history peer too. Add one of the two archives:
--bootstrap /ip4/168.144.46.203/tcp/30303/p2p/12D3KooWS5usdMPvsbaWSaT2xtKndHvDYurwmueoNvaRRarb2wRf # obs1
--bootstrap /ip4/206.81.29.236/tcp/30303/p2p/12D3KooWPocyZGcNB9Vp68VSfgVvNXhSzBBoJa4h9R1yMkp71XHh # rand-archive-2
Omit --validator to run an observer. Startup: open storage, check this build’s guests against the genesis hc_bundle and hc_auth, run the integrity check (truncating a damaged tail and keeping the vote-safety state), resume consensus from the persisted head, start networking and RPC, then sync from any peer that is ahead. RUST_LOG defaults to info,libp2p=warn,libp2p_mdns=off; Ctrl-C or SIGTERM, systemd’s stop signal, shuts down cleanly. Restarting from the same --datadir resumes from the persisted head.
Topology rules from the fullnode: nodes behind NAT dial out to nodes with public addresses; open TCP 30303 inbound on public nodes; bind the RPC to 127.0.0.1 unless it is firewalled — it accepts transactions from anyone who can reach it, and once a viewing key has been imported into it, treat the port as key-bearing. Keep --block-interval-ms at 1000 or slower: a bundle proof takes about two minutes and its anchor is valid for 1,024 blocks on chain 20 (256 on chain 19).
A validator needs a synchronised clock. Since chain 14 wall-clock time is part of consensus (bridge hardening B2). A block whose timestamp is more than 60 s past its parent’s is invalid, and a validator withholds its vote from a block whose timestamp is more than 15 s ahead of its own clock. A validator whose clock runs slow therefore stops voting without logging a fault, and eighteen of twenty-six must vote to commit on chain 21. Run NTP on every validator and check it before you start the node:
timedatectl # want "System clock synchronized: yes" and "NTP service: active"
chronyc tracking
apt-get install -y chrony && systemctl enable --now chrony # if it is not synchronised
Reserved peers
Since v0.6.8 a node accepts at most 256 established and 64 half-open inbound connections, and a libp2p identity costs nothing to mint, so strangers could hold every slot. Three kinds of peer are reserved, admitted past the cap and served from the validators’ share of the sync budget: every --bootstrap peer, every --reserved-peer, and every validator identity learned from a signed peer binding. Each --validator node signs its libp2p identity with its validator key and gossips it; a node that knows the key from a validator set reserves that peer and keeps the record across restarts. A freshly cut chain’s store holds no bindings, so until the first announcements arrive only the first two kinds are reserved: on a cut, pass the fleet’s validator peer ids with --reserved-peer or keep them in the bootstrap list (rand-node address --key <file> prints a key’s peer id). rand_status.reserved_peers counts them; on a fleet node it should reach the validator count within a minute of start.
Check it
rand-node status # rand_status verbatim
rand --rpc http://127.0.0.1:8545 status
rand peers
rand_status reports height, view, syncing, sync_target, peer_count, connected_peers, reserved_peers, mempool_size, is_validator, active_validator, faucet, confidential, testnet, fri_profile, programs, notes, nullifiers, tree_root, hc_bundle, hc_auth, hc_auth_multisig, msauth_key, binding_domain, gas_prices, compact_fetched, the node’s address and peer_id, and the sync diagnostics (sync_inflight_age_ms, sync_failures, sync_late_batches), the disk (disk_free_bytes, disk_low), and the retention floor (prune_floor, 0 on a node that keeps everything; prune_history_secs, null when unset). On chain 21 expect hc_bundle 60af094a…, hc_auth 1e4e347f…, hc_auth_multisig 9705f8e2…, msauth_key "loaded", faucet true, confidential true, testnet true, binding_domain 1, fri_profile "production", and gas_prices between 100 and 10,000 per gas and 800 and 80,000 per KiB. rand_getGenesisHash must answer ca76c276…ca9a and rand_getVersion 0.8.0 with git_sha c0878c3f….
rand_getHealth answers disk_low (with free_bytes) ahead of ok, syncing or behind when free space is under four times --min-free-disk-mb.
History pruning
A node started with --prune-history 24h keeps the ledger (notes, nullifiers, anchors, the validator register, programs) whole and deletes older blocks, their certificates, transactions and receipts. A pass runs every 16th committed block and deletes up to 512 blocks older than the head’s timestamp minus the window; genesis, the head and its parent are never deleted, and the floor only rises. Disk comes back after RocksDB compacts. On chain 21, as on chains 19 and 20, 24 of the 26 validators run 24h and hold about a day of blocks. When pruning was switched on for chain 14 (2026-09-25), each validator’s block tables fell from 19 GB to 3.3–4.4 GB once the backlog drained; The other two, obs1 and rand-archive-2, validators too since 2026-09-27, run without the flag and keep everything. A mainnet node never passes the flag.
On a pruned node, a height-addressed lookup below the floor (rand_getBlockByHeight, rand_getFinality by height, a rand_getBlocks or rand_getCompactBlocks range reaching it) answers error -32010 with data.floor: ask an archive. A hash-addressed lookup answers null for a block the node no longer holds, as for one that never existed. Wallet scanning is unaffected, since notes and nullifiers are never pruned.
A pruned node’s startup check is structural (block 0 and the retained blocks, their QCs, parent links and leaders), not a replay from genesis, and it does not repair itself: if it fails, re-sync from an archive. Do not open a pruned data directory with a build older than v0.5.7 unless it runs with --verify-chain off: its default check replays from genesis, finds block 1 missing, and truncates the ledger to genesis.
Hosting a prover
Since chain 17 a wallet makes two proofs for every bundle, and the larger one needs about 6 GB of memory (measured on chain 18; nothing is measured on chain 21 yet). A private-multisig spend (Command line) is the exception: its tier-18 auth proof is always made on the signer’s own machine and needs about 76 GB. A wallet that cannot hold it can hand that proof to a delegated prover it is paired with: the separate rand-prover binary, or the same service hosted by a node.
rand-prover --home /root/prover keygen
rand-prover --home /root/prover pair --name laptop --url https://prover.example.net
rand-node run --datadir ./data --key node.key.json --prover 127.0.0.1:8600 --prover-home /root/prover
pair prints a randprover: link. The wallet takes it with rand prover pair <link> and then proves through it with the global --prover option. The prover’s listener is its own, never a method of the RPC: an address equal to --rpc is refused. With --prover the node refuses to start unless <prover-home>/prover.key.json exists (default <datadir>/prover) and 5.74 GB per proving slot plus 1 GiB of memory is available; --prover-max-parallel sets the slots, one by default.
Since chain 20 the prover is sent the wallet’s viewing key and a fresh salt, not the spend key. The wallet makes the authorisation proof over the spend key itself, so a paired prover can read that wallet’s whole history and cannot spend. Keep the listener on loopback unless a TLS-terminating proxy fronts it, and do not host it on a machine that serves a public RPC. A stop waits for the proof in flight, so give a systemd unit TimeoutStopSec=180.
Verify and repair
rand-node verify --datadir ./data # full: hashes, QCs, signatures, proofs, ledger replay, the supply counters
rand-node verify --datadir ./data --mode quick # structure and ledger replay
rand-node verify --datadir ./data --repair # truncate a damaged tail; peers resync the rest
Stop the node first. Exit code 0 if consistent, 2 if a problem was found and --repair was not given. Replay re-verifies every bundle proof, so a build whose constraint set differs from the one that made the chain stops at the first bundle.
Becoming a validator
A validator joins a running chain by bonding, not by a new genesis: start the node with --validator, print a registration with rand-node register --key node.key.json --payout <rand1…> --v2 (chains 20 and 21 refuse a v1 registration), have a wallet bond at least 1,000 RAND with rand bond <address> 1000 --registration <hex>, and the node proposes once the bond activates: on chain 21, three 1,000-block epoch boundaries after the bond at the earliest. Since chain 20 the bond comes second. The genesis sets staking.admission_by_vote, so a bond that registers a new key is refused until validators holding more than two thirds of the voting weight have voted the key in: each signs with rand-node admit sign --candidate <key or registration> --key <validator key> --genesis-hash ca76c2766ed6090ac6ba5dad86b724dd5d2b80c0edfaed287cd8b129e663ca9a, and one of them submits the votes with rand-node admit submit (Command line). rand_getAdmitted lists the keys voted in and not yet registered. No more than 2,500 bps of the active weight becomes new weight at one epoch boundary. Staking walks it through. For a private test chain, rand-node genesis cuts one:
rand-node genesis --chain-id 88 \
--validator a.key.json,1000,rand1<a's payout address> \
--validator <hex public key of b>,1000,rand1<b's payout address> \
--alloc rand1<address>=1000 \
--epoch-blocks 1000 --faucet --fri-profile production \
--out genesis.json
and scripts/local-testnet.sh runs two validators on one machine with RPC on 8545 and 8546. Without further flags the genesis has no hc_auth, no gas section and no memo. Chain 20 was cut with --hardening-v6 --bundle-guest v3 --auth-guest --gas-price 100 --byte-price 800 --bundle-gas-limit 20479 --gas-dynamic 10485760,262144,1250 --max-gas-price 10000 --max-byte-price 80000 --gas-byte-load paying --envelope-bytes 1860 --testnet --binding-domain 1 --proof-window-blocks 1024 --program-state-cell-fee 10000000 on top (chains 18 and 19 had the flags up to --envelope-bytes), and its bridge and staking sections and consensus_domain were spliced in by the cut script. Chain 21 (deploy/cut-chain21-genesis.sh) added --max-proof-bytes 6291456 --max-block-bytes 19922944 --gas-dynamic 9961472,262144,1250 --fees fees.json --multisig MULTISIG.JSON --auth-multisig-guest, and the perps section was spliced in after zUSD. A new chain of your own should pass --binding-domain 1: since v0.6.8 a wallet proves the genesis-bound form on every chain id outside 14 to 19.
Deploying on a server
The repository’s deploy/ directory holds the scripts the fleet uses: deploy/push-to-vps.sh <ip> <letter> "<bootstraps>" [validator|observer] provisions a Linux server (rsync the source, build, install /usr/local/bin/{rand-node,rand}, initialise a datadir keyed on the genesis hash, install a rand-node systemd service), and deploy/rebuild-vps.sh <ip> rebuilds on a new commit and restarts with the data kept. Two cautions. The provisioning script initialises from deploy/genesis.json, which is not chain 21’s file, so a node for the testnet is set up by hand from deploy/genesis-chain21.json (on main, see above); the fleet’s own cut-over was deploy/cutover-fleet-chain21.sh, and a rolling build update is deploy/roll-all.sh, which also installs the verifier key. Compact blocks (v0.8.0, a node-only wire change: a leader gossips the header, its signature and the transaction hashes, and replicas rebuild the block from their mempool): an old node rejects the new message, so a build that changes the wire is rolled observers and archives first, then every validator at once. And both scripts rsync /root/fullnode with --delete, so a key kept inside that tree is deleted by the next rebuild: keep it in /root/keys, as the fleet does and as the v0.6.8 provisioning script now does by default (v0.6.7’s wrote the key under /root/fullnode/deploy/). Service management is systemctl status rand-node, journalctl -u rand-node -f. The server needs build-essential clang cmake pkg-config libssl-dev and a Rust toolchain. A 1-vCPU, 2 GB server cannot build the binaries; build elsewhere and copy them in. Running a validator and an aggregator has the sizes and the step-by-step setup.
The explorer
randscan.org runs beside validator E and indexes chain 21; its REST API and WebSocket feed are the easiest way to read the chain without a node (Explorer API). The explorer is open source (randprotocol/randscan) and its indexer needs only a node’s rand_* JSON-RPC and a PostgreSQL database, so it can be run against your own node.