Ta strona wyjaśnia, co dzieje się z transakcją po tym, jak rand_sendTransaction ją przyjmie, a zanim zawrze ją blok. Dowiesz się z niej, dlaczego transakcja mogła zostać odrzucona, dlaczego może zniknąć z puli, choć nigdy nie zakończyła się błędem, w jakiej kolejności wypełniany jest blok oraz co powódź błędnych dowodów może, a czego nie może zrobić z walidatorem. Mempool znajduje się w crates/randprotocol-node/src/mempool.rs; zabezpieczenia przed nim to admission.rs.
Brak nadawców, więc brak nonce’ów
W łańcuchu opartym na kontach mempool porządkuje transakcje każdego nadawcy według nonce. W łańcuchu shielded nie ma nadawców: pakiet nie zawiera adresu ani podpisu, a dwa pakiety są ze sobą powiązane tylko wtedy, gdy dotyczą tej samej noty. Nie zostaje więc żadne porządkowanie według nadawcy. Miejsce księgowania nonce’ów zajmuje śledzenie konfliktów: pula indeksuje nullifiery i zobowiązania każdej oczekującej transakcji i odrzuca nowo przybyłą, która zgłasza do któregoś z nich roszczenie. To samo dotyczy kilku liczników, dla których księga przyjmuje dokładnie jedną transakcję na wartość: nonce rejestru walidatora (Unbond lub Withdraw), mint_nonce tokenu (wspólny dla TokenMint i SetAuthority), indeks rejestru, do którego przypisana jest RegisterToken, oraz pause_nonce i list_nonce mostu. Każdy z nich jest miejscem, które może zająć co najwyżej jedna transakcja z puli. Z dwóch takich transakcji do bloku mogłaby trafić najwyżej jedna, a utrzymywanie drugiej kosztowałoby proposera miejsce w bloku, a każdy węzeł weryfikację dowodu w każdej rundzie gossipu.
Pula mieści co najwyżej 10 000 transakcji (Mempool::new(10_000) w node.rs) i, od v0.6.8, co najwyżej bajty o objętości ośmiu bloków: MAX_POOL_BLOCKS = 8 razy max_block_bytes łańcucha, czyli 160 MiB w łańcuchu 20 (audyt v6, CH-7). Wcześniej ograniczona była tylko liczba, podczas gdy jedna transakcja z dowodem zajmuje od około 1,3 MB do ponad 12 MB. Każda transakcja jest walidowana względem księgi na czubku drzewa konsensusu, a nie w ostatnim zatwierdzonym bloku: czyli względem stanu, na którym proposer faktycznie budowałby blok.
Przyjęcie: najpierw tanie, potem drogie
Transakcja nadchodząca przez RPC lub gossip przechodzi kolejno przez poniższe kroki, a pierwsze odrzucenie jest błędem, który widzisz:
- Duplikat. Hasz jest już w puli:
already in mempool. - Konflikt. Nullifier, zobowiązanie albo jedno z opisanych wyżej miejsc nonce i indeksu jest już zajęte przez oczekującą transakcję:
conflicts with a pending transaction over <hex>. Dla miejsca tokenu hex jest kluczem wyświetlanym: słowem-znacznikiem (tokndlamint_nonce,tidxdla indeksu rejestracji), a następnie indeksem i nonce. Poświadczenie mostu, o które ścigało się dwóch relayerów, jest rozróżniane po swoim skrócie:conflicts with a pending transaction over bridge attestation <hash>. - Pełna pula.
mempool full. Sprawdzenie pojemności jest ostatnie z trzech. Przy limicie bajtów nowo przybyła transakcja może zająć miejsce wpisów z puli, które plasują się ściśle poniżej niej — według opłaty ponad bieżący próg każdego wpisu na KiB, a następnie według opłaty — wypierając najpierw najtańsze, aż się zmieści; jeśli to nie wystarczy albo tylko remisuje, odrzucona zostaje ona. Akcje zarządcze są poza oboma limitami i nigdy nie są wypierane: cztery akcje zarządcze mostuPauseMints,UnpauseMints,RegisterBridgedToken,ListBackingoraz, od v0.6.8,RotatePqGuardians,RotatePauseKey, ich formy…V2,CancelRotation,AdmitValidatoriSlashEquivocation. Ich bajty i wpisy nadal wliczają się do limitów.PauseMintsjest hamulcem na wypadek kompromitacji klucza strażników: bez pakietu, bez opłaty i jedyna transakcja, która musi trafić do bloku, gdy atakujący wytwarza tokeny, więc pełna pula nie może jej zatrzymać. Wyjątek ogranicza krok konfliktu:pause_nonceilist_noncedopuszczają po jednej transakcji w puli naraz, więc cztery akcje mostu mogą dodać co najwyżej dwa wpisy ponad limit; pozostałe zużywają każda własny nonce lub własne kworum. - Tania połowa
Ledger::validate. Wszystko, co może się zestarzeć między przyjęciem a włączeniem do bloku i nie wymaga dowodu: kotwica jest jednym z ostatnich 1024 korzeni w łańcuchu 20 (proof_window_blocksw genezie; 256 bez niego),timemieści się w oknie, nullifiery są niewydane, zobowiązania nieobecne, skrót poświadczenia nie jest zajęty, nonce rejestru akcji stakingowej jest aktualny,mint_noncelub indeks rejestracji akcji tokenu to ten, na którym stoi rejestr (wrong mint nonce,wrong token index, gdy łańcuch już go minął), a nonce wstrzymania lub listingu jest bieżącym nonce mostu. Obliczane są tu także zobowiązania, które transakcja tworzy, w tym nota, którą księga sama wyprowadza dlaWithdraw,BridgeAttestlub wypłaty agregatora, której nie ma w przesyle, ale która również nie może kolidować. Limity rozmiaru sprawdzane są przed jakimkolwiek dekodowaniem: dowód ponadmax_proof_bytesłańcucha (domyślnie 2 MiB, 4 MiB w łańcuchu 20), poświadczenie ponad 16 KiB albo transakcja większa niż blok (max_block_bytes: domyślnie 4 MiB, 20 MiB w łańcuchu 20) są odrzucane od razu, więc przerośnięty blob nie kupuje żadnej pracy. Dowód pakietu w zapieczętowanej (przyciętej) postaci znacznikowej jest odrzucany w tym samym kroku poza synchronizacją w postaci zapieczętowanej, jakoPrunedFormOutsideSync. Następnie, wciąż niczego nie weryfikując, pula odczytuje nagłówki dowodów. Najpierw obowiązują trzy zasady: wywołanie, którego tabela wejścia, keccak lub sha256 ma mniej niż 2^7 wierszy, nie trafia do puli (CallRevealsPrivateInputs), podobnie jak wdrożenie, którego żadne wywołanie nie mogłoby objąć (ProgramUncallable), ani transakcja, której dowód pakietu lub wywołania ma pole nagłówka, którego uczciwy dowodzący by nie zapisał (NonCanonicalProof). Dwie ostatnie są w łańcuchu 20 także regułami księgi, w ramachhardening_v6. Od v0.6.8 nagłówek, którego zadeklarowany stopień przekracza pułap najwyższej tabeli, jest odrzucany jakoNonCanonicalProof, zanim sprawdzenie go przejdzie (audyt v6, ZKV-4), a panika w dowolnym miejscu tego wstępnego przesiewu oznacza odrzucenie tej transakcji (VerifierPanicked), a nie martwy węzeł. Potem próg opłaty: w łańcuchu 20 pula dekoduje limit gazu zadeklarowany przez dowód wywołania lub invoke i odrzuca opłatę niższą niżBUNDLE_BASE + gas_price × limit + byte_price × ⌈bytes/1024⌉przy cenach z czubka, plus opłatę za komórki w przypadku invoke, jakoFeeTooLow(Opłaty). To jest reguła samego bloku, więc pula żąda dokładnie tego, czego żąda blok. - Droga połowa, poza pętlą konsensusu. Transakcja trafia do kolejki, co najwyżej 64 w głąb, do jednego z czterech workerów weryfikacji. Worker wykonuje resztę
Ledger::validatew zadaniu blokującym: skrót pakietu, limit gazu zadeklarowany przez dowód pakietu (w łańcuchu 20 dokładnie 20 479, w przeciwnym razieBundleGasLimit), dowód pakietu i, od łańcucha 17, jego dowód autoryzacji względem wiązania transakcji, a dla wywołania jego własny dowód i próg opłaty. Waliduje względem migawki czubka, klonowanej leniwie, co najwyżej raz na zmianę czubka i tylko wtedy, gdy coś czeka. Transakcja z gossipu, która zastaje pełną kolejkę, jest porzucana, ani niezweryfikowana, ani nieprzekazana dalej, zamiast blokować pętlę, która głosuje; następna runda gossipu u peera niesie ją ponownie. - Ponowne sprawdzenie i wprowadzenie do puli.
insert_verifiedponownie wykonuje krok 4 względem czubka, na którym transakcja faktycznie trafia do puli, bo czubek mógł się przesunąć w trakcie weryfikacji dowodu, po czym ją przyjmuje i, jeśli pochodzi z gossipu, przekazuje dalej.
Kroki od 1 do 4 to Mempool::precheck; krok 5 jest jedynym, który weryfikuje STARK. Ponieważ ten koszt ponosi worker, powódź błędnych dowodów zajmuje cztery wątki i nigdy pętli konsensusu. Transakcje i akcje podają własną kolejność przyjmowania w księdze, którą kroki 4 i 5 razem wykonują.
Dwie zapory i pamięć podręczna dla gossipu
Przed przyjęciem działają dwie rzeczy, które dotyczą wyłącznie transakcji nadchodzących od peerów; zgłoszenia przez RPC je pomijają, ponieważ ten port należy do samego operatora i jest już ograniczony limitem rozmiaru treści żądania. Jedyną ścieżką RPC, która jest mierzona, jest faucet, opisany niżej.
- Miernik bajtów na peera. Od v0.6.8 każda ramka gossip jest księgowana na budżet bajtów przekazującego jeszcze przed zdekodowaniem: serię ośmiu ramek i uzupełnianie dwóch ramek o maksymalnym rozmiarze transmisji na sekundę. Ramka ponad ten limit jest odrzucana niezdekodowana (audyt v6, GOSSIP-1). Identyfikator transakcji, który jest skrótem całego dowodu, jest liczony tylko w granicach limitu przekazującego.
- Miernik transakcji na peera. Wiadro z tokenami (token bucket) na każdego przekazującego peera: seria 16, uzupełnianie 4 na sekundę (
PEER_TX_BURST,PEER_TX_PER_SEC). Mierzy peera, który przekazał transakcję, a nie jej autora, którego łańcuch osłonięty (shielded) nie potrafi wskazać. - Pamięć podręczna werdyktów. Transakcja, którą ten węzeł już odrzucił z powodu zależnego wyłącznie od jej bajtów, takiego jak błędny dowód, błędny podpis czy zbyt duża część, jest odrzucana ponownie z pamięci podręcznej FIFO na 8 192 wpisy (
REFUSED_CACHE_ENTRIES), zamiast być weryfikowana drugi raz. Werdykt, który mówi coś o stanie tego węzła, jak nieznany anchor czy wydany nullifier, nigdy nie trafia do pamięci podręcznej: węzeł spóźniony o jeden blok zatruwałby się w ten sposób przeciw transakcjom, które za chwilę staną się ważne. Lista działa jak lista dozwolonych, więc nowy błąd jest stanem, dopóki ktoś nie zdecyduje inaczej. Te wpisy pokazują, którędy biegnie granica:BadPauseSignature(the pause signature does not verify) od v0.6.8 nie jest trwały (audyt v6, BRG-18). Sprawdza podpis względem bieżącego klucza pauzy łańcucha, który reguły mostu v2 mogą rotować, więc jest stanem: węzeł spóźniony o jeden blok względem rotacji odrzucałbyPauseMintsnowego posiadacza aż do restartu. Z tego samego powodu z pamięci podręcznej wypadł indeks współpodpisu PQ spoza zakresu. Do v0.6.7, w łańcuchach 18 i 19, oba były buforowane jako trwałe. Powtórzona błędna pauza kosztuje teraz jedną weryfikację Dilithium2 na każde dostarczenie, ograniczoną miernikiem na peera.PrunedFormOutsideSynccelowo nie jest trwały. Forma znacznikowa zapieczętowanego pakietu ma skrót równy identyfikatorowi surowej transakcji, a pamięć podręczna indeksuje po tym identyfikatorze, więc zbuforowanie odrzucenia pozwoliłoby dowolnemu peerowi gossip ocenzurować uczciwą transakcję w całej sieci.- Zadeklarowany limit gazu dowodu to jego własne bajty zestawione ze stałą genesis, więc
BundleGasLimitiAuthGasLimitsą trwałe.FeeTooLownie jest: w łańcuchu 20 dolny próg zmienia się razem z cenami. Trzy polityki nagłówka z kroku 4 również nigdy nie są buforowane: każda z nich jest wiedzą tej wersji, którą późniejsza wersja może przesunąć. - W łańcuchu z listą dozwolonych faucetu (łańcuch 20)
FaucetRecipientNotAllowedjest trwały: porównuje własnego odbiorcę mintu ze stałą genesis.FaucetBudgetExhaustednie jest: to stan, a ten sam mint zostanie przyjęty ponownie w następnej epoce. BadSignatureakcji tokena również nie jest trwały, inaczej niż w akcji stakingowej: uprawnienie do mintowania tokena jest stanem,SetAuthoritymoże przekazać je innemu kluczowi, a te same bajty mogą zostać odrzucone przed zatwierdzeniem tej rotacji i przyjęte po nim. Odrzucenia tokenów na poziomie bajtów (błędna nazwa lub symbol, zbyt wiele miejsc dziesiętnych, kwota zero, indeks tokena 0) są buforowane; tak samo pole burn, którego akcja nie może ustawiać, mint, którego zobowiązanie nie otwiera się do jego kwoty, oraz depozyt mostu, którego blinding nie jest wyprowadzonym.
Limit faucetu
rand_mint jest wolny od opłat i kosztuje jedną transakcję w puli na wywołanie, a w łańcuchu 20 faucet jest włączony i stoi za nim most przechowujący wartość. Genesis ogranicza, co może zrobić: mint płaci tylko 16 portfelom z listy dozwolonych i co najwyżej 10 000 RAND na epokę liczącą 1 000 bloków (Staking), a podpisać go może tylko jeden z 18 kluczy z listy staking.faucet_minters w genesis (MinterNotAllowed, nigdy niebuforowany: reguła księgi od łańcucha 16, polityka węzła w łańcuchu 15, gdzie listą byli walidatorzy genesis). Na wierzchu każdy węzeł mierzy własny faucet: osiem mintów jeden po drugim, uzupełnianie po jednym na sekundę (FAUCET_MINT_BURST, FAUCET_MINT_PER_SEC). Ponad ten limit wywołanie odpowiada faucet is rate limited on this node (8 mints back to back, refilling at 1/s); try again shortly. Limit dotyczy procesu węzła, nie wywołującego, ponieważ port RPC nie ma tożsamości peera, którą dałoby się mierzyć, a chronionym zasobem jest pula tego węzła. Razem ze zwolnieniem z limitu pojemności opisanym wyżej powódź żądań do faucetu nie może zapełnić puli przed PauseMints.
Gossipsub działa z walidacją na poziomie aplikacji, więc transakcja jest przekazywana innym peerom dopiero po tym, jak przeszła tu weryfikację. Peer zalewający niepoprawnymi transakcjami jest dławiony przez miernik, obsługiwany z pamięci podręcznej i nie obciąża pętli konsensusu.
Budowanie bloku
Gdy walidator prowadzi widok (view), prosi pulę o kandydatów. candidates_within zachowuje tylko transakcje, które nadal mają zastosowanie do księgi, na której buduje, ponownie wykonując tańszą połowę przyjęcia: anchor nadal w oknie, time nadal w nim, nullifiery jeszcze niewydane, zobowiązania jeszcze nieobecne. Odrzuca też akcję tokena, której mint_nonce lub indeks rejestracji łańcuch już minął, oraz pauzę lub notowanie z nieaktualnym nonce. Od v0.6.8 wycenia ponownie każde wywołanie (call) i invoke według cen obowiązujących na czubku, na którym buduje, na podstawie limitu gazu zapamiętanego przy przyjęciu, i pomija transakcję, której opłata jest teraz poniżej progu; pozostaje ona w puli i jest oferowana ponownie, gdy ceny spadną (audyt v6, CH-9). W łańcuchu 20 porządkuje następnie resztę najpierw według governance, potem według opłaty ponad bieżący próg, na KiB transakcji, potem według opłaty, od najwyższej, remisy rozstrzygane skrótem, tak aby każdy uczciwy proposer budujący na tej samej puli wybrał ten sam blok, i wypełnia go do budżetu 2 000 transakcji i limitu bloku łańcucha (MAX_BLOCK_TXS; max_block_bytes, domyślnie 4 MiB, a w łańcuchu 20 20 MiB). Blokowi brakuje bajtów, dlatego nadwyżkę liczy się na KiB. Łańcuch bez sekcji gas i węzeł bez polityki gazu pomijają ten klucz i porządkują według governance, opłaty i skrótu, tak jak łańcuch 15. Governance na początku to druga połowa gwarancji pauzy: PauseMints z założenia nie płaci opłaty, bo musi działać z portfela bez RAND, więc zwykła kolejność według opłaty ustawiłaby hamulec bezpieczeństwa za każdą transakcją, która płaci.
Jedna reguła więcej działa, gdy gotowe jest wywołanie lub invoke (audyt v6, POOL-2; polityka węzła w każdym łańcuchu, nie reguła ważności). Tylko one płacą cenę za bajt, a transfer płacący odrobinę ponad swoją stałą opłatę bazową wyprzedza je na KiB, więc strumień transferów mógłby trzymać gotowe wywołanie poza każdym blokiem. Dlatego transakcje ze stałą opłatą zajmują w pierwszym przebiegu najwyżej trzy czwarte bajtów bloku; po umieszczeniu wywołań pozostałe miejsce wypełniają wstrzymane transfery, w ich kolejności. Żadna przestrzeń bloku nie przepada, a gdy żadne wywołanie nie jest gotowe, nic się nie zmienia (Fees).
Transakcja zbyt duża na pozostałe miejsce jest pomijana, a nie kończy bloku: mniejsza za nią nadal jedzie. Transfer w łańcuchu 18 to około 2,85 MB, czyli dowód pakietu 1,49 MB i dowód autoryzacji 1,36 MB, więc jego blok 20 MiB mieści około siedmiu, a domyślny blok 4 MiB jeden. W łańcuchu 15, przy około 1,43 MB na transfer, te same 20 MiB mieściły czternaście. Pod obciążeniem o tym, które wejdą, decyduje opłata (Fees).
Przycinanie
Po każdym zatwierdzeniu prune usuwa z puli dokładnie to, co candidates_within by pominęło: transakcje, których notę wydał ktoś inny, których zobowiązanie już istnieje, których nonce lub indeks rejestracji łańcuch już minął albo których anchor lub time wypadł z okna (1 024 bloki w łańcuchu 20, 256 do łańcucha 19). Pakiet, który przegrywa wyścig podwójnego wydania, znika więc sam, bez zwracania komukolwiek błędu. Przyjęcie przez rand_sendTransaction nie jest zatwierdzeniem: odpytuj rand_getTransaction, aż zwróci blok, a jeśli okno anchora minie bez niego, zbuduj pakiet od nowa na świeżym anchorze i wyślij ponownie.
Obserwacja
rand_status raportuje mempool_size, verify_queue (transakcje czekające na workera) i refused_cache (wpisy w pamięci podręcznej werdyktów). verify_queue stojące na 64 oznacza, że dowody napływają szybciej, niż cztery workery je weryfikują, około 16 ms na dowód przy ciepłym kluczu; mempool_size, który rośnie, gdy bloki pozostają pełne, oznacza, że o włączeniu decyduje opłata, a nie kolejność przybycia.
Czego jeszcze nie ma
- Brak zastępowania. Ceny gazu i bajtów w chain 20 zmieniają próg wywołania w każdym bloku, aż do ich pułapów; próg transferu jest stały. Transakcja w puli nie może zostać zastąpiona transakcją z wyższą opłatą dotyczącą tych samych noty, czego zabrania reguła konfliktów. Wywołanie, którego cena przekracza ofertę, czeka w puli, nieoferowane, aż ceny spadną lub minie jego okno kotwicy. W wersji v0.6.7 (chains 18 i 19) wywołanie w puli zachowywało próg, przy którym zostało przyjęte, było nadal oferowane po wzroście cen i kończyło się w próbnym zastosowaniu proposera błędem
FeeTooLow. Dokument fullnode o przestrzeni bloku wymienia mempool uporządkowany według opłat jako krok przed większym blokiem, a agregację na poziomie bloku jako to, co podnosi pułap na blok. - Brak wygasania z upływem czasu. Transakcja w puli opuszcza ją tylko przez włączenie do bloku, przez wyparcie przy osiągnięciu limitu bajtów albo przez okna kotwicy i czasu: 1024 bloki w chain 20, około 23 minut przy 1,34 s na blok, jak podawał tam eksplorator. Do chain 19 włącznie okna wynosiły 256 bloków, około pięciu minut.
- Brak RPC mempoola. Nie ma metody wypisującej oczekujące transakcje; jedynymi widokami są
mempool_sizewrand_statusoraz liczba, bajty i najstarszy wiek wrand_getMempoolInfo, a eksplorator indeksuje wyłącznie zatwierdzone bloki.