Read this first (2026-09-30, audit v6 DOC-6). This page was written for chains 8 to 14 and its body is kept as the dated record of those cuts. The live chain is 18: genesis
a7cb020cc99a33c83fc38cfa0ec1db357f67fbf8b6dab13ab1d9812280b4da76(deploy/genesis-chain18.json), live since 2026-09-29 04:56 UTC, build v0.6.786941a1, 26 validators (quorum 18), 24-hour pruning on the validators with two archives (obs1, rand-archive-2), 4 MiB proofs and 20 MiB blocks, agassection, and 1 860-byte note envelopes with the encrypted memo. It was cut bydeploy/cut-chain18-genesis.shand rolled bydeploy/cutover-fleet-chain18.sh; the bridge steps aredeploy/chain18-bridge-steps.md. The current facts table isdocs/deploy.md, "The network today"; the launch records of chains 15 to 18 are inAGENTS.md. The per-node scripts named below (cutover-droplet.sh,cutover-droplet-chain14.sh,cutover-droplet-rand.sh) are history: a cut since chain 15 is a fleet script, underdocs/deploy.md, "Cut policy". Keys on a droplet live in/root/keys/node-<name>.key.json, as the first note below says.
No key in this repository is tracked any more (audit v3, OPS-1).
deploy/node-a..f.key.jsonanddeploy/payout/*.key.jsonwere committed on purpose for chains 8–13 so any machine could pull and run the fleet; they were untracked in the chain-14 preparation (66cd6b1) and.gitignorehas no!deploy/*.key.jsonexception left. The working copies are still on disk and still run chains 8–13, but they are published seeds. Chain 14 runs on eighteen fresh validator keys and eighteen fresh payout wallets, generated off-repo bydeploy/gen-chain14-keys.shinto$KEYDIR(default~/.rand-chain14, backed up by the user off the laptop) — seedeploy/cut-chain14-genesis.shanddeploy/cutover-droplet-chain14.sh. Peer ids changed with those keys, sodeploy/nodes.envwas replaced by the generated$KEYDIR/public/nodes-chain14.envat the cut; a droplet's key now lives at/root/keys/node-<name>.key.json, outside/root/fullnodesorebuild-vps.sh'srsync --deletecan never remove it.
Chain 14 (cut 2026-09-20) is v0.5: RPL tokens and a hardened, zUSD-backed guardian bridge. The genesis carries a
tokenssection (registration_fee,mint_cap_per_day, no listed token — zUSD is registered by transaction after the cut) and abridgesection at guardian set 0 with sixpq_guardians(index-aligned with set 1 until the guardian-set rotation) and onepause_key. The wire and consensus changes underneath: one 4-in/4-out hidden-asset bundle replaces the two-bundle token transfer (theTokenTransferaction is gone); every bundle proof is bound to its whole transaction (docs/confidential.md, "Transaction binding"); aMint's commitment opening (pk,time,r) is checked, not merely declared (POOL-1); a block's timestamp is bounded going forward as well as back (B2,docs/bridge.md§16) — every validator on this chain needs NTP. The full launch record — chain-14 facts, the zUSD deploy, the mainnet round-trip evidence table and what remains pending — isAGENTS.md's v0.5 entry and the cut runbook,docs/superpowers/handoffs/2026-09-20-chain14-cut-runbook.md. The build is chain-14-only: a chain-13 binary cannot open this genesis and this binary cannot run chain 13. Like chains 9–13 it is cut without theaggregationsection (the hidden-asset bundle's declared shape needs re-measurement first;node::check_build_runs_genesisnow refuses to start any genesis carrying one at all). The chain-13 and earlier records are under "History" and "What the … rollout actually did".
| chain id | 14 |
| genesis hash | 1cff3b7da248d93ab547aef5c05bb7d0d22da510b592dab9cf7374807de7c7ff |
| genesis file | deploy/genesis-chain14.json (cut 2026-09-20 with deploy/cut-chain14-genesis.sh; chain 13's stays at deploy/genesis-chain13.json, chain 12's at deploy/genesis-chain12.json, chain 11's at deploy/genesis-chain11.json, chain 10's at deploy/genesis-chain10.json) |
| pinned build | b3c594c; binaries rand-node and rand in bin-b3c594c/ (macOS, node A now runs from ~/rand-node-a via deploy/run-a.sh) and E's /root/fullnode/target/release (Linux); .update-pin content is b3c594c. main (docs and the genesis file itself, no code) moved on to ed88241 after the cut. Chain-14-only: never same-chain-update it onto chain 13 |
max_program_words / max_proof_bytes / max_block_bytes / max_call_envelope_bytes / max_program_public_words |
unchanged from chain 13: 65 535 / 8 388 608 (8 MiB) / 20 971 520 (20 MiB) / 65 536 / 32 768 |
| aggregation | off, and not optional (no aggregation section; the node refuses to start one) |
| zkVM | constraint set 6 (the public input segment) unchanged; the hidden-asset bundle guest (bundle_hidden, tier 14, ~100 s/proof) replaces the two-slot bundle() guest for every transfer, bond, burn and bridge action |
hc_bundle |
83d3a370… (chain 13's was 4a27356f… — read it off the build that cuts the next genesis, do not copy this value forward) |
| validators | 18, every one staked at exactly 1000 RAND (the staking minimum), on fresh keys held off-repo — see the note above |
| quorum | 13 of 18 (strictly more than two thirds of 18 000 RAND of stake) |
| epochs | --epoch-blocks 1000; UNBONDING_EPOCHS = 2, so unbonded stake releases ~2000 blocks later |
| genesis value | 18 000 RAND staked in the register + 5 × 1000 RAND as deposit notes (with openings, core I-2) = 23 000 RAND |
| faucet | on (rand faucet [address], up to 100 RAND per call on any node, now rate-limited: burst 8, 1/s per node) |
| confidential computation | on, production FRI profile (80 queries; the proof cap is the genesis max_proof_bytes, 8 MiB) |
| tokens (RPL) | registration_fee 1 RAND, mint_cap_per_day 100 000 × 10⁸ per backing per day, no token listed at genesis (docs/tokens.md) |
| bridge | guardian set 0, six pq_guardians (B3), one pause_key (B1), emitters for chains 2/3/4/5, no bridged token listed at genesis — zUSD registered by transaction (docs/bridge.md §§13–20) |
Chain 8 is a fresh genesis because constraint set 5 cannot replay chain 7: a chain-7 proof and a
constraint-set-5 verifier do not round-trip in either direction, so a 03c9fb9 node can neither join
the chain-7 fleet nor replay its blocks. Same reason chain 5 → 6 and 6 → 7 were new chains. The
build also merges phases S2 (staking: a register, bonding, epochs, per-validator payouts) and S3
(the bridge as notes, and call envelopes), neither of which the chain-7 fleet's build (01dc23d,
S3-only) understands.
hc_bundleis not a constraint-set marker. It is the digest of the vendored bundle guest, and that guest is byte-identical between01dc23dand03c9fb9— chain 6, chain 7 and chain 8 all pin4a27356f…. So a chain-7-era binary pointed at this genesis file will accept thehc_bundleand only diverge later, on proof verification. The chain id is what actually keeps the two fleets apart. Check the build, not the bundle digest:bin-03c9fb9/,.update-pin=03c9fb9.
Every tag bumps the workspace version. Cargo.toml's [workspace.package] version is what
rand_getVersion reports, and it read 0.1.0 through v0.2, v0.3 and v0.4 because nothing bumped
it and the test compared the crate with itself (audit v3, PROC-3). The RPC test now pins the
expected string, so it fails until the bump is made.
This table is chains 8–13 history. Those chains shared one set of validator and payout keys (peer ids unchanged cut to cut), tracked in this repository until OPS-1 untracked them for the chain-14 preparation. Chain 14 runs on eighteen entirely fresh validator and payout keys, held off-repo in
$KEYDIR(default~/.rand-chain14); its own address/pubkey/payout table is$KEYDIR/public/validators.tsvon the controller's machine, not in this repository. Node A's row below (laptop, LAN address, chain-7-era validator address) is likewise chain ≤13 only — chain 14's node A key and address live in the same off-repo$KEYDIR.
Every validator holds exactly 1000 RAND, which is MIN_STAKE — one unit less and genesis
refuses the file outright (a validator below the minimum would sit in the register but in no epoch's
set). That is deliberate: this fleet is here to exercise the minimum-stake rule, the epoch
boundary and the withdraw path on a real network, so every node sits exactly on the line. Chain 7
gave each of the same 18 nodes 100 000 RAND; chain 8 gives them 1/100th of that.
Consequence to plan around: 13 of the 18 must be online to commit anything. With 12 up, views advance and nothing finalises. Stake is uniform, so quorum is a headcount here.
payout is where this validator's rewards and released stake are paid, as a note. It is register
state and therefore part of the genesis hash. Each validator has its own wallet in
deploy/payout/<name>.key.json — read deploy/payout/README.md before trusting the regional names
in this table: a…f are verified against the key files in deploy/, lon1…mem1 are inferred
from chain 7's validator order and can only be confirmed on the droplet itself.
| name | where (chain-7 fleet) | validator address | pubkey | payout (deploy/payout/…) |
|---|---|---|---|---|
| a | laptop, LAN 192.168.100.123 (NAT) | 2nRdFChBXRmKoe2sQE3ZYDzvdg53QmBZJJ9iweY7hk1v |
15321b04570d7095… |
rand11qgA3ETvT…uLm4n9 |
| b | DigitalOcean sgp1 168.144.102.43 (droplet since 2026-09-19; was the MacBook Air, 192.168.100.79 NAT) | ByDkxsEfDCR5DrmDufKftvcRsgvufypnZ4SgDQzJAQ7Z |
4d491d8a5540f005… |
rand188sGyCRpX…hY5T8k |
| c | DigitalOcean fra1 164.90.239.200 | F6rYLexPhyMmwPNqbEmyyp5FiTmtQqDgZyqScUqYY4F6 |
f3557730c8a76b43… |
rand14wXebNPJc…rxb3sy |
| d | DigitalOcean ric1 165.245.173.74 | 5tMgLSzXL8keU1vg2wtGEXRJkmfBK6GzhjNjxrCFgCaj |
e95ecd68c5621147… |
rand13SKXv49EH…thJHN4 |
| e | DigitalOcean sgp1 188.166.235.187 (RandScan explorer) | CxeG7vJaxUoKBZZe8U8LGXohH2FvcCbE47AufK6Mp2jf |
9ffac1e8ff6b687d… |
rand12nJ3vSqUc…E3YbGt |
| f | DigitalOcean nyc3 159.89.185.254 | DcuuZrzDSJedhFnynLFchNfYmW4UKiZT2nEKbcs2ojmJ |
b277b8eb8863fca2… |
rand1CE5oAjb8S…VFDQs2 |
| lon1 | DigitalOcean lon1 139.59.160.76 | 8UcsaXDSSWcC6fT3CWT89fgUvG4ubaYd61FUQHKiUDLv |
7edc60fc6e6e35f6… |
rand12kmqp9BMq…2h77jF |
| sfo3 | DigitalOcean sfo3 24.144.89.22 | 9rex7stS6d9QxaAh5nghjaUEratKJFUAmLAoxRFmP7LM |
5394ea2fc122a869… |
rand1cjU3K9faj…x2sB7S |
| tor1 | DigitalOcean tor1 146.190.243.29 | CRW3fQsuFa7YSdU5ZDrRMxQjJ4ET9kzf6hg1D8CAhoWH |
71528cc5c8b11715… |
rand191phQ9Ffb…YCHDJc |
| blr1 | DigitalOcean blr1 167.71.235.108 | ASzbnFwqVnwsrN81iytkuhNbs8f93Q3h4rQjXPDUNMjU |
ca6b8bdc43f95a4f… |
rand1aD55SbVMS…HJjUTs |
| syd1 | DigitalOcean syd1 170.64.226.65 | 8cHAjP3wgGDv55Ym2qZrYyu2Mvc7b86jsrN6vWcwZoJV |
574d78491644c873… |
rand1CdUmb2fXv…v2YX2p |
| atl1 | DigitalOcean atl1 165.245.142.90 | BV2BfMZJo2Lpo3pR7RfAjytnFxmMhzCWxoUHhXhjT2qL |
c82e55fd7ff180e0… |
rand13iwyU6yJM…cJcDx3 |
| ams3 | DigitalOcean ams3 146.190.233.230 | 6orzEKd3Ds3ZFmp21pidbF19kAe6AczzPCtnMVrKRLeA |
76bcc53c2370404b… |
rand16ct7YSQsS…DsRzq9 |
| nyc1 | DigitalOcean nyc1 192.81.214.91 | F5RVdAe3Wo2AWHsjKZjFKmGcnTHvJrDpdSHL8gnF4wN1 |
59bd8ed1017f2743… |
rand15eMjSnZxL…GqYsZj |
| nyc2 | DigitalOcean nyc2 107.170.49.234 | 9hU2qrPFxbcZjSse6MJnapYRs4RchALWtJH4RRrbwDGo |
2c111e425d2fa4d3… |
rand1AY2hQjQ3X…MHYjq9 |
| sfo2 | DigitalOcean sfo2 143.110.135.126 | 6NGLfkZcTs3i5LTTbqoYKM9aQCs7ZXygHirTg2AtWhsA |
c18d93c46e9f8cc5… |
rand18BY14Pv39…EwrVu6 |
| mkc1 | DigitalOcean mkc1 201.79.35.212 | 8m5BoX2RA4xTVNfDwKMbWQ3JSChrj69hvJzdSEKMsZDM |
38c1921b1738182a… |
rand1qtwxvqK9M…7obifJ |
| mem1 | DigitalOcean mem1 168.144.61.10 | 6T5WyBUrYN1noLi3tDKC1Uw5Vf3xabExnWCDdik7VTPV |
ce48ef3e62463c08… |
rand15ijmGYjDG…N7co5n |
The rows are in genesis order (which is chain 7's validator order). C, D, E and the rest of the
droplets have public IPs and act as bootstrap nodes; A and B are behind NAT and dial out
(deploy/run-a.sh, deploy/run-b.sh). Full multiaddrs and peer ids are in deploy/nodes.env.
A shielded chain has no per-validator allocation: value exists only as a note someone holds the
spend key for, so --alloc takes a shielded address (the long form: pk and the ML-KEM
encapsulation key the deposit envelope is sealed to) and creates one deposit note for it. As on
chains 7–13, 1000 RAND goes to each of the five wallets in wallets/ (gitignored — those keys live
on the machine that cut this genesis and nowhere else); chain 14 kept the same five wallets
rather than rotating them (the cut script's ALLOC_WALLETS default), so the addresses below are
unchanged, though the note commitments themselves are fresh (r is never reused across a cut). On
a tokens-genesis chain each note also carries its opening (pk, time, r) so rand-node init
can recompute the commitment at asset 0 (core review I-2):
| wallet | address | amount |
|---|---|---|
wallets/shielded-1.key.json |
rand14mvBw1njf…BcuX6X |
1000 RAND |
wallets/shielded-2.key.json |
rand17twtYjPKv…6nQRjq |
1000 RAND |
wallets/shielded-3.key.json |
rand18NRCt4GE9…AxGBHY |
1000 RAND |
wallets/shielded-4.key.json |
rand1BFkPNgb7v…xS8jqF |
1000 RAND |
wallets/shielded-5.key.json |
rand1DoRgCizWr…DhyMfb |
1000 RAND |
Chain 8 has no bridge section, exactly like chain 7. The bridge's guardian set, emitters and asset
registry are genesis state and go into the genesis hash, so a bridge cannot be added to a running
chain: switching it on means cutting chain 9. The S3 code is in this build and its bridge RPCs
answer, but with no guardian set there is nothing to attest a deposit, so rand bridge-mint and
rand bridge-burn have nothing to do here. If the fleet wants to exercise the bridge, that is a
new genesis with a bridge section and a decision about who the guardians are — worth doing
deliberately rather than bolting onto this cut.
What chain 8 does add over chain 7 is the staking half: bonding, unbonding, withdraw, epoch sets and per-validator payouts (phase S2), and call envelopes (S3).
Chain 13 was cut on 2026-09-19 with deploy/cut-chain13-genesis.sh. It makes the same
rand-node genesis call as chain 12's script below, with --chain-id 13 and the five limit flags
added (--max-program-words 65535 --max-proof-bytes 8388608 --max-block-bytes 20971520 --max-call-envelope-bytes 65536 --max-program-public-words 32768). It reads the twelve regional
public keys from deploy/genesis-chain12.json. Chain 12's command, which it extends:
deploy/cut-chain12-genesis.sh, run from the repo root against the 17db41d release binaries:
NODE=bin-17db41d/rand-node WALLET=bin-17db41d/rand ./deploy/cut-chain12-genesis.sh # ALLOC_WALLETS=wallets by defaultIt expands to one rand-node genesis call — 18 --validator <key or hex pubkey>,1000,<payout>
and 5 --alloc <rand1…>=1000, exactly chain 10's shape:
rand-node genesis --chain-id 12 \
--validator "deploy/node-a.key.json,1000,$(payout a)" \
... (b, c, d, e, f the same way) ... \
--validator "<chain-10 pubkey #6>,1000,$(payout lon1)" \
... (sfo3 … mem1, chain-10 pubkeys #7..#17) ... \
--alloc "$(addr 1)=1000" ... --alloc "$(addr 5)=1000" \
--epoch-blocks 1000 --faucet --fri-profile production \
--out deploy/genesis-chain12.jsonwhere payout n is rand --key deploy/payout/$n.key.json address and addr i is
rand --key wallets/shielded-$i.key.json address — both the long form again. The twelve regional
public keys are read out of deploy/genesis-chain10.json (committed here as history for exactly
this reason), because those nodes' key files were generated on the droplets and exist nowhere in
this repo.
Re-running the script does not reproduce this genesis hash: every deposit note carries fresh
commitment randomness, so the same --alloc list yields a different hash every time. A
deterministic r would let anyone confirm a guess at a genesis note's owner and amount by
recomputing the commitment. Cut once, distribute deploy/genesis-chain12.json byte-identically,
keep it.
Chain 11's genesis carried 23 signed receiver records (deploy/payout/<name>.record.json,
committed, and wallets/shielded-<i>.record.json, gitignored) because every payout and alloc
owner was a receiver id that had to resolve to a note key and an ML-KEM key. Chain 12 has no
records: a payout and an alloc owner are the long address again, which carries both keys itself.
The record files and deploy/cut-chain11-genesis.sh stay in the tree as the record of that cut;
a 17db41d node neither reads nor writes them.
Full step-by-step is docs/superpowers/handoffs/2026-09-20-chain14-cut-runbook.md; this is the
outcome. The deploy/gen-chain14-keys.sh / cut-chain14-genesis.sh / cutover-droplet-chain14.sh
scripts are the current cut-over tooling — the numbered checklist and the sed-based unit edit
below are chain-8-through-13 history, kept for the pattern they established, not for chain 14's own
commands.
- Eighteen fresh validator keys and eighteen fresh payout wallets were generated off-repo into
~/.rand-chain14(deploy/gen-chain14-keys.sh), producing new peer ids and a freshnodes-chain14.env. - The genesis was cut with
deploy/cut-chain14-genesis.sh, spliced with the fixedbridgesection (guardian set 0,pq_guardiansindex-aligned with set 1,pause_key) and thetokenssection (registration_fee1 RAND,mint_cap_per_day100 000 × 10⁸, no listed token);rand-node initon the finished file — notrand-node genesis's own printout of the unbridged file — gave the real hash,1cff3b7da248d93ab547aef5c05bb7d0d22da510b592dab9cf7374807de7c7ff. - Fleet disk survey first. Seven droplets (c, f, lon1, sfo3, blr1, ams3, tor1) were at or near
0 bytes free: chain-11 (
79123fa7, 7 GB) and chain-12 (605eb783, 21 GB) data dirs had never been deleted after their cuts, and chain 13 itself was growing ~16 GB/day. Both retired data dirs were removed on the affected droplets (guarded to only the unit's own current datadir) and the journal vacuumed, before touching anything else. - E was rebuilt at
b3c594c(deploy/rebuild-vps.sh), then all seventeen droplets were rolled withdeploy/cutover-droplet-chain14.sh— C and D (the bootstraps) first, then the rest one at a time, each waited torand_getHealth: okbefore the next, B and E last of the droplets (E serves the explorer), node A (the laptop) last of all, restarted from~/rand-node-aviadeploy/run-a.sh. All 18 came up on chain 14, state root and block hash identical at height 90 across A/C/E/mem1,hc_bundle 83d3a370…fleet-wide, faucet working. - randscan was deployed from its
feat/rpl-tokensbranch (merged to its ownmain,adfe129) in the same window. Its indexer stuck at height 255 on first deploy: the database's migration version 8 had been burned by the reverted chain-11 receivers migration, so the new migration, originally numbered008, was silently skipped. Renumbered to009; re-deployed; the indexer passed 256 and token pages render. mainmoved froma2c9896(the mergedfeat/rpl+feat/bridge-hardening) through three deploy-only commits (66cd6b1,971ca75,b3c594c— the pinned build) toed88241(the committed genesis file,nodes.env,run-a.sh), pushed.- zUSD was then registered and listed by transaction (index 1, six
ListBackings), and bridge-06 ran the mainnet round trip — seeAGENTS.md's v0.5 entry for the full evidence table.
https://rpc.randprotocol.org — served since 2026-09-20 03:04 UTC (within the hour F's Caddy
hop was written) by the web droplet's own vhost: Cloudflare → web droplet nginx → the sale service's
filtered proxy → randscan's /rpc route on E → E's 127.0.0.1:8545 (until 2026-09-27; since then
the proxy's upstream is an SSH tunnel to obs1, the archive). F's Caddy is not in the path
and does not run; deploy/caddy/README.md has the chain and the evidence. Every droplet's node RPC
stays bound to 127.0.0.1, per the topology rule in docs/deploy.md.
Nothing about chain 11 is reusable — different chain id, a genesis format a chain-11 node cannot
parse — so this is a clean start on every machine. Keep chain 11's data directory around if you
want to keep serving it; the new DATA name is keyed on the genesis hash, so the two never
collide.
- Binary:
bin-4504a03/rand-nodeandbin-4504a03/rand(orcargo build --releaseat the pinned commit; the Linux ones are built once on E and fanned out bydeploy/cutover-droplet.shat a chain cut, or bydeploy/update-droplet.shfor a same-chain binary update). Every node on the fleet must run this one build — a genesis format change is a fork, and a mixed fleet stalls. From the v0.4 chain on, whose genesis setsmax_program_words, the binary must be a v0.4 build beforeinit: an older one silently drops the field it does not know, builds a different genesis hash and fails to join (docs/cli.md,rand-node init). - Genesis:
deploy/genesis-chain12.json, byte-identical everywhere.rand-node initprints the hash; it must read605eb7830963833ef897455b98cd2a641aec58e0291460898a5d19ab88760ef0, and the file has noreceiversarray. - Fresh data dir:
data-<letter>-605eb783(never reuse a chain-11 directory). .update-pinon the MacBook Air (node B, auto-updater): set its content to4504a03, or the updater drags B onto whatever build it last pinned and B drops out of the set.- Start, and check
rand-node status:heightclimbing,chain id 12,hc_bundle 4a27356f…,active_validator: true,notes: 5at genesis. - Quorum: nothing commits until 13 of the 18 are up. Bring the droplets up before declaring a stall.
- Explorer on E re-indexes itself on a chain id change; it must run randscan
ba6fee6or later (the receivers indexer reverted — the chain-11 explorer polls an RPC this build does not have). The activity loop's two wallets on E are version-2 key files again (the v0.2 client had rewritten them as version 3 with akem_version; the spend key is unchanged). - Per droplet, same service and peer ids as chain 11 —
deploy/cutover-droplet.sh <ip> 79123fa7 605eb783 deploy/genesis-chain12.json(old prefix, new prefix, then the genesis file; peer ids do not change, so this is the whole cut-over).
rand-node init --datadir data-c-8c742fc9 --genesis deploy/genesis-chain8.json
rand-node run --datadir data-c-8c742fc9 --key deploy/node-c.key.json --validator \
--block-interval-ms 1000 --bootstrap /ip4/<ip>/tcp/30303/p2p/<peer id>Every droplet already had a rand-node systemd unit pointing at its chain-7 data dir. The only
edit the unit needed was the data-dir name — the key path, listen address, --rpc 127.0.0.1:8545,
--no-mdns and both --bootstrap multiaddrs stay exactly as they were, because the peer ids did
not change with the genesis:
# 1. new data dir, from the chain-8 genesis (must print 8c742fc9…, chain id 8)
systemctl stop rand-node
rand-node init --datadir /root/data-$(hostname)-8c742fc9 \
--genesis /root/fullnode/deploy/genesis-chain8.json
# 2. repoint the unit: the datadir suffix, and nothing else
cp -a /etc/systemd/system/rand-node.service /root/rand-node.service.chain7.bak
sed -i 's/-55668ebf/-8c742fc9/g' /etc/systemd/system/rand-node.service
# 3. restart
systemctl daemon-reload && systemctl restart rand-node
rand status # chain id 8 via rand_chainId, height climbing, peer_count → 15–16-55668ebf occurs exactly once per unit (in --datadir), so the sed is safe as written; the
chain-7 backup of each unit is left at /root/rand-node.service.chain7.bak. Data dirs are keyed
on the genesis hash, so /root/data-<hostname>-55668ebf survives untouched next to the new one.
Two things worth knowing before the next cut-over:
- Swap the binary and chain 7 stops there and then. Build
03c9fb9cannot open a chain-7 data dir at all: it needs apayouton every validator in the stored genesis, and chain 7's genesis has none, so the node exits withjson: missing field `payout`andRestart=alwaysturns that into a crash loop. Distributing the binaries is the cut-over — do it immediately before the init, not as a separate earlier step, and expect no graceful overlap between the two chains. - Distribute through E, not from the laptop. 36 MB per node over a home uplink is minutes each;
copying to E once and fanning out droplet-to-droplet (
ssh -Afrom the laptop, so E uses the forwarded agent and never holds a private key) moves all 15 in well under a minute. Superseded (ops review OPS-3): a forwarded agent lets root on every droplet it reaches use every key the laptop's agent holds for the length of the session.update-droplet.shandcutover-droplet-chain14.shno longer pass-A; they relay build host → laptop scratch → droplet with the sha256 checked at each hop (deploy/lib/relay-binaries.sh, asroll-all.shalready did) — slower over a home uplink, never an agent on a droplet. The older chain-7…13 cutover scripts (cutover-droplet.sh,cutover-droplet-rand.sh) still use-A; do not reuse them.
The 16 droplets went to chain 8 in this order — the 12 regional validators in batches of four, then
F, C, D, and E last because it also serves the explorer. The 13th node (F) came up at
07:10:55Z and block 1 committed at 07:10:58.438Z, 3.4 s later: quorum is 13 of 18 and the 16
droplets alone carry it, so the chain started without waiting for A or B. Nothing on E's explorer
(/root/randscan, /etc/randscan, /etc/caddy, randscan-*) was touched, and E's RPC stayed on
127.0.0.1:8545.
Keep --block-interval-ms at 1000 (the default) or slower. A bundle proof takes ~100 s and its
anchor is valid for 256 blocks, so a faster chain rejects honest transfers whose anchor expired
mid-proof (docs/shielded.md §5); at 1000 ms the anchor window is ~256 s, which holds. The
integrated S2+S3 branch's own cluster tests run at 3000 ms because proving on a contended laptop is
much slower than on the fleet — if a node is also proving locally, slow its chain down, not the
fleet's.
On A and B: ./deploy/run-a.sh / ./deploy/run-b.sh (they init data-{a,b}-8c742fc9 from
deploy/genesis-chain8.json themselves). Droplets:
deploy/push-to-vps.sh <ip> <letter> "<bootstrap multiaddrs>" [validator|observer] provisions from
scratch, deploy/rebuild-vps.sh <ip> rebuilds on a new commit and restarts. Service name:
rand-node.
-
E was rebuilt at
86af6ebwithdeploy/rebuild-vps.sh 188.166.235.187. -
All 16 droplets were cut over with
deploy/cutover-droplet.sh <ip> 605eb783 8123ccac deploy/genesis-chain13.json
-
mkc1 (201.79.35.212) and mem1 (168.144.61.10) had full 29 GB disks. Their dead chain-11 data dirs (
data-*-79123fa7, 7 GB) were deleted first, leaving 7 GB free. Later on 2026-09-19 both were resized froms5-1vcpu-2gb-30gbtos5-1vcpu-2gb-80gb(same CPU and memory, an 80 GB disk;doctl compute droplet-action resize <id> --size s5-1vcpu-2gb-80gb --resize-disk, one at a time,rand-nodestopped first). Each now has 77 GB with 55 GB free. -
Node A was restarted on
bin-86af6eb/(deploy/run-a.sh). -
Node B now runs on a droplet, not the MacBook Air, which is offline.
sesh-node-b, sgp1,s-2vcpu-4gb, 168.144.102.43 (created 2026-09-19): the same build (cc20bf84…), keydeploy/node-b.key.json(peer id12D3KooWDBrMjejqsbYGLgmDAZd2K4N98yz5VKntcgfpCLsCFUJD, unchanged), data dir/root/data-rand-node-b-8123ccac, a unit like C's plus E as a bootstrap. Never run node B on the MacBook Air while this droplet runs: the same validator key in two places signs two different blocks at one height (equivocation). Leave the Air's.update-pinon a chain-12 build (it cannot follow chain 13), or retire its auto-updater. -
Survey after the cut-over:
check result droplets active 16 / 16 rand-nodesha256cc20bf84…on all 16peers 16 each block rate about 1 block/s -
RandScan re-indexed chain 13 on its own. On E, the randscan activity loop's note stores and program were moved aside (
*.chain12-stale) and its timer was restarted.
Live results on chain 13 the same day, from the genesis wallet shielded-1
(docs/translators.md §4.6, §4.6.1, §5.4):
| action | tx | height | fee |
|---|---|---|---|
deploy the translated ERC-20 (f074c4eb…, 11 686 words) |
c7a66dc4…53acd0 |
committed at 381 | — |
deploy the translated SPL Token (74023691…, 65 096 words, 27 151 public words) |
626fc938…927530 |
anchored at 400 | 9.2257 RAND |
call the ERC-20 (approve, tier 16, 3 412 405-byte proof) |
279e1f62…c4bfa7 |
anchored at 802, committed at 919 | 0.00357 RAND |
The RPC release (tag v0.3: eleven methods, the receipts and transaction WebSocket topics,
the receipts_by_program column family) went out as a same-chain binary update, no chain cut:
deploy/rebuild-vps.sh 188.166.235.187from a detached worktree of4504a03. The script now writes.git-revand passesRAND_BUILD_SHAto the build, sorand_getVersionreports4504a03d…even though E's tree still carries a stale.git. E built in 4 min 17 s; its first start loggedbuilt the receipts-by-program index from 117 receipts.- Startup is ~4 minutes, not ~20 s: the node runs a quick chain verification before the RPC
opens (239 s at 59 183 blocks). A rolling update must wait for
rand_getHealth=okon each node before the next, which the loop did (a fixed sleep would stop two validators at once). The first start of a new build still runs the full check (plan the pause for it); a restart of the same build skips it (startup checkpoint, v0.8.0+). deploy/update-droplet.sh <ip>over the other fifteen, one at a time, each waited torand_getHealthok: 06:33–07:38 UTC, ~4–6 min per droplet, all fifteenupdated to 16e41293, chain producing throughout.- A: stopped, its 10 GB data dir copied, restarted from
deploy/run-a.shonbin-4504a03/(121 receipts indexed; height 62 276, 16 peers,active_validator: true). B was not reached; its.update-pinshould read4504a03. - Rollback rehearsed on the copy of A's database: v0.3 migrated it (121 receipts); the
c66e6b8binary then refused it withColumn families not opened: receipts_by_program;rand-node db drop-receipts-indexdropped the family and the marker;c66e6b8then verified all 62 202 blocks. The procedure is "Rolling back v0.3" above. - Live test against E (every new method, and the
transactiontopic both for a fresh faucet mint and for a call committed before the subscribe), all as specified:rand_getVersiongit_sha4504a03d…; genesis605eb783…; healthok;rand_getFinalitycommittedby height and by head hash;rand_getProposer [0, 2^64-1]refused with-32602before any work;rand_getReceiptsfor program509fee7a…found its call at 62 201;rand_getTransactionStatusread the call and the deploycommittedand a zero hashunknown;rand_getWitnesses [0, 1, 10^12]gave two 32-step paths and anull; the fresh mint's subscription fired 0.9 s after submit, and the late subscription to the 62 201 call fired 0.8 s after subscribing with its original height and index; both subscriptions removed themselves (rand_unsubscribeansweredfalse).
Main moved past 17db41d by a test-only line (c66e6b8), and the fleet was re-pinned to it
without a chain cut: deploy/rebuild-vps.sh 188.166.235.187 from a detached worktree of
c66e6b8 (E rebuilt in 4 minutes and restarted — the same sha256 b7983e56… as before, because
the change is inside #[cfg(test)]), then deploy/update-droplet.sh <ip> over the other
fifteen droplets. That script compares the droplet's installed rand-node with the build host's
sha256 and only stops, swaps and restarts on a mismatch, so all fifteen reported "already on
b7983e56" and were left running. A was restarted from deploy/run-a.sh on bin-c66e6b8/ (the
laptop build is byte-identical to bin-17db41d/ too). B was unreachable again; its
.update-pin should read c66e6b8, though 17db41d runs the same bytes.
The revert commit 17db41d on main; cargo build --release there on the laptop (bin-17db41d/)
and on E (4.5 minutes, from an rsync of the tree); deploy/cut-chain12-genesis.sh with those
binaries (genesis 605eb783…, hc_bundle unchanged at 4a27356f… — the revert touches no
guest). Then deploy/cutover-droplet.sh <ip> 79123fa7 605eb783 deploy/genesis-chain12.json with
SERVICE=rand-node BIN_NODE=rand-node BIN_WALLET=rand: lon1 alone first, then the other eleven
regional validators, F, C, D, E, one at a time from a shell loop — about 12 minutes for all 16,
no refusals, peer ids and bootstraps unchanged. Blocks were committing before E's own cut-over
finished; a minute later C, D, E, lon1 and mkc1 stood at heights 59–69 with 15–16 peers each,
~1 block/s. A was stopped and restarted from deploy/run-a.sh (chain 12, 5 genesis notes,
active_validator: true). On E the activity loop's two key files (/root/randscan-wallets/) were
rewritten from the v0.2 client's version 3 back to version 2 with the same spend keys (backups
*.v3-chain11.bak, chain-11 note stores moved aside), and the explorer redeployed at randscan
ba6fee6 (the receivers indexer reverted) with its deploy/push-to-vps.sh. B (the MacBook Air)
was not reachable: it needs bin-17db41d/ and .update-pin = 17db41d. Chain 11's data dirs
are left in place on every droplet (data-rand-node-<name>-79123fa7).
Build ee716d7 (tag v0.2) on E, then deploy/cutover-droplet.sh <ip> 4d757f11 79123fa7 deploy/genesis-chain11.json with SERVICE=rand-node BIN_NODE=rand-node BIN_WALLET=rand, one
droplet at a time: the twelve regional validators, then F, C, D, E — about 16 minutes for all
16; peer ids and bootstraps unchanged from chain 10. E was at height 24 when its own cut-over
finished, i.e. the chain was committing before the last node joined. A followed from
deploy/run-a.sh; ten minutes later every node sat at 16 peers, ~1 block/s. The explorer was
redeployed with the receiver registry (randscan d580e5b) right after E: healthy, lag 0. The
activity loop's two wallets were faucet-funded with their own records (the first-contact path,
live) and registered so the loop's transfers resolve through /api/v1/receivers. B (the MacBook
Air) is still unreachable: it needs bin-ee716d7/ and .update-pin = ee716d7.
deploy/cutover-droplet-rand.sh, one droplet at a time: a peer-id pass first (the new binary on
every droplet, rand-node address --key, nothing else touched) to regenerate deploy/nodes.env
and the bootstraps, then C and D first — every peer id changed with the rename, so the nodes
that bootstrap the fleet had to be on their new identities before anyone dialled them — then E,
F, and the twelve regional validators. The script writes a fresh rand-node unit (bootstraps
included), renames the hostname to rand-node-<name>, and retires the shrugg-node unit. All
16 were over in about 12 minutes; heights were climbing at ~1 block/s with 16 peers each by the
time the last one finished, and A followed from deploy/run-a.sh. The explorer on E was
redeployed with the rand_* RPC names (randscan 286f399) right after E's cut-over: healthy,
lag 0. Old binaries and every retired chain's data dir were then removed from all droplets.
Order: lon1 alone first (to prove deploy/cutover-droplet.sh), then sfo3/tor1/blr1, ams3,
nyc1/nyc2/sfo2, mkc1/mem1, syd1/atl1, then F, C, D, E. Binaries built once on E (4 cores, 8 GB)
from 5f8c6f9 and fanned out droplet-to-droplet over the forwarded agent — the deploy key
(~/.ssh/id_ed25519) has to be in the agent (ssh-add) for the hop to E to authenticate.
Quorum (13 of 18) landed during the last batch; D reported height 23 while E was still cutting
over, and ten minutes later all 16 droplets plus A sat at 16 peers each, ~1 block/s. B (the
MacBook Air) was unreachable and stays on chain 8 until its .update-pin is set to 5f8c6f9.
Three things went wrong and are fixed in the scripts:
deploy/cutover-fleet.shunder macOS bash 3.2 re-ran droplets that were already over, and the per-droplet script stopped the service before its unit check — nyc1, nyc2 and sfo2 were left stopped for a few minutes. The unit check now comes first, and the fleet script says to use bash ≥ 4 or go one droplet at a time.- mkc1 and mem1 (30 GB disks) had filled up with three chains' data dirs and crash-looped for days
before the rollout (RocksDB
No space left on deviceon every restart, 10k and 29k restarts); the chain-8 resync after clearing the stale dirs filled them again. Their chain-8 data dirs were deleted at cut-over (a set-6 binary cannot serve chain 8 anyway); 25 GB free each now. Resize those two before chain 9 grows past ~20 GB. - Chain 9 is cut without the aggregation section — see the note at the top.
This applies to chain 12 and is kept as history. Chain 13 cannot be rolled back to a v0.3 build:
the v0.3 builds cannot run the chain-13 genesis, and the Deploy, ProgramRecord and
CallReceipt encodings changed.
v0.3 (the RPC release) is interoperable on the wire, but its database is forward-only: its first
start adds a sixteenth column family, receipts_by_program, and RocksDB refuses to open a
database holding a family the caller does not list. A pre-v0.3 binary lists fifteen, so a plain
re-pin to the old build leaves the node failing at startup with Column families not opened: receipts_by_program. Drop the index first, with the v0.3 binary, per node:
systemctl stop rand-node
# the --datadir the unit runs with (`systemctl cat rand-node`); on A it is data-a-605eb783
rand-node db drop-receipts-index --datadir /root/data-rand-node-<name>-605eb783It prints what it removed (the family and the receipts_by_program_built meta key) and is safe
to re-run: a second run reports both absent. It fails while the node still holds the database
lock, which is why the stop comes first. Nothing else is touched — the receipts themselves stay,
and a later v0.3 start rebuilds the index from them.
Then re-pin as for any same-chain update: rebuild the old commit on E with
deploy/rebuild-vps.sh 188.166.235.187 from a detached worktree of it (E is the build host, so
stop E and drop its index before this — the script restarts E on the rebuilt binary), then
deploy/update-droplet.sh <ip> over the other droplets one at a time; it installs the old
binaries and restarts the stopped service. On A, run deploy/run-a.sh with BINDIR set to the
old build's bin-<sha>/.
rand --key wallets/shielded-1.key.json balance # scans the tree; 1000 RAND at genesis
rand --key wallets/shielded-1.key.json send "$(rand --key wallets/shielded-2.key.json address)" 1.5 --yes
rand faucet "$(rand --key wallets/shielded-3.key.json address)"
rand --key wallets/shielded-1.key.json program build --guest private_payment --arg 1000 --out pp.json
rand --key wallets/shielded-1.key.json program deploy pp.json
rand --key wallets/shielded-1.key.json call <program-id> --input 400 --input 250 --input 300 --input 75rand-node status reports notes, nullifiers, tree_root and hc_bundle next to the usual
height and peer counts — the quickest check that a fresh fleet agrees. is_validator (this node
holds a key in the register) and active_validator (that key is in the current epoch's set) are
different things from phase S2 on, and the second is the one that says whether a node is producing.
No new genesis is needed to add or remove a validator (docs/staking.md): the operator runs
rand-node register --key node-x.key.json --payout <rand1…>, a wallet with 1000 RAND runs
rand bond <its address> 1000 --registration <hex>, and the node — started with --validator
all along — begins proposing at the next epoch boundary (1000 blocks). Leaving is
rand-node unbond, then rand-node withdraw two epochs (~2000 blocks) later, which pays the
stake into a note at the payout address. Check the quorum arithmetic first: at 18 validators of
equal stake, 13 must stay online, and every validator added or removed moves that number.
Because every validator here sits exactly at MIN_STAKE, any unbond at all drops that node out of
the next epoch's set. That is the rule this fleet exists to exercise — just do it to one node at a
time.
rand-node initfromdeploy/genesis-chain8.jsoninto two separate data dirs: both print genesis8c742fc9…, chain id 8.- Nodes A and B started from it and gossiped (
peer_count: 1each), agreeing onhead_hash 8c742fc9…,tree_root 1ff29374…,hc_bundle 4a27356f…,notes: 5,active_validator: true. rand_getValidators: 18 rows, eachstake 1000000000000(1000 RAND),active: true,nonce: 0,rewards: 0, each with its own payout address.rand_getEpoch: epoch 0,epoch_blocks 1000, an 18-addressnext_set.rand_getSupply:genesis_staked 18 000,genesis_deposited 5000,total_supply 23 000RAND,invariant_holds: true.- Height stayed 0 and the view advanced — correct, not a bug. Two of eighteen validators are not a quorum, so no QC forms. Block production on this genesis cannot be tested with fewer than 13 of the 18 keys, and 12 of them are on droplets.
- Control run, same binary, a throwaway 2-validator genesis (chain id 88, same flags): 31 blocks in
~40 s at
--block-interval-ms 1000, both nodes on the same head, 2 register rows with payouts,invariant_holds: true. So the build commits; chain 8 waits on its fleet.
Chain 1 (2 validators, RAND) and chain 2 (4 validators + 2 observers) ran 2026-09-09; chain 3
followed the RAND → RAND rename and added the faucet; chain 4 (2026-09-10) added confidential
computation on constraint set 2 and ended at 31 952 blocks; chain 5 (2026-09-11) moved to constraint
set 4; chain 6 was the first shielded genesis (phase S1: the note ledger, bundles, the shielded
wallet — an account balance stopped existing); chain 7 (build 01dc23d) added S3's bridge-as-notes
and call envelopes with 18 validators at 100 000 RAND each, genesis
55668ebfe1cb842c48bf67fe58bb9e97d344be1e07f8f36b35eb8b405aef8c1f, and ran to height ~12 620,
where it stopped when the fleet moved to chain 8 on 2026-09-13. Its genesis file is kept here as
deploy/genesis-chain7.json and every node still has its data-*-55668ebf directory, but build
03c9fb9 cannot open one — replaying chain 7 needs a 01dc23d binary.
Chain 10 (cut and rolled out 2026-09-16, build a00c88c) was the RAND rename: every crate,
binary, RPC method (rand_*), address prefix (rand1…), hash domain, transaction id and p2p
identity carried the new name over chain 9's constraint-set-6, cut-without-aggregation genesis,
18 validators at 1000 RAND each, genesis
4d757f11cbbf48daaf2040fbd091d70ca66d5ff6fcb228e8319c9014a99bb7ed. All 16 droplets plus A were
over in about 12 minutes ("What the chain-10 rollout actually did", above), and it ran until the
fleet moved to chain 11 for the short-shielded-address feature. Its genesis file is kept here as
deploy/genesis-chain10.json; a chain-12 node cannot open a chain-10 data dir either (different
chain id, fresh note randomness), though the two formats are the same.
Chain 11 (cut and rolled out 2026-09-17, build ee716d7, tag v0.2) was the short shielded
address: a 53–55-char receiver id, a signed receiver record per payout and alloc owner (23 filed
at genesis), an on-chain registry with rand_getReceiver and rand register, versioned KEM
keys, genesis 79123fa75a2b946e21248e4b77e86444f134d0dc879035af9d4996c55eadeec2. It ran for
one day, to height ~12 000, and was reverted the same day (17db41d): a receiver id is a hash and
a sender cannot seal a note to a hash, so the first payment to a wallet that had never registered
needed a payment request or a hand-carried record, and registration was a paid self-transfer an
empty wallet cannot make. Its genesis, the record files and the cut script are kept here; the
spec and plan stay under docs/superpowers/ for the hybrid-address design that succeeds it.
Chain 12 (cut and rolled out 2026-09-17, build 17db41d, re-pinned to c66e6b8 the same
day and to 4504a03, tag v0.3, on 2026-09-18; genesis
605eb7830963833ef897455b98cd2a641aec58e0291460898a5d19ab88760ef0, deploy/genesis-chain12.json)
was chain 10's form again: the short shielded address of chain 11 was reverted. An address is
rand1 + base58(pk ‖ kem_ek), 1,666–1,667 characters, carrying the 1,184-byte ML-KEM
encapsulation key a sender needs to seal a note; payouts and genesis deposit notes are bare
addresses; there is no receiver record, no registry, no rand_getReceiver. Why: a receiver id
is a hash and a sender cannot seal a note to a hash, so on chain 11 the first payment to a wallet
that had never registered needed a payment request or a hand-carried record, and registration
itself was a paid self-transfer an empty wallet cannot make — the revert commit 17db41d says
the rest, and the design that keeps a short address and unconditional sendability and
privacy (a hybrid address: X25519 inline, ML-KEM by lookup) is the next spec. No hash-domain or
peer-id change from chains 10 and 11, so nodes.env was untouched. Same code otherwise as chain
10 (constraint set 6, the pre-v0.1 security fixes, libp2p 0.57) plus the guardian-set
attestation bound (544926c), and like chains 9–11 cut without the aggregation section. It set
none of the call limits, so its program cap was 4 096 words and its proof cap 2 MiB. It ran until
2026-09-19, when the fleet moved to chain 13 ("What the chain-13 rollout actually did", above).
The v0.3 builds cannot run chain 13, and the chain-13 build cannot run chain 12.
Chain 13 (cut and rolled out 2026-09-19, build 86af6eb, v0.4; genesis
8123ccac1883a45750e4df6964fb7cd3f0b321798cde4c0ef406a0293939ece3, deploy/genesis-chain13.json)
added five call-limit fields to chain 12's genesis (max_program_words, max_proof_bytes,
max_block_bytes, max_call_envelope_bytes, max_program_public_words) so the translated ERC-20
and SPL Token programs could deploy and call ("What the chain-13 rollout actually did", above). Same
18 validators, five alloc notes, peer ids and bootstraps as chain 12; cut without the aggregation
section. It ran until 2026-09-20, when the fleet moved to chain 14. The v0.4 build cannot run chain
14, and the chain-14 build cannot run chain 13 — the Action set, the Mint/bundle wire format and
the genesis format all changed underneath it.
Chain 14 (cut and rolled out 2026-09-20, pinned build b3c594c, main at ed88241, v0.5;
genesis 1cff3b7da248d93ab547aef5c05bb7d0d22da510b592dab9cf7374807de7c7ff,
deploy/genesis-chain14.json) is RPL, RandProtocol's own token standard, and a hardened,
zUSD-backed guardian bridge ("What the chain-14 rollout actually did", above; the full launch
record is AGENTS.md's v0.5 entry). Eighteen entirely fresh validator and payout keys, generated
off-repo (OPS-1) — new peer ids, nodes.env regenerated. The wire and consensus changes: one
4-in/4-out hidden-asset bundle for every transfer, bond, burn and bridge action (TokenTransfer is
gone); every bundle proof bound to its whole transaction; a Mint's commitment opening checked, not
declared (POOL-1); a forward bound on block timestamps (B2) — every validator needs NTP. Genesis
carries a tokens section (no token listed) and a bridge section at guardian set 0 with a PQ
co-signature quorum (B3) and a pause key (B1); zUSD (one token, seven backings across four chains)
was registered and listed by transaction after the cut, under that PQ quorum (B4), and a live
mainnet round trip (lock → mint → transfer → burn → release) has since run once per chain. Cut
without the aggregation section, and not optional this time: the node refuses to start any
genesis carrying one, pending re-measurement of the hidden-asset bundle's declared shape.