Rand Protocol
Documentation
Example programs

Four programs on chain 20

Four complete confidential programs deployed on the testnet - an anonymous vote, a proof of liabilities, an off-chain ledger and a private credit decision - with their program ids, the setup they share, what chain 18 changed for a program author, and how to call them yourself.

These four programs are each a complete application of a confidential call: a problem, an interface, a Rust guest, the host-side tooling, and a deploy and a call on the live testnet. They are longer than the tutorials, and each leans on a different part of the machine. Write a program in Rust or C is the guide to the model they all use. Read it first if read_input, the public segment or the eight output slots are new to you.

Everything here was first run against chain 18 with fullnode v0.6.7, on 2026-09-30, and every output on these pages is pasted from those runs. Programs do not carry over a chain cut, so on 2026-10-01 the four were deployed again, unchanged, on chain 19 and then on chain 20 (fullnode v0.6.8), and the calls were repeated each time. A program id is a hash of the code and the public segment, not of the chain, so every id is the same; the transaction hashes in the transcripts are chain 18’s, and the table below links chain 20’s. On chain 20 a program can also keep state between calls (RPL-2); these four do not use it, and Program state and the example-programs repository cover programs that do.

The four programs

programwhat a call provesleans onwordstier
An anonymous votethe caller is a member of a published list, without saying which, and has not voted beforeKeccak-256, a public segment42712
A proof of liabilitiesprivate customer balances add up to no more than a published reserve, under a root each customer can checkSHA-256, a tree built inside the program99414
An off-chain ledgera batch of transfers takes a ledger from one state to the next, with every sender authorised and nothing createdPoseidon2, state carried between calls77314
A private credit decisiona published model approves or declines private applicant data that an attester vouched forinteger arithmetic, a model in the public segment46212

All four were live on chain 20 until it was retired on 2026-10-12. Chain 21 (fullnode v0.8.0) started with no programs, as every cut does, and the four have not been redeployed there yet; the ids below do not change when they are, since an id hashes the code and the public segment, not the chain. Each call in the table repeats, with the same inputs, the chain-18 call shown on that program’s page:

programprogram iddeployed on chain 20 infirst call on chain 20
anonymous votead2f719f12d01c9f3c0962092b9898b75b0914bbb817031d51be13e54c05b970f6ae5528…0f823a304e7a…427b
proof of liabilitiesfce8474dfdf80b9733e04fb72d4d8d19c40ba5ab646b3d867322daf4b9b599c0a8bc281b…17d96116c15f…d62a
off-chain ledgerc4ddd388f2d94299610c7c3409dfdfb4dcb4ae3e0bc9ec78d0c45f51d8231cab72cc95f2…3d519c9998bb…66eb
private credit decision7bbec14289f4446231b3a9a5ac18cb0613a373ca8a14fe4f046f3fde7ae6c73c413e6658…e42bd3ceb306…124a

The explorer lists every program and its calls at randscan.org/programs. A testnet chain is replaced from time to time, and programs do not carry over: after the next cut these ids will be gone and the programs will need deploying again.

Setup

The toolchain. The guests build with rand-guest from the circuits repository, at the commit fullnode v0.6.7 and v0.6.8 both vendor:

git clone https://github.com/randprotocol/zkp-circuits circuits
cd circuits
git checkout aeacf31                      # branch feat/v067-circuits
rustup toolchain install 1.98.1
rustup target add riscv32im-unknown-none-elf --toolchain 1.98.1
(cd rand-guest && cargo +1.98.1 build --release)
alias rand-guest="$PWD/rand-guest/target/release/rand-guest"

The commit matters for two reasons. hc is a function of the source and the toolchain, so a guest rebuilt at another commit of guest-sdk can get a different hc. And the SDK’s signatures moved: at aeacf31, keccak and sha256_compress take array references where older commits took raw pointers.

The sources. All four guests, their host scripts and the demo data are one download. Unpack them inside the checkout, because rand-guest build finds guest-sdk by looking upward from the guest’s directory:

mkdir -p my-guests
curl -sSf https://randprotocol.org/examples/rand-example-programs.tar.gz | tar -xz --strip-components=1 -C my-guests
ls my-guests
# allowlist  ledger  ledger-host  score  solvency

The files are also browsable one by one under https://randprotocol.org/examples/programs/, for example allowlist/src/main.rs.

