Rand Protocol
Dokumentacja
Podstawowe pojęcia

Notes i drzewo zobowiązań

Model notatek, na którym zbudowany jest każdy transfer - zobowiązania, nullifiery, koperty, drzewo o głębokości 32, kotwice oraz okna kotwic i czasu - a także sposób, w jaki portfel skanuje w poszukiwaniu własnych notatek.

Ta strona służy do budowania i audytowania wszystkiego, co odczytuje pulę: portfela skanującego w poszukiwaniu swoich not, indeksera śledzącego zobowiązania i nullifiery albo narzędzia sprawdzającego, co publikuje transakcja. Wartość w tym łańcuchu jest zbiorem not; łańcuch przechowuje ich zobowiązania, nigdy ich zawartość.

Nota

Nota to { pk, from, amount, asset, time, r }: 28 słów po 32 bity, 112 bajtów, w układzie pk(8) from(8) amount_lo amount_hi asset time r(8). pk to publiczna wartość noty właściciela (wyprowadzona z klucza podglądu), from to wartość nadawcy, amount to u64 w jednostkach (1 RAND = 10⁹ jednostek), asset to u32 (0 dla RAND, w pozostałych przypadkach gęsty indeks rejestru tokenów dla tokena RPL, pomostowanego lub natywnego; zob. tokeny RPL), time to wysokość bloku, którą wskazał nadawca, a r to osiem losowych słów zaślepienia.

Treść jawna nigdy nie pojawia się w łańcuchu. Pojawia się natomiast:

  • jej zobowiązanie cm = H(CM, words), skrót Poseidon2 z 28 słów pod znacznikiem domeny 4, dołączany jako liść drzewa;
  • jej koperta, treść jawna zapieczętowana na adres właściciela tak, by mógł ją odczytać tylko właściciel albo nadawca za pomocą swojego wychodzącego klucza podglądu. Od łańcucha 18 (łącznie z łańcuchem 20) koperta zawiera także zaszyfrowany memoriał. Kilka not, które łańcuch oblicza z pól publicznych, nie ma koperty w ogóle (poniżej).

Wydanie noty publikuje jej nullifier H(NF, nk, cm), skrót Poseidon2 pod znacznikiem domeny 3 z klucza podglądu właściciela i zobowiązania. Bez nk nie można powiązać nullifiera z jego zobowiązaniem. Zbiór nullifierów zapobiega podwójnemu wydaniu.

Koperta

Envelope { kem_ct, to_receiver, to_sender, body }
częśćbajtyczym jest
kem_ct1088enkapsulacja ML-KEM-768 do adresu odbiorcy
to_receiver60klucz transakcji TxKey opakowany pod zenkapsulowanym sekretem
to_sender60ten sam TxKey opakowany pod wychodzącym kluczem podglądu nadawcy ovk
body652112-bajtowa nota i 512-bajtowe pole memoriału pod TxKey (140 bajtów, sama nota, do łańcucha 17 włącznie)

Każda część jest szyfrowana ChaCha20-Poly1305 z losowym 12-bajtowym nonce na początku i 16-bajtowym tagiem, a dane powiązane to znacznik (rand-envelope-receiver, rand-envelope-sender, rand-envelope-body), po którym następuje zobowiązanie noty. Dzięki temu klucz nie otworzy koperty, której nie zapieczętował, a otwarta nota jest związana z liściem, pod którym leży. Daje to 1860 bajtów szyfrogramu; z prefiksami długości bincode jest to 1892 na łączu. Genesis łańcucha 20 ustawia envelope_bytes: 1860, a jego księga odrzuca kopertę noty o jakiejkolwiek innej długości (envelope is 1348 bytes, this chain requires exactly 1860); niczego innego w niej nie sprawdza. Do łańcucha 17 koperta nie miała pola memoriału i miała 1348 bajtów (1380 na łączu), a księga jedynie ograniczała ją do 2048 bajtów, co nadal obowiązuje w łańcuchu, którego genesis nie ma envelope_bytes.

