Rand Protocol
文檔
核心概念

票據與承諾樹

每筆轉賬所依據的票據模型——承諾、nullifier、信封、深度為 32 的樹、錨點,以及錨點窗口和時間窗口——以及錢包如何掃描屬於自己的票據。

使用本頁來構建或審計任何讀取資金池的工具:掃描自身票據的錢包、跟蹤承諾與 nullifier 的索引器,或檢查一筆交易公開內容的工具。這條鏈上的價值是一組票據;鏈上保存的是它們的承諾,絕不是它們的內容。

票據

票據為 { pk, from, amount, asset, time, r }:28 個 32 位元字,共 112 位元組,佈局為 pk(8) from(8) amount_lo amount_hi asset time r(8)。pk 是擁有者的票據公開值(由查看密鑰派生),from 是發送者的票據公開值,amount 是以單位計的 u64(1 RAND = 10⁹ 單位),asset 是 u32(RAND 為 0,否則為 RPL 代幣在代幣註冊表中的緊湊索引,不論是跨鏈橋接還是原生代幣;參見 RPL tokens),time 是發送者指定的區塊高度,r 是八個隨機的盲化字。

明文從不出現在鏈上。出現在鏈上的是:

  • 它的承諾 cm = H(CM, words),即對這 28 個字在域標籤 4 下計算的 Poseidon2 雜湊,作為葉節點附加到樹中;
  • 它的信封,即以擁有者地址封裝的明文,只有擁有者,或發送者經其出向查看密鑰,才能讀取。自 chain 18 起(包括 chain 21),信封還附帶一份加密備註。鏈根據公開欄位計算的少數票據完全不帶信封(見下文)。

花費票據會公開它的 nullifier H(NF, nk, cm),即擁有者的查看密鑰與承諾在域標籤 3 下計算的 Poseidon2 雜湊。沒有 nk,nullifier 便無法與其承諾關聯。nullifier 集合正是防止雙重花費的機制。

信封

Envelope { kem_ct, to_receiver, to_sender, body }
部分位元組說明
kem_ct1,088向接收者地址作出的 ML-KEM-768 封裝
to_receiver60在封裝所得秘密之下包裹的每筆交易密鑰 TxKey
to_sender60在發送者的出向查看密鑰 ovk 之下包裹的同一個 TxKey
body652在 TxKey 之下的 112 位元組票據和 512 位元組備註欄位(chain 17 及之前為 140 位元組,僅含票據)

每個部分均為 ChaCha20-Poly1305,前置隨機的 12 位元組 nonce 並附 16 位元組標籤,關聯數據則是一個標籤(rand-envelope-receiver、rand-envelope-sender、rand-envelope-body)後接票據的承諾,因此密鑰無法打開並非由它封裝的信封,而打開後的票據也與其所在的葉節點綁定。密文合共 1,860 位元組;加上 bincode 的長度前綴,傳輸時為 1,892 位元組。Chain 20 的創世區塊設定 envelope_bytes: 1860,其帳本拒絕任何其他長度的票據信封(envelope is 1348 bytes, this chain requires exactly 1860);除此之外不檢查信封的任何其他方面。直至 chain 17,信封沒有備註欄位,長度為 1,348 位元組(傳輸時 1,380),帳本只將其上限定為 2,048 位元組;創世區塊沒有 envelope_bytes 的鏈仍沿用這條規則。

備註欄位為 len (u16 LE) ‖ UTF-8 text ‖ zero padding,共 512 位元組,故文字最多 510 位元組。自 chain 18 起,每個票據信封都帶有這個欄位,不論有沒有備註:找零輸出、虛設輸出和不帶備註的付款都以同樣方式填充,鏈上沒有任何東西可以區分備註、其長度或其有無。備註不在票據、承諾或任何證明之內。只有本來就能讀取該票據的人才能讀取它:擁有者、經 ovk 的發送者,或獲得該輸出 TxKey 的任何人。畸形的備註欄位(長度錯誤、UTF-8 無效、填充非零)會被視為沒有備註打開,絕不會令擁有者損失該票據。備註可以是任何人的文字,因為任何人都可以向公開地址支付一筆微額票據:應將其視為不可信內容顯示,並替換控制字元和 Unicode 格式字元,rand 錢包即是如此處理。rand send --memo 會將備註封入付款輸出;rand notes --memo 和 rand history --memo 會顯示備註。

