Rand Protocol
Documentation
Guides

Staking

Enregistrement d'un validateur, admission par le vote de l'ensemble, mise en caution (bonding) de stake depuis un portefeuille, dissociation (unbonding) et retrait, adresse de paiement, epochs, budget d'entrées de stake, et pourquoi il n'y a pas de slashing sur la chaîne en production.

Utilisez cette page pour rejoindre l’ensemble des validators d’une chaîne en fonctionnement, pour le quitter, et pour relire le registre. Le poids de consensus doit être public ; le registre des validators est donc la seule table de cette chaîne qui contienne des montants en clair. La mise sort néanmoins d’une note protégée et revient dans une note protégée.

Le registre

Une ligne par validator ayant déjà effectué un bond, indexée par son adresse 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
}

Chaque champ est public et le registre est haché dans la racine d’état de chaque bloc. Une ligne n’est jamais supprimée : un validator qui retire toute sa mise (unbond) conserve une ligne avec stake = 0.

constantevaleursignification
MIN_STAKE1 000 RANDmise dont une ligne a besoin pour figurer dans l’ensemble d’une epoch
MAX_VALIDATORS100taille maximale de l’ensemble d’une epoch
UNBONDING_EPOCHS2epochs pendant lesquelles un montant retiré (unbonded) attend avant de pouvoir être retiré du registre (withdraw)
epoch_blocks1 000 sur la chaîne 20blocs par epoch ; paramètre de genèse

Les epochs

epoch(h) = h / epoch_blocks. L’ensemble de l’epoch e ≥ 1 est dérivé du registre tel qu’il est au dernier bloc de l’epoch e − 1 : chaque ligne avec stake ≥ MIN_STAKE, les MAX_VALIDATORS premières selon (stake décroissante, adresse croissante), triées par adresse. Le leader d’une vue, les votants d’un bloc et la vérification d’un certificat utilisent tous l’ensemble de l’epoch à laquelle appartient le bloc. Deux conséquences :

  • Un bond ne devient du poids qu’à une frontière ultérieure. Il figure aussitôt dans le registre, mais l’ensemble ne change qu’à une frontière : la suivante sur une chaîne sans section staking, plus tard sur la chaîne 20 (ci-dessous).
  • Un unbond fait perdre du poids à la frontière suivante, tandis que le montant attend deux epochs avant de pouvoir être retiré. Un validator cesse de proposer bien avant de pouvoir retirer.

Si une dérivation est vide, l’ensemble précédent est reconduit. Un nœud lancé avec --validator dont la clé ne figure dans aucun ensemble courant observe jusqu’à ce qu’une epoch l’admette ; un validator qui effectue son bond après la genèse n’a donc pas besoin de redémarrer. rand_status indique séparément is_validator (la clé est présente) et active_validator (elle figure dans l’ensemble courant).

La section staking

Une genèse peut comporter une section staking (fullnode v0.5.4, audit v4 STAKE-2). Sans elle, les règles ci-dessus disent tout, comme sur la chaîne 14. Toute chaîne depuis la chaîne 15 la comporte. Celle de la chaîne 20 active :

"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…"]
}
champchaîne 20règle
bond_activation_epochs2un bond appliqué à l’epoch e est mis en file pour l’epoch e + 1 + 2 ; inscription et complément confondus. Son montant figure aussitôt dans stake mais n’est du poids qu’à ce moment-là, et une mise en file ne peut pas faire l’objet d’un unbond
max_stake_entry_bps_per_epoch2 500 (25 %)la mise maximale, inscriptions et compléments confondus, qui devient du poids à une frontière, en fraction du poids déjà actif à cet instant : ⌊active · bps / 10,000⌋. Les lignes échues au-delà attendent la frontière suivante, dans l’ordre des bonds. Sur les 26 000 RAND de mise active de la chaîne 20, cela fait 6 500 RAND par frontière. La part d’un entrant après k frontières vaut au plus 1 − 1.25^−k à toute échelle, soit environ un tiers après deux
admission_by_votetrueun bond qui inscrit une nouvelle clé est refusé (NotAdmitted) tant que des validators détenant plus des deux tiers du poids de vote n’ont pas voté pour admettre la clé (ci-dessous) ; un complément n’exige aucun vote
max_weight_bps3 333le poids d’aucun validator ne dépasse 33,33 % du total de son ensemble ; l’excédent est plafonné dans l’ensemble, la stake du registre reste inchangée
registration_v2trueune inscription signe rand-register-2 sur le hash de genèse, l’identifiant de chaîne, l’adresse du validator et le payout ; une inscription v1 est refusée
faucet_budget_per_epoch10 000 RANDle faucet émet au plus ce montant par epoch (FaucetBudgetExhausted au-delà ; le compteur repart à l’epoch suivante)
faucet_recipients16 walletsun Mint du faucet ne paie que ces wallets (FaucetRecipientNotAllowed) ; cette liste permet à un faucet de coexister avec le bridge
faucet_minters18 validatorsun Mint du faucet ne peut être signé que par ces clés de validators (MinterNotAllowed), vérifié à l’admission et à l’application d’un bloc, quelle que soit l’ancienneté du bond d’une autre clé. Sur la chaîne 20, ce sont 18 des 26 validators de genèse. La liste figure dans toutes les genèses depuis la chaîne 16
slashingabsentpas de section slashing, donc aucun validator n’est sanctionné (slashed) ni mis en prison (jailed) (Ce qui est public)