Pole memoriału ma postać len (u16 LE) ‖ tekst UTF-8 ‖ dopełnienie zerami, 512 bajtów, czyli co najwyżej 510 bajtów tekstu. Każda koperta noty w łańcuchu 20 je zawiera, niezależnie od tego, czy memoriał istnieje: reszta, wyjście-atrapa i płatność bez memoriału są dopełniane tak samo, a nic w łańcuchu nie odróżnia memoriału, jego długości ani jego braku. Memoriał nie należy do noty, zobowiązania ani żadnego dowodu. Może go odczytać dokładnie ten, kto już może odczytać notę: właściciel, nadawca przez ovk albo każdy, komu przekazano TxKey tego wyjścia. Źle sformatowane pole memoriału (zła długość, nieprawidłowy UTF-8, niezerowe dopełnienie) otwiera się jako brak memoriału i nigdy nie kosztuje właściciela noty. Memoriał to tekst dowolnej osoby, ponieważ każdy może wysłać notę-pył na publiczny adres: wyświetlaj go jako niezaufany, z zastąpionymi znakami sterującymi i znakami formatu Unicode, jak robi to portfel rand. rand send --memo pieczętuje go w wyjściu płatności; rand notes --memo i rand history --memo go pokazują.

Drzewo, kotwice i okna

Zobowiązania są dołączane do jednego drzewa Poseidon2 o głębokości 32, od najstarszego, i nigdy nie są usuwane. Liść i to i-ta nota, jaką łańcuch kiedykolwiek utworzył; noty depozytów genesis są pierwszymi liśćmi. Pakiet dołącza cztery liście i cztery nullifiery bez względu na to, co przenosi, wraz z atrapami. Pusty liść to same zera, a węzeł to H(NODE, left, right) (znacznik domeny 7). Węzeł przechowuje tylko krawędź drzewa, więc stan konsensusu nigdy nie zawiera liści; rand_getWitness odbudowuje na żądanie całe drzewo z zapisanych liści, dlatego jest to najdroższy odczyt, jaki obsługuje węzeł.

Pakiet dowodzi przynależności swoich wejść względem kotwicy, czyli korzenia drzewa. Dwie reguły utrzymują uczciwość kotwicy:

staławartośćreguła
ANCHOR_WINDOW256 bloków; 1024 w łańcuchu 20kotwica musi być jednym z ostatnich window korzeni końca bloku; korzeń, przez który drzewo przechodzi w środku bloku, nigdy nie jest kotwicą
TIME_WINDOW256 bloków; 1024 w łańcuchu 20time pakietu musi mieścić się w przedziale [height − window, height]

Oba okna mają po 256 bloków, chyba że genesis ustawia proof_window_blocks (v0.6.8, od 256 do 4096), które zastępuje oba naraz. Łańcuch 20 ustawia 1024; łańcuchy od 14 do 19 nie miały tego pola.

Okna istnieją, ponieważ dowodzenie zajmuje realny czas: dowód pakietu warstwy 14 trwa około 100 sekund na laptopie (a od łańcucha 17 portfel generuje drugi dowód autoryzacji warstwy 10, około 7 sekund). 256 bloków to było około pięciu minut przy blokach 1,17 s w łańcuchu 18, a dowód delegowany na wolnym proverze został odrzucony po 294 s; 1024 bloki łańcucha 20 to około 23 minut przy 1,34 s, które zgłaszał tam eksplorator. Portfel, który przegra ten wyścig, dostaje anchor is not one of the last 1024 roots i dowodzi od nowa. rand_getAnchor bez parametru zwraca korzeń końca bloku głowy łańcucha, jedyną kotwicę, względem której prover powinien budować dowód.

Pakiet

