Verwenden Sie diese Seite, um alles zu bauen oder zu prüfen, was den Pool liest: eine Wallet, die nach ihren Notes sucht, einen Indexer, der Commitments und Nullifier verfolgt, oder ein Werkzeug, das prüft, was eine Transaktion veröffentlicht. Wert besteht auf dieser Chain aus einer Menge von Notes; die Chain speichert ihre Commitments, niemals ihren Inhalt.
Eine Note
Eine Note ist { pk, from, amount, asset, time, r }: 28 Wörter zu je 32 Bit, 112 Bytes, angeordnet als pk(8) from(8) amount_lo amount_hi asset time r(8). pk ist der öffentliche Note-Wert des Eigentümers (aus dem Viewing Key abgeleitet), from der des Absenders, amount ein u64 in Einheiten (1 RAND = 10⁹ Einheiten), asset ein u32 (0 für RAND, andernfalls der dichte Index der Token-Registry für einen RPL-Token, überbrückt oder nativ; siehe RPL-Token), time eine Blockhöhe, die der Absender angepeilt hat, und r acht zufällige Wörter als Blinding.
Der Klartext erscheint nie on-chain. Was erscheint, ist:
- ihr Commitment
cm = H(CM, words), ein Poseidon2-Hash der 28 Wörter unter dem Domain-Tag 4, der als Blatt an den Baum angehängt wird; - ihr Envelope, der an die Adresse des Eigentümers versiegelte Klartext, sodass nur der Eigentümer oder der Absender über seinen Outgoing Viewing Key ihn lesen kann. Seit Chain 18 (Chain 20 eingeschlossen) enthält der Envelope außerdem ein verschlüsseltes Memo. Einige Notes, die die Chain aus öffentlichen Feldern berechnet, tragen gar keinen Envelope (unten).
Das Ausgeben der Note veröffentlicht ihren Nullifier H(NF, nk, cm), einen Poseidon2-Hash unter dem Domain-Tag 3 aus dem Viewing Key des Eigentümers und dem Commitment. Ohne nk lässt sich ein Nullifier nicht mit seinem Commitment verknüpfen. Die Nullifier-Menge verhindert einen Double Spend.
Der Envelope
Envelope { kem_ct, to_receiver, to_sender, body }
| Teil | Bytes | Was es ist |
|---|---|---|
kem_ct | 1.088 | eine ML-KEM-768-Kapselung an die Adresse des Empfängers |
to_receiver | 60 | der Schlüssel pro Transaktion TxKey, verpackt unter dem gekapselten Geheimnis |
to_sender | 60 | derselbe TxKey, verpackt unter dem Outgoing Viewing Key ovk des Absenders |
body | 652 | die 112 Byte große Note und ein 512 Byte großes Memo-Feld unter TxKey (140 Bytes, nur die Note, bis Chain 17) |
Jeder Teil ist ChaCha20-Poly1305 mit einer vorangestellten zufälligen 12-Byte-Nonce und einem 16-Byte-Tag; die zugehörigen Daten (associated data) bestehen aus einem Tag (rand-envelope-receiver, rand-envelope-sender, rand-envelope-body) gefolgt vom Commitment der Note. So kann ein Schlüssel keinen Envelope öffnen, den er nicht versiegelt hat, und eine geöffnete Note ist an das Blatt gebunden, unter dem sie liegt. Das ergibt 1.860 Bytes Chiffretext; durch die Längenpräfixe von bincode sind es 1.892 auf der Leitung. Das Genesis von Chain 20 setzt envelope_bytes: 1860, und ihr Ledger weist einen Note-Envelope jeder anderen Länge zurück (envelope is 1348 bytes, this chain requires exactly 1860); mehr prüft es an ihm nicht. Bis Chain 17 hatte der Envelope kein Memo-Feld und war 1.348 Bytes groß (1.380 auf der Leitung), und das Ledger begrenzte ihn nur auf 2.048 Bytes, was weiterhin die Regel auf einer Chain ist, deren Genesis kein envelope_bytes enthält.
Das Memo-Feld ist len (u16 LE) ‖ UTF-8 text ‖ zero padding, 512 Bytes, also höchstens 510 Bytes Text. Jeder Note-Envelope auf Chain 20 hat es, ob mit oder ohne Memo: Ein Wechselgeld-Output, ein Dummy-Output und eine Zahlung ohne Memo werden gleich aufgefüllt, und nichts on-chain unterscheidet ein Memo, seine Länge oder sein Fehlen. Das Memo ist weder in der Note noch im Commitment noch in irgendeinem Beweis enthalten. Lesen kann es genau, wer die Note ohnehin lesen kann: der Eigentümer, der Absender über ovk oder jeder, dem der TxKey dieses Outputs übergeben wurde. Ein fehlerhaftes Memo-Feld (falsche Länge, ungültiges UTF-8, Padding ungleich null) öffnet sich als kein Memo und kostet den Eigentümer nie die Note. Ein Memo ist der Text von irgendwem, da jeder eine Dust-Note an eine öffentliche Adresse zahlen kann: Zeigen Sie es als nicht vertrauenswürdig an, wobei Steuer- und Unicode-Formatzeichen ersetzt werden, wie es die rand-Wallet tut. rand send --memo versiegelt eines in den Zahlungs-Output; rand notes --memo und rand history --memo zeigen es an.
Der Baum, Anker und Fenster
Commitments werden an einen einzigen Poseidon2-Baum der Tiefe 32 angehängt, das älteste zuerst, und nie entfernt. Blatt i ist die i-te Note, die die Chain je erzeugt hat; die Genesis-Einzahlungsnotes sind die ersten Blätter. Ein Bundle hängt vier Blätter und vier Nullifier an, was auch immer es bewegt, Dummies eingeschlossen. Das leere Blatt besteht aus lauter Nullen, und ein Knoten ist H(NODE, left, right) (Domain-Tag 7). Ein Knoten hält nur die Frontier, sodass der Konsenszustand die Blätter nie enthält; rand_getWitness baut den vollständigen Baum bei Bedarf aus den gespeicherten Blättern neu auf, weshalb es der teuerste Lesezugriff ist, den ein Knoten bedient.
Ein Bundle beweist die Zugehörigkeit seiner Inputs gegenüber einem Anker, einer Baumwurzel. Zwei Regeln halten einen Anker ehrlich:
| Konstante | Wert | Regel |
|---|---|---|
ANCHOR_WINDOW | 256 Blöcke; 1.024 auf Chain 20 | der Anker muss eine der letzten window Block-Ende-Wurzeln sein; eine Wurzel, die der Baum mitten im Block durchläuft, ist nie ein Anker |
TIME_WINDOW | 256 Blöcke; 1.024 auf Chain 20 | die time des Bundles muss in [height − window, height] liegen |
Beide Fenster umfassen 256 Blöcke, sofern das Genesis nicht proof_window_blocks setzt (v0.6.8, 256 bis 4.096), was beide zusammen ersetzt. Chain 20 setzt 1.024; die Chains 14 bis 19 hatten das Feld nicht.
Die Fenster existieren, weil das Beweisen echte Zeit braucht: Ein Tier-14-Bundle-Beweis dauert auf einem Laptop etwa 100 Sekunden (und seit Chain 17 erstellt die Wallet einen zweiten Tier-10-Auth-Beweis, etwa 7 Sekunden). 256 Blöcke waren bei den 1,17-s-Blöcken von Chain 18 etwa fünf Minuten, und ein delegierter Beweis auf einem langsamen Prover wurde nach 294 s abgelehnt; die 1.024 Blöcke von Chain 20 sind bei den dort vom Explorer gemeldeten 1,34 s etwa 23 Minuten. Eine Wallet, die das Rennen verliert, erhält anchor is not one of the last 1024 roots und beweist erneut. rand_getAnchor ohne Parameter liefert die Block-Ende-Wurzel des Heads, den einzigen Anker, gegen den ein Prover bauen sollte.
Das Bundle
Jede Transaktion, die Wert bewegt, trägt genau ein festes 4-in-4-out-Hidden-Asset-Bundle (seit Chain 14; bis Chain 13 war es 2-in-2-out), bewiesen vom Hidden-Asset-zkVM-Guest, dessen Digest hc_bundle in der Genesis-Datei festgeschrieben ist (60af094acfe65d85fdb18fb3d06cf9085dcf28c96e59e87f1ee527226e6e3fce, Bundle-Guest v3, auf den Chains 17 bis 20; auf den Chains 14 und 15 war es 83d3a3704a1fcdb9bae7136c0a947ffa34bd53f055388395ed705fe8cacd0ef8, Guest v1):
Bundle { anchor, nullifiers[4], commitments[4], fee, burn_a, burn_r, burn_asset, time, envelopes[4], proof,
auth_commit, auth_proof } // the last two since chain 17
Die Slots 0–1, ein- wie ausgehend, tragen ein privates Asset A; die Slots 2–3 tragen immer RAND. Bei einer reinen RAND-Zahlung sind die Slots 0–1 Dummies, und der Wert sowie die Gebühr bewegen sich über die Slots 2–3. Bei einem Token-Transfer bewegt sich der Token über die Slots 0–1, und die RAND-Gebühr wird aus den Slots 2–3 desselben Beweises bezahlt. A ist ein Witness-Wort des Beweises und steht in keinem öffentlichen Feld, sodass ein Transfer eines beliebigen RPL-Tokens Action::None mit burn_a = burn_r = burn_asset = 0 ist, Feld für Feld die Form einer RAND-Zahlung.
Der Beweis stellt fest, dass jeder echte Input ein Blatt unter anchor ist und das Asset seines Slots trägt, dass die vier Nullifier die veröffentlichten sind, dass jede Gruppe ausgeglichen ist (in0 + in1 = out0 + out1 + burn_a im privaten Asset, in2 + in3 = out2 + out3 + fee + burn_r in RAND) und dass der Ausgebende die Schlüssel hält – ohne zu offenbaren, welche Blätter, welche Beträge, welches Asset oder wer. Die drei Burn-Felder sind die einzige öffentliche Aussage des Bundles über Wert, der den Pool verlässt: burn_a und burn_asset sind nur bei einem TokenBurn oder einem BridgeBurn ungleich null, wo sie dem Betrag und dem Asset der Aktion entsprechen; burn_r ist verbranntes RAND, nur bei einem Bond (der Stake) und einem RegisterAggregator (die Bond-Sicherheit) ungleich null. Die Gebühr ist immer RAND.
Seit Chain 17 ist die Berechtigung zum Ausgeben auf zwei Beweise verteilt (Split Authorisation). Der Bundle-Guest v3 nimmt den Viewing Key nk und ein Salt, wo die früheren Guests den Spend Key nahmen: Er leitet pk_self = H(PK, nk) ab, verlangt, dass jeder Input ihm gehört, und faltet c = H(AUTH, nk ‖ salt) in den Digest ein, den er veröffentlicht. Ein zweiter Guest, der Auth-Guest (Genesis hc_auth, 1e4e347f44cf86750b30a9a4bdf9ec9256efe353d4ff8017451eca7d195639c1), nimmt den Spend Key sk und dasselbe Salt, leitet nk = H(NK, sk) ab und veröffentlicht dasselbe c. Das Bundle trägt c als auth_commit und den Tier-10-Beweis des Auth-Guests als auth_proof, und das Ledger verlangt, dass beide Beweise gegen dieselbe Transaktionsbindung verifizieren und die beiden Werte von c übereinstimmen. Ein Prover, der nk und das Salt hält, kann also den Bundle-Beweis erstellen, nicht aber den Auth-Beweis, worauf das delegierte Beweisen beruht. Das Salt sind 256 frische Zufallsbits pro Bundle, sodass auth_commit öffentlich ist und nichts verknüpft. Eine Wallet, die für sich selbst beweist, erstellt ebenfalls beide Beweise, sodass eine selbst bewiesene und eine delegierte Transaktion on-chain gleich aussehen.
Die Form ist festgelegt, daher sind Slots oft Platzhalter: Ein Dummy-Input ist eine Note mit dem Wert null, die dem Ausgebenden gehört (ihr Nullifier wird trotzdem veröffentlicht), ein Dummy-Output ist eine Note mit dem Betrag null, und der Wechselgeld-Output ist auch dann vorhanden, wenn das Wechselgeld null beträgt. Eine Transaktion, die ohne Wechselgeld anders aussähe, würde verraten, dass es keines gab. Für Wallets folgt daraus: Jede der beiden Gruppen eines Bundles gibt höchstens zwei Noten aus. Eine RAND-Zahlung, eine Bond-Transaktion, ein Deploy oder ein Call gibt höchstens zwei RAND-Noten aus; ein Token-Transfer oder -Burn gibt höchstens zwei Noten des Tokens und höchstens zwei RAND-Noten für die Gebühr aus. Verteilt sich ein Guthaben innerhalb einer Gruppe auf drei oder mehr Noten, muss es zuvor konsolidiert werden, durch eine Überweisung an die eigene Adresse.
Seit Chain 14 ist der Beweis zudem an seine Transaktion gebunden: Er wird über einen Hash der gesamten Transaktion mit geleerten Beweisen erstellt und gegen diesen verifiziert, sodass er sich nicht auf eine andere Aktion, andere Envelopes oder eine andere Chain übertragen lässt; auf Chain 20 deckt der Hash auch den Genesis-Hash ab, sodass er sich auch nicht auf eine Chain übertragen lässt, die dieselbe Chain-ID verwendet (Transaktionen).
Was öffentlich ist und was verborgen bleibt
| öffentlich auf der Chain | verborgen | |
|---|---|---|
| ein Transfer, von RAND oder von einem beliebigen RPL-Token | anchor, alle vier Nullifier, alle vier Commitments, Gebühr, burn_a = burn_r = burn_asset = 0, time, vier Envelope-Chiffretexte, der Beweis, auth_commit und der Auth-Beweis | wer gesendet hat, wer bezahlt wird, der Betrag, das Memo, welcher Asset bewegt wurde, das Wechselgeld, welche Blätter ausgegeben wurden, welche Slots Dummies waren |
| ein Token-Burn oder ein Bridge-Burn | alles, was ein Transfer veröffentlicht, mit burn_asset und burn_a gleich dem Asset und dem Betrag der Aktion (und, bei einem Bridge-Burn, der Relayer-Gebühr, dem Backing-Token und dem Ziel) | welche Noten verbrannt wurden und wer sie verbrannt hat |
| ein Faucet-Mint | das Commitment, der Envelope, der Betrag, der prägende Validator und seit Chain 14 die Öffnung der Note (pk, time, r), aus der die Zulassung das Commitment neu berechnet | jede spätere Ausgabe der Note |
| eine Genesis-Einzahlung | { cm, envelope, amount, opening: { pk, time, r } } in der Genesis-Datei, bei einer Note eines gelisteten Tokens zusätzlich mit asset in der Öffnung (das Genesis von Chain 20 enthält zwei zUSD-Noten): Das pk des Eigentümers, der Betrag und der Asset sind bewusst öffentlich | wann und wofür sie später ausgegeben wird |
| ein Withdraw, eine Bridge-Einzahlung, ein Token-Mint oder der Anfangs-Mint einer Registrierung | der Betrag, der Empfänger, das time der Note und der Blinding-Faktor r (die Chain berechnet das Commitment selbst) | jede spätere Ausgabe der Note |
| die Auszahlung eines Invoke oder der Programm-Mint (Chain 20) | der Betrag, der Asset, der Empfänger und r; das time der Note ist das des Bundles | jede spätere Ausgabe der Note |
| eine Bridge-Gebührennote (Chain 20) | der Betrag, der Asset und time, ein abgeleiteter Blinding-Faktor, die Genesis-Treasury als Eigentümerin und kein Envelope | jede spätere Ausgabe der Note |
Wert gelangt mit einem öffentlichen Betrag in den Pool — ein Faucet-Mint, eine Genesis-Einzahlung, der Withdraw eines Validators, eine Bridge-Attestierung samt ihrer Gebührennote, ein Token-Mint, die Auszahlung eines Programm-Vaults — und verlässt ihn als öffentliche Gebühr oder als Burn. Alles dazwischen ist eine Note.
Die Tabelle zeigt, was die Beweise von Chain 20 verbergen; das war nicht immer so. Auf den Chains 14 und 15 legte der Bundle-Beweis selbst mehr offen (das zkVM-Review vom 2026-09-27, INT-2 / GV-1): Die laufende LogUp-Summe jeder Tabelle wurde ungeblindet veröffentlicht, und der Guest v1 übersprang bei einem Dummy-Input die Merkle- und Asset-Prüfungen und verzweigte bei jedem Bit des Merkle-Pfads. So konnte jeder aus einem Bundle-Beweis ablesen, welche der vier Input-Slots echt waren, und damit, ob er einen Token oder nur RAND bewegte, sowie die Anzahl der 1-Bits in jedem ausgegebenen Blattindex. Zwei Änderungen haben das geschlossen. Chain 16 wechselte zu Constraint-Set 7, das jeden LogUp-Abschluss blindet und jede deklarierte Tabelle auf mindestens 2^7 Zeilen anhebt, und zum verzweigungsfreien Bundle-Guest v2, in dem jeder Slot, ob Dummy oder echt, die vollständige Mitgliedschafts- und Asset-Prüfung über dieselben Instruktionen durchläuft. Guest v3, den die Chains 17 bis 20 ausführen, ist die Relation von v2 mit nk und dem Salt anstelle des Spend-Schlüssels und ebenfalls verzweigungsfrei. Auf den Chains 14 und 15 committete Bundles behalten, was sie preisgaben. Was ein Beweis auf Chain 20 weiterhin zeigt, ist sein Header: Bei einem Bundle ist das eine feste Form (Tier 14, ein deklariertes Gas-Limit von 20,479 bei jedem Bundle), bei einem Call oder einem Invoke sind es das Tier, das deklarierte Gas-Limit und ob und ungefähr wie oft der Call den keccak- oder sha256-Precompile verwendet hat (Beweise).
Noten ohne Envelope
Die meisten Noten findet man, indem man ihren Envelope öffnet. Einige berechnet die Chain aus öffentlichen Feldern, und bei diesen fehlt der Envelope entweder oder ist nicht vertrauenswürdig.
- Bridge-Gebührennoten (Chain 20). Unter
bridge.feeshängt die Chain bei jeder Einzahlung und jedem Burn eine zUSD-Note an die Genesis-Treasury an (RPL-Tokens). Sie hat keinen Envelope: Der Speicher indexiert sie mit einem leeren, an niemanden versiegelten, undrand_getCompactBlocksundrand_getCommitmentszeigen sie so an, bei einer Einzahlung nach der Einzahlungsnote und bei einem Burn nach den vier Noten des Bundles. Jedes Wort der Note ist öffentlich: der Betrag aus der Transaktion und dem Genesis-Kurs, der Asset undtimeaus der Transaktion,from = 0, der Blinding-Faktorblake3("rand-bridge-fee-r-1" ‖ mu)bei einer Einzahlung oderblake3("rand-bridge-burn-fee-r-1" ‖ txid)bei einem Burn, und als Eigentümer derrecipientaus dem Genesis. - Noten, deren Envelope nichts bindet. Eine Bridge-Einzahlung, ein Token-Mint, der Anfangs-Mint einer Registrierung und, auf Chain 20, die Auszahlungen und Programm-Mints eines Invoke tragen einen Envelope, der von demjenigen versiegelt wird, der die Transaktion eingereicht hat; die Chain berechnet die Note jedoch aus den öffentlichen Feldern der Aktion und prüft den Envelope nie gegen sie. Ein nachlässiger oder böswilliger Einreicher kann Unsinn versiegeln, daher darf sich ein Empfänger nicht darauf verlassen.
Die rand-Wallet baut diese Noten deshalb selbst nach (wallet::rebuilt_notes_with), in einem öffentlichen Durchlauf neben der Trial-Decryption: Für jede solche Transaktion, die ihr eigenes pk nennt, und für die Gebührennoten, wenn ihre Adresse die Treasury ist, berechnet sie die Note und trägt sie an dem Blatt ein, dessen Commitment sie ergibt. Eine nachgebaute Note ist nur ein Kandidat: Ein Knoten, der über eine Transaktion, den Gebührensatz oder einen Hash lügt, bewirkt, dass eine Note nicht gefunden wird, nie, dass eine gutgeschrieben wird. Seit v0.6.8 enthalten die Header, die rand_getBlocks zurückgibt, die Einzahlungs-, Mint- und Registrierungstransaktionen in public_notes, auf einer Chain mit Bridge-Gebühren auch die Burns, ohne Beweise und unter ihrer echten ID, sodass der Durchlauf nur Header-Seiten liest. Invokes stehen in v0.6.8 nicht in public_notes; gegen einen v0.6.8-Knoten findet die Wallet die Auszahlung eines Invoke daher nur über ihren Envelope, den der Aufrufer versiegelt und den nichts prüft. Fullnode main führt Invokes seit d0bb73fb ebenfalls dort, ohne Beweise; das ist eine reine Knotenänderung, noch nicht auf die Flotte von Chain 20 ausgerollt, und die Wallet braucht dafür keine Änderung.
Scanning: Wie eine Wallet ihre Noten findet
Ein Knoten antwortet „hier ist der ganze Baum“, nie „hier ist dein Guthaben“. Eine Wallet:
- liest die Blätter seitenweise mit
rand_getCommitments(from_index, limit)(höchstens 1,000 pro Aufruf) oder über den blockweisen Streamrand_getCompactBlocks(from_height, to_height)(höchstens 128 Blöcke pro Aufruf); jede Transaktion mit Bundle listet dort vier Noten und vier Nullifier auf, und kein Leser kann einen Dummy-Slot von einem echten unterscheiden; - entschlüsselt jeden Envelope per Trial-Decryption: als Empfänger über ihren ML-KEM-Decapsulation-Key, als Sender über
ovk; behält die Noten, derenpkihr eigenes ist; und baut die Noten mit öffentlichem Betrag aus ihren Transaktionen nach (oben); - berechnet den Nullifier jeder behaltenen Note und markiert diejenigen, die in
rand_getNullifiers(from_height, limit)erscheinen; - summiert den Rest.
rand_getTreeInfo gibt an, wie weit gescannt werden muss: { "next_index", "root", "nullifiers" }. Für eine Ausgabe benötigt die Wallet den Merkle-Pfad mit 32 Ebenen jedes echten Inputs. Die rand-Wallet führt eine eigene Kopie des Commitment-Baums, die sie während des Scans aus denselben rand_getCommitments-Seiten aufbaut, und berechnet jeden Pfad selbst; die einzige Baumfrage, die ein Send an den Knoten richtet, ist daher rand_getAnchor, die kein Blatt nennt. rand_getWitness(index) bleibt für andere Clients auf dem Knoten. Der Aufruf verrät dem Knoten, welches Blatt der Aufrufer gleich ausgeben wird, und das war vor dem lokalen Baum das größte Leck des Pools: Ein Client, der ihn verwenden muss, sollte einen eigenen Knoten betreiben oder neben den benötigten Witnesses auch solche anfordern, die er nicht braucht.
rand sync # scanned 41 leaves and 37 blocks; 1 notes, 1 unspent
rand notes # index, asset, amount, height, spent, pending, memo
rand history # notes this wallet created for others, opened through ovk
Eine Note ist pending zwischen rand send --no-wait und dem nächsten sync, der entweder ihren Nullifier oder Blöcke jenseits von time + window sieht (1.024 auf chain 20, gelesen aus rand_getLimits.proof_window_blocks; 256 ohne diesen Wert). Danach kann das Bundle nie mehr zugelassen werden, und die Note ist wieder ausgebbar.