The wallet. Deploying and calling need the rand wallet from fullnode v0.6.8 (cargo build --release -p randprotocol-client in a fullnode checkout at tag v0.6.8, or the rand attached to the v0.6.8 release), a key, and RAND. On chain 21 the wallet is rand from fullnode v0.8.0 (tag v0.8.0): constraint set 9 changed every verifier key, so neither the v0.6.8 nor a v0.7.x wallet can deploy or call there. An older wallet could not transact on chain 20 either: chain 20 binds every proof and signed message to its genesis, and a v0.6.7 wallet does not.

An RPC. The public endpoint, https://rpc.randprotocol.org, is enough for everything on these pages: reading programs and receipts, syncing a wallet, deploying and calling. A wallet’s first sync reads every block header and every block that carries a note, 645 requests at chain 18’s height on 2026-09-30, and took about three minutes through the endpoint. The endpoint allows each address 1,200 reads and 6 sends a minute. A node of your own (Running a node) has no such limits.

export RAND_RPC=https://rpc.randprotocol.org
export RAND_KEY=$HOME/wallet.key.json

On chain 18, one of the votes counted on the anonymous vote’s page, fdbcc960…fc2b, was proved on a laptop and sent through the public endpoint alone. On chains 19 and 20 all thirteen transactions each time, the four deploys and nine calls, went that way.

What chain 18 changed for a program

A call declares a gas limit, and pays for it. Constraint set 8 meters every run. An ordinary cycle is 1 gas. The hash syscalls are the exceptions:

instructiongas
any RV32IM instruction, HALT, WRITE_OUTPUT, READ_INPUT, READ_PUBLIC1
POSEIDON2 over n words3 + 3 × ⌈n / 4⌉
SHA256, one compression64
KECCAK, one permutation192

The proof carries one new public value, the declared limit, and the circuit enforces that the run’s gas is at or under it. The exact gas stays private. rand call runs the program in the emulator first and prints both:

gas: 5636 (declaring 6144, tier 12)

By default the wallet rounds the exact gas up to a multiple of 2^(tier − 2), which is 1,024 at tier 12 and 4,096 at tier 14, so the declared limit says little about the run. --gas-limit max declares the tier’s ceiling and says nothing beyond the tier. --gas-limit N declares an exact figure.

The fee follows the declared limit and the bytes.

floor = 0.001 RAND  +  gas price × declared limit  +  byte price × ⌈bytes / 1024⌉

The prices started at 100 units per gas and 800 units per KiB (a unit is 10⁻⁹ RAND), and move a little each block toward a target. rand_getLimits serves the current ones. The wallet adds two price steps of headroom, a factor of 1.125², because a call lands two or three blocks after it is priced.

One detail decides most of the fee. Since chain 18 a call’s proof is bound to its own transaction, so the wallet has to fix the fee before the proof exists, and it prices the proof at the chain’s cap of 4 MiB instead of its real size. For the anonymous vote that is (1,000,000 + 100 × 6,144 + 800 × 4,097) × 1.125² = 6,191,437 units, 0.0062 RAND, of which the gas is about an eighth. The four calls on these pages cost between 0.0057 and 0.0075 RAND each. --fee overrides the default.

Every bundle is two proofs. Chain 18 runs split authorisation: the bundle that pays a fee carries a small tier-10 authorisation proof beside the tier-14 bundle proof. The wallet makes both and prints a line for each. A call is therefore three proofs: the call, the authorisation and the bundle. On an Apple Silicon laptop the four calls here took between three and five and a half minutes each, with a peak of 6.1 to 6.6 GB of memory. The bundle proof is about two minutes of that every time. A tier-12 call proof adds half a minute and a tier-14 one adds two minutes.

The caps a call must fit.

limitchains 18 to 20
tierat most 14: 16,383 cycles, digest rows included
Poseidon2 permutations2,048 at tier 14, the program, input and public digests included
Keccak permutations128
SHA-256 compressions128
call proof4,194,304 bytes (4 MiB)
program8,180 words with no public segment, a few fewer with one: chain 21, like chains 18 to 20, refuses a deploy that no tier-14 call could hold (ProgramUncallable), whatever max_program_words (65,535) allows
public segment32,768 words, fixed at deploy
private input with a sealed transcript16,071 words

