Rand Protocol
Dokumentation
Grundkonzepte

Konsens und Finalität

Chained HotStuff, wie der Node es ausführt – Quoren, Leader, die Commit-Regel mit drei aufeinanderfolgenden Views, Timeouts, Epochen – und was „final“ für einen Client auf dem testnet bedeutet.

Auf dieser Seite erfahren Sie, wann eine Transaktion final ist, wie viele Validatoren online sein müssen und was die Block-Felder bedeuten, die Sie in der RPC und im Explorer sehen. Der Konsens ist ein verketteter HotStuff-BFT mit stakegewichteten Quoren; auf Chain 20 sind das 18 von 26 Validatoren.

Die Regeln, die der Node ausführt

RegelUmsetzung
QuorumEin Quorum-Zertifikat (QC) ist eine Menge von Dilithium2-Stimmen von Validatoren, die strikt mehr als 2/3 des gesamten Stakes halten
Leader-Zeitplanreines Round Robin: Der Leader der View v ist validators[v mod n], wobei die Menge nach Adresse sortiert ist. Der Stake gewichtet Stimmen, nicht die Leader-Auswahl
VorschlagenDer Leader der View v schlägt vor, sobald er ein QC für v − 1 oder ein Quorum von NewView-Nachrichten für v hält. Ein Vorschlag, der eine ungültige Transaktion anwenden würde, verwirft diese Transaktion und nicht den Block
Abstimmen und SicherheitEin Validator stimmt einmal pro View ab, und nur für einen Block, der bezüglich seiner Sperre sicher ist (sein Justify-QC ist neuer als das gesperrte QC, oder der Block erweitert den gesperrten Block). Der Sicherheitszustand wird per fsync in RocksDB geschrieben, bevor eine Stimme den Node verlässt
SignaturdomäneBei einem Genesis mit consensus_domain: 1 (Chain 20 und jede Chain seit 15) deckt jede Signatur für Stimme, New-View und Vorschlag den Genesis-Hash ab (rand-vote-2, rand-newview-2, rand-block-2), sodass eine Signatur für eine Chain auf keiner anderen verifiziert. Chain 14 und früher signierten ohne ihn. Das binding_domain: 1 von Chain 20 dehnt dieselbe Regel auf Transaktionsbeweise und signierte Aktionen aus (Transactions)
Geschwister-VorschlägeEin Replikat hält pro (View, Leader) einen Block; ein zweiter, abweichender Block dieses Leaders für diese View wird als Equivocation abgelehnt. Ein Vorschlag, der mehr als 8 Views vor der eigenen View des Replikats liegt (PROPOSAL_VIEW_WINDOW), wird abgelehnt, bevor seine Signatur geprüft wird; ein voller spekulativer Baum verdrängt Blöcke abseits des Zweigs, bevor er irgendetwas ablehnt
Freigabe der SperreDie Sperre wird nie automatisch gesenkt (CON-4, v0.6.8). Sie weicht nur einem Vorschlag, dessen Justify-QC sie übertrifft, oder dem Eintreffen des gesperrten Blocks. Ein Quorum signierter NotHeld-Antworten wird für den Betreiber protokolliert und gibt nichts frei, und ein Validator zeichnet seine Stimmen auf, sodass er nie NotHeld für einen Block signiert, für den er abgestimmt hat. Jeder zertifizierte Block oberhalb des Heads wird persistiert und beim Neustart wiederhergestellt, sodass ein Neustart der gesamten Flotte kein QC auf einem Block zurücklässt, den niemand hält. Ein Validator, dessen gesperrter Block endgültig verloren ist, wird von Hand und offline freigegeben: rand-node safety release-lock
QC-AufbauJeder Validator baut QCs lokal aus per Gossip verbreiteten Stimmen auf, sodass die Chain weiter committet, wenn der nächste Leader offline ist
Commitdie verkettete Drei-QC-Regel: Ein Block wird committet, sobald drei Zertifikate in aufeinanderfolgenden Views v, v+1, v+2 auf ihm aufbauen und diese Kette bis zum committeten Head zurückreicht
Timeouts und View-WechselBasis --view-timeout-ms 3.000 ms, bei jedem aufeinanderfolgenden Timeout verdoppelt, bei jedem Fortschritt zurückgesetzt. Ein Schrittmacher mit Timeout-Zertifikaten (CH-1, v0.6.8): Ein Replikat, dessen Timer abläuft, bleibt in seiner View und signiert eine NewView, die um die nächste bittet. Es tritt in eine View ein, wenn ein QC für die vorherige View vorliegt oder wenn Validatoren mit mehr als 2/3 des Stakes um diese oder eine spätere View gebeten haben; es schließt sich an (signiert seine eigene Bitte), wenn mehr als 1/3 um eine View jenseits seiner eigenen bitten. Die NewView eines einzelnen Validators bewegt niemanden. Views sind begrenzt (MAX_VIEW_AHEAD = 10⁶)
TaktungDer Leader schlägt einen leeren Block vor, wenn nichts aussteht, frühestens --block-interval-ms (Standard 1.000 ms) nach dem letzten von einem beliebigen Proposer gesehenen Block
Grenzen für Stimmen und GossipStimmen, die mehr als 8 Views voraus sind, werden ignoriert, und pro (View, Stimmgeber) zählt eine Stimme; der Konsens-Gossip wird pro weiterleitendem Peer begrenzt (Burst 256, 64/s); ein Vorschlag mit unbekanntem Elternblock wird nur dann als Waise behalten, wenn sein Proposer in der Menge der aktuellen Epoche oder der nächsten seine View führt, er innerhalb der Baumgrenze des committeten Heads liegt und seine Größe und sein Tx-Root stimmen (Waisen-Pool auf 64 MiB begrenzt, seit v0.6.1 zudem auf 16 Waisen pro Proposer). Seit v0.6.8 wird ein Vorschlag nur dann weitergeleitet und eine Waise nur dann behalten, wenn seine Transaktionsliste auf die Wurzel hasht, auf die sein Header sich festlegt, und keine Transaktion wiederholt ist (GOSSIP-2)
Blockgrenzendie max_block_bytes der Chain an Transaktionsbytes (standardmäßig 4 MiB, 20 MiB im Genesis von Chain 20) und 2.000 Transaktionen, vom Proposer durchgesetzt und von jedem Replikat in Ledger::apply_block erneut
Blockzeitauf einer Chain mit einem bridge-Abschnitt: Der timestamp_ms eines Blocks darf nicht vor dem seines Elternblocks liegen (TimestampRewind) und ihn nicht um mehr als 60 s überschreiten (TimestampLeap), und ein Validator hält seine Stimme für einen Block zurück, der mehr als 15 s vor seiner eigenen Uhr liegt; siehe unten