Sous cette section, le compteur du faucet et la file des bonds sont ajoutés à la racine d’état, sous le domaine rand-state-5, et la feuille de validator est rand-validator-leaf-4. La chaîne 20 comporte aussi des prix du gas dynamiques et l’état des programmes RPL-2, ajoutés à la suite, ce qui porte son domaine à rand-state-8 ; l’admission par vote enveloppe cette racine une fois de plus sous la forme H("rand-state-admitted-1", root ‖ admitted_root). La chaîne 15 n’avait pas de liste faucet_minters : chaque nœud y mettait en pool et relayait un Mint du faucet seulement si un validator de genèse l’avait signé (MinterNotAllowed, en tant que politique de nœud), de sorte qu’un validator ayant effectué son bond après la genèse ne puisse pas épuiser le budget via le pool. Avec la liste, le même refus devient une règle de validité.

Historique : les chaînes 15 à 19 n’avaient pas de admission_by_vote ; toute clé détenant 1 000 RAND pouvait donc s’inscrire, et leur budget d’entrée était fixé par un max_stake_entry_per_epoch constant de 10 000 RAND par frontière. Un chiffre fixe est dimensionné pour une seule genèse ; la fraction maintient un entrant à la même part quelle que soit la mise. Les deux champs ne peuvent pas être définis ensemble.

Admission par vote

Nouveau dans fullnode v0.6.8 (audit v6, STAKE-2) et activé pour la chaîne 20. Avec 26 clés de 1 000 RAND chacune, un tiers bloquant de l’ensemble représente environ 13 000 RAND de mise achetée, et rien n’est sanctionné. Sous cette règle, c’est l’ensemble qui décide quelles nouvelles clés peuvent s’inscrire.

  • Le vote est une action, AdmitValidator { candidate, signatures }, sans bundle et sans frais. Chaque signature est une signature Dilithium2 d’un validator sur blake3("rand-admit-validator-1" ‖ genesis hash ‖ candidate address), dans l’ordre croissant des adresses des votants. Les votants doivent détenir strictement plus des deux tiers du poids de l’ensemble votant, l’arithmétique d’un quorum certificate : sur la chaîne 20, 18 des 26 validators. Un vote est lié à une seule genèse et n’exige aucun nonce : une admission est consommée par l’inscription qu’elle autorise.
  • L’ensemble votant est la dérivation propre au registre pour l’epoch courante. Un bond en file n’y est pas du poids, et un unbond en sort aussitôt. Un candidat ne peut pas voter pour sa propre admission.
  • L’ensemble des admis est un état de consensus, d’au plus 256 clés en attente d’inscription (AdmissionSetFull au-delà). rand_getAdmitted le sert ; rand_getLimits.admission_by_vote indique si une chaîne exige le vote.
# 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

Le bond se poursuit ensuite comme ci-dessous. rand_getGenesisHash renvoie le hash dont admit sign a besoin.

Les quatre commandes

Quel binaire possède quelle action découle de la clé qui la signe : un bond dépense des notes, c’est donc une commande de wallet ; un unbond et un withdraw sont signés par la clé Dilithium2 du validator, que seul le nœud détient.

actioncommandes’appuie surpaie
registerrand-node registerrien — imprime un blobrien
Bondrand bondun bundle dont le burn_r est la mise0,001 RAND prélevés sur les notes du wallet
Unbondrand-node unbondrien (bundle: null)rien
Withdrawrand-node withdrawrien (bundle: null)0,001 RAND prélevés sur le montant retiré

Register