rand-guest run --tier 14 checks the first two before you deploy. A hash table makes a proof larger. The ledger’s tier-14 proof, with no hash table, is 1.50 MB. The SHA-256 table takes the liabilities proof to 1.91 MB, and the Keccak table takes the vote’s to 3.35 MB, which is most of the 4 MiB cap.

Conventions the four share

  • Slot 0 is a status, and 0 is never a valid one. Every outcome, failures included, has a number from 1 up, and a failure halts cleanly so that it can be proved. An all-zero receipt cannot be mistaken for a result.
  • Slot 7 is left alone. A Rust guest that panics writes 0xdeadbeef (3,735,928,559) there. A reader checks slot 7 first.
  • Slots 2 to 6 carry a 160-bit value: a nullifier, a root, a link, a commitment. A full 256-bit hash is eight words and would leave no room for a status. Truncating to five words keeps 80 bits of collision resistance, and each page says what that is protecting.
  • Byte order follows the hash. Keccak values are little-endian words and SHA-256 values are big-endian words, because that is how each syscall reads its block. A host script and a guest that disagree on this produce digests that simply never match.
  • Anything that varies per instance is in the public segment; anything per call is private input. A poll’s root, a reserve figure and a model’s weights are fixed at deploy and bound into the program id. A call carries no public words of its own.
  • Input words are decimal on the command line. The pages turn a witness file into flags with $(sed 's/[0-9][0-9]*/--input &/g' witness.txt).

Call one yourself

With the sources unpacked, the toolchain built and a funded wallet (v0.6.8 on chain 20, where these runs were made; v0.8.0 on chain 21), a vote as one of the demo members is four commands:

cd my-guests/allowlist
python3 allowlist.py witness 10 1 demo-secrets.txt 6 1 > witness.txt    # member 6, choice 1
rand-guest build . --max-words 65535      # only needed to run it in the emulator first
rand call ad2f719f12d01c9f3c0962092b9898b75b0914bbb817031d51be13e54c05b970 \
    $(sed 's/[0-9][0-9]*/--input &/g' witness.txt)

Calling does not need the image: rand call fetches the program’s code and public segment from the node, checks that they hash to the id, and proves over them.

Reading a result needs no wallet and no node. Any receipt is one request to the public endpoint:

$ curl -s https://rpc.randprotocol.org -H 'content-type: application/json' \
    -d '{"jsonrpc":"2.0","id":1,"method":"rand_getReceipt","params":["8aa13d1f76d28f63a46c30bb09ef44b1ba35083ca80ba7301997f98e5f783666"]}'
{"id":1,"jsonrpc":"2.0","result":{"h_in":"ea71e7701e62d49cac37b239bef132178f481835027d62b34da45fd6cf2667c6","h_pub":"2296cfca8db9d38bd0b7579056c9dea3c8d8005df7b42e2b1e954a75fed67510","height":98335,"index":0,"outputs":[1,2,392974773,873526743,1921224601,3950868247,2248722827,0],"program":"ad2f719f12d01c9f3c0962092b9898b75b0914bbb817031d51be13e54c05b970","tier":12,"tx":"8aa13d1f76d28f63a46c30bb09ef44b1ba35083ca80ba7301997f98e5f783666"}}

The explorer’s API gives a program’s calls in one request: https://randscan.org/api/v1/programs/<program id> returns call_count and recent_calls (Explorer API).

What none of them do

A program on Rand is a proof about a computation, and the chain acts on none of its outputs. None of these programs moves RAND or a token, holds a balance, or remembers a previous call. The vote is counted by whoever reads the receipts. The liabilities are compared with reserves that need their own evidence. The ledger’s balances are numbers in an off-chain tree. The credit decision is a bit a lender chooses to honour. Each page ends with a list of what its proof leaves to the people using it.

Last verified against fullnode commit 48dc04b (chain 18) on 2026-09-30, and circuits `aeacf31` (`guest-sdk/src/lib.rs`, `rand-guest`), the commit fullnode v0.6.7 vendors; `docs/fees.md`, `docs/zkvm.md`, `docs/cli.md` and `crates/randprotocol-client/src/main.rs` at fullnode `48dc04b`; the four deploys and the four calls were made on chain 18 on 2026-09-30, and every receipt was read back from `https://rpc.randprotocol.org`. Where the code and this page disagree, the code is right; tell us.