使用本页可以在与链交互时选择合适的库,对照节点实际强制执行的值核对某个参数,并自行重新计算某个 id 或承诺。这里的每个值都是链 20(fullnode v0.6.8)上代码实际运行所用的值。
原语及其用途
| 原语 | 参数 | 用途 |
|---|---|---|
| Dilithium2 | 1,312 字节公钥,2,420 字节签名;密钥对由 32 字节种子派生 | 验证者投票与区块提议;faucet 铸币;Unbond、Withdraw 以及验证者的 Registration;AdmitValidator 的投票(链 20);aggregator 操作;RPL 代币的 Key 铸造权限(TokenMint、SetAuthority);桥的后量子守护者联署签名、其暂停密钥,以及在链 20 上轮换时新密钥的持有证明(见下文) |
| ML-KEM-768 | 1,184 字节封装密钥,1,088 字节密文 | 将票据信封的每笔交易密钥密封给接收者的地址;将调用输入信封密封给审计方;将证明任务密封给配对的 prover |
| ChaCha20-Poly1305 | 32 字节密钥,前置随机 12 字节 nonce,16 字节标签 | 每个信封部分:票据主体、被包装的交易密钥、调用输入记录;配对 prover 的任务与回复 |
| BLAKE3 | 32 字节输出,始终在前面加上域字符串 | 地址、交易 id、交易绑定与调用绑定、程序 id、代币资产 id、状态根及其 Merkle 树、p2p 身份、签名消息、桥资产 id、桥存款的盲化因子以及桥手续费票据的盲化因子 |
| Poseidon2(Goldilocks 上) | 宽度 8,速率 4,容量 4,无填充的覆写海绵;消息元素为 32 位字;86 个轮常数自链 15 切换起是节点源码(poseidon2_constants.rs)中的字面表,与此前种子抽取所产生的值相同 | 证明内部的一切:密钥、票据承诺、nullifier、承诺树、程序摘要 hc、输入承诺 H_IN 与 H_PUB、捆绑摘要、授权承诺 |
| secp256k1 ECDSA + keccak256 | 低 s 签名,以太坊式 20 字节守护者地址,mu = keccak256(keccak256(body)) | 桥守护者证明,使同一密钥可用于 EVM 合约、Solana 程序和 Rand;在 Rand 上,每份证明还需要下文的 Dilithium2 法定人数 |
| bech32m(BIP-350) | 人类可读部分 rpl,32 字节负载,52 个数据字符 + 6 个校验字符 = 62 | RPL 代币资产 id 的文本形式 rpl1… |
| Plonky3 批量 STARK | Goldilocks p = 2⁶⁴ − 2³² + 1,隐藏式 FRI,blowup 8,折叠 arity 8,80 次查询,20 个工作量证明位 | 每个捆绑证明、授权证明和调用证明;捆绑证明及其授权证明以其交易的八个绑定字作为公开输入段 |
白皮书的目标参数在若干处与实现实际运行的参数不同(例如论文中的 384 位转录哈希,对应 zkVM 的 Poseidon2 转录)。这些页面描述的是代码。
桥上的后量子联署签名
桥守护者的证明采用 secp256k1 ECDSA,量子攻击者可以伪造。自链 14 起,每个 BridgeAttest(存款与守护者集合轮换皆然)还必须携带同一批守护者针对同一摘要的 Dilithium2 签名法定人数:
M = "rand-bridge-pq-cosign-1" || rand_chain_id (u64 BE) || mu // 63 bytes, deterministic signing
其中 mu = keccak256(keccak256(body)) 即 ECDSA 法定人数所签的内容。M 中的 Rand 链 id 使测试网的联署签名即使在守护者密钥被复用时,也无法在另一条 Rand 链上通过验证。M 只绑定链 id,链 20 上亦然:它是 binding_domain 没有移到创世哈希之后的唯一一条签名消息,因为对其签名的守护者守护进程位于另一个代码仓库。使用相同链 id 和相同守护者集合的链副本会接受其存款的联署签名;守护者集合索引不在 M 中,因此联署签名在 ECDSA 集合轮换后依然有效。该操作的 pq_signatures 字段是 PqSignature { index: u8, signature } 的列表,先检查开销小的项,再检查开销大的项:quorum(n) ≤ len ≤ n(PqNoQuorum),索引严格递增(PqIndexOrder),每个索引都小于 n(PqIndexOutOfRange),每个签名恰为 2,420 字节(PqBadSignatureLength),最后用 pq_guardians[index] 验证每个签名(PqBadSignature)。PQ 签名者不必与 ECDSA 签名者相同;两个法定人数各自独立计数。该字段位于交易绑定之内,因此无法在副本上被剥除或替换。
PQ 集合即创世 bridge.pq_guardians:链 20 上为八个 1,312 字节的密钥,与链 15 至 19 相同,与创世 ECDSA 集合按索引对齐,守护者集合 1(链 14 为六个,对齐集合 0)。ECDSA 守护者集合轮换从不改变它。在链 14 上,它在链的整个生命周期内固定不变;自链 15 起,创世包含桥的 rules_v2 组,在其下 PQ 法定人数可以替换整个 PQ 集合(RotatePqGuardians)或暂停密钥(RotatePauseKey),二者均受桥的 rotation_nonce 约束。链 20 的创世新增 bridge.rotation:轮换必须是 V2 操作,并携带每个新密钥对同一消息的 Dilithium2 签名(持有证明),且在提交一天后生效,除非当前暂停密钥将其取消(Bridge)。rand_getBridgeState 提供这些密钥,解码后的 bridge_attest 列出 pq_signers,即已签名者的索引。同一个 PQ 法定人数还授权桥的治理消息,每条消息都是固定的大端布局,携带链 id 和一个 nonce。在链 20 上(创世 binding_domain: 1),每个域的版本数字为 2,且 32 字节的创世哈希位于域与链 id 之间;链 14 至 19 使用 -1 形式:
| 消息 | 域(链 14–19 / 链 20) | 签名者 |
|---|---|---|
M_pause | rand-bridge-pause-1 / -2 | 唯一的创世暂停密钥(Dilithium2),它只能暂停,不能取消暂停 |
M_unpause | rand-bridge-pq-unpause-1 / -2 | PQ 守护者法定人数 |
M_list | rand-bridge-pq-list-1 / -2 | PQ 守护者法定人数(ListBacking) |
M_register | rand-bridge-pq-register-1 / -2 | PQ 守护者法定人数(RegisterBridgedToken) |
M_rotate_pq | rand-bridge-pq-rotate-pq-1 / -2 | 当前 PQ 集合的法定人数(RotatePqGuardians,rules_v2 链);在链 20 上还包括每个新密钥,作为其持有证明 |
M_rotate_pause | rand-bridge-pq-rotate-pause-1 / -2 | 当前 PQ 集合的法定人数(RotatePauseKey,rules_v2 链);在链 20 上还包括新密钥 |
M_cancel | rand-bridge-cancel-rotation-1 / -2,末尾附一个 kind 字节 | 当前暂停密钥(CancelRotation,带有 bridge.rotation 的链:链 20) |
生产环境中的 STARK
zkVM 处于约束集 8:九张必备表(program、cpu、memory、alu、range、nibble、poseidon2、input、public),外加一张 keccak 表和一张 sha256 表(仅当其 guest 调用了相应的 syscall 时,证明才携带),由十七条总线连接,并作为一个批量 STARK 进行证明。自约束集 7 起,每张已声明的表至少有 2⁷ 行,且每张表的 LogUp 终值都通过第十七条总线 BLIND 进行盲化。证明的公开接口为 35 个域元素(pv::NUM):入口 pc、层级、八个输出字、程序摘要 hc(8 个字)、加盐的私有输入承诺 H_IN(8 个)、不加盐的公开段摘要 H_PUB(8 个),以及自约束集 8 起的已声明 gas 上限 GAS_LIMIT。
| 参数 | 生产值 | 备注 |
|---|---|---|
| FRI 查询次数 | 80 | 约束集 3 和 4 的 27 次查询调整在 2026-09-12 审计后被撤销:它只留下约 42 个已证明位 |
| blowup | 8(log_blowup = 3) | 每张表的最大约束次数都被固定在上限 8 之下 |
| 折叠 arity | 8 | |
| 工作量证明位 | 20 | |
| 可靠性 | 3·80 + 20 = 260 位(猜想,ethSTARK);在邻近间隙下约 86 位(已证明) | |
| 零知识 | 统计性:LDE 与 FRI 批量多项式均经随机化,因此同一次运行的两个证明互不相同 | 自约束集 7 起,每张已声明的表至少为 2⁷ 行,多于证明所打开的 82 个点,且 LogUp 终值已被盲化 |
| 层级 | 10、12、14、16、18、20 | 证明会揭示层级,但绝不揭示周期数;自 fullnode v0.5.6 起,高于层级 14 的调用证明会被拒绝(CallTierTooHigh),捆绑证明固定在层级 14,授权证明固定在层级 10 |
| 证明上限 | 创世 max_proof_bytes:链 17 至 20 为 4 MiB(4,194,304);链 13 至 16 为 8 MiB;没有该字段的创世(如链 12)为 2 MiB(MAX_PROOF_BYTES) | 2 MiB 高于已部署 guest 所能产生的所有不含哈希表的证明,低于携带 keccak 的证明;4 MiB 两者皆可容纳 |
| gas | 每个 cpu 行 1;每个 Poseidon2 absorb 行 3;每个 SHA256 行 64;每个 KECCAK 行 192 | 在 cpu 表的 GAS 列中累计,并被证明不超过 GAS_LIMIT;验证者拒绝高于 gas_max(t, k, s) = (2ᵗ − 1) + 2ᵗ⁻² + 191·(2ᵏ/32) + 63·(2ˢ/64) 的上限,对不含哈希的层级 10 头部为 1,279,对层级 14 头部为 20,479 |
在约束集 6 下的实测大小(Apple Silicon,release 构建):不含 keccak 的生产证明在档位 10 为 1,298,729 字节,在档位 12 为 1,359,978 字节;含 keccak 的档位 10 证明为 3,198,430 字节,超过 2 MiB 的默认上限,低于链 13 至 15 的 8 MiB。已停用的 2 进 2 出捆绑包 guest 程序的档位 14 证明,在链 8 上实测为 1,321,773 字节,在链 13 上为 1,418,406 至 1,420,423 字节。链 14 的 4 进 4 出隐藏资产捆绑包 guest 程序仍为档位 14,两张哈希表均不使用,其证明在链 14 上实测为 1,422,116 和 1,423,170 字节(2026-09-20);链 15 固定了同一个 guest 程序(hc_bundle 未变);捆绑包证明在那里声明 public_log_height 为 4,即八个绑定字的高度,节点在构建验证密钥之前会拒绝任何其他值。2026-09-19 在链 13 上生成的含 keccak 的 ERC-20 approve 调用证明(档位 16)为 3,412,405 字节;自 fullnode v0.5.6 起,此后的每条链都会拒绝该调用,因其档位为 16。
约束集 7 的盲化列和表下限使无哈希证明增大约 5%:fullnode 于 2026-09-28 实测,fib 在档位 10 为 1,367,688 字节,在档位 12 为 1,424,299 字节,含 keccak 的档位 10 证明为 3,283,898 字节,档位 14 的隐藏资产捆绑包证明为 1,498,821 字节(guest v1)和 1,497,156 字节(guest v2)。自那时起,捆绑包证明声明 public_log_height 为 7。约束集 8 的增幅不到 1%。2026-09-30 在链 18 上的实测(Apple Silicon):认证证明为 1,364,940 至 1,375,371 字节(档位 10),捆绑包证明为 1,494,151 至 1,504,260 字节(档位 14,guest v3),调用证明在档位 14、无哈希表时为 1,498,589 字节,在档位 14、使用 SHA-256 表时为 1,911,152 字节,在档位 12、使用 Keccak 表时为 3,346,865 字节。全部符合链 18 至 20 的 4 MiB 上限。
验证在验证密钥已预热时约需 16 ms,首次构建某个 (tier, heights) 密钥时约需 230 ms。test FRI 配置(16 次查询)仅供测试套件使用,并不安全;链 20 的创世块固定使用 production。Fullnode v0.6.8 不改变任何约束:链 20 固定的 hc_bundle 和 hc_auth 与链 17 至 19 相同。
可以复现的哈希构造
带域的 BLAKE3:blake3(domain || data),域字符串之后不加分隔符。
绑定域。 链 20 的创世块设置了 binding_domain: 1(fullnode v0.6.8,审计 v6 BIND-1)。在这样的链上,此前仅绑定链 id 的每一个证明绑定和每一条已签名的操作消息,现在还会绑定创世哈希:域的版本号加一,32 字节的创世哈希置于原像的最前面,位于链 id 之前。于是,为某一条链生成的证明或签名在其他任何链上都无法通过验证,即便那条链复用了同一个链 id 和密钥,重新创建链时就是如此。绑定字是证明的公开输入,因此没有任何 guest 程序或验证密钥发生变化。下表给出两种形式;链 14 至 19 逐字节使用第一种。此前已绑定创世块、且保持不变的有:投票和提案(自 consensus_domain: 1 起)、v2 Registration,以及 AdmitValidator 投票。未变动的有:桥的铸造联合签名(见上文)。钱包需要 v0.6.8 才能在链 20 上发送交易,并按链 id 而不是节点自称的内容来决定使用哪种形式;rand_getLimits.binding_domain 会报告该值。
| 值 | 构造 |
|---|---|
| 透明地址 | blake3("rand-address" || dilithium2_public_key),base58 |
| 交易 id | blake3("rand-txid-3" || bincode(view)),其中 view 是将捆绑包的证明字节替换为 blake3(proof)、将其认证证明替换为 blake3(auth_proof) 后的交易;auth_commit 原样进入。直到链 16,域为 rand-txid-2,不含认证字段 |
| 交易绑定 | 链 14 至 19 为 blake3("rand-tx-bind-1" || bincode(chain_id, bundle', action'));链 20 为 blake3("rand-tx-bind-2" || bincode(genesis_hash, chain_id, bundle', action'))。八个小端序 u32 字,其中 ' 表示将 bundle.proof、bundle.auth_proof 以及 Aggregate 的 proof 替换为空向量;bincode 使用定宽小端序整数。每个捆绑包证明和认证证明都基于这些字生成,并依据这些字验证 |
| 调用绑定 | blake3("rand-call-bind-1" || …),在链 20 上为 rand-call-bind-2,创世哈希在最前:即交易绑定的构造,并将 Call(或 Invoke)自身的 proof 也置空。在 hardening_v6 创世块上,例如链 20,调用证明的公开段是程序的部署时公开字,后接这八个字;Invoke 的公开段还会在其后追加其状态转换作为上下文字 |
| 程序 id | blake3("rand-program" || base_pc as 4 LE bytes || words as 4 LE bytes each) |
| p2p 身份 | 由 blake3("rand-p2p-identity" || seed) 得到的 ed25519 密钥 |
| 桥资产 id(按每个支持资产) | blake3("rand-bridge-asset" || token_chain as 2 BE bytes || token_address (32 bytes))——rand_bridgeAssetId 计算它,rand_getAssets 按每个支持资产行打印它;它在链路上标识一种源币,而不是代币 |
| RPL 代币资产 id,原生 | blake3("rand-rpl-asset" || bincode(name, symbol, decimals, authority, initial: Option<InitialMint>, salt)),salt 为创建者选定的 32 字节;以十六进制和 rpl1… 形式显示 |
| RPL 代币资产 id,桥接 | blake3("rand-rpl-asset" || bincode(name, symbol, BRIDGE_DECIMALS, salt))——不基于 (chain, token) 对,因为一种桥接代币可跨越多个支持资产 |
| 桥存款盲化因子 | r = blake3("rand-deposit-r-1" || mu),这是准入对 BridgeAttest 接受的唯一 r(否则为 WrongDepositBlinding) |
| 桥手续费票据盲化因子(链 20) | 存款的手续费票据:r = blake3("rand-bridge-fee-r-1" || mu);销毁的手续费票据:r = blake3("rand-bridge-burn-fee-r-1" || txid);两者都读作八个小端序字。这些票据没有信封:每个字段都是公开的,所有者是创世块中的 bridge.fees.recipient |
| 桥接收方 | blake3("rand-shielded-recipient" || pk_bytes || kem_ek),作用于完整的屏蔽地址 |
水龙头 Mint 消息 | blake3("rand-mint-2" || bincode(chain_id, cm, pk, time, r, envelope, amount));在链 20 上为 blake3("rand-mint-3" || bincode(genesis_hash, chain_id, cm, pk, time, r, envelope, amount)) |
TokenMint、SetAuthority 消息 | blake3("rand-rpl-mint-1" or "rand-rpl-authority-1", chain_id || asset_id || nonce || body);铸造的 body 包含 blake3(bincode(envelope))。在链 20 上为 rand-rpl-mint-2 和 rand-rpl-authority-2,创世哈希在最前 |
Registration 消息 | 在设有 staking.registration_v2 或 binding_domain: 1 的创世块上,例如链 20(那里拒绝 v1 注册):blake3("rand-register-2" || bincode(genesis_hash, chain_id, validator, payout));其他情况为 blake3("rand-register" || bincode(chain_id, payout)) |
AdmitValidator 投票(链 20) | blake3("rand-admit-validator-1" || genesis_hash || candidate_address),由每位投票的验证者签名 |
Unbond 消息 | blake3("rand-unbond" || bincode(chain_id, validator, amount, nonce));在链 20 上为 blake3("rand-unbond-2" || bincode(genesis_hash, chain_id, validator, amount, nonce)) |
Withdraw 消息 | blake3("rand-withdraw" || bincode(chain_id, validator, amount, nonce, time, r, envelope));在链 20 上为 rand-withdraw-2,创世哈希在最前 |
| Merkle 节点(状态树) | blake3("rand-merkle-node" || left || right) |
| 代币注册表叶子 | blake3("rand-token-leaf-1" || bincode(TokenInfo)),按索引顺序,位于 tokens_root 之下 |
| 状态根 | 在链 20 上,为 blake3("rand-state-admitted-1" || inner || admitted_root),包裹着 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),其中 program_state_root = blake3("rand-program-state-1" || cells_root || vaults_root)(RPL-2),两个水龙头计数器和两个实时价格各为 8 个大端序字节,外层包装承诺了已准入集合(staking.admission_by_vote)。链 19 的状态根是 blake3("rand-state-7" || …),作用于直到 byte_price 的相同字段(创世块的 staking 部分会在 tokens_root 之后追加这三项,域为 rand-state-5,如链 15 至 17;创世块的 gas.dynamic 部分会追加两个价格,并将域改为 rand-state-7);链 14 上为作用于前六项的 rand-state-4;在没有 bridge、tokens 或 aggregation 部分的链上,为作用于前四项的 rand-state-2(共识) |
带域标签的 Poseidon2:H(domain, msg) = sponge_hash([domain, msg...]),标签是第一个被吸收的字,每个字是一个 Goldilocks 元素,4 个元素的摘要被拆分为八个 u32 字(每个元素先低半部分,后高半部分)。域标签:
| 标签 | 值 | 构造 |
|---|---|---|
NK | 1 | 查看密钥 nk = H(1, sk) |
PK | 2 | 票据公开值 pk = H(2, nk) |
NF | 3 | 作废符 H(3, nk || cm) |
CM | 4 | 票据承诺 H(4, pk(8) || from(8) || amount_lo || amount_hi || asset || time || r(8)),共 28 个字 |
OVK, KEM_SEED | 5, 6 | 出站查看密钥与 ML-KEM 种子,通过计数器模式的宽哈希 [domain, nk, counter] 得到 |
NODE | 7 | 树节点 H(7, left || right);空叶子全为零 |
HC | 8 | 程序摘要;标签、base_pc 和 len 位于第一次置换的容量通道中,随后每个分组吸收四个字 |
OUT | 9 | 转账客户程序的输出摘要 |
IN | 10 | H_IN:容量头 [10, n_in, 0],然后是 4 个字的盐值,再后是输入 |
BUNDLE | 11 | 已退役的 2 入 2 出捆绑交易对 47 个公开字计算的摘要;现在没有任何链再固定该客户程序 |
| hidden bundle | 64 | 隐藏资产捆绑交易对 81 个字计算的摘要,用于链 14 至 16:锚点、四个作废符、四个承诺、fee、burn_a、burn_r(各两个字)、burn_asset、time,最后是污点字。在链 17 至 20 上(捆绑客户程序 v3)为 89 个字:授权承诺 c 的八个字紧接在污点字之前。私有资产 A 不在其中。该标签仅限节点本地使用,与引入的标签 1 至 15 保持距离 |
AUTH | 65 | 授权承诺 c = H(65, nk || salt),16 个字,每个捆绑交易使用新的 256 位盐值;由捆绑证明在其摘要中发布,并由授权证明作为其八个输出字发布,由捆绑交易以 auth_commit 携带(链 17 至 20)。与 64 一样仅限节点本地使用 |
PUB | 15 | H_PUB:头 [15, n_pub, 0],无盐值;对于程序,在链 20 上这些字在部署时固定(至多 32,768 个),每次调用的段是这些字后接调用绑定的八个字,并且调用依据该段的摘要进行校验;对于捆绑证明和授权证明,该段是交易的八个绑定字;未设置 max_program_public_words 的创世配置只允许空段 |
hc、H_IN 和 H_PUB 不是简单的带消息前缀的海绵函数:头部放入容量中,使 cpu 表中的摘要行每次置换恰好吸收四个字。请使用引入的 crate(randprotocol_zkvm::hash::{input_digest, public_digest}、isa::Program::digest)来复现它们,而不要手工实现。
普通海绵函数不做填充,因此在第一个分组内 [a] 与 [a, 0] 的哈希相同。上面的每种构造都在各自的标签下对定长消息做哈希。对于长度可变的数据,约束集 7 增加了一个绑定长度的海绵函数(hash::sponge_hash_len,即 POSEIDON2_LEN 系统调用):它从状态 [0, 0, 0, 0, n, 0, 0, 0] 开始(n 为字数),并且至少置换一次。对同样的字,它给出的是另一个哈希;链上的票据、作废符、根或摘要都不使用它。
需要记住两点:hc 具有绑定性但不具有隐藏性(没有盐值,因此可以确认对某个程序的猜测,同一程序的两次部署也可以被关联);H_IN 既有绑定性又有隐藏性(每个证明使用新的 128 位盐值),因此在持盐者将其打开之前,公布 H_IN 不会泄露任何关于输入的信息。
密钥派生
| 密钥 | 来源 |
|---|---|
| Dilithium2 密钥对(节点) | 由 32 字节种子确定性生成 |
| libp2p ed25519(节点) | blake3("rand-p2p-identity" || seed) |
查看密钥、pk、ovk、ML-KEM 密钥对(钱包) | 由花费密钥按上表做 Poseidon2 派生;ML-KEM 密钥对为 MlKem768::from_seed(kem_seed) |
论文页面上的论文给出了安全性论证:后量子方案的选择、FRI 可靠性核算以及保留 80 次查询的原因,还有轨迹长度的上界。