Ta strona pokazuje, kiedy transakcja jest sfinalizowana, ilu validatorów musi działać oraz co oznaczają pola bloku widoczne w RPC i w eksploratorze. Konsensus to łańcuchowy HotStuff BFT z kworami ważonymi stawką; w chain 20 oznacza to 18 z 26 validatorów.
Reguły, według których działa węzeł
| reguła | implementacja |
|---|---|
| Kworum | certyfikat kworum (QC) to zbiór głosów Dilithium2 od validatorów, którzy posiadają ściśle więcej niż 2/3 całkowitej stawki |
| Harmonogram liderów | czysty round robin: liderem widoku v jest validators[v mod n], a zbiór jest posortowany według adresu. Stawka waży głosy, nie wybór lidera |
| Proponowanie | lider widoku v proponuje blok, gdy ma QC dla v − 1 albo kworum komunikatów NewView dla v. Propozycja, która zastosowałaby nieprawidłową transakcję, odrzuca tę transakcję, a nie cały blok |
| Głosowanie i bezpieczeństwo | validator głosuje raz na widok i tylko na blok bezpieczny względem swojej blokady (jego justify QC jest nowszy niż zablokowany QC albo blok rozszerza zablokowany blok). Stan bezpieczeństwa jest zapisywany przez fsync do RocksDB, zanim głos opuści węzeł |
| Domena podpisu | w genesis z consensus_domain: 1 (chain 20 i każdy łańcuch od 15) każdy podpis głosu, new-view i propozycji obejmuje hash genesis (rand-vote-2, rand-newview-2, rand-block-2), więc podpis złożony dla jednego łańcucha nie weryfikuje się w żadnym innym. Chain 14 i wcześniejsze podpisywały bez niego. binding_domain: 1 w chain 20 rozszerza tę samą regułę na dowody transakcji i podpisane akcje (Transakcje) |
| Propozycje rodzeństwa | replika przechowuje jeden blok na parę (widok, lider); drugi, inny blok tego lidera dla tego widoku jest odrzucany jako equivocation. Propozycja wyprzedzająca widok repliki o więcej niż 8 widoków (PROPOSAL_VIEW_WINDOW) jest odrzucana, zanim sprawdzony zostanie jej podpis; pełne drzewo spekulatywne usuwa bloki spoza gałęzi, zanim cokolwiek odrzuci |
| Zwolnienie blokady | blokada nigdy nie jest obniżana automatycznie (CON-4, v0.6.8). Ustępuje tylko propozycji, której justify QC ma wyższą rangę, albo nadejściu zablokowanego bloku. Kworum podpisanych odpowiedzi NotHeld jest zapisywane w logu dla operatora i niczego nie zwalnia, a validator rejestruje swoje głosy, by nigdy nie podpisać NotHeld dla bloku, na który zagłosował. Każdy certyfikowany blok powyżej głowy jest utrwalany i przywracany po restarcie, więc restart całej floty nie pozostawia QC na bloku, którego nikt nie ma. Validator, którego zablokowany blok został bezpowrotnie utracony, jest zwalniany ręcznie, offline: rand-node safety release-lock |
| Składanie QC | każdy validator składa QC lokalnie z rozgłaszanych głosów, więc łańcuch zatwierdza bloki dalej, nawet gdy następny lider jest offline |
| Zatwierdzanie | łańcuchowa reguła trzech QC: blok zostaje zatwierdzony, gdy trzy certyfikaty łączą się nad nim w kolejnych widokach v, v+1, v+2, a ten łańcuch sięga z powrotem do zatwierdzonej głowy |
| Timeouty i zmiany widoku | bazowy --view-timeout-ms 3 000 ms, podwajany przy każdym kolejnym timeoucie i zerowany po jakimkolwiek postępie. Pacemaker z certyfikatem timeoutu (CH-1, v0.6.8): replika, której zegar wygasa, pozostaje w swoim widoku i podpisuje NewView z prośbą o następny. Wchodzi do widoku po QC dla widoku poprzedzającego albo gdy validatorzy posiadający ponad 2/3 stawki poprosili o ten widok lub późniejszy; dołącza (podpisuje własną prośbę), gdy więcej niż 1/3 prosi o widok wyższy od jej własnego. Żaden pojedynczy NewView validatora nikogo nie przesuwa. Widoki są ograniczone (MAX_VIEW_AHEAD = 10⁶) |
| Tempo | lider proponuje pusty blok, gdy nic nie czeka, nie wcześniej niż --block-interval-ms (domyślnie 1 000 ms) po ostatnim bloku widzianym od dowolnego proposera |
| Limity głosów i gossipu | głosy wyprzedzające o więcej niż 8 widoków są ignorowane, a na parę (widok, głosujący) liczy się jeden głos; gossip konsensusu jest limitowany dla każdego przekazującego peera (burst 256, 64/s); propozycja z nieznanym rodzicem jest przechowywana jako sierota tylko wtedy, gdy jej proposer prowadzi swój widok w zbiorze bieżącej epoki lub następnej, mieści się w granicy drzewa od zatwierdzonej głowy, a jej rozmiar i tx root się zgadzają (pula sierot ograniczona do 64 MiB, a od v0.6.1 do 16 sierot na proposera). Od v0.6.8 propozycja jest przekazywana dalej, a sierota przechowywana, tylko jeśli lista jej transakcji daje hash równy korzeniowi, do którego zobowiązuje się nagłówek, i żadna transakcja się nie powtarza (GOSSIP-2) |
| Limity bloku | max_block_bytes bajtów transakcji w łańcuchu (domyślnie 4 MiB, 20 MiB w genesis chain 20) oraz 2 000 transakcji, egzekwowane przez proposera i ponownie przez każdą replikę w Ledger::apply_block |
| Czas bloku | w łańcuchu z sekcją bridge: timestamp_ms bloku nie może poprzedzać znacznika jego rodzica (TimestampRewind) ani go przekraczać o więcej niż 60 s (TimestampLeap), a validator wstrzymuje głos na blok wyprzedzający jego własny zegar o więcej niż 15 s; zob. niżej |
Reguła zatwierdzania w trzech kolejnych widokach jest kluczowa: jej osłabienie otwiera ponownie błąd sprzecznej finalności, na który fullnode ma test regresyjny. Wszystko, co klient widzi przez RPC, jest zatwierdzone: serwowane są tylko zatwierdzone bloki, a strumień WebSocket newHeads wysyła jedno powiadomienie na zatwierdzony blok, w kolejności.
Tabela opisuje v0.6.8, wydanie, na którym działa chain 20. Trzy z jej reguł są nowe w v0.6.8 (audit v6) i obowiązują od uruchomienia chain 20 w dniu 2026-10-01: pacemaker z certyfikatem timeoutu (CH-1), brak automatycznego zwalniania blokady (CON-4) oraz sprawdzanie listy transakcji w propozycji (GOSSIP-2). W chain 18 i 19 (v0.6.7) pojedynczy podpisany NewView jednego validatora przesuwał każdą replikę do jego widoku, a blokada była obniżana po podpisanych odpowiedziach NotHeld od kworum stawki. Pacemaker i reguła blokady zmieniają to, co validatorzy podpisują, i kiedy, więc v0.6.8 wdrożono przez zatrzymanie i uruchomienie całej floty naraz wraz z chain 20, nigdy węzeł po węźle.
Liczby testnetu
Chain 20 ma 26 validatorów, każdy ze stawką dokładnie 1 000 RAND, wszyscy 26 w jego genesis, na kluczach i identyfikatorach peerów, na których działały chain 17–19. W chain 15 osiemnastu było w genesis, na kluczach chain 14, a ośmiu kolejnych związało stawkę 2026-09-27 w rejestracjach v2 i były aktywne od epoki 43. Stawka jest jednolita, więc kworum to liczba głów: aby cokolwiek zatwierdzić, musi być online 18 z 26. Przy 17 działających widoki postępują, a nic się nie finalizuje. Zbiór n równych validatorów potrzebuje ponad 2/3 stawki: 2 z 2, 3 z 4, 5 z 6, 13 z 18, 18 z 26. Limit wagi w genesis (3 333 bps na validatora) nie ogranicza zbioru równych stawek.
Bloki są tworzone w domyślnym tempie 1 000 ms. W chain 15 pojawiały się w praktyce mniej więcej co 1,2 s (średnio 1,18 s w pierwszych 29 godzinach); w chain 20 eksplorator podał średnią 1,34 s na wysokości 3 270 (2026-10-01, avg_block_time_ms). Eksplorator podaje średni czas bloku, który obserwuje (avg_block_time_ms w GET /stats). Widok validatora wyprzedza wysokość za każdym razem, gdy widoki przekraczają timeout, dlatego rand_getHead zwraca oba.
Epoki
Zbiór validatorów jest wyprowadzany na nowo z rejestru co epoch_blocks = 1 000 bloków (parametr genesis). epoch(h) = h / 1000; zbiór dla epoki e ≥ 1 jest wyprowadzany z rejestru według stanu na ostatnim bloku epoki e − 1: każdy wiersz ze stake ≥ 1,000 RAND, 100 najwyższych według stawki, posortowanych według adresu. W łańcuchu bez sekcji staking związana stawka staje się wagą na następnej granicy. W chain 20 czeka jeszcze dwie epoki oraz na budżet wejść na epokę równy 2 500 bps aktywnej związanej wagi na granicy (6 500 RAND, gdy zbiór liczy 26 000), a związanie stawki rejestrujące nowego validatora jest odrzucane, chyba że zbiór najpierw wybrał ten klucz głosowaniem AdmitValidator (staking.admission_by_vote). Budżet chain 19 wynosił stałe 10 000 RAND na epokę i nie miał głosowania o przyjęciu. Rozwiązanie wiązania (unbond) odbiera wagę na następnej granicy, a sama kwota czeka dwie epoki, zanim będzie można ją wypłacić. rand_getEpoch zwraca epokę, jej długość oraz prognozę następnego zbioru. Zob. Staking.
Co niesie blok
{
"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 to widok, w którym blok zaproponowano, a justify_view to widok QC, który blok niesie dla swojego rodzica. timestamp_ms ustawia proposer, w granicach opisanych w następnej sekcji. state_root zobowiązuje do stanu księgi po bloku. W 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 zatwierdzał te same pola bez program_state_root, pod rand-state-7, bez opakowania.
Składniki zależą od genesis. Łańcuch bez żadnej z sekcji opcjonalnych zatwierdza pierwsze cztery pod rand-state-2. Sekcja bridge dołącza bridge_root. Sekcja tokens dołącza po nim tokens_root i zmienia domenę całego skrótu na rand-state-4; genesis z sekcją bridge musi mieć sekcję tokens, więc chain 20 ma obie. Sekcja aggregation dołącza następnie korzeń agregatorów (pod rand-state-3 w łańcuchu bez tokenów); chain 20 jej nie ma. Sekcja staking dołącza na końcu dwa liczniki epok faucetu i korzeń kolejki bondów (rand-bond-queue-1) i zmienia domenę całego skrótu na rand-state-5, niezależnie od tego, co jest włączone poza nią; chain 14 nie miał takiej sekcji i haszował pod rand-state-4, a chain 15 do 17 haszowały pod rand-state-5. Sekcja vesting dołącza po tym korzeń rejestru vestingu i zmienia domenę skrótu na rand-state-6; chain 20 jej nie ma. Sekcja gas z kontrolerem dynamic dołącza bieżące gas_price i byte_price, każde jako u64 w porządku big-endian, po wszystkim innym i zmienia domenę skrótu na rand-state-7. Taka była domena chain 19, a chain 20 zachowuje ceny w swoim korzeniu: zmieniają się raz na blok, po transakcjach bloku i przed korzeniem (Opłaty). Sekcja gas ze stałymi cenami nie wchodzi do korzenia. Sekcja program_state (RPL-2) dołącza po cenach korzeń komórek i skarbców każdego programu i zmienia domenę skrótu na rand-state-8: to domena chain 20. Stan stakingu z audytu v6 jest wpleciony wokół tego, a nie do wnętrza: przy staking.admission_by_vote (chain 20) korzeń to blake3("rand-state-admitted-1" || inner || admitted_root), a przy staking.slashing (jeszcze żaden łańcuch) drugi wrapper dodaje zbiór uwięzionych (jailed). tokens_root jest korzeniem Merkle liści blake3("rand-token-leaf-1" || bincode(TokenInfo)) w kolejności indeksów, więc metadane każdego tokenu, jego uprawnienia, mint_nonce, podaż i kwota locked każdego zabezpieczenia są stanem konsensusu: dwa węzły, które różnią się co do jednego wiersza tokenu, różnią się co do łańcucha. W chain 20 korzeń tokenów obejmuje też rozszerzenie rejestru (limit max_tokens, spaloną opłatę rejestracyjną, ograniczenie wartości note oraz kroczące okna limitów mintowania mostu) pod rand-token-registry-3. Korzeń mostu obejmuje emitentów, zbiory strażników (guardian sets), wykorzystane skróty attestacji, sekwencję spaleń, klucze strażników PQ, klucz pauzy, flagę pauzy i dwa nonce’y zarządcze; rejestr aktywów został z niego przeniesiony do tokens_root. Most chain 20 ma rules_v2, więc dołączane są nonce rotacji i globalne parametry limitów, a domeną jest rand-bridge-state-5 (w chain 14 była to rand-bridge-state-4); ten korzeń jest następnie opakowany przez dolną granicę powtórzeń dla ruchu przychodzącego (rand-bridge-replay-floor-1), a w chain 20 także przez reguły rotacji i oczekujące rotacje (rand-bridge-rotation-1) oraz grupę opłat (rand-bridge-fees-1). Rejestr walidatorów jest haszowany do validators_root (domena liścia rand-validator-leaf-4 w chain 20, z activation_epoch każdego wiersza; rand-validator-leaf-2 w łańcuchu bez sekcji staking), więc węzeł, który różni się co do jednego wiersza rejestru, różni się co do łańcucha.
Czas bloku jest ograniczony (B2)
W łańcuchu z mostem znacznik czasu bloku rozstrzyga o wygaśnięciu zbioru strażników i o dniu, do którego wlicza się dzienny limit mintowania zabezpieczenia, więc lider, który mógłby przesunąć zegar do przodu, mógłby przedwcześnie zakończyć okres karencji rotowanego zbioru albo ominąć limit danego dnia. Chain 20 ma sekcję bridge, więc obowiązują trzy reguły, wszystkie od niej uzależnione:
| reguła | ograniczenie | rodzaj |
|---|---|---|
| brak cofnięcia | timestamp_ms ≥ parent.timestamp_ms (równość jest dozwolona), w przeciwnym razie BlockError::TimestampRewind; proposer emituje max(now, parent.timestamp_ms) | ważność, sprawdzana także przy odtwarzaniu i synchronizacji |
| brak skoku | timestamp_ms ≤ parent.timestamp_ms + MAX_TIMESTAMP_STEP_MS (60 000 ms), w przeciwnym razie BlockError::TimestampLeap | ważność, sprawdzana także przy odtwarzaniu i synchronizacji |
| dryf zegara | walidator odmawia głosu na blok wyprzedzający jego własny zegar o więcej niż MAX_CLOCK_DRIFT_MS (15 000 ms) | wyłącznie reguła głosowania: blok zostaje w drzewie spekulatywnym, kworum, którego zegary się zgadzają, nadal może go poświadczyć, a odtwarzanie zatwierdzonej historii nigdy jej nie stosuje |
Każdy walidator w łańcuchu z mostem potrzebuje NTP. Walidator, którego zegar odbiega o więcej niż 15 sekund, jest wadliwym liderem (jego propozycje mogą nie dostać głosów) i wadliwym głosującym (może odmówić głosu na punktualny blok uczciwego lidera). Żadne z tych zjawisk nie zatrzymuje łańcucha, dopóki zegary uczciwej większości się zgadzają, ale niezsynchronizowana flota traci margines żywotności bez żadnego powodu.
Znacznik czasu timestamp_ms w genesis musi być bliski rzeczywistemu czasowi startu. Gdy jest zbyt odległy w przeszłości, czas bloku pozostaje w tyle za zegarem ściennym, aż ograniczenie kroku pozwoli go nadrobić, najwyżej 60 s na blok. Gdy jest o więcej niż 15 s w przyszłości, blok 1 nie zostaje zatwierdzony wcale: każdy głos, także samego lidera, jest wstrzymywany, aż czas rzeczywisty go dogoni. Po długim zatrzymaniu to samo nadrabianie może przewinąć dzień limitu mintowania szybciej niż raz na rzeczywistą dobę, w granicach jednego dodatkowego dziennego limitu na każdą pełną dobę zgromadzonego opóźnienia.
Finalność z perspektywy klienta
- Transakcja jest finalna, gdy
rand_getTransaction(hash)zwraca blok. Przyjęcie przezrand_sendTransactionoznacza jedynie dopuszczenie do mempoolu. - Blok serwowany przez RPC lub zindeksowany przez eksplorator jest zatwierdzony. Nie ma okna finalności, które trzeba przeczekać; przewodnik eksploratora mówi to samo.
- Head z
rand_subscribe["newHeads"]to head, którego węzeł nie utraci: nic nie jest wysyłane, zanim blok zostanie zatwierdzony w pamięci trwałej. - Węzeł, który zostaje w tyle, weryfikuje wszystko ponownie: każdy certyfikat, każdy bundle i dowód wywołania, każde potwierdzenie (receipt), zanim przyjmie zsynchronizowany blok. Podpisy finalności nigdy nie zastępują dowodów.
Ściąga z odzyskiwania, z dokumentacji samego fullnode’a: brak wzrostu wysokości przy niskim peer_count to problem sieciowy; brak wzrostu wysokości przy poprawnych peerach oznacza, że online jest mniej niż 2/3 stake’u; CORRUPT CHAIN przy starcie oznacza, że węzeł obciął uszkodzony koniec łańcucha i synchronizuje się ponownie.