Die Commit-Regel mit drei aufeinanderfolgenden Views ist tragend: Lockert man sie, öffnet sich ein Fehler mit widersprüchlicher Finalität wieder, für den der Fullnode einen Regressionstest hat. Alles, was ein Client über die RPC sieht, ist committet: Es werden nur committete Blöcke ausgeliefert, und der WebSocket-Stream newHeads sendet pro committetem Block genau eine Benachrichtigung, in Reihenfolge.

Die Tabelle gilt für v0.6.8, das Release, das Chain 20 ausführt. Drei ihrer Regeln sind neu in v0.6.8 (Audit v6) und gelten, seit Chain 20 am 2026-10-01 live ging: der Schrittmacher mit Timeout-Zertifikaten (CH-1), keine automatische Freigabe der Sperre (CON-4) und die Prüfung der Transaktionsliste eines Vorschlags (GOSSIP-2). Auf den Chains 18 und 19 (v0.6.7) bewegte die signierte NewView eines einzelnen Validators jedes Replikat in ihre View, und die Sperre wurde aufgrund signierter NotHeld-Antworten eines Quorums des Stakes gesenkt. Der Schrittmacher und die Sperrregel ändern, was Validatoren signieren und wann; deshalb wurde v0.6.8 mit dem Chain-20-Cut ausgerollt, bei dem alle gestoppt und alle neu gestartet wurden, nie Node für Node.

Die Zahlen des testnet

