Verwenden Sie diese Seite, um zu verstehen, was mit einer Transaktion geschieht, nachdem rand_sendTransaction sie angenommen hat und bevor ein Block sie aufnimmt: warum sie gegebenenfalls abgelehnt wurde, warum sie aus dem Pool verschwinden kann, ohne je fehlgeschlagen zu sein, in welcher Reihenfolge ein Block gefüllt wird und was eine Flut ungültiger Proofs bei einem Validator bewirken kann und was nicht. Der Mempool ist crates/randprotocol-node/src/mempool.rs; die vorgeschalteten Schutzmechanismen sind admission.rs.
Keine Absender, also keine Nonces
In einer Account-Chain ordnet ein Mempool die Transaktionen jedes Absenders nach Nonce. Eine Shielded-Chain hat keine Absender: Ein Bundle trägt weder eine Adresse noch eine Signatur, und zwei Bundles stehen nur dann in Beziehung zueinander, wenn sie dieselbe Note berühren. Eine Ordnung pro Absender gibt es also nicht mehr. An die Stelle der Nonce-Buchführung tritt das Konflikt-Tracking: Der Pool indiziert die Nullifier und Commitments jeder ausstehenden Transaktion und weist einen Neuankömmling ab, der eines davon beansprucht. Dasselbe gilt für die wenigen Zähler, bei denen das Ledger je Wert genau eine Transaktion akzeptiert: die Register-Nonce eines Validators (ein Unbond oder ein Withdraw), die mint_nonce eines Tokens (gemeinsam genutzt von TokenMint und SetAuthority), den Registry-Index, an den eine RegisterToken gebunden ist, sowie die pause_nonce und die list_nonce der Bridge. Jeder davon ist ein Slot, den höchstens eine gepoolte Transaktion belegen darf. Von zwei solchen Transaktionen könnte ohnehin höchstens eine aufgenommen werden; die andere mitzuführen würde den Proposer Blockplatz kosten und jeden Knoten pro Gossip-Runde eine Proof-Verifikation.
Der Pool fasst höchstens 10,000 Transaktionen (Mempool::new(10_000) in node.rs) und seit v0.6.8 höchstens die Bytes von acht Blöcken: MAX_POOL_BLOCKS = 8 mal max_block_bytes der Chain, auf Chain 20 sind das 160 MiB (Audit v6, CH-7). Zuvor war nur die Anzahl begrenzt, obwohl eine Transaktion mit Proof zwischen etwa 1.3 MB und über 12 MB umfasst. Jede Transaktion wird gegen das Ledger an der Spitze des Konsensbaums validiert, nicht gegen den zuletzt committeten Block: den Zustand, auf dem ein Proposer tatsächlich aufbauen würde.
Admission, erst billig, dann teuer
Eine Transaktion, die über RPC oder Gossip eintrifft, durchläuft diese Schritte der Reihe nach, und die erste Ablehnung ist der Fehler, den Sie sehen:
- Duplikat. Der Hash befindet sich bereits im Pool:
already in mempool. - Konflikt. Ein Nullifier, ein Commitment oder einer der oben genannten Nonce- und Index-Slots wird bereits von einer ausstehenden Transaktion beansprucht:
conflicts with a pending transaction over <hex>. Bei einem Token-Slot ist der Hex-Wert ein Anzeigeschlüssel: ein Tag-Wort (toknfür einemint_nonce,tidxfür einen Registrierungsindex), dann der Index und die Nonce. Eine Bridge-Attestation, um die zwei Relayer gewetteifert haben, wird anhand ihres Digests unterschieden:conflicts with a pending transaction over bridge attestation <hash>. - Voll.
mempool full. Die Kapazitätsprüfung steht von den dreien an letzter Stelle. Am Byte-Limit darf ein Neuankömmling den Platz gepoolter Einträge einnehmen, die strikt unter ihm rangieren – nach Gebühr oberhalb der jeweils aktuellen Untergrenze pro KiB, dann nach Gebühr –, wobei die günstigsten zuerst verdrängt werden, bis er passt; reicht das nicht aus oder gleicht er ihnen nur, wird er selbst abgelehnt. Die Governance-Aktionen liegen außerhalb beider Limits und werden nie verdrängt: die vier Bridge-Governance-AktionenPauseMints,UnpauseMints,RegisterBridgedToken,ListBackingsowie seit v0.6.8RotatePqGuardians,RotatePauseKey, deren…V2-Formen,CancelRotation,AdmitValidatorundSlashEquivocation. Ihre Bytes und Einträge zählen dennoch zu den Limits.PauseMintsist die Notbremse bei einer Kompromittierung des Guardian-Schlüssels: ohne Bundle, ohne Gebühr, und die eine Transaktion, die in einen Block gelangen muss, während ein Angreifer mintet; ein voller Pool darf sie daher nicht aussperren. Die Ausnahme wird durch den Konfliktschritt begrenzt:pause_nonceundlist_noncelassen je eine gepoolte Transaktion gleichzeitig zu, sodass die vier Bridge-Aktionen höchstens zwei Einträge über das Limit hinaus hinzufügen können; die übrigen verbrauchen jeweils eine eigene Nonce oder ein eigenes Quorum. - Die günstige Hälfte von
Ledger::validate. Alles, was zwischen Admission und Aufnahme veralten kann und keinen Proof kostet: Der Anchor ist eine der letzten 1,024 Roots auf Chain 20 (Genesisproof_window_blocks; ohne sie 256),timeliegt im Fenster, die Nullifier sind unverbraucht, die Commitments nicht vorhanden, der Digest einer Attestation ist unbeansprucht, die Register-Nonce einer Staking-Aktion ist aktuell, diemint_nonceoder der Registrierungsindex einer Token-Aktion ist der, bei dem die Registry steht (wrong mint nonce,wrong token index, sobald die Chain darüber hinausgegangen ist), die Nonce einer Pause oder einer Listung ist die aktuelle der Bridge. Auch die Commitments, die eine Transaktion erzeugt, werden hier berechnet, einschließlich der Note, die das Ledger selbst für einWithdraw, eineBridgeAttestoder eine Aggregator-Auszahlung ableitet, die nicht auf der Leitung liegt, aber ebenfalls nicht kollidieren darf. Größenlimits werden geprüft, bevor irgendetwas dekodiert wird: Ein Proof über demmax_proof_bytesder Chain (standardmäßig 2 MiB, auf Chain 20 4 MiB), eine Attestation über 16 KiB oder eine Transaktion, die größer ist als ein Block (max_block_bytes: standardmäßig 4 MiB, auf Chain 20 20 MiB), wird sofort abgelehnt, sodass ein überdimensionierter Blob keine Arbeit erkauft. Ein Bundle-Proof in der versiegelten (gekürzten) Markerform wird im selben Schritt abgelehnt, außer bei einer Sealed-Form-Synchronisation, alsPrunedFormOutsideSync. Dann liest der Pool, weiterhin ohne etwas zu verifizieren, die Proof-Header. Drei Richtlinien kommen zuerst: Ein Call, dessen Input-, keccak- oder sha256-Tabelle weniger als 2^7 Zeilen hat, wird nicht gepoolt (CallRevealsPrivateInputs), ebenso wenig ein Deploy, den kein Call halten könnte (ProgramUncallable), und keine Transaktion, deren Bundle- oder Call-Proof ein Header-Feld trägt, das der ehrliche Prover nicht schreiben würde (NonCanonicalProof). Die letzten beiden sind auf Chain 20 unterhardening_v6auch Ledger-Regeln. Seit v0.6.8 wird ein Header, dessen deklarierter Grad über der Obergrenze der höchsten Tabelle liegt, alsNonCanonicalProofabgelehnt, bevor die Prüfung ihn durchläuft (Audit v6, ZKV-4), und eine Panik irgendwo in diesem Vorab-Screening ist eine Ablehnung dieser Transaktion (VerifierPanicked), kein toter Knoten. Danach die Gebührenuntergrenze: Auf Chain 20 dekodiert der Pool das Gas-Limit, das ein Call- oder Invoke-Proof deklariert, und lehnt eine Gebühr unterBUNDLE_BASE + gas_price × limit + byte_price × ⌈bytes/1024⌉zu den Preisen der Spitze ab, zuzüglich der Cell-Gebühr eines Invoke, alsFeeTooLow(Gebühren). Das ist die eigene Regel des Blocks, der Pool verlangt also genau das, was ein Block verlangt. - Die teure Hälfte, außerhalb der Konsensschleife. Die Transaktion wird eingereiht, höchstens 64 tief, für einen von vier Verifikations-Workern. Ein Worker führt den Rest von
Ledger::validateauf einem blockierenden Task aus: den Bundle-Digest, das Gas-Limit, das der Bundle-Proof deklariert (auf Chain 20 genau 20,479, sonstBundleGasLimit), den Bundle-Proof und, seit Chain 17, seinen Autorisierungs-Proof gegen das Binding der Transaktion, und bei einem Call dessen eigenen Proof sowie seine Gebührenuntergrenze. Die Validierung erfolgt gegen einen Snapshot der Spitze, der lazy geklont wird, höchstens einmal pro Spitzenwechsel und nur, solange etwas wartet. Eine per Gossip empfangene Transaktion, die die Warteschlange voll vorfindet, wird verworfen, weder verifiziert noch weitergeleitet, statt die Schleife zu blockieren, die abstimmt; die nächste Gossip-Runde des Peers bringt sie erneut mit. - Erneute Prüfung und Aufnahme in den Pool.
insert_verifiedführt Schritt 4 erneut gegen die Spitze aus, auf der die Transaktion tatsächlich gepoolt wird, weil sich die Spitze während der Proof-Verifikation bewegt haben kann, nimmt sie dann auf und leitet sie bei Gossip-Verkehr weiter.
Die Schritte 1 bis 4 sind Mempool::precheck; Schritt 5 ist der einzige, der einen STARK verifiziert. Weil diese Kosten auf einem Worker anfallen, belegt eine Flut ungültiger Proofs vier Threads und nie die Konsensschleife. Transaktionen und Aktionen listet die eigene Admission-Reihenfolge des Ledgers auf, die die Schritte 4 und 5 zusammen ausführen.
Zwei Schutzmechanismen und ein Cache für Gossip
Vor der Admission greifen zwei Dinge nur bei Transaktionen, die von Peers eintreffen; RPC-Einreichungen überspringen sie, weil dieser Port dem Operator selbst gehört und bereits durch das Request-Body-Limit begrenzt ist. Der einzige RPC-Pfad, der gedrosselt wird, ist der Faucet, weiter unten.
- Ein Byte-Zähler pro Peer. Seit v0.6.8 wird jeder Gossip-Frame dem Byte-Budget seines Weiterleiters belastet, noch bevor er dekodiert wird: ein Burst von acht und eine Auffüllung von zwei Frames der maximalen Übertragungsgröße pro Sekunde. Darüber hinaus wird der Frame ungelesen verworfen (Audit v6, GOSSIP-1). Die Transaktions-ID, die den gesamten Beweis hasht, wird nur innerhalb des Kontingents des Weiterleiters berechnet.
- Ein Transaktionszähler pro Peer. Ein Token-Bucket pro weiterleitendem Peer, Burst 16, Auffüllung mit 4 pro Sekunde (
PEER_TX_BURST,PEER_TX_PER_SEC). Er bemisst den Peer, der die Transaktion weitergeleitet hat, nicht ihren Urheber, den eine abgeschirmte Chain nicht benennen kann. - Ein Urteils-Cache. Eine Transaktion, die dieser Node bereits aus einem Grund abgelehnt hat, der allein eine Funktion ihrer Bytes ist, etwa ein ungültiger Beweis, eine ungültige Signatur oder ein übergroßer Teil, wird aus einem FIFO-Cache mit 8.192 Einträgen (
REFUSED_CACHE_ENTRIES) erneut abgelehnt, statt ein zweites Mal verifiziert zu werden. Ein Urteil, das etwas über den Zustand dieses Nodes aussagt, etwa ein unbekannter Anker oder ein bereits ausgegebener Nullifier, wird nie zwischengespeichert: Ein Node, der einen Block zurückliegt, würde sich sonst gegen Transaktionen vergiften, die gleich gültig werden. Die Liste ist eine Positivliste, ein neuer Fehler gilt also als Zustand, bis jemand anders entscheidet. Diese Einträge zeigen, wo die Grenze verläuft:BadPauseSignature(the pause signature does not verify) ist seit v0.6.8 nicht dauerhaft (Audit v6, BRG-18). Der Fehler prüft die Signatur gegen den aktuellen Pause-Schlüssel der Chain, den die Bridge-Regeln v2 rotieren können, er ist also Zustand: Ein Node, der eine Rotation lang zurückliegt, würde sonst diePauseMintsdes neuen Inhabers bis zum Neustart ablehnen. Ein PQ-Kosignatur-Index außerhalb des Bereichs hat den Cache aus demselben Grund verlassen. Bis v0.6.7 wurden auf den Chains 18 und 19 beide als dauerhaft zwischengespeichert. Eine wiederholte ungültige Pause kostet nun pro Zustellung eine Dilithium2-Verifikation, begrenzt durch den Zähler pro Peer.PrunedFormOutsideSyncist bewusst nicht dauerhaft. Die Markerform eines versiegelten Bundles hasht auf die eigene ID der Roh-Transaktion, und der Cache schlüsselt nach dieser ID. Würde die Ablehnung zwischengespeichert, könnte jeder Gossip-Peer die ehrliche Transaktion netzwerkweit zensieren.- Das deklarierte Gas-Limit eines Beweises sind seine eigenen Bytes gegen eine Genesis-Konstante, daher sind
BundleGasLimitundAuthGasLimitdauerhaft.FeeTooLowist es nicht: Auf Chain 20 bewegt sich die Untergrenze mit den Preisen. Die drei Header-Richtlinien aus Schritt 4 werden ebenfalls nie zwischengespeichert: Jede ist das Wissen dieses Builds, das ein späterer Build verschieben kann. - Auf einer Chain mit Faucet-Positivliste (Chain 20) ist
FaucetRecipientNotAlloweddauerhaft: Der Fehler vergleicht den eigenen Empfänger des Mints mit einer Genesis-Konstante.FaucetBudgetExhaustedist es nicht: Er ist Zustand, und derselbe Mint wird in der nächsten Epoche wieder zugelassen. - Das
BadSignatureeiner Token-Aktion ist ebenfalls nicht dauerhaft, anders als bei einer Staking-Aktion: Die Mint-Berechtigung eines Tokens ist Zustand, einSetAuthoritykann sie an einen anderen Schlüssel übergeben, und dieselben Bytes können vor dem Commit dieser Rotation abgelehnt und danach akzeptiert werden. Die Token-Ablehnungen auf Byte-Ebene (ein ungültiger Name oder Ticker, zu viele Dezimalstellen, ein Betrag von null, Token-Index 0) werden zwischengespeichert, ebenso ein Burn-Feld, das die Aktion nicht setzen darf, ein Mint, dessen Commitment sich nicht zu seinem Betrag öffnen lässt, und eine Bridge-Einzahlung, deren Blinding nicht das abgeleitete ist.
Das Faucet-Limit
rand_mint ist gebührenfrei und kostet pro Aufruf eine Transaktion im Pool, und auf Chain 20 ist der Faucet aktiv, mit einer Bridge, die Werte dahinter hält. Der Genesis begrenzt, was er kann: Ein Mint zahlt nur an die 16 zugelassenen Wallets und höchstens 10.000 RAND pro Epoche von 1.000 Blöcken (Staking), und nur einer der 18 Schlüssel auf der Genesis-Liste staking.faucet_minters darf einen signieren (MinterNotAllowed, nie zwischengespeichert: seit Chain 16 eine Ledger-Regel, auf Chain 15, wo die Liste die Genesis-Validatoren waren, eine Node-Richtlinie). Darüber hinaus bemisst jeder Node seinen eigenen Faucet: acht Mints in Folge, Auffüllung mit einem pro Sekunde (FAUCET_MINT_BURST, FAUCET_MINT_PER_SEC). Darüber hinaus antwortet der Aufruf mit faucet is rate limited on this node (8 mints back to back, refilling at 1/s); try again shortly. Das Limit gilt pro Node-Prozess, nicht pro Aufrufer, weil der RPC-Port keine Peer-Identität zum Bemessen hat und die geschützte Ressource der Pool dieses Nodes ist. Zusammen mit der oben beschriebenen Kapazitätsausnahme kann eine Faucet-Flut den Pool nicht vor einem PauseMints füllen.
Gossipsub läuft mit Validierung auf Anwendungsebene, eine Transaktion wird also erst an andere Peers weitergeleitet, nachdem sie hier verifiziert wurde. Ein Peer, der ungültige Transaktionen flutet, wird durch den Zähler gedrosselt, aus dem Cache beantwortet und kostet die Konsensschleife nichts.
Einen Block bauen
Wenn ein Validator eine View anführt, fragt er den Pool nach Kandidaten. candidates_within behält nur Transaktionen, die auf das Ledger, auf dem er aufbaut, noch anwendbar sind, indem es die günstige Hälfte der Zulassung erneut ausführt: Anker noch im Fenster, time noch darin, Nullifier noch nicht ausgegeben, Commitments noch nicht vorhanden. Es verwirft außerdem eine Token-Aktion, deren mint_nonce oder Registrierungsindex die Chain überschritten hat, sowie eine Pause oder ein Listing mit veralteter Nonce. Seit v0.6.8 bepreist es jeden Call und Invoke neu, zu den Preisen, die auf dem Tip gelten, auf dem es aufbaut, aus dem bei der Zulassung gemerkten Gas-Limit, und lässt eine Transaktion aus, deren Gebühr nun unter ihrer Untergrenze liegt; sie bleibt im Pool und wird wieder angeboten, sobald die Preise fallen (Audit v6, CH-9). Auf Chain 20 ordnet es den Rest dann zuerst nach Governance, dann nach der Gebühr über der aktuellen Untergrenze pro KiB Transaktion, dann nach Gebühr, höchste zuerst, Gleichstände nach Hash aufgelöst, sodass jeder ehrliche Proposer, der auf demselben Pool aufbaut, denselben Block wählt, und füllt bis zum Budget von 2.000 Transaktionen und dem Block-Limit der Chain (MAX_BLOCK_TXS; max_block_bytes, standardmäßig 4 MiB und auf Chain 20 20 MiB). Bytes sind das, woran es einem Block mangelt, weshalb der Überschuss pro KiB genommen wird. Eine Chain ohne gas-Abschnitt und ein Node ohne Gas-Richtlinie überspringen diesen Schlüssel und ordnen nach Governance, Gebühr und Hash, wie es Chain 15 tat. Governance zuerst ist die andere Hälfte der Garantie der Pause: Ein PauseMints zahlt konstruktionsbedingt keine Gebühr, da es von einer Wallet ohne RAND funktionieren muss, sodass eine reine Gebührenreihenfolge die Notbremse hinter jede zahlende Transaktion sortieren würde.
Eine weitere Regel gilt, solange ein Call oder ein Invoke bereit ist (Audit v6, POOL-2; Node-Richtlinie auf jeder Chain, keine Gültigkeitsregel). Nur diese zahlen den Byte-Preis, und eine Überweisung, die ein wenig über ihrer pauschalen Basis zahlt, übertrifft sie pro KiB, sodass ein Strom von Überweisungen einen bereiten Call aus jedem Block fernhalten könnte. Transaktionen mit Pauschalgebühr nehmen daher in einem ersten Durchgang höchstens drei Viertel der Block-Bytes ein; sobald die Calls platziert sind, wird der verbleibende Platz mit den zurückgehaltenen Überweisungen in ihrer Reihenfolge gefüllt. Es geht kein Blockplatz verloren, und ist kein Call bereit, ändert sich nichts (Fees).
Eine Transaktion, die für den verbleibenden Platz zu groß ist, wird übersprungen, nicht das Blockende erklärt: Eine kleinere dahinter wird weiterhin befördert. Eine Überweisung auf Chain 18 umfasst etwa 2,85 MB, ein Bundle-Beweis von 1,49 MB und ein Autorisierungsbeweis von 1,36 MB, sodass ihr Block von 20 MiB Platz für etwa sieben bietet und ein Standardblock von 4 MiB für eine. Auf Chain 15 fasste dieselbe Größe von 20 MiB bei etwa 1,43 MB pro Überweisung vierzehn. Unter Last entscheidet die Gebühr, welche es sind (Fees).
Pruning
Nach jedem Commit entfernt prune aus dem Pool genau das, was candidates_within übersprungen hätte: Transaktionen, deren Note von jemand anderem ausgegeben wurde, deren Commitment nun existiert, deren Nonce oder Registrierungsindex die Chain überschritten hat oder deren Anker oder time aus dem Fenster gerutscht ist (1.024 Blöcke auf Chain 20, 256 bis Chain 19). Ein Bundle, das ein Double-Spend-Rennen verliert, verschwindet daher von selbst, ohne jemals einen Fehler an irgendjemanden zu erzeugen. Die Annahme durch rand_sendTransaction ist kein Commit: Fragen Sie rand_getTransaction ab, bis es einen Block zurückgibt, und bauen Sie das Bundle auf einem frischen Anker neu auf und senden Sie es erneut, wenn das Anker-Fenster ohne einen solchen verstreicht.
Beobachtung
rand_status meldet mempool_size, verify_queue (Transaktionen, die auf einen Worker warten) und refused_cache (Einträge im Urteils-Cache). Eine verify_queue, die bei 64 stehen bleibt, bedeutet, dass Beweise schneller eintreffen, als vier Worker sie verifizieren, etwa 16 ms je Beweis mit warmem Schlüssel; ein mempool_size, der wächst, während die Blöcke voll bleiben, bedeutet, dass die Gebühr, nicht die Ankunftsreihenfolge, über die Aufnahme entscheidet.
Was noch fehlt
- Kein Ersetzen. Die Gas- und Byte-Preise von Chain 20 verschieben die Untergrenze eines Aufrufs mit jedem Block bis zu ihren Obergrenzen; die Untergrenze einer Überweisung ist fest. Eine im Pool liegende Transaktion wird nicht durch eine Transaktion mit höherer Gebühr über dieselben Notes ersetzt, was die Konfliktregel verweigert. Ein Aufruf, der preislich überholt wurde, wartet im Pool, ohne angeboten zu werden, bis die Preise fallen oder sein Anchor-Fenster abläuft. Unter v0.6.7 (Chains 18 und 19) behielt ein im Pool liegender Aufruf die Untergrenze, zu der er aufgenommen wurde, wurde nach einem Preisanstieg weiterhin angeboten und scheiterte im Probelauf des Proposers als
FeeTooLow. Das Block-Space-Dokument des Fullnode nennt den nach Gebühr geordneten Mempool als den Schritt vor einem größeren Block und die Aggregation auf Blockebene als das, was die Obergrenze pro Block anhebt. - Kein Ablauf nach Alter. Eine im Pool liegende Transaktion verlässt ihn nur durch Aufnahme in einen Block, durch Verdrängung an der Byte-Obergrenze oder durch die Anchor- und Zeitfenster: 1.024 Blöcke auf Chain 20, etwa 23 Minuten bei den 1,34 s pro Block, die der Explorer dort meldete. Bis Chain 19 betrugen die Fenster 256 Blöcke, etwa fünf Minuten.
- Kein Mempool-RPC. Es gibt keine Methode, die ausstehende Transaktionen auflistet;
mempool_sizeinrand_statussowie Anzahl, Bytes und ältestes Alter inrand_getMempoolInfosind die einzigen Einblicke, und der Explorer indexiert nur bestätigte Blöcke.