樹、錨點與窗口

承諾按先後次序附加到同一棵深度為 32 的 Poseidon2 樹中,最舊的在前,且永不移除。葉節點 i 是鏈創建的第 i 張票據;創世存款票據是最先的葉節點。一個捆綁無論轉移甚麼,都會附加四個葉節點和四個 nullifier,虛設者亦計算在內。空葉節點為全零,節點為 H(NODE, left, right)(域標籤 7)。節點只保留前沿,因此共識狀態從不保存葉節點;rand_getWitness 按需從已存儲的葉節點重建整棵樹,這也是節點提供的最昂貴的讀取操作。

捆綁針對錨點(即樹根)證明其輸入的成員資格。兩條規則確保錨點可信:

常量值規則
ANCHOR_WINDOW256 個區塊;chain 20 和 21 為 1,024錨點必須是最近 window 個區塊末樹根之一;樹在區塊中途經過的樹根絕不可作為錨點
TIME_WINDOW256 個區塊;chain 20 和 21 為 1,024捆綁的 time 必須位於 [height − window, height] 之內

除非創世區塊設定 proof_window_blocks(v0.6.8,256 至 4,096),否則兩個窗口均為 256 個區塊,該欄位會同時取代兩者。Chain 20 和 21 設定為 1,024;chain 14 至 19 沒有此欄位。

設置窗口是因為證明需要真實時間:tier-14 捆綁證明在筆記本電腦上實測約需 100 秒(自 chain 17 起,錢包還要生成第二份 tier-10 授權證明,約 7 秒)。以 chain 18 的 1.17 秒區塊計,256 個區塊約為五分鐘,而在慢速 prover 上的委託證明曾在 294 秒時被拒絕;chain 20 的 1,024 個區塊按瀏覽器所報告的 1.34 秒計,約為 23 分鐘。輸掉這場競賽的錢包會收到 anchor is not one of the last 1024 roots,並需重新證明。不帶參數的 rand_getAnchor 返回鏈頭的區塊末樹根,這是 prover 唯一應當依據的錨點。

捆綁