Chain 20 hat 26 Validatoren, jeder mit genau 1.000 RAND gestakt, alle 26 in ihrem Genesis, auf den Schlüsseln und Peer-IDs, die die Chains 17 bis 19 verwendeten. Auf Chain 15 waren achtzehn im Genesis, auf den Schlüsseln von Chain 14, und acht weitere wurden am 2026-09-27 unter v2-Registrierungen gebondet und waren ab Epoche 43 aktiv. Der Stake ist einheitlich, daher ist das Quorum eine Kopfzahl: 18 der 26 müssen online sein, um irgendetwas zu committen. Bei 17 aktiven Validatoren laufen die Views weiter, und nichts wird final. Eine Menge von n gleichen Validatoren braucht mehr als 2/3 des Stakes: 2 von 2, 3 von 4, 5 von 6, 13 von 18, 18 von 26. Die Gewichtsobergrenze im Genesis (3.333 bps pro Validator) greift bei einer Menge gleicher Stakes nicht.

Die Blöcke sind auf den Standardwert von 1.000 ms getaktet. Auf Chain 15 kamen sie in der Praxis etwa alle 1,2 s (1,18 s im Mittel über die ersten 29 Stunden); auf Chain 20 meldete der Explorer bei Höhe 3.270 einen Durchschnitt von 1,34 s (2026-10-01, avg_block_time_ms). Der Explorer meldet die durchschnittliche Blockzeit, die er beobachtet (avg_block_time_ms in GET /stats). Die View eines Validators läuft der Höhe voraus, wann immer Views in einen Timeout laufen; deshalb meldet rand_getHead beides.

Epochen

Die Validatormenge wird alle epoch_blocks = 1.000 Blöcke (ein Genesis-Parameter) aus dem Register neu abgeleitet. epoch(h) = h / 1000; die Menge für die Epoche e ≥ 1 wird aus dem Register zum Stand des letzten Blocks der Epoche e − 1 abgeleitet: jede Zeile mit stake ≥ 1,000 RAND, die obersten 100 nach Stake, nach Adresse sortiert. Auf einer Chain ohne staking-Abschnitt wird ein Bond an der nächsten Grenze zu Gewicht. Auf Chain 20 wartet er zwei weitere Epochen und ein Eintrittsbudget pro Epoche von 2.500 bps des aktiven gebondeten Gewichts an der Grenze (6.500 RAND, solange die Menge 26.000 hält), und ein Bond, der einen neuen Validator registriert, wird abgelehnt, es sei denn, die Menge hat den Schlüssel zuvor per AdmitValidator hineingewählt (staking.admission_by_vote). Das Budget von Chain 19 war fest 10.000 RAND pro Epoche, und es hatte keine Zulassungsabstimmung. Ein Unbond kostet an der nächsten Grenze Gewicht, während der Betrag zwei Epochen wartet, bis er abhebbar wird. rand_getEpoch liefert die Epoche, die Epochenlänge und eine Projektion der nächsten Menge. Siehe Staking.

Was ein Block enthält

{
  "hash": "647b…", "height": 50, "view": 92, "parent": "2d41…",
  "proposer": "3v3VBJ…", "timestamp_ms": 1788000123456,
  "tx_root": "0000…", "state_root": "a1b2…", "justify_view": 91,
  "tx_count": 0, "transactions": []
}

view ist die View, in der der Block vorgeschlagen wurde, und justify_view die View des QC, das er für seinen Elternblock trägt. timestamp_ms wird vom Proposer gesetzt, innerhalb der Grenzen des nächsten Abschnitts. state_root legt das Ledger nach dem Block fest. Auf Chain 20:

inner      = blake3("rand-state-8" || tree_root || nullifier_root || validators_root || programs_root
                                   || bridge_root || tokens_root
                                   || faucet_epoch || faucet_minted_in_epoch || bond_queue_root
                                   || gas_price || byte_price
                                   || program_state_root)
state_root = blake3("rand-state-admitted-1" || inner || admitted_root)

Chain 19 committete dieselben Felder ohne program_state_root, unter rand-state-7, ohne Wrapper.

