Usa questa pagina per entrare nel set di validatori di una chain in esecuzione, per uscirne e per rileggere il registro. Il peso nel consenso deve essere pubblico, quindi il registro dei validatori è l’unica tabella di questa chain che contiene importi in chiaro; lo stake esce comunque da una nota schermata (shielded) e vi ritorna.
Il registro
Una riga per ogni validatore che abbia mai effettuato un bond, indicizzata dal suo indirizzo base58:
ValidatorEntry {
public_key, // Dilithium2: signs blocks, and this validator's staking actions
stake: u64, // bonded: the weight an epoch's set gives it
pending: Vec<(u64, u64)>, // unbonding, as (release_epoch, amount), oldest first
rewards: u64, // fees earned as a block proposer, unpaid
payout: ShieldedAddress, // rand1… — where a withdraw pays
nonce: u64, // replay protection for its signed actions
activation_epoch: u64, // under a `staking` section: the first epoch this row may be in a set
}
Ogni campo è pubblico e il registro è incluso nell’hash della state root di ogni blocco. Una riga non viene mai eliminata: un validatore che esegue l’unbond di tutto conserva una riga con stake = 0.
| costante | valore | significato |
|---|---|---|
MIN_STAKE | 1,000 RAND | stake di cui una riga ha bisogno per rientrare nel set di una epoch |
MAX_VALIDATORS | 100 | set più grande che una epoch possa avere |
UNBONDING_EPOCHS | 2 | epoch che un importo sottoposto a unbond attende prima di poter essere prelevato |
epoch_blocks | 1,000 on chain 20 | blocchi per epoch; un parametro del genesis |
Epoch
epoch(h) = h / epoch_blocks. Il set per la epoch e ≥ 1 è derivato dal registro così com’è all’ultimo blocco della epoch e − 1: ogni riga con stake ≥ MIN_STAKE, i primi MAX_VALIDATORS per (stake descending, address ascending), ordinati per indirizzo. Il leader di una view, i votanti di un blocco e la verifica di un certificato usano tutti il set della epoch a cui appartiene il blocco. Due conseguenze:
- Un bond non è peso fino a un confine successivo. È subito nel registro, ma il set cambia solo a un confine: il successivo su una chain senza sezione
staking, uno più tardi su chain 20 (sotto). - Un unbond costa peso al confine successivo, mentre l’importo attende due epoch prima di diventare prelevabile. Un validatore smette di proporre molto prima di poter prelevare.
Se una derivazione è vuota, il set precedente viene riportato avanti. Un nodo avviato con --validator la cui chiave non è in nessun set corrente resta in osservazione finché una epoch non lo ammette, quindi un validatore che effettua il bond dopo il genesis non ha bisogno di un riavvio; rand_status riporta separatamente is_validator (la chiave è presente) e active_validator (è nel set corrente).
La sezione staking
Un genesis può contenere una sezione staking (fullnode v0.5.4, audit v4 STAKE-2). Senza di essa le regole qui sopra sono tutta la storia, come su chain 14. Ogni chain a partire dalla chain 15 la contiene. Quella di chain 20 attiva:
"staking": {
"faucet_budget_per_epoch": "10000000000000", "bond_activation_epochs": 2,
"max_weight_bps": 3333, "max_stake_entry_bps_per_epoch": 2500,
"admission_by_vote": true, "registration_v2": true,
"faucet_recipients": ["rand1…", "…16 wallets…"],
"faucet_minters": ["<validator address>", "…18 validators…"]
}
| campo | chain 20 | regola |
|---|---|---|
bond_activation_epochs | 2 | un bond applicato nella epoch e è accodato per la epoch e + 1 + 2; vale sia per una registrazione sia per un top-up. Il suo importo è subito in stake ma non è peso fino ad allora, e lo stake in coda non può essere sottoposto a unbond |
max_stake_entry_bps_per_epoch | 2,500 (25 %) | il massimo stake, registrazioni e top-up insieme, che diventa peso a un confine, come frazione del peso già attivo in quel punto: ⌊active · bps / 10,000⌋. Le righe in scadenza oltre questo limite attendono il confine successivo, in ordine di bond. Sui 26,000 RAND di stake attivo di chain 20 sono 6,500 RAND a confine. La quota di un nuovo entrante dopo k confini è al massimo 1 − 1.25^−k a qualsiasi scala, circa un terzo dopo due |
admission_by_vote | true | un bond che registra una chiave nuova viene rifiutato (NotAdmitted) finché i validatori che detengono più di due terzi del peso di voto non hanno votato per ammettere la chiave (sotto); un top-up non richiede alcun voto |
max_weight_bps | 3,333 | il peso di nessun validatore supera il 33.33 % del totale del proprio set; l’eccesso viene limitato nel set, mentre lo stake del registro resta invariato |
registration_v2 | true | una registrazione firma rand-register-2 sull’hash del genesis, sull’id della chain, sull’indirizzo del validatore e sul payout; una registrazione v1 viene rifiutata |
faucet_budget_per_epoch | 10,000 RAND | il faucet conia al massimo questa quantità per epoch (FaucetBudgetExhausted oltre il limite; il contatore riparte alla epoch successiva) |
faucet_recipients | 16 wallets | un Mint del faucet paga solo questi wallet (FaucetRecipientNotAllowed); questo elenco è ciò che permette a un faucet di stare accanto al bridge |
faucet_minters | 18 validators | un Mint del faucet può essere firmato solo da queste chiavi di validatore (MinterNotAllowed), verificato all’ammissione e quando un blocco viene applicato, per quanto tempo un’altra chiave sia stata in bond. Su chain 20 sono 18 dei 26 validatori del genesis. L’elenco è presente in ogni genesis a partire da chain 16 |
slashing | absent | nessuna sezione slashing, quindi nessun validatore viene sottoposto a slashing né jailed (Che cosa è pubblico) |
Con la sezione presente, il contatore del faucet e la coda dei bond vengono aggiunti alla state root, sotto il dominio rand-state-5, e la foglia del validatore è rand-validator-leaf-4. Chain 20 ha anche prezzi del gas dinamici e lo stato dei programmi RPL-2, aggiunti dopo di essi, che spostano il suo dominio a rand-state-8, e l’ammissione tramite voto avvolge quella root un’ulteriore volta come H("rand-state-admitted-1", root ‖ admitted_root). Chain 15 non aveva alcun elenco faucet_minters: lì, ogni nodo metteva in pool e inoltrava un Mint del faucet solo quando lo firmava un validatore del genesis (MinterNotAllowed, come policy del nodo), in modo che un validatore con bond effettuato dopo il genesis non potesse svuotare il budget attraverso il pool. Con l’elenco, lo stesso rifiuto è una regola di validità.
Storia: le chain da 15 a 19 non avevano admission_by_vote, quindi qualsiasi chiave con 1,000 RAND poteva registrarsi, e fissavano il budget di ingresso come un max_stake_entry_per_epoch fisso di 10,000 RAND a confine. Una cifra fissa è dimensionata su un solo genesis; la frazione tiene un entrante alla stessa quota qualunque sia lo stake. I due campi non possono essere impostati insieme.
Ammissione tramite voto
Novità di fullnode v0.6.8 (audit v6, STAKE-2) e attiva per chain 20. Con 26 chiavi da 1,000 RAND ciascuna, un terzo di blocco del set equivale a circa 13,000 RAND di stake acquistato, e nulla viene sottoposto a slashing. Con questa regola è il set a decidere quali nuove chiavi possano registrarsi.
- Il voto è un’azione,
AdmitValidator { candidate, signatures }, senza bundle e senza commissioni. Ogni firma è una firma Dilithium2 di un validatore sublake3("rand-admit-validator-1" ‖ genesis hash ‖ candidate address), in ordine crescente degli indirizzi dei votanti. I votanti devono detenere strettamente più di due terzi del peso del set di voto, l’aritmetica di un quorum certificate: su chain 20, 18 dei 26 validatori. Un voto è vincolato a un solo genesis e non richiede nonce: un’ammissione viene consumata dalla registrazione che consente. - Il set di voto è la derivazione propria del registro per la epoch corrente. Un bond in coda non è peso al suo interno, e un unbond ne esce subito. Un candidato non può votarsi da solo.
- Il set degli ammessi è stato di consenso, con al massimo 256 chiavi in attesa di registrarsi (
AdmissionSetFulloltre il limite).rand_getAdmittedlo restituisce;rand_getLimits.admission_by_voteindica se una chain richiede il voto.
# each voting validator, offline
rand-node admit sign --candidate <registration hex or public key> --key node.key.json --genesis-hash <hash>
# whoever collects the lines
rand-node admit submit --candidate <…> --signature <line> --signature <line> … --rpc http://127.0.0.1:8545
Poi il bond procede come descritto sotto. rand_getGenesisHash restituisce l’hash di cui admit sign ha bisogno.
I quattro comandi
Quale binario possiede quale azione dipende da quale chiave la firma: un bond spende note, quindi è un comando del wallet; un unbond e un withdraw sono firmati dalla chiave Dilithium2 del validatore, che solo il nodo possiede.
| azione | comando | si appoggia a | paga |
|---|---|---|---|
| register | rand-node register | nulla — stampa un blob | nulla |
Bond | rand bond | un bundle il cui burn_r è lo stake | 0.001 RAND dalle note del wallet |
Unbond | rand-node unbond | nulla (bundle: null) | nulla |
Withdraw | rand-node withdraw | nulla (bundle: null) | 0.001 RAND dall’importo prelevato |
Register
rand-node register --key node.key.json --payout "$(rand --key payout.key.json address)" --v2 [--rpc http://127.0.0.1:8545]
Stampa l’indirizzo del validatore e una Registration in esadecimale: la sua chiave pubblica, l’indirizzo di payout e una firma. Con --v2, richiesto da chain 20, la firma è sotto rand-register-2 sull’hash del genesis della chain, sull’id della chain, sull’indirizzo del validatore e sul payout; senza, la forma v1, blake3("rand-register" ‖ bincode(chain_id, payout)), che chain 20 rifiuta (su una chain con binding_domain: 1 come chain 20 il messaggio di registrazione è comunque quello v2). L’RPC viene interrogata per l’id della chain e, con --v2, per l’hash del genesis, quindi una registrazione firmata per una chain viene rifiutata su un’altra. L’indirizzo di payout è un indirizzo di wallet, non una chiave del nodo, ed è l’unico campo che un top-up successivo non può modificare. Consegna l’esadecimale a chi detiene lo stake.
Bond
rand bond <validator base58> <amount in RAND> [--registration <hex>] [--fee <RAND>] [--no-wait] [--cuda]
Una normale transazione schermata: il wallet dimostra il bundle di asset nascosti 4-in-4-out che ogni transazione porta con sé dalla chain 14, con l’importo messo in stake nel suo burn di RAND, burn_r, e il ledger ammette un bond solo quando burn_r == amount. Sulla chain 20 il bundle porta due prove, come ogni bundle dalla chain 17: la prova del bundle di tier 14 e una prova di autorizzazione di tier 10 sulla chiave di spesa. Entrambe sono generate localmente per impostazione predefinita, in circa due minuti su un portatile Apple Silicon (Memoria di proving). Con l’opzione --prover del wallet la prova del bundle è generata invece da un prover abbinato, mentre la prova di autorizzazione resta sulla macchina del wallet. --registration è richiesto esattamente quando il validatore non è ancora nel registro, e rifiutato quando lo è; il wallet interroga prima il registro. Sulla chain 20 un bond di registrazione è rifiutato con NotAdmitted a meno che l’insieme non abbia prima votato la chiave (Ammissione tramite voto); un top-up non richiede alcun voto. La registrazione richiede un bond di almeno 1.000 RAND; un top-up può essere di qualsiasi importo superiore a zero. Ciò che la chain apprende è che lo stake di questo validatore è cresciuto di questa quantità; quali note hanno pagato, e di chi fossero, resta nascosto come in un trasferimento: chiunque può fare stake su un validatore senza rivelare altro che l’importo.
submitted bond 8c3f…e1
0 RAND out, 1000 RAND burned, 0.999 RAND change, fee 0.001 RAND, anchored at height 412
2nRdFC…: stake 1000 RAND, counting as consensus weight from epoch 69
L’epoch che il wallet stampa è il prossimo confine. Non conta il ritardo di attivazione: sulla chain 20 un bond registrato nell’epoch 68 ha peso dall’epoch 71 al più presto, più tardi se il budget di ingresso, un quarto del peso attivo per confine, è esaurito.
Unbond
rand-node unbond <amount in RAND> --key node.key.json [--rpc …] [--no-wait]
Sposta amount da stake a pending con release_epoch = epoch + 2 e incrementa il nonce. Gratuito, senza bundle, firmato su blake3("rand-unbond" ‖ bincode(chain_id, validator, amount, nonce)) sulle chain da 14 a 19 e su blake3("rand-unbond-2" ‖ bincode(genesis_hash, chain_id, validator, amount, nonce)) sulla chain 20 (genesis binding_domain: 1); viene registrato nel tempo di un blocco. rand-node v0.6.8 sceglie la forma adatta alla chain con cui comunica. Un unbond superiore allo stake, o pari a zero, è rifiutato; più unbond nella stessa epoch confluiscono in un’unica riga pending.
Withdraw
rand-node withdraw <amount in RAND> --key node.key.json [--rpc …] [--no-wait]
Paga il valore rilasciato — rewards più le righe pending il cui release_epoch è arrivato, a partire dalla più vecchia — in una nota di deposito all’indirizzo payout del registro. È firmato dal validatore (sulla chain 20 con rand-withdraw-2, con l’hash del genesis per primo, come l’unbond) e senza bundle, ma non gratuito: la base di 0,001 RAND va al proponente del blocco che lo applica, detratta dall’importo, quindi un prelievo di 1.000 RAND crea una nota da 999,999 e un importo che non copre la base è rifiutato. La chain calcola la nota da sé:
cm = H_CM(payout.pk, from = 0, amount − 0.001 RAND, asset 0, time, r)
time è l’altezza della testa quando il comando è stato eseguito (stampata, e vincolata dalla firma), ammessa in qualsiasi punto all’interno della finestra del bundle (1.024 blocchi sulla chain 20, il cui genesis imposta proof_window_blocks; 256 sulle chain precedenti); r è pubblicato così che la chain non prenda mai un commitment dalla rete. Poi il wallet di payout trova la nota scansionando, senza che gli venga detto nulla:
rand --key payout.key.json balance # balance: 999.999 RAND
Entrare e uscire
# on the joining validator's machine
rand-node register --key node.key.json --payout "$(rand --key payout.key.json address)" --v2
rand-node run --datadir ./data --key node.key.json --validator --bootstrap /ip4/…/p2p/…
# it syncs and observes; rand_status says is_validator true, active_validator false
# on chain 20: validators holding more than two thirds of the weight vote the key in
rand-node admit sign --candidate <hex from above> --key <their node.key.json> --genesis-hash <hash>
rand-node admit submit --candidate <hex from above> --signature <line> … --rpc http://127.0.0.1:8545
# on the machine holding the stake
rand bond <validator base58> 1000 --registration <hex>
rand validators # the new row, active false until it activates
# from its activation epoch on (on chain 20, three boundaries after the bond at the earliest)
rand-node status # active_validator: true
rand-node unbond 1000 --key node.key.json # out of the set at the next boundary
# wait two epochs
rand-node withdraw 1000 --key node.key.json # a note worth 999.999 RAND at the payout address
Verifica l’aritmetica del quorum prima di fare unbond: un insieme di n richiede più di 2/3 del proprio stake online. Sulla chain 20 tutti i 26 validatori sono esattamente a MIN_STAKE, quindi qualsiasi unbond fa uscire quel nodo dall’insieme dell’epoch successiva, e 18 dei 26 devono restare attivi. Anche un validatore attivo che non vota conta contro il quorum: dalla chain 14 un validatore trattiene il proprio voto da un blocco con più di 15 s di anticipo rispetto al proprio orologio, quindi un validatore il cui orologio è in ritardo smette di votare senza alcun errore. Esegui NTP su di esso (Eseguire un validatore).
Il genesis popola direttamente il registro — una voce per ogni --validator <key or hex pubkey>,<stake>,<payout> — e rifiuta uno stake inferiore al minimo. Il genesis della chain 20 popola 26 righe da 1.000 RAND ciascuna, con le stesse chiavi della chain 19.
Dalla v0.5.11 la build include anche il vesting al genesis: un registro pubblico di allocazioni soggette a timelock, da cui una voce irrevocabile può fare bond di RAND bloccati come stake di un validatore (rand-node vesting bond) e riprenderli (rand-node vesting unbond). Richiede una sezione vesting nel genesis. Nessuna chain ne ha una, chain 20 compresa, e senza di essa quelle azioni sono rifiutate all’ammissione. (La v0.6.8 consente inoltre di revocare una voce di vesting revocabile solo a una soglia di firmatari verso un tesoro fisso, di nuovo solo con una sezione vesting.)
Cosa è pubblico
| pubblico | nascosto | |
|---|---|---|
| il registro | ogni riga: indirizzo, stake, coda di unbonding, ricompense non pagate, indirizzo di payout, nonce | niente |
Bond | il validatore, l’importo, se è stata allegata una registrazione, il burn_r del bundle | quali note hanno pagato, chi le possiede, il resto |
AdmitValidator (chain 20) | il candidato e ogni votante con la sua firma | non esiste altro |
Unbond | il validatore, l’importo, il nonce | non esiste altro |
Withdraw | il validatore, l’importo, il nonce, il time e l’r della nota | chi può aprire la nota, e ogni sua spesa successiva |
Le ricompense sono le commissioni dei blocchi che un validatore ha proposto. Non ci sono né ricompense di blocco né inflazione, e sulla chain 20 non c’è né slashing né jailing dei validatori: un validatore che si comporta male non costa nulla al proprio stake, e il rimedio spetta agli operatori. Il fullnode v0.6.8 include una regola di slashing per l’equivocazione del leader (audit v6, STAKE-1: due header di blocco firmati per una stessa view da una stessa chiave distruggono equivocation_bps dello stake e mettono la chiave in jail), ma solo con una sezione genesis staking.slashing, e la chain 20 non ne ha (rand_getLimits.slashing è null); il fullnode la raccomanda solo quando lo stake è detenuto da più di un operatore. (Il codice di aggregazione dei blocchi include anche un’azione SlashAggregator per gli aggregatori che firmano due aggregati diversi con lo stesso nonce, e non è attiva sulla chain 20, che non ha una sezione di aggregazione.)
Rileggerlo
rand validators # the whole register
rand status # is_validator / active_validator for the node asked
→ {"method":"rand_getValidators","params":[]}
← [{ "address": "2nRdFC…", "stake": "1000000000000",
"pending": [{ "release_epoch": 71, "amount": "1000000000000" }],
"rewards": "4000000", "payout": "rand1…", "nonce": 3, "active": true, "jailed_until": null }]
→ {"method":"rand_getAdmitted","params":[]}
← { "admission_by_vote": true, "admitted": [], "max": 256 }
→ {"method":"rand_getEpoch","params":[]}
← { "epoch": 69, "epoch_blocks": 1000, "next_set": ["2nRdFC…", "ByDkxs…"] }
Gli importi sono stringhe decimali di unità. active indica se la riga fa parte dell’insieme che esegue l’epoch corrente; jailed_until è null salvo con una sezione di slashing; admitted elenca le chiavi votate che non si sono ancora registrate; next_set è una proiezione che ogni bond e unbond prima del confine sposta. Il GET /validators dell’explorer aggiunge share_percent, blocks_proposed e l’ultima altezza proposta.