Każda transakcja przenosząca wartość zawiera dokładnie jeden stały pakiet 4-wejścia-4-wyjścia z ukrytym aktywem (od łańcucha 14; do łańcucha 13 było to 2-na-2), udowodniony przez gościa zkVM ukrytego aktywa, którego skrót hc_bundle jest przypięty w pliku genesis (60af094acfe65d85fdb18fb3d06cf9085dcf28c96e59e87f1ee527226e6e3fce, gość pakietu v3, w łańcuchach od 17 do 20; w łańcuchach 14 i 15 był to 83d3a3704a1fcdb9bae7136c0a947ffa34bd53f055388395ed705fe8cacd0ef8, gość 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

Sloty 0–1, wejściowe i wyjściowe, niosą aktywo prywatne A; sloty 2–3 zawsze niosą RAND. W płatności wyłącznie w RAND sloty 0–1 są atrapami, a wartość i opłata przechodzą przez sloty 2–3. W transferze tokena token przechodzi przez sloty 0–1, a opłata w RAND jest pobierana ze slotów 2–3 tego samego dowodu. A jest słowem świadka dowodu i nie występuje w żadnym polu publicznym, więc transfer dowolnego tokena RPL to Action::None z burn_a = burn_r = burn_asset = 0, pole w pole w kształcie płatności w RAND.

Dowód stwierdza, że każde rzeczywiste wejście jest liściem pod anchor i niesie aktywo swojego slotu, że cztery nullifiery są tymi opublikowanymi, że każda grupa się bilansuje (in0 + in1 = out0 + out1 + burn_a w aktywie prywatnym, in2 + in3 = out2 + out3 + fee + burn_r w RAND) i że wydający posiada klucze — nie ujawniając, które liście, jakie kwoty, które aktywo ani kto. Trzy pola burn są jedynym publicznym oświadczeniem pakietu o wartości opuszczającej pulę: burn_a i burn_asset są niezerowe tylko przy TokenBurn lub BridgeBurn, gdzie równają się kwocie i aktywu akcji; burn_r to spalony RAND, niezerowy tylko przy Bond (stawka) i RegisterAggregator (kaucja). Opłata jest zawsze w RAND.

Od łańcucha 17 uprawnienie do wydawania jest rozdzielone na dwa dowody (podzielona autoryzacja). Gość pakietu v3 przyjmuje klucz podglądu nk i sól tam, gdzie wcześniejsi goście przyjmowali klucz wydawania: wyprowadza pk_self = H(PK, nk), wymaga, by każde wejście należało do niego, i wkłada c = H(AUTH, nk ‖ salt) do skrótu, który publikuje. Drugi gość, gość autoryzacji (genesis hc_auth, 1e4e347f44cf86750b30a9a4bdf9ec9256efe353d4ff8017451eca7d195639c1), przyjmuje klucz wydawania sk i tę samą sól, wyprowadza nk = H(NK, sk) i publikuje to samo c. Pakiet niesie c jako auth_commit, a dowód warstwy 10 gościa autoryzacji jako auth_proof, i księga wymaga, aby oba dowody weryfikowały się względem tego samego wiązania transakcji, a obie wartości c były zgodne. Prover posiadający nk i sól może więc wykonać dowód pakietu, ale nie dowód autoryzacji, na czym opiera się dowodzenie delegowane. Sól to 256 świeżych losowych bitów na pakiet, więc auth_commit jest publiczny i niczego nie łączy. Portfel, który dowodzi sam dla siebie, wykonuje też oba dowody, więc transakcja udowodniona samodzielnie i delegowana wyglądają w łańcuchu tak samo.

Kształt jest stały, więc sloty bywają fikcyjne: fikcyjne wejście to nota o zerowej wartości, należąca do wydającego (jej nullifier jest mimo to publikowany), fikcyjne wyjście to nota o zerowej kwocie, a wyjście reszty występuje nawet wtedy, gdy reszta wynosi zero. Transakcja wyglądająca inaczej, gdy reszty nie ma, zdradzałaby, że jej nie było. Konsekwencja dla portfeli: każda z dwóch grup pakietu wydaje co najwyżej dwie noty. Płatność w RAND, obligacja, wdrożenie lub wywołanie wydaje co najwyżej dwie noty RAND; transfer lub spalenie tokena wydaje co najwyżej dwie noty tokena i co najwyżej dwie noty RAND na opłatę. Wartość rozłożoną na trzy lub więcej not w jednej grupie trzeba najpierw skonsolidować, wysyłając ją na własny adres.

Od chain 14 dowód jest także powiązany ze swoją transakcją: tworzy się go i weryfikuje względem skrótu całej transakcji z wyzerowanymi dowodami, więc nie można go skopiować na inną akcję, inne koperty ani inny łańcuch; na chain 20 skrót obejmuje również hash genesis, więc nie można go skopiować także na łańcuch o tym samym identyfikatorze łańcucha (Transakcje).

Co jest jawne, a co ukryte

jawne w łańcuchuukryte
transfer, w RAND lub w dowolnym tokenie RPLanchor, wszystkie cztery nullifiery, wszystkie cztery zobowiązania, opłata, burn_a = burn_r = burn_asset = 0, time, cztery szyfrogramy kopert, dowód, auth_commit i dowód autoryzacjikto wysłał, kto otrzymał, kwota, memo, który składnik majątku został przesłany, reszta, które liście zostały wydane, które sloty były fikcyjne
spalenie tokena lub spalenie mostuwszystko, co publikuje transfer, z burn_asset i burn_a równymi składnikowi majątku i kwocie akcji (a dla spalenia mostu także opłata przekaźnika, token zabezpieczający i miejsce docelowe)które noty zostały spalone i kto je spalił
mint z faucetuzobowiązanie, koperta, kwota, walidator, który wykonał mint, a od chain 14 także otwarcie noty (pk, time, r), z którego przy przyjęciu przeliczane jest zobowiązaniekażde późniejsze wydanie noty
depozyt genesis{ cm, envelope, amount, opening: { pk, time, r } } w pliku genesis, z asset w otwarciu dla noty tokena z listy (genesis chain 20 zawiera dwie noty zUSD): pk właściciela, kwota i składnik majątku są jawne z założeniakiedy i na co zostaje później wydana
withdraw, depozyt mostu, mint tokena lub początkowy mint rejestracjikwota, odbiorca, time noty i czynnik zaślepiający r (zobowiązanie łańcuch oblicza sam)każde późniejsze wydanie noty
wypłata invoke lub mint programu (chain 20)kwota, składnik majątku, odbiorca i r; time noty pochodzi z pakietukażde późniejsze wydanie noty
nota opłaty mostu (chain 20)kwota, składnik majątku i time, wyprowadzony czynnik zaślepiający, skarbiec genesis jako właściciel i brak kopertykażde późniejsze wydanie noty

Wartość wchodzi do puli z jawną kwotą — mint z faucetu, depozyt genesis, withdraw walidatora, atestacja mostu i jego nota opłaty, mint tokena, wypłata z sejfu programu — a opuszcza ją jako jawna opłata lub spalenie. Wszystko pomiędzy jest notą.

Tabela pokazuje, co ukrywają dowody chain 20, i nie zawsze tak było. Na chain 14 i 15 sam dowód pakietu ujawniał więcej (przegląd zkVM z 2026-09-27, INT-2 / GV-1): bieżąca suma LogUp każdej tabeli była publikowana bez zaślepienia, a guest v1 pomijał dla fikcyjnego wejścia sprawdzenia Merkle i składnika majątku oraz rozgałęział się na każdym bicie ścieżki Merkle, więc każdy mógł odczytać z dowodu pakietu, które z czterech slotów wejściowych były prawdziwe, a zatem czy przenosił token, czy tylko RAND, oraz liczbę bitów równych 1 w indeksie każdego wydanego liścia. Zamknęły to dwie zmiany. Chain 16 przeszedł na zestaw ograniczeń 7, który zaślepia każdy terminal LogUp i ustala dla każdej zadeklarowanej tabeli dolną granicę 2^7 wierszy, oraz na guesta pakietu v2 bez rozgałęzień, w którym każdy slot, fikcyjny czy prawdziwy, wykonuje pełne sprawdzenie przynależności i składnika majątku tymi samymi instrukcjami. Guest v3, na którym działają chain 17–20, to relacja v2 z nk i solą w miejscu klucza wydawania, również bez rozgałęzień. Pakiety zatwierdzone na chain 14 i 15 zachowują to, co ujawniły. To, co dowód nadal pokazuje na chain 20, to jego nagłówek: dla pakietu jest to jeden stały kształt (tier 14, zadeklarowany limit gazu 20,479 w każdym pakiecie), a dla Call lub Invoke jest to tier, zadeklarowany limit gazu oraz to, czy i mniej więcej jak często wywołanie korzystało z prekompilacji keccak lub sha256 (Dowody).

Noty bez koperty

Większość not odnajduje się, otwierając ich koperty. Niektóre łańcuch oblicza z pól jawnych, a dla nich koperta jest albo nieobecna, albo niegodna zaufania.

  • Noty opłat mostu (chain 20). W bridge.fees łańcuch dołącza notę zUSD do skarbca genesis przy każdym depozycie i każdym spaleniu (Tokeny RPL). Nie ma ona koperty: magazyn indeksuje ją z pustą, zaadresowaną do nikogo, a rand_getCompactBlocks i rand_getCommitments pokazują ją w ten sposób, przy depozycie po nocie depozytu, a przy spaleniu po czterech notach pakietu. Każde słowo noty jest jawne: kwota wynika z transakcji i stawki z genesis, składnik majątku i time z transakcji, from = 0, czynnik zaślepiający to blake3("rand-bridge-fee-r-1" ‖ mu) dla depozytu lub blake3("rand-bridge-burn-fee-r-1" ‖ txid) dla spalenia, a właścicielem jest recipient z genesis.
  • Noty, których koperta niczego nie wiąże. Depozyt mostu, mint tokena, początkowy mint rejestracji oraz, na chain 20, wypłaty invoke i minty programów mają kopertę zapieczętowaną przez tego, kto złożył transakcję, ale łańcuch oblicza notę z jawnych pól akcji i nigdy nie sprawdza koperty względem niej. Nieostrożny lub wrogi nadawca może zapieczętować śmieci, więc odbiorca nie może na niej polegać.

Portfel rand odbudowuje więc te noty samodzielnie (wallet::rebuilt_notes_with), w jawnym przebiegu obok próbnego odszyfrowywania: dla każdej takiej transakcji wskazującej jego pk, a dla not opłat, gdy jego adres jest skarbcem, oblicza notę i zapisuje ją w liściu, z którego zobowiązaniem się zgadza. Odbudowana nota jest tylko kandydatem: węzeł, który kłamie o transakcji, stawce opłaty lub haszu, sprawia, że nota pozostaje nieodnaleziona, ale nigdy nie zalicza fałszywej. Od v0.6.8 nagłówki zwracane przez rand_getBlocks niosą transakcje depozytów, mintów i rejestracji w public_notes, a w łańcuchu z opłatami mostu także spalenia, z usuniętymi dowodami, pod ich prawdziwym identyfikatorem, więc przebieg czyta wyłącznie strony nagłówków. Invoke nie ma w public_notes w v0.6.8, więc wobec węzła v0.6.8 portfel znajduje wypłatę invoke tylko przez jego kopertę, którą pieczętuje wywołujący i której nic nie sprawdza. Gałąź main fullnode niesie tam także invoke, z usuniętymi dowodami, od d0bb73fb; jest to zmiana tylko po stronie węzła, jeszcze nie wdrożona na flotę chain 20, a portfel nie wymaga w związku z nią żadnej zmiany.

Skanowanie: jak portfel znajduje swoje noty

Węzeł odpowiada „oto całe drzewo”, nigdy „oto twoje saldo”. Portfel:

  1. pobiera liście stronami przez rand_getCommitments(from_index, limit) (co najwyżej 1,000 na wywołanie) lub strumień blok po bloku rand_getCompactBlocks(from_height, to_height) (co najwyżej 128 bloków na wywołanie); każda transakcja niosąca pakiet wymienia tam cztery noty i cztery nullifiery, a żaden czytelnik nie odróżni slotu fikcyjnego od prawdziwego;
  2. próbnie odszyfrowuje każdą kopertę: jako odbiorca przez swój klucz dekapsulacji ML-KEM, jako nadawca przez ovk; zachowuje noty, których pk jest jego własnym; i odbudowuje noty z jawną kwotą na podstawie ich transakcji (wyżej);
  3. oblicza nullifier każdej zachowanej noty i oznacza te, które występują w rand_getNullifiers(from_height, limit);
  4. sumuje pozostałe.

rand_getTreeInfo mówi, jak daleko sięga skanowanie: { "next_index", "root", "nullifiers" }. Do wydania portfel potrzebuje 32-poziomowej ścieżki Merkle każdego prawdziwego wejścia. Portfel rand utrzymuje własną kopię drzewa zobowiązań, budowaną podczas skanowania z tych samych stron rand_getCommitments, i sam oblicza każdą ścieżkę, więc jedyne pytanie o drzewo, jakie wysyłanie zadaje węzłowi, to rand_getAnchor, które nie wskazuje żadnego liścia. rand_getWitness(index) pozostaje w węźle dla innych klientów. Mówi ono węzłowi, który liść wywołujący zamierza wydać, co przed lokalnym drzewem było największym wyciekiem puli: klient, który musi go użyć, powinien uruchomić własny węzeł albo pytać o świadectwa, których nie potrzebuje, obok tych, których potrzebuje.

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

Notatka ma stan pending od rand send --no-wait do następnego sync, który zobaczy jej nullifier albo bloki po time + window (1,024 w chain 20, odczytywane z rand_getLimits.proof_window_blocks; 256 bez tego pola). Po tym czasie paczka nie może już zostać przyjęta, a notatka znów jest możliwa do wydania.

Ostatnio zweryfikowano względem commitu fullnode c0878c3f (łańcuch 21) dnia 2026-10-12. Jeśli kod i ta strona się różnią, rację ma kod; prosimy o zgłoszenie.