Die Komponenten hängen vom Genesis ab. Eine Chain ohne optionale Abschnitte committet die ersten vier unter rand-state-2. Ein bridge-Abschnitt hängt bridge_root an. Ein tokens-Abschnitt hängt danach tokens_root an und versieht den gesamten Hash mit der neuen Domäne rand-state-4; ein Genesis mit einem bridge-Abschnitt muss einen tokens-Abschnitt haben, daher hat Chain 20 beide. Ein aggregation-Abschnitt hängt als Nächstes eine Aggregatoren-Wurzel an (unter rand-state-3 auf einer Chain ohne Tokens); Chain 20 hat keinen. Ein staking-Abschnitt hängt zuletzt die beiden Epochenzähler des Faucets und die Wurzel der Bond-Warteschlange (rand-bond-queue-1) an und versieht den gesamten Hash mit der neuen Domäne rand-state-5, unabhängig davon, was sonst aktiv ist; Chain 14 hatte keinen solchen Abschnitt und hashte unter rand-state-4, die Chains 15 bis 17 hashten unter rand-state-5. Ein vesting-Abschnitt hängt danach die Wurzel des Vesting-Registers an und versieht den Hash mit der neuen Domäne rand-state-6; Chain 20 hat keinen. Ein gas-Abschnitt mit einem dynamic-Regler hängt den aktuellen gas_price und byte_price, jeweils ein Big-Endian-u64, nach allem anderen an und versieht den Hash mit der neuen Domäne rand-state-7. Das war die Domäne von Chain 19, und Chain 20 behält die Preise in ihrer Wurzel: Sie ändern sich einmal pro Block, nach den Transaktionen des Blocks und vor der Wurzel (Gebühren). Ein gas-Abschnitt mit festen Preisen ist nicht in der Wurzel enthalten. Ein program_state-Abschnitt (RPL-2) hängt nach den Preisen die Wurzel der Zellen und Vaults aller Programme an und versieht den Hash mit der neuen Domäne rand-state-8: die von Chain 20. Der Staking-Zustand aus Audit v6 wird um diese Wurzel herum eingefaltet, nicht in sie hinein: Unter staking.admission_by_vote (Chain 20) ist die Wurzel blake3("rand-state-admitted-1" || inner || admitted_root), und unter staking.slashing (noch keine Chain) fügt ein zweiter Wrapper die Menge der inhaftierten (jailed) Validatoren hinzu. tokens_root ist die Merkle-Wurzel der Blätter blake3("rand-token-leaf-1" || bincode(TokenInfo)) in Indexreihenfolge, sodass die Metadaten, die Autorität, mint_nonce, das Angebot und der locked-Betrag jedes Backings Konsenszustand sind: Zwei Nodes, die sich über eine Token-Zeile uneinig sind, sind sich über die Chain uneinig. Auf Chain 20 faltet die Token-Wurzel zudem die Registry-Erweiterung ein (die Obergrenze max_tokens, die verbrannte Registrierungsgebühr, die Schranke für Note-Werte und die rollierenden Mint-Cap-Fenster der Bridge) unter rand-token-registry-3. Die Bridge-Wurzel umfasst die Emitter, die Guardian-Sets, die verbrauchten Attestierungs-Digests, die Burn-Sequenz, die PQ-Guardian-Schlüssel, den Pause-Schlüssel, das Pause-Flag und die beiden Governance-Nonces; die Asset-Registry wurde aus ihr herausgelöst und in tokens_root verlagert. Die Bridge von Chain 20 hat rules_v2, daher werden die Rotations-Nonce und die globalen Cap-Parameter angehängt und die Domäne lautet rand-bridge-state-5 (die von Chain 14 war rand-bridge-state-4); diese Wurzel wird dann vom Floor für eingehende Replays (rand-bridge-replay-floor-1) umschlossen, auf Chain 20 zusätzlich von den Rotationsregeln und ausstehenden Rotationen (rand-bridge-rotation-1) und der Gebührengruppe (rand-bridge-fees-1). Das Validatoren-Register wird zu validators_root gehasht (Blatt-Domäne rand-validator-leaf-4 auf Chain 20, mit der activation_epoch jeder Zeile; rand-validator-leaf-2 auf einer Chain ohne staking-Abschnitt), sodass ein Node, der sich über eine Registerzeile uneinig ist, sich über die Chain uneinig ist.

Blockzeit ist begrenzt (B2)