rand-node register --key node.key.json --payout "$(rand --key payout.key.json address)" --v2 [--rpc http://127.0.0.1:8545]

Imprime l’adresse du validator et une Registration en hexadécimal : sa clé publique, l’adresse de payout et une signature. Avec --v2, qu’exige la chaîne 20, la signature porte sous rand-register-2 sur le hash de genèse de la chaîne, l’identifiant de chaîne, l’adresse du validator et le payout ; sans cette option, c’est la forme v1, blake3("rand-register" ‖ bincode(chain_id, payout)), que la chaîne 20 refuse (sur une chaîne binding_domain: 1 comme la chaîne 20, le message d’inscription est de toute façon celui de la v2). Le RPC est lu pour l’identifiant de chaîne et, avec --v2, pour le hash de genèse, de sorte qu’une inscription signée pour une chaîne est refusée sur une autre. L’adresse de payout est une adresse de wallet, pas une clé de nœud, et c’est le seul champ qu’un complément ultérieur ne peut pas modifier. Remettez l’hexadécimal à celui qui détient la mise.

Bond

rand bond <validator base58> <amount in RAND> [--registration <hex>] [--fee <RAND>] [--no-wait] [--cuda]

Une transaction protégée ordinaire : le wallet prouve le bundle d’actifs cachés 4 entrées, 4 sorties que porte chaque transaction depuis la chain 14, avec le montant de la mise dans sa destruction de RAND, burn_r, et le ledger n’admet un dépôt que si burn_r == amount. Sur la chain 20, le bundle porte deux preuves, comme tout bundle depuis la chain 17 : la preuve de bundle de tier 14 et une preuve d’autorisation de tier 10 portant sur la clé de dépense. Les deux sont prouvées localement par défaut, en environ deux minutes sur un portable Apple Silicon (Mémoire de preuve). Avec l’option --prover du wallet, la preuve de bundle est produite par un prover associé, et la preuve d’autorisation reste sur la machine du wallet. --registration est requis exactement lorsque le validator n’est pas encore dans le registre, et refusé lorsqu’il y est ; le wallet interroge d’abord le registre. Sur la chain 20, un dépôt d’enregistrement est refusé avec NotAdmitted si l’ensemble n’a pas d’abord voté pour admettre la clé (Admission par vote) ; un complément n’exige aucun vote. L’enregistrement dépose au moins 1,000 RAND ; un complément peut porter sur n’importe quel montant supérieur à zéro. Ce que la chaîne apprend, c’est que la mise de ce validator a augmenté de ce montant ; quelles notes ont payé, et à qui elles appartenaient, reste caché comme dans un transfert : n’importe qui peut miser sur un validator sans rien révéler d’autre que le montant.

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 affichée par le wallet est la prochaine frontière. Elle ne compte pas le délai d’activation : sur la chain 20, un dépôt engagé à l’epoch 68 pèse à partir de l’epoch 71 au plus tôt, plus tard si le budget d’entrée, un quart du poids actif par frontière, est épuisé.

Retrait de la mise

rand-node unbond <amount in RAND> --key node.key.json [--rpc …] [--no-wait]

Déplace amount de stake vers pending avec release_epoch = epoch + 2 et incrémente le nonce. Gratuit, sans bundle, signé sur blake3("rand-unbond" ‖ bincode(chain_id, validator, amount, nonce)) sur les chains 14 à 19 et sur blake3("rand-unbond-2" ‖ bincode(genesis_hash, chain_id, validator, amount, nonce)) sur la chain 20 (genesis binding_domain: 1) ; il s’inscrit en un temps de bloc. rand-node v0.6.8 choisit la forme correspondant à la chain avec laquelle il communique. Retirer plus que la mise, ou zéro, est refusé ; plusieurs retraits dans une même epoch fusionnent en une seule ligne pending.

Retrait des gains

rand-node withdraw <amount in RAND> --key node.key.json [--rpc …] [--no-wait]

Verse la valeur libérée — rewards plus les lignes pending dont le release_epoch est arrivé, les plus anciennes d’abord — dans une note de dépôt à l’adresse payout du registre. Il est signé par le validator (sur la chain 20 sous rand-withdraw-2, sur le hash de genesis en premier, comme le retrait de mise) et sans bundle, mais pas gratuit : la base de 0.001 RAND va au proposer du bloc qui l’applique, prélevée sur le montant, de sorte qu’un retrait de 1,000 RAND crée une note de 999.999 et qu’un montant ne couvrant pas la base est refusé. La chaîne calcule elle-même la note :

cm = H_CM(payout.pk, from = 0, amount − 0.001 RAND, asset 0, time, r)

time est la hauteur de la tête au moment de l’exécution de la commande (affichée, et liée par la signature), admise n’importe où dans la fenêtre du bundle (1,024 blocs sur la chain 20, dont le genesis fixe proof_window_blocks ; 256 sur les chains antérieures) ; r est publié afin que la chaîne ne prenne jamais un engagement depuis le réseau. Ensuite, le wallet de paiement retrouve la note par balayage, sans qu’on lui dise rien :

rand --key payout.key.json balance      # balance: 999.999 RAND

Rejoindre et quitter

# 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

Vérifiez l’arithmétique du quorum avant de retirer une mise : un ensemble de n exige plus de 2/3 de sa mise en ligne. Sur la chain 20, les 26 validators sont exactement à MIN_STAKE, donc tout retrait, même minime, fait sortir ce nœud de l’ensemble de l’epoch suivante, et 18 des 26 doivent rester actifs. Un validator actif qui ne vote pas compte aussi contre le quorum : depuis la chain 14, un validator retient son vote pour un bloc de plus de 15 s en avance sur sa propre horloge, si bien qu’un validator dont l’horloge retarde cesse de voter sans erreur. Faites-y tourner NTP (Exploiter un validator).

Le genesis alimente directement le registre — une entrée par --validator <key or hex pubkey>,<stake>,<payout> — et refuse une mise inférieure au minimum. Le genesis de la chain 20 alimente 26 lignes de 1,000 RAND chacune, avec les mêmes clés que celui de la chain 19.

Depuis v0.5.11, le build porte aussi le vesting de genesis : un registre public d’allocations verrouillées dans le temps, à partir duquel une entrée irrévocable peut déposer des RAND verrouillés comme mise d’un validator (rand-node vesting bond) et les reprendre (rand-node vesting unbond). Il faut une section vesting dans le genesis. Aucune chain n’en a une, la chain 20 comprise, et sans elle ces actions sont refusées à l’admission. (v0.6.8 permet aussi de ne révoquer une entrée de vesting révocable que par un seuil de signataires vers une trésorerie fixe, là encore uniquement sous une section vesting.)

Ce qui est public

publiccaché
le registrechaque ligne : adresse, mise, file de retrait, gains non versés, adresse de paiement, noncerien
Bondle validator, le montant, si un enregistrement était joint, le burn_r du bundlequelles notes ont payé, à qui elles appartiennent, la monnaie rendue
AdmitValidator (chain 20)le candidat et chaque votant avec sa signaturerien d’autre n’existe
Unbondle validator, le montant, le noncerien d’autre n’existe
Withdrawle validator, le montant, le nonce, le time et le r de la notequi peut ouvrir la note, et chacune de ses dépenses ultérieures

Les gains sont les frais des blocs qu’un validator a proposés. Il n’y a ni récompense de bloc ni inflation, et sur la chain 20 il n’y a ni slashing ni jailing des validators : un validator qui se comporte mal ne coûte rien à sa mise, et le remède revient aux opérateurs. Le fullnode v0.6.8 porte bien une règle de slashing pour l’équivocation du leader (audit v6, STAKE-1 : deux en-têtes de bloc signés pour une même vue par une même clé détruisent equivocation_bps de la mise et mettent la clé en jail), mais uniquement sous une section genesis staking.slashing, et la chain 20 n’en a pas (rand_getLimits.slashing vaut null) ; le fullnode ne la recommande que lorsque la mise est détenue par plus d’un opérateur. (Le code d’agrégation de blocs porte aussi une action SlashAggregator pour les aggregators qui signent deux agrégats différents au même nonce ; elle n’est pas active sur la chain 20, qui n’a pas de section d’agrégation.)

Relire l’état

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…"] }

Les montants sont des chaînes décimales d’unités. active indique si la ligne fait partie de l’ensemble qui exécute l’epoch courante ; jailed_until vaut null sauf sous une section de slashing ; admitted liste les clés admises par vote qui ne se sont pas encore enregistrées ; next_set est une projection que chaque dépôt et chaque retrait de mise avant la frontière déplacent. Le GET /validators de l’explorer ajoute share_percent, blocks_proposed et la dernière hauteur proposée.

Dernière vérification par rapport au commit c0878c3f de fullnode (chaîne 21) le 2026-10-12. Lorsque le code et cette page divergent, c'est le code qui a raison ; signalez-le-nous.