每筆轉移價值的交易都恰好攜帶一個固定的 4 入 4 出隱藏資產捆綁(自 chain 14 起;chain 13 及之前為 2 入 2 出),由隱藏資產 zkVM guest 證明,其摘要 hc_bundle 固定於創世文件中(60af094acfe65d85fdb18fb3d06cf9085dcf28c96e59e87f1ee527226e6e3fce,捆綁 guest v3,用於 chain 17 至 20;chain 14 和 15 為 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

輸入與輸出的 0–1 號槽位攜帶私有資產 A;2–3 號槽位始終攜帶 RAND。在純 RAND 付款中,0–1 號槽位為虛設,價值和手續費經 2–3 號槽位轉移。在代幣轉移中,代幣經 0–1 號槽位轉移,RAND 手續費則由同一證明的 2–3 號槽位支付。A 是證明的見證字,不出現在任何公開欄位中,因此任何 RPL 代幣的轉移都是 Action::None,且 burn_a = burn_r = burn_asset = 0,逐欄位與 RAND 付款的形態一致。

證明聲明:每個真實輸入都是 anchor 之下的葉節點並帶有其槽位的資產;四個 nullifier 就是所公開的那些;每一組均保持平衡(私有資產為 in0 + in1 = out0 + out1 + burn_a,RAND 為 in2 + in3 = out2 + out3 + fee + burn_r);並且花費者持有密鑰,而不披露是哪些葉節點、哪些金額、哪種資產或是誰。三個銷毀欄位是捆綁中關於價值離開資金池的唯一公開陳述:burn_a 和 burn_asset 只在 TokenBurn 或 BridgeBurn 時非零,此時它們等於該操作的金額和資產;burn_r 是銷毀的 RAND,只在 Bond(質押)和 RegisterAggregator(保證金)時非零。手續費一律以 RAND 支付。

自 chain 17 起,花費授權被拆分到兩份證明之上(拆分授權)。捆綁 guest v3 取用查看密鑰 nk 和一個鹽值,而此前的 guest 取用花費密鑰:它派生 pk_self = H(PK, nk),要求每個輸入均由它擁有,並將 c = H(AUTH, nk ‖ salt) 摺入其公開的摘要。第二個 guest,即授權 guest(創世 hc_auth,1e4e347f44cf86750b30a9a4bdf9ec9256efe353d4ff8017451eca7d195639c1),取用花費密鑰 sk 和相同的鹽值,派生 nk = H(NK, sk) 並公開相同的 c。捆綁將 c 作為 auth_commit 攜帶,並將授權 guest 的 tier-10 證明作為 auth_proof 攜帶,帳本要求兩份證明針對同一筆交易綁定驗證通過,且兩個 c 值一致。因此,持有 nk 和鹽值的 prover 可以生成捆綁證明,卻無法生成授權證明,委託證明正是建基於此。鹽值為每個捆綁 256 位全新的隨機位,故 auth_commit 雖屬公開,卻不關聯任何東西。為自己生成證明的錢包也會同時生成兩份證明,因此自證交易與委託交易在鏈上看起來並無分別。

形狀是固定的,因此欄位常常是虛設的:虛設輸入是由花費者持有的零值 note(其 nullifier 仍會公布),虛設輸出是零金額的 note,而找贖輸出即使找贖為零也會存在。若交易在沒有找贖時看起來不同,便會洩露沒有找贖這一事實。對錢包的一項後果是:一個 bundle 的兩組各至多花費兩個 note。一筆 RAND 付款、一次質押、一次部署或一次調用,至多花費兩個 RAND note;一筆代幣轉賬或銷毀,至多花費兩個該代幣的 note,以及至多兩個用於繳付費用的 RAND note。分散在同一組內三個或以上 note 的價值,必須先經一筆發送至自己地址的交易合併。

自 chain 14 起,證明亦與其交易綁定:證明是針對整筆交易(證明已清空)的哈希而生成和驗證的,因此不能複製到另一個操作、其他信封或另一條鏈上;自 chain 20 起,該哈希亦涵蓋創世哈希,因此也不能複製到與其共用同一 chain id 的鏈上(交易)。

哪些公開,哪些隱藏

鏈上公開隱藏
RAND 或任何 RPL 代幣的轉賬anchor、全部四個 nullifier、全部四個承諾、費用、burn_a = burn_r = burn_asset = 0、time、四個信封密文、證明、auth_commit 及授權證明誰發送、誰收款、金額、備註、轉移了哪種資產、找贖、花費了哪些葉子、哪些欄位是虛設的
代幣銷毀或橋接銷毀轉賬所公布的一切,另加 burn_asset 與 burn_a,分別等於該操作的資產與金額(橋接銷毀則還有中繼者費用、支持代幣及目的地)銷毀了哪些 note,以及誰銷毀了它們
水龍頭鑄造承諾、信封、金額、鑄造的驗證者,以及自 chain 14 起該 note 的開啟資料(pk、time、r),准入時據此重新計算承諾該 note 其後的每一次花費
創世存入創世檔案中的 { cm, envelope, amount, opening: { pk, time, r } },若為已列出代幣的 note,則 opening 中另有 asset(chain 21 的創世載有三個 zUSD note,chain 20 的載有兩個):擁有者的 pk、金額及資產按設計是公開的其後何時及轉入何處花費
提取、橋接存入、代幣鑄造或註冊的初始鑄造金額、收款人、該 note 的 time 及盲化因子 r(承諾由鏈自行計算)該 note 其後的每一次花費
invoke 的支付或程式鑄造(自 chain 20 起)、多重簽名賬戶的付款或永續合約的支付(chain 21)金額、資產、收款人及 r;該 note 的 time 即 bundle 的 time該 note 其後的每一次花費
橋接費用 note(自 chain 20 起)金額、資產及 time、一個派生的盲化因子、以創世金庫為擁有者,且沒有信封該 note 其後的每一次花費

價值以公開金額進入資金池——水龍頭鑄造、創世存入、驗證者的提取、橋接證明及其費用 note、代幣鑄造、程式金庫的支付——並以公開的費用或銷毀離開。兩者之間的一切都是 note。

此表所列是 chain 21 的證明所隱藏的內容,chain 20 的亦然,但情況並非一直如此。在 chain 14 與 15 上,bundle 證明本身披露了更多資料(2026-09-27 的 zkVM 審查,INT-2 / GV-1):每個表的 LogUp 運行總和以未盲化的形式公布,而 v1 guest 對虛設輸入跳過 Merkle 及資產檢查,並對每一個 Merkle 路徑位元作分支,因此任何人都可從 bundle 證明讀出四個輸入欄位中哪些是真實的,進而得知它轉移的是代幣還是只有 RAND,以及每個被花費葉子索引中 1 位元的數目。兩項改動堵住了這一點。chain 16 改用約束集 7,將每個 LogUp 終值盲化,並把每個已聲明的表下限定為 2^7 行;同時改用無分支的 bundle guest v2,其中每個欄位,不論虛設或真實,都透過相同的指令執行完整的成員資格及資產檢查。chain 17 至 20 所運行的 guest v3 是 v2 的關係,以 nk 及鹽值取代花費密鑰,同樣無分支。在 chain 14 與 15 上已提交的 bundle 仍保留其洩露的內容。在 chain 20 上,證明仍顯示的是其標頭:對 bundle 而言是一個固定形狀(tier 14,每個 bundle 的已聲明 gas 上限為 20,479),對 Call 或 Invoke 而言則是 tier、已聲明的 gas 上限,以及該調用是否使用過 keccak 或 sha256 預編譯、大約使用過多少次(證明)。

沒有信封的 note

大部分 note 是透過開啟其信封而找到的。有些則由鏈根據公開欄位計算,對這些 note 而言,信封或不存在,或不可信。

  • 橋接費用 note(自 chain 20 起)。 在 bridge.fees 之下,鏈於每次存入及每次銷毀時,向創世金庫附加一個 zUSD note(RPL 代幣)。它沒有信封:儲存層以一個空信封為其建立索引,不向任何人密封,rand_getCompactBlocks 與 rand_getCommitments 亦如此顯示——存入的費用 note 排在存入 note 之後,銷毀的費用 note 排在該 bundle 的四個 note 之後。該 note 的每一個字都是公開的:金額來自交易及創世費率,資產及 time 來自交易,from = 0,盲化因子為存入時的 blake3("rand-bridge-fee-r-1" ‖ mu) 或銷毀時的 blake3("rand-bridge-burn-fee-r-1" ‖ txid),擁有者為創世的 recipient。
  • 信封不約束任何內容的 note。 橋接存入、代幣鑄造、註冊的初始鑄造,以及自 chain 20 起 invoke 的支付與程式鑄造(在 chain 21 上還有多重簽名賬戶的付款及永續合約的支付),所附信封是由提交交易者密封的,但鏈根據該操作的公開欄位計算 note,從不對照該 note 檢查信封。粗心或懷有惡意的提交者可以密封垃圾資料,因此收款人不得依賴它。

因此,rand 錢包自行重建這些 note(wallet::rebuilt_notes_with),在試解密之旁以一次公開掃描進行:對每一筆指明其自身 pk 的此類交易,以及在其地址即金庫時的費用 note,錢包計算該 note,並把它記錄在其承諾哈希所對應的葉子上。重建出來的 note 只是候選:若節點就某筆交易、費率或哈希說謊,結果只會是某個 note 找不到,絕不會憑空記入一個 note。自 v0.6.8 起,rand_getBlocks 所返回的區塊頭,把存入、鑄造及註冊交易載於 public_notes 之中;在有橋接費用的鏈上,銷毀交易亦然,證明已剔除,沿用其真實 id,因此該次掃描只讀取區塊頭頁面。invoke 在 v0.6.8 中不在 public_notes 內,因此對 v0.6.8 節點而言,錢包只能透過信封找到 invoke 的支付,而該信封由調用者密封,沒有任何檢查。Fullnode main 自 d0bb73fb 起也把 invoke 載於其中,證明已剔除;這只是節點端的改動,尚未推出到 chain 20 的節點群,錢包亦無須改動。

掃描:錢包如何找到自己的 note

節點只回答「這是整棵樹」,從不回答「這是你的餘額」。錢包:

  1. 以 rand_getCommitments(from_index, limit) 分頁讀取葉子(每次調用至多 1,000 個)——或自 fullnode v0.8.0 起,以 rand_getScanLeaves(from_index, limit) 讀取其掃描頭:同樣的行,但信封截短至首 1,208 個位元組(kem_ct ‖ to_receiver ‖ to_sender),這已足以進行試解密,因此錢包只為其密鑰能開啟的葉子讀取完整信封,在 chain 20 的葉子上每個葉子少讀 2.26 倍的位元組,並在節點以 -32601 作答時退回完整頁面——或讀取逐區塊串流 rand_getCompactBlocks(from_height, to_height)(每次調用至多 128 個區塊);每一筆載有 bundle 的交易在此都列出四個 note 及四個 nullifier,任何讀取者都無法分辨虛設欄位與真實欄位;
  2. 試解密每個信封:作為收款方透過其 ML-KEM 解封裝密鑰,作為發送方透過 ovk;保留 pk 為其自身的 note;並從其交易重建帶有公開金額的 note(見上文);
  3. 計算每個保留 note 的 nullifier,並標記出現在 rand_getNullifiers(from_height, limit) 中的那些;
  4. 把其餘的加總。

rand_getTreeInfo 說明還有多少內容需要掃描:{ "next_index", "root", "nullifiers" }。進行支付時,錢包需要每個真實輸入的 32 層 Merkle 路徑。rand 錢包在掃描期間,根據同一批 rand_getCommitments 頁面自行建立承諾樹的副本,並自行計算每一條路徑,因此發送交易時唯一需要向節點查詢的樹相關問題是 rand_getAnchor,而它不會指明任何葉節點。rand_getWitness(index) 仍保留在節點上,供其他客户端使用。它會向節點透露呼叫者即將花費哪一個葉節點,這在引入本地樹之前是資金池最大的洩漏來源:不得不使用它的客户端應當運行自己的節點,或在請求所需見證的同時,一併請求一些並不需要的見證。

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

自 rand send --no-wait 起,至下一次 sync 看到該 note 的 nullifier 或區塊高度超過 time + window 為止(在鏈 20 和 21 上為 1,024,讀取自 rand_getLimits.proof_window_blocks;若無此欄位則為 256),該 note 處於 pending 狀態;此後該批次永遠無法被接納,該 note 可再次被花費。

最後一次對照 fullnode 提交 c0878c3f(chain 21)核實的日期為 2026-10-12。 如代碼與本頁內容有出入,以代碼為準;請告知我們。