Auf einer überbrückten Chain bestimmt der Block-Zeitstempel, wann ein Guardian-Set abläuft und an welchem Tag das tägliche Mint-Cap eines Backings zählt. Ein Leader, der die Uhr vorstellen könnte, könnte daher die Karenzfrist eines rotierten Sets vorzeitig beenden oder das Cap eines Tages überspringen. Chain 20 hat einen bridge-Abschnitt, daher gelten drei Regeln, alle daran gebunden:

RegelSchrankeArt
kein Zurückspulentimestamp_ms ≥ parent.timestamp_ms (Gleichheit ist erlaubt), sonst BlockError::TimestampRewind; der Proposer setzt max(now, parent.timestamp_ms)Gültigkeit, auch bei Replay und Sync geprüft
kein Sprungtimestamp_ms ≤ parent.timestamp_ms + MAX_TIMESTAMP_STEP_MS (60.000 ms), sonst BlockError::TimestampLeapGültigkeit, auch bei Replay und Sync geprüft
UhrenabweichungEin Validator verweigert die Stimme für einen Block, der mehr als MAX_CLOCK_DRIFT_MS (15.000 ms) seiner eigenen Uhr voraus istnur eine Abstimmungsregel: Der Block bleibt im spekulativen Baum, ein Quorum mit übereinstimmenden Uhren kann ihn dennoch zertifizieren, und das Replay committeter Historie wendet sie nie an

Jeder Validator auf einer überbrückten Chain braucht NTP. Ein Validator, dessen Uhr um mehr als 15 Sekunden abweicht, ist ein fehlerhafter Leader (seine Vorschläge erhalten möglicherweise keine Stimmen) und ein fehlerhafter Wähler (er verweigert möglicherweise den rechtzeitigen Block eines ehrlichen Leaders). Beides legt die Chain nicht lahm, solange die Uhren der ehrlichen Mehrheit übereinstimmen, aber eine unsynchronisierte Flotte verliert ohne Not Liveness-Spielraum.

Der timestamp_ms des Genesis muss nahe am tatsächlichen Startzeitpunkt liegen. Liegt er zu weit in der Vergangenheit, hinkt die Blockzeit der Wanduhr hinterher, bis die Schrittgrenze ihr erlaubt aufzuholen, höchstens 60 s pro Block. Liegt er mehr als 15 s in der Zukunft, wird Block 1 gar nicht committet: Jede Stimme, auch die des Leaders selbst, wird zurückgehalten, bis die Wanduhrzeit ihn erreicht. Nach einem langen Stillstand kann dasselbe Aufholen einen Mint-Cap-Tag schneller als einmal pro realem Tag weiterrollen, begrenzt auf das Cap eines zusätzlichen Tages pro vollem Tag aufgelaufenen Rückstands.

Finalität für einen Client

  • Eine Transaktion ist final, wenn rand_getTransaction(hash) einen Block zurückgibt. Die Annahme durch rand_sendTransaction bedeutet nur die Aufnahme in den Mempool.
  • Ein Block, den der RPC ausliefert oder der Explorer indiziert, ist committet. Es gibt kein Finalitätsfenster abzuwarten; der Leitfaden des Explorers sagt dasselbe.
  • Ein Head aus rand_subscribe ["newHeads"] ist ein Head, den der Node nicht verlieren wird: Nichts wird gesendet, bevor der Block im Speicher committet ist.
  • Ein Node, der zurückfällt, verifiziert alles erneut: jedes Zertifikat, jedes Bundle und jeden Call-Beweis, jeden Beleg, bevor er einen synchronisierten Block akzeptiert. Finality-Signaturen werden nie anstelle von Beweisen vertraut.

Kurzübersicht zur Wiederherstellung, aus dem des Fullnodes: Wenn die Höhe nicht steigt und peer_count niedrig ist, handelt es sich um ein Netzwerkproblem; wenn die Höhe nicht steigt, die Peers aber in Ordnung sind, ist weniger als 2/3 des Stakes online; CORRUPT CHAIN beim Start bedeutet, dass der Node ein beschädigtes Ende abgeschnitten hat und neu synchronisiert.

Zuletzt geprüft gegen den fullnode-Commit c0878c3f (Chain 21) am 2026-10-12. Wo Code und diese Seite voneinander abweichen, hat der Code recht; sagen Sie uns Bescheid.