Questi quattro programmi sono ciascuno un’applicazione completa di una chiamata riservata: un problema, un’interfaccia, un guest in Rust, gli strumenti lato host, un deploy e una chiamata sulla testnet attiva. Sono più lunghi dei tutorial e ognuno si appoggia a una parte diversa della macchina. Scrivere un programma in Rust o C è la guida al modello che condividono tutti. Leggetela prima se read_input, il segmento pubblico o i otto slot di output sono nuovi per voi.
Tutto qui è stato eseguito per la prima volta su chain 18 con fullnode v0.6.7, il 2026-09-30, e ogni output di queste pagine è incollato da quelle esecuzioni. I programmi non sopravvivono a un cambio di chain, quindi il 2026-10-01 i quattro sono stati distribuiti di nuovo, senza modifiche, su chain 19 e poi su chain 20 (fullnode v0.6.8), e le chiamate sono state ripetute ogni volta. L’id di un programma è l’hash del codice e del segmento pubblico, non della chain, quindi ogni id è lo stesso; gli hash di transazione nelle trascrizioni sono quelli di chain 18, mentre la tabella qui sotto rimanda a quelli di chain 20. Su chain 20 un programma può anche mantenere uno stato tra una chiamata e l’altra (RPL-2); questi quattro non lo usano, e Stato del programma e il repository example-programs trattano i programmi che lo usano.
I quattro programmi
| programma | che cosa dimostra una chiamata | si appoggia a | parole | tier |
|---|---|---|---|---|
| Un voto anonimo | il chiamante è membro di una lista pubblicata, senza dire quale, e non ha già votato | Keccak-256, un segmento pubblico | 427 | 12 |
| Una prova di passività | i saldi privati dei clienti sommano a non più di una riserva pubblicata, sotto una radice che ogni cliente può verificare | SHA-256, un albero costruito dentro il programma | 994 | 14 |
| Un ledger off-chain | un lotto di trasferimenti porta un ledger da uno stato al successivo, con ogni mittente autorizzato e nulla creato | Poseidon2, stato portato tra le chiamate | 773 | 14 |
| Una decisione di credito privata | un modello pubblicato approva o rifiuta dati privati di un richiedente garantiti da un attestatore | aritmetica intera, un modello nel segmento pubblico | 462 | 12 |
Tutti e quattro sono attivi su chain 20. Ogni chiamata della tabella ripete, con gli stessi input, la chiamata su chain 18 mostrata nella pagina di quel programma:
| programma | id del programma | distribuito su chain 20 in | prima chiamata su chain 20 |
|---|---|---|---|
| anonymous vote | ad2f719f12d01c9f3c0962092b9898b75b0914bbb817031d51be13e54c05b970 | f6ae5528…0f82 | 3a304e7a…427b |
| proof of liabilities | fce8474dfdf80b9733e04fb72d4d8d19c40ba5ab646b3d867322daf4b9b599c0 | a8bc281b…17d9 | 6116c15f…d62a |
| off-chain ledger | c4ddd388f2d94299610c7c3409dfdfb4dcb4ae3e0bc9ec78d0c45f51d8231cab | 72cc95f2…3d51 | 9c9998bb…66eb |
| private credit decision | 7bbec14289f4446231b3a9a5ac18cb0613a373ca8a14fe4f046f3fde7ae6c73c | 413e6658…e42b | d3ceb306…124a |
L’explorer elenca ogni programma e le sue chiamate su randscan.org/programs. Una chain di testnet viene sostituita di tanto in tanto e i programmi non sopravvivono: dopo il prossimo cambio questi id non esisteranno più e i programmi andranno distribuiti di nuovo.
Configurazione
La toolchain. I guest si compilano con rand-guest dal repository dei circuiti, al commit che fullnode v0.6.7 e v0.6.8 includono entrambi come 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"
Il commit conta per due ragioni. hc è una funzione del sorgente e della toolchain, quindi un guest ricompilato a un altro commit di guest-sdk può ottenere un hc diverso. E le firme dell’SDK sono cambiate: ad aeacf31, keccak e sha256_compress prendono riferimenti ad array dove i commit più vecchi prendevano puntatori grezzi.
I sorgenti. Tutti e quattro i guest, i loro script host e i dati di dimostrazione sono un unico download. Decomprimeteli dentro il checkout, perché rand-guest build trova guest-sdk cercando verso l’alto dalla directory del guest:
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
I file si possono anche consultare uno per uno sotto https://randprotocol.org/examples/programs/, ad esempio allowlist/src/main.rs.
Il wallet. Per distribuire e chiamare servono il wallet rand di fullnode v0.6.8 (cargo build --release -p randprotocol-client in un checkout di fullnode al tag v0.6.8, oppure il rand allegato alla release v0.6.8), una chiave e RAND. Un wallet più vecchio non può transare su chain 20: chain 20 lega ogni prova e ogni messaggio firmato al proprio genesis, e un wallet v0.6.7 non lo fa.
Un RPC. L’endpoint pubblico, https://rpc.randprotocol.org, basta per tutto ciò che è in queste pagine: leggere programmi e ricevute, sincronizzare un wallet, distribuire e chiamare. La prima sincronizzazione di un wallet legge ogni header di blocco e ogni blocco che contiene una nota, 645 richieste all’altezza di chain 18 il 2026-09-30, e ha richiesto circa tre minuti attraverso l’endpoint. L’endpoint consente a ciascun indirizzo 1.200 letture e 6 invii al minuto. Un nodo proprio (Eseguire un nodo) non ha tali limiti.
export RAND_RPC=https://rpc.randprotocol.org
export RAND_KEY=$HOME/wallet.key.json
Su chain 18, uno dei voti contati nella pagina del voto anonimo, fdbcc960…fc2b, è stato dimostrato su un laptop e inviato attraverso il solo endpoint pubblico. Su chain 19 e 20 sono passate di lì tutte e tredici le transazioni, ogni volta: i quattro deploy e le nove chiamate.
Che cosa ha cambiato chain 18 per un programma
Una chiamata dichiara un limite di gas e lo paga. Il constraint set 8 misura ogni esecuzione. Un ciclo ordinario costa 1 gas. Le eccezioni sono le syscall di hash:
| istruzione | gas |
|---|---|
qualsiasi istruzione RV32IM, HALT, WRITE_OUTPUT, READ_INPUT, READ_PUBLIC | 1 |
POSEIDON2 su n parole | 3 + 3 × ⌈n / 4⌉ |
SHA256, una compressione | 64 |
KECCAK, una permutazione | 192 |
La prova porta un nuovo valore pubblico, il limite dichiarato, e il circuito impone che il gas dell’esecuzione sia pari o inferiore a esso. Il gas esatto resta privato. rand call esegue prima il programma nell’emulatore e stampa entrambi:
gas: 5636 (declaring 6144, tier 12)
Per impostazione predefinita il wallet arrotonda per eccesso il gas esatto a un multiplo di 2^(tier − 2), cioè 1.024 al tier 12 e 4.096 al tier 14, quindi il limite dichiarato dice poco sull’esecuzione. --gas-limit max dichiara il massimo del tier e non dice nulla oltre al tier. --gas-limit N dichiara una cifra esatta.
La commissione segue il limite dichiarato e i byte.
floor = 0.001 RAND + gas price × declared limit + byte price × ⌈bytes / 1024⌉
I prezzi sono partiti da 100 unità per gas e 800 unità per KiB (un’unità è 10⁻⁹ RAND), e a ogni blocco si spostano un poco verso un obiettivo. rand_getLimits serve quelli correnti. Il wallet aggiunge due passi di prezzo di margine, un fattore 1,125², perché una chiamata arriva due o tre blocchi dopo essere stata prezzata.
Un dettaglio decide gran parte della commissione. Dal chain 18 la prova di una chiamata è legata alla propria transazione, quindi il wallet deve fissare la commissione prima che la prova esista, e prezza la prova al tetto della chain di 4 MiB invece che alla sua dimensione reale. Per il voto anonimo questo dà (1,000,000 + 100 × 6,144 + 800 × 4,097) × 1.125² = 6.191.437 unità, 0,0062 RAND, di cui il gas è circa un ottavo. Le quattro chiamate di queste pagine sono costate tra 0,0057 e 0,0075 RAND ciascuna. --fee sostituisce il valore predefinito.
Ogni bundle è fatto di due prove. Chain 18 esegue un’autorizzazione divisa: il bundle che paga una commissione porta una piccola prova di autorizzazione di tier 10 accanto alla prova del bundle di tier 14. Il wallet produce entrambe e stampa una riga per ciascuna. Una chiamata è quindi tre prove: la chiamata, l’autorizzazione e il bundle. Su un laptop Apple Silicon le quattro chiamate qui descritte hanno richiesto da tre a cinque minuti e mezzo ciascuna, con un picco di memoria da 6,1 a 6,6 GB. La prova del bundle ne occupa circa due minuti ogni volta. La prova di una chiamata di tier 12 aggiunge mezzo minuto e una di tier 14 ne aggiunge due.
I limiti entro cui una chiamata deve rientrare.
| limite | chain da 18 a 20 |
|---|---|
| tier | al massimo 14: 16.383 cicli, righe dei digest incluse |
| permutazioni Poseidon2 | 2.048 al tier 14, inclusi i digest di programma, input e pubblici |
| permutazioni Keccak | 128 |
| compressioni SHA-256 | 128 |
| prova della chiamata | 4.194.304 byte (4 MiB) |
| programma | 8.180 parole senza segmento pubblico, qualcuna in meno con uno: chain 20, come chain 18 e 19, rifiuta un deploy che nessuna chiamata di tier 14 potrebbe contenere (ProgramUncallable), qualunque cosa consenta max_program_words (65.535) |
| segmento pubblico | 32.768 parole, fissate al deploy |
| input privato con una trascrizione sigillata | 16.071 parole |
rand-guest run --tier 14 verifica i primi due prima del deploy. Una tabella di hash rende più grande una prova. La prova di tier 14 del ledger, senza tabella di hash, è di 1,50 MB. La tabella SHA-256 porta la prova delle passività a 1,91 MB, e la tabella Keccak porta quella del voto a 3,35 MB, che è gran parte del tetto di 4 MiB.
Convenzioni condivise dai quattro
- Lo slot 0 è uno stato, e 0 non è mai uno stato valido. Ogni esito, fallimenti inclusi, ha un numero da 1 in su, e un fallimento si arresta in modo pulito così da poter essere provato. Una ricevuta con tutti zeri non può essere scambiata per un risultato.
- Lo slot 7 non si tocca. Una guest in Rust che va in panic vi scrive
0xdeadbeef(3.735.928.559). Chi legge controlla prima lo slot 7. - Gli slot da 2 a 6 contengono un valore a 160 bit: un nullifier, una radice, un collegamento, un impegno. Un hash completo a 256 bit occupa otto parole e non lascerebbe spazio per uno stato. Il troncamento a cinque parole mantiene 80 bit di resistenza alle collisioni, e ogni pagina indica che cosa protegge.
- L’ordine dei byte segue l’hash. I valori Keccak sono parole little-endian e i valori SHA-256 sono parole big-endian, perché è così che ciascuna syscall legge il proprio blocco. Uno script host e una guest che non concordano su questo producono digest che semplicemente non coincidono mai.
- Tutto ciò che varia per istanza sta nel segmento pubblico; tutto ciò che varia per chiamata è input privato. La radice di un sondaggio, una cifra di riserva e i pesi di un modello sono fissati al deploy e vincolati all’id del programma. Una chiamata non porta parole pubbliche proprie.
- Le parole di input sono decimali sulla riga di comando. Le pagine trasformano un file di witness in flag con
$(sed 's/[0-9][0-9]*/--input &/g' witness.txt).
Chiamarne uno di persona
Con i sorgenti decompressi, la toolchain compilata e un wallet v0.6.8 con fondi, un voto come uno dei membri dimostrativi richiede quattro comandi:
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)
Per chiamare non serve l’immagine: rand call recupera dal nodo il codice e il segmento pubblico del programma, verifica che abbiano per hash l’id e produce la prova su di essi.
Per leggere un risultato non servono né un wallet né un nodo. Qualsiasi ricevuta si ottiene con una sola richiesta all’endpoint pubblico:
$ 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"}}
L’API dell’explorer restituisce le chiamate di un programma in una sola richiesta: https://randscan.org/api/v1/programs/<program id> restituisce call_count e recent_calls (API dell’explorer).
Che cosa non fa nessuno di essi
Un programma su Rand è una prova su un calcolo, e la chain non agisce su nessuno dei suoi output. Nessuno di questi programmi sposta RAND o un token, detiene un saldo o ricorda una chiamata precedente. Il voto viene conteggiato da chi legge le ricevute. Le passività vengono confrontate con riserve che richiedono una propria evidenza. I saldi del ledger sono numeri in un albero off-chain. La decisione di credito è un bit che un finanziatore sceglie di rispettare. Ogni pagina termina con un elenco di ciò che la sua prova lascia a chi la utilizza.