使用本頁來構建或審計任何讀取資金池的工具:掃描自身票據的錢包、跟蹤承諾與 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_ct | 1,088 | 向接收者地址作出的 ML-KEM-768 封裝 |
to_receiver | 60 | 在封裝所得秘密之下包裹的每筆交易密鑰 TxKey |
to_sender | 60 | 在發送者的出向查看密鑰 ovk 之下包裹的同一個 TxKey |
body | 652 | 在 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_WINDOW | 256 個區塊;chain 20 和 21 為 1,024 | 錨點必須是最近 window 個區塊末樹根之一;樹在區塊中途經過的樹根絕不可作為錨點 |
TIME_WINDOW | 256 個區塊;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
節點只回答「這是整棵樹」,從不回答「這是你的餘額」。錢包:
- 以
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,任何讀取者都無法分辨虛設欄位與真實欄位; - 試解密每個信封:作為收款方透過其 ML-KEM 解封裝密鑰,作為發送方透過
ovk;保留pk為其自身的 note;並從其交易重建帶有公開金額的 note(見上文); - 計算每個保留 note 的 nullifier,並標記出現在
rand_getNullifiers(from_height, limit)中的那些; - 把其餘的加總。
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 可再次被花費。