Rand Protocol
文档
指南

桥接

按实际构建的守护者桥:源链锁定、附带 Dilithium2 联合签名的守护者证明、存入一个 note 的 BridgeAttest、在单个捆绑包中完成提现的 BridgeBurn;并说明线路格式、zUSD 的七种底层资产、zUSD 在铸造与销毁时的费用、铸造上限与暂停机制、带持有证明与延迟的 rules-v2 轮换,以及 chain 21 上已上线的内容。

使用本页面接入桥:在源链上构建存款、在 Rand 上提交证明(attestation)、将桥接资产销毁并转回源链,以及读取桥的公开状态。链 21,即当前的 testnet(自 2026-10-12 10:31 UTC 起运行,fullnode v0.8.0),原样沿用链 20 的 bridge 部分:八名守护者(guardian),起始守护者集合索引为 1;八名后量子守护者;一把暂停密钥;面向 Ethereum、BSC、Tron 和 Solana 的发送方(emitter)(前三者为 2026-09-30 重新部署的端点);rules-v2 规则组,其全局铸造上限为每滚动 24 小时 4,000 zUSD;入站重放下限;以及 v0.6.8(链 20)新增的两个规则组:对每笔铸造和每笔销毁收取 10 bps 的费用,以 zUSD 计收;以及轮换规则,要求后量子集合或暂停密钥的轮换证明对新密钥的持有,并等待一天。与链 14 不同,其创世文件列出了 zUSD 的全部七种支撑资产,并带有自链 14 起历次切换中一路沿袭下来的托管资产(自链 14 沿袭)。rand_getAssets 与浏览器的 /bridge/assets 给出每种支撑资产当前的 locked。

The flow

source chain (Ethereum / BSC / Tron / Solana)                 Rand
┌────────────────────────────────┐                           ┌────────────────────────────────┐
│ lock tokens, emit              │  guardians observe,       │ BridgeAttest transaction        │
│ (amount, token, to, fee)       │  sign mu = keccak²(body)  │   admission → one deposit note  │
└────────────────────────────────┘  threshold n·2/3 + 1      │   computed by the chain          │
              ▲                     + Dilithium2 co-signers  │                                  │
   release after guardian         guardian committee         │ BridgeBurn transaction           │
   signatures on the burn  ◄───── secp256k1 keys ◄─────────  │   one bundle: slots 0–1 burn the │
   message                        read the burn log          │   asset, 2–3 pay the RAND fee;   │
                                                             │   emits a sequenced burn message │
                                                             └────────────────────────────────┘

入站:价值在源链上被锁定,守护者出具证明,任何人都可以将该证明提交到 Rand,账本随即为接收方追加一个存款 note——该 note 的 asset 字为被桥接代币的注册表索引。出站:一笔 Rand 交易销毁该资产的 note 并发出一条消息;守护者对该消息出具证明;源链合约予以释放。Rand 从不验证源链的区块头或状态证明:守护者委员会的签名就是全部的信任模型。

Trust model and chain ids

与合约共用的桥内链 id(bridge-codec):

chainid
Rand1
Ethereum2
BSC3
Tron4
Solana5

守护者是一个由 secp256k1 密钥组成的委员会;守护者的身份是 keccak256(uncompressed_pubkey[1..]) 的最后 20 字节,即 Ethereum 的地址派生方式,因此同一把密钥可用于 EVM 合约、Solana 程序和 Rand。规模为 n 的集合,法定人数为 n·2/3 + 1。每个签名都必须是 low-s(s ≤ n/2),以杜绝可延展性。守护者签署的摘要为 mu = keccak256(keccak256(body_bytes))。守护者集合的轮换是来自治理发送方 (chain 1, keccak256("rand-bridge-governance")) 的一份证明,必须携带 new_index == current + 1,并且必须由当前集合签署,而不是已被取代的集合;离任集合为在途转账保留 86,400 秒的宽限窗口。(这指的是 ECDSA 集合。后量子集合和暂停密钥通过各自的操作轮换,自链 20 起这些操作需等待一天的延迟:见下文。)如果 n·2/3 + 1 名守护者合谋,他们可以铸造任意数额的价值;桥加固限定了数额和速度的上界,但并不能消除这一风险。

交易绑定。 Rand 交易不携带签名:销毁由其 bundle 的 STARK 证明授权。自链 14 起,每个 bundle 证明都是针对 Transaction::binding 生成并据其验证的——这是对整笔交易(链 id、含四个信封的唯一 bundle、动作)取的哈希,仅将证明字节置空。自链 17 起,一个 bundle 携带两个证明,即 bundle 证明和花费授权证明:绑定将二者都置空,保留 bundle 的 auth_commit,二者都据此验证。如果一份待处理销毁的副本更改了 to、relayer_fee 或 (to_chain, token),或者一份 attest 的副本更改了其 time 或信封,则证明验证失败(TxError::InvalidBundleProof)。自链 20 起,绑定还纳入了创世哈希(创世文件 binding_domain: 1:对创世哈希、链 id、bundle 和动作使用 rand-tx-bind-2),因此为另一条共用同一链 id 的链生成的证明同样无法通过验证。钱包和中继者必须运行为其所提交的链构建的二进制:链 21 上为 v0.8.0(链 20 上为 v0.6.8,链 18 和 19 上为 v0.6.7);较旧的 rand 生成的证明和签名会被链 21 拒绝——而在链 21 上,较旧钱包的证明甚至无法解码,因为约束集 9 更改了每一个验证者密钥以及证明编码。

守护者和中继者守护进程不属于全节点;全节点携带共享的编解码器和接收端部分。

Wire format

所有整数均为大端序;线路版本为 1。

证明信封(Attestation envelope)

offsetlenfield
01version = 1
14guardian_set_index
51n_sigs
666·n_sigssignatures: index (1), r (32), s (32), v (1)
6 + 66·n_sigs…body

消息体头部——51 字节,其后为载荷

offsetlenfield
04timestamp
44nonce
82emitter_chain
1032emitter_address
428sequence
501consistency_level
51…payload

Transfer 载荷——id 1,133 字节

offsetlenfield
01id = 1
132amount (u256; Rand uses the low 128 bits, and a deposit must also fit a note’s u64)
3332token_address
652token_chain
6732to — the recipient hash, see below
992to_chain
10132fee (u256): the relayer’s portion of amount

GuardianSetUpgrade 载荷——id 2:id(1)、new_index(4)、n(1),随后是 n 个 20 字节的密钥。

Ethereum、BSC 和 Tron 的地址为 20 字节,在 32 字节槽位中左侧以 12 个零字节填充;Solana 地址则占满整个槽位。Transfer 必须来自 emitter_chain 所对应的该链已注册发送方,且 token_chain 必须等于发出消息的链。一份证明最大为 16 KiB。桥加固中的任何措施都没有更改线路格式、编解码器或已部署的源链端点:后量子联合签名和治理消息随 Rand 交易传递,而不在证明之中。

The 32-byte recipient

一个 shielded 地址为 1,216 字节,而线路上只有 32 字节的空间。to 字段携带的是它的哈希:

to = blake3("rand-shielded-recipient" || pk_bytes || kem_ek)     // ShieldedAddress::recipient_hash

存款人在源链上写明该哈希,守护者对其签名,提交者将完整地址放入 BridgeAttest 动作,账本重新计算哈希并拒绝不一致的情况——否则提交者就可以选择由谁接收他人的存款。源链前端必须接受这种形式;钱包会将其打印出来。链 11 曾短暂地将该槽位直接作为接收方 id;该地址形式已随链 12 被回退(账户与地址)。

One token, many backings: zUSD

资产注册表不再属于桥。它已移入账本层面的 RPL 代币注册表,由原生代币和桥接代币共用,链的状态根以 tokens_root 对其作出承诺。桥接代币是一个注册表条目,其铸造权限为 Bridge { backings },不再对应单个 (chain, token) 对。第一个此类代币 zUSD 由锁定在四条链上的 USDT 和 USDC 支撑——Ethereum、Binance Smart Chain 和 Solana 同时承载这两种币,Tron 只有 USDT(Circle 已停止在 Tron 上提供 USDC):

chain  token (32-byte wire form)                                                  coin  decimals
2      000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7           USDT  6
2      000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48           USDC  6
3      00000000000000000000000055d398326f99059ff775485246999027b3197955           USDT  18
3      0000000000000000000000008ac76a51cc950d9822d68b83fe1ad97b32cd580d           USDC  18
4      000000000000000000000000a614f803b6fd780986a42c78ec9c7f77e6ded13c           USDT  6
5      ce010e60afedb22717bd63192f54145a3f965a33bb82d2c7029eb2ce1e208264           USDT  6
5      c6fa7af3bedbad3a3d65f36aabc97431b1bbe4c2d2f6e0e47ca60203452f5d61           USDC  6

在链 21 上,zUSD 的索引为 1,rpl1xtj6k2x8strx8c2d5fjs50ltztck5ykms4ve7nmzmstf6fhn0v0spelqtx,名称为 “Shielded USD”,8 位小数,列于创世文件中(链 21 的创世文件仅列出这一种代币),与链 15 至 20 相同,其名称、符号和盐值均与链 14 上相同,因此其资产 id(32e5ab28c782c663e14da2650a3feb12f16a12db85599f4f62dc169d26f37b1f)和 rpl1… id 原样沿用(浏览器:GET /tokens/1)。在链 14 上,它由一笔 register_bridged_token 交易在高度 256 处注册。

Backing { chain: u16, token: [u8; 32], decimals: u8, locked: u64, minted_today: u64, mint_day: u32 }

一个 (chain, token) 对在整个注册表中至多支撑一种代币(TokenError::BackingTaken);一种代币有 1 至 32 个支撑资产;decimals 是源币自身的位数,0 至 18,绝非被桥接代币的 8。池化支撑是一项明示的风险:这七种币可按面值与 zUSD 相互兑换,因此其中任何一种脱锚都会稀释每一位 zUSD 持有者,且 Rand 一侧对单一支撑资产的占比没有上限。

zUSD 是一篮子资产。 持有者承担的是所有已列入储备资产的风险,而不只是自己所存入那种币的风险:一枚 zUSD 是对资金池的债权,可对任何 locked 足够的储备资产主张。没有任何机制为储备资产定价。锁定任一已列入币种 1 个单位即铸造 1 枚 zUSD,销毁时可指定任何 locked 足以覆盖的储备资产,因此已脱锚的币种可以按面值存入,再将 zUSD 销毁以换出仍保持锚定的币种,直到健康的储备资产被抽空。损失受滚动铸造窗口和实际锁定的托管资产所限:在链 20 和链 21 上,任意 24 小时内所有储备资产合计最多可铸造 4,000 zUSD,每种储备资产最多 100,000 zUSD(规则 v2),且经由健康储备资产流出的数额不会超过该储备资产所持有的数额。窗口限制的是速率,而非总量:它为暂停铸造争取时间,暂停后弱势币种的新存款被阻止,而销毁仍保持开放。没有按储备资产设置的赎回限额,也没有按储备资产的暂停开关。在链 14 上、规则 v2 之前,唯一的限制是源端点自身的暂停者和按代币设置的速率上限。

不变式:total_supply == Σ backings.locked。 TokenRegistry::lock(即 BridgeAttest)在一次调用中将证明的数额同时加到该储备资产的 locked 和代币的 total_supply 上;TokenRegistry::release(即 BridgeBurn)则从两者中同时减去释放的数额。没有其他操作会写入这两个字段,并且代币叶子把每个储备资产的 locked 都承诺进 tokens_root,因此两个节点只要对其中一个存在分歧,就会在状态根上出现分叉。源端点在锁定时不收取费用,释放时扣除全部已证明的数额,因此每个储备资产的 locked 等于该端点的托管资产,传输中的消息除外。自链 20 起的费用不会破坏这一点:费用是一张仍保持已发行且有储备支持的 zUSD 票据,所以存款仍锁定总额,而销毁只释放 amount − fee。在链 20 切换时,端点自身在释放时收取的 10 bps USDT 费用被设为 0;现在费用在 Rand 上收取。

链上传输的数额以 8 位小数为单位,无论源币种本身的小数位数是多少:从 Ethereum 存入 1 USDT(6 位小数)会以 100_000_000 个单位锁定。只有索引 0,即 RAND,使用本链的九位小数。

代币必须在首次存款之前列入。 不再有首次出现即注册的机制:若证明所指的 (chain, token) 无人列入,则以 BridgeError::UnlistedToken(token … of chain … is not listed on this chain)拒绝。资金会留在源端点的托管中,直到后量子守护者法定人数将该币种列入(创世后列入)。

asset_id = blake3("rand-bridge-asset" || token_chain (2 bytes BE) || token_address (32 bytes))

rand_bridgeAssetId(token_chain, token_address) 在任何链上仍然会计算此值,Transfer 载荷的 (token_chain, token_address) 也解析为它,但它不再是注册表的键。票据和钱包所携带的是代币的索引,一个紧凑的 u32:索引 0 是 RAND,且从不在注册表中。rand_getAssets 为每个储备资产返回一行,同一代币的所有行使用同一个索引:

[{ "index": 1, "chain": 2, "token": "000000000000000000000000dac17f…", "asset_id": "…", "decimals": 6,
   "locked": "0", "mint_cap_per_day": "10000000000000", "minted_today": "0", "mint_day": 20716 }]

locked、mint_cap_per_day 和 minted_today 是以 8 位小数为单位的十进制字符串,与自链 14 起 RPC 提供的每个 u64 数额一样;index、chain、decimals 和 mint_day 仍为数字。代币级方法 rand_getTokens、rand_getToken、rand_getTokenSupply 将一个桥接代币作为一行返回,其中包含它的所有储备资产(RPL 代币)。浏览器的 GET /bridge/tokens 是桥接所接受币种的静态许可列表。

入站:BridgeAttest

Action::BridgeAttest { attestation: Vec<u8>, recipient: ShieldedAddress, r: Word8, time: u32,
                       asset: u32, envelope: Envelope, pq_signatures: Vec<PqSignature> }

PqSignature { index: u8, signature: Vec<u8> }        // Dilithium2, 2,420 bytes

由一笔普通的屏蔽交易承载,其唯一的隐藏资产捆绑只支付 0.001 RAND 的基础费用,别无其他:其中继者是在替一位尚未持有 RAND 的存款人付费。存款票据 { pk: recipient.pk, from: 0, amount, asset: <index>, time, r } 由链而非提交者根据守护者所签署的数额计算,因此中继者无法虚增或改向铸造;其承诺是交易不在链上传输中携带的那一个叶子。自链 20 起,票据的 amount 是总额减去铸造费用,链还会向金库追加第二张票据,即费用票据(铸造与销毁的费用)。

盲化因子是推导出来的,而非自行选择。 r 必须是

r = blake3("rand-deposit-r-1" || mu)        // read as eight little-endian words; mu = keccak256(keccak256(body))

即守护者法定人数所签署的摘要;否则以 BridgeError::WrongDepositBlinding 拒绝,且在任何签名运算之前就已拒绝。该字段仍保留在链上传输中,钱包的公开字段重建会从中读取它,只是现在对它施加了约束,且该规则同样适用于轮换的证明。在此之前,任何看到中继者待处理交易的人,都可以用自己的盲化因子提交同一份证明:接收者仍会得到一张票据,但中继者的交易在其为证明付出代价之后就失效了。固定了 r 之后,同一份证明在同一 time 下的两个提交者指向同一张票据,资金池会在承诺上将二者判为冲突。

time 和 asset 是存款人的两项预测,这样信封可以在交易存在之前就封好。time 受到与捆绑的 time 相同的窗口限制(自链 20 起为 1,024 个区块,取自创世的 proof_window_blocks;此前为 256),且刻意不做推导:没有任何规则能给出所有提交者都认同的 time,所以抢跑者仍可在窗口内选择另一个 time,得到同一接收者、同一数额、同一资产的另一张票据,接收者的钱包会以同样的方式找到它,而这两次提交则会在证明摘要上发生冲突。asset 必须等于注册表已为 (chain, token) 分配的索引,否则返回 TxError::AttestAssetMismatch。

Transfer 载荷的 fee 会被携带但不支付给任何人:没有可供支付的提交者身份,所以存款是总额,仅在创世设定了铸造费用的链上(链 20 和链 21)减去链自身的铸造费用。重放保护通过摘要实现:每个已消耗的 mu 都进入桥的 spent 集合,内存池会拒绝第二个争抢同一份证明的中继者。存款还受储备资产铸造上限的约束,自链 20 起也受全局上限约束,并在铸造暂停时被拒绝(B1、规则 v2)。两项上限都按总额计算。在创世设定了重放下限的链上(包括链 20 和链 21),若转账的 sequence 低于其源链的下限,则在检查任何签名之前以 BelowReplayFloor 拒绝(从链 14 沿用)。

rand bridge-mint <attestation hex or @file> --pq <JSON or @file> [--to rand1…] [--fee <RAND>] [--no-wait]
rand bridge-rotate <rotation hex or @file> --pq <JSON or @file> [--fee <RAND>] [--no-wait]

--pq 是必需的:即守护者的 Dilithium2 联合签名法定人数,格式为 [{"index":0,"signature":"<4840 hex>"},…]。在进行任何证明之前,钱包会解码证明、对照节点的 pq_guardians 和链 id 检查法定人数、检查接收者的哈希是否等于被证明的接收者,并读取已列入的索引;随后推导 r,封好接收者的信封(在有费用的链上按净额),并用自己的一个 RAND 捆绑支付。bridge-rotate 提交守护者集合的轮换(载荷 2),它不存入任何资产,并拒绝任何不是将 Rand 的集合推进一步的请求。由于存款票据的每个字段在那一笔交易中都是公开的,接收者的钱包通过遍历已提交的 bridge_attest 交易来重建发给它的存款票据,无需打开任何信封:恶意中继者可以封入垃圾数据,但无法隐藏票据。

出站:BridgeBurn

Transaction {
  chain_id,
  bundle: <hidden-asset bundle: slots 0–1 spend the token, slots 2–3 pay the RAND fee;
           burn_asset = asset, burn_a = amount, burn_r = 0, fee >= BRIDGE_BURN_FEE>,
  action: BridgeBurn { asset, amount, relayer_fee, to_chain, token, to }
}

自链 14 的隐藏资产捆绑起,一次销毁是一个捆绑(此前为两个):槽位 0–1 证明销毁者拥有该资产的票据,总额不少于 amount,其中 burn_a 是离开资金池的价值;槽位 2–3 以 RAND 支付费用。该费用为 BRIDGE_BURN_FEE,即 0.01 RAND,覆盖了捆绑的基础费用。relayer_fee 是 amount 的一部分(fee ≤ amount),由目标链上的释放合约划出,不是额外收费,因此 burn_a 恰好等于 amount。自链 20 起,链还会以 amount 的 10 bps 作为一张 zUSD 票据向金库收取销毁费用,释放额为 amount − fee;中继者的份额必须包含在该释放额之内(铸造与销毁的费用)。

销毁必须指明币种。 (to_chain, token) 必须是被销毁资产的某个储备资产(否则返回 TokenError::NotABacking),并且有两条规则在动作步骤检查,早于捆绑的证明:

  • 释放单位。 源小数位数 d < 8 的储备资产,以 10^(8−d) 为单位释放(6 位小数的币种为 100;18 位小数的为 1)。释放数额和 relayer_fee 都必须是其整数倍,否则返回 TokenError::NotReleasable(… is not a multiple of the release unit …):低于源币种最小单位的零头会滞留在托管中。
  • 该储备资产自身的托管。 释放数额必须 ≤ locked(对应该储备资产),否则返回 TokenError::InsufficientBacking(only … is locked in that backing, not the … asked for)。代币的总供应量可能足以覆盖释放,但必须足够的是这一个储备资产的托管。

在没有费用的链上,释放数额为 amount;在链 20 和链 21 上为 amount − fee。

to 必须具有正确的形态(链 2、3 和 4 上为以零填充的 20 字节;不得全为零)。账本在严格递增的 burn_sequence 下记录一条 BridgeBurnRecord,发送者槽位中为交易哈希,守护者读取销毁日志:

→ {"method":"rand_getBridgeBurn","params":[13]}
← { "sequence": 13, "amount": "99900000", "fee": "99900", "release_amount": "99800100",
    "body_hex": "…", "digest": "a4bc1c7d…63f4", "tx": "ceb9e3c2…0be7", "height": 2899 }

body_hex 是出站消息本身,守护者必须原样对其哈希并签名;其 Transfer 金额即 release_amount。上面的记录是 chain 20 的首笔销毁:在高度 2,899 处向 BSC USDT 销毁 0.999 zUSD,其中 0.000999 进入国库,0.998001 被释放。

rand bridge-burn <ASSET> <AMOUNT> <TO_CHAIN> <TOKEN> <TO> [--relayer-fee N] [--fee <RAND>] [--no-wait] [--yes]
rand bridge-message <SEQUENCE>       # one outbound message, verbatim, for a guardian to sign

钱包首先检查——在任何证明之前,依据对桥状态和代币注册表的一次读取——该链有桥,ASSET 在注册表中,TO_CHAIN 上的 TOKEN 为其提供支持且锁定量至少为 AMOUNT,并且 AMOUNT 与中继方费用均为整数个释放单位。在收取费用的链上,它随后会打印桥费用和将被释放的金额,并在证明之前询问确认(--yes 跳过该询问)。销毁随后证明一个捆绑包:在笔记本电脑上约 100 秒,证明约 1.5 MB;并且与 chain 17 以来的每个捆绑包一样,旁边还附带一份花费授权证明(几秒,约 1.37 MB)。

铸造与销毁的费用

自 fullnode v0.6.8 起新增,在 chain 20 上设定,并由创世组原样沿用至 chain 21

"bridge": { …, "fees": { "mint_bps": 10, "burn_bps": 10, "recipient": "rand1…" } }

链对每一笔桥接铸造和每一笔桥接销毁收取费用,以被桥接的代币计价,在 Rand 上以屏蔽票据(shielded note)的形式支付给国库地址 recipient。每项费率至多为 100 bps(1 %);在 chain 20 和 21 上两者均为 10 bps,即 0.1 %。费用为 ⌊amount · bps / 10⁴⌋,向下取整到支持资产的整数个释放单位(10^(8 − source decimals):6 位小数的 USDT 为 100 个单位,18 位小数的币为 1 个),因此费用与其余部分均为整数单位,取整后为零的费用不生成票据(ledger/bridge_notes.rs,bridge_fee)。

总额为 g 的存入(BridgeAttest)数额为 amount 的销毁(BridgeBurn)
费用⌊g · mint_bps / 10⁴⌋,向下取整到一个释放单位⌊amount · burn_bps / 10⁴⌋,向下取整到一个释放单位
用户一侧一张 g − fee 的存入票据销毁 amount(burn_a == amount,客户程序无找零);出站消息体证明 release_amount = amount − fee,端点恰好释放该数额
国库的票据fee,盲化因子 blake3("rand-bridge-fee-r-1" ‖ mu),取存入的 time 和资产fee,盲化因子 blake3("rand-bridge-burn-fee-r-1" ‖ txid),取捆绑包的 time
locked 与 total_supply增加 g减少 release_amount
校验依据按支持资产和全局的铸造上限,针对 g针对 release_amount 的 InsufficientBacking 与 NotReleasable;中继方的份额必须在其范围内(FeeExceedsAmount)

两张票据均已发行并有支持,因此 total_supply == Σ locked == custody 仍然成立:费用票据与其他票据一样是 zUSD,其价值一直锁定在源链上,直到有人将其销毁。BRIDGE_BURN_FEE,即销毁在其 RAND 槽位中支付的 0.01 RAND,保持不变且与此无关。

chain 20 的首次往返,读自节点:在高度 1,664 处从 BSC 存入 1 zUSD(4f6e6c54…1a64),存入者得到 0.999 zUSD,国库得到 0.001;在高度 2,899 处(序号 13)向 BSC USDT 销毁 0.999 zUSD,保留 0.000999 zUSD,释放 0.998001。之后(读取于 2026-10-01),BSC USDT 的支持量为 22.001999 zUSD,该代币的供应量 99.001999 zUSD 等于其各支持量之和。

国库如何找到其票据。 费用票据没有信封:它的每个字段都是公开的(金额来自交易和创世费率,资产和 time 来自交易,盲化因子由推导得出,所有者为创世 recipient)。地址为 recipient 的钱包在重建存入票据的同一次公开扫描中重建费用票据,每次同步读取一次 rand_getBridgeState.fees 以及各支持资产的小数位数。谎报费用的节点只能让某张票据找不到,而无法记入任何一张。存入者的钱包以同样方式计算自己的存入额,即总额减去费用。v0.6.8 或更高版本的钱包会这样做;在 chain 21 上该钱包为 v0.8.0,而更旧的钱包无论如何都无法在其上发送交易。

没有该组的链,即 chain 14 至 19,不收费用:存入按总额记入,销毁释放 amount。在 chain 20 之前,唯一的桥费用是源端点在释放时对 USDT 自行收取的 10 bps,发生在宿主链上;它在 chain 20 的切换时被设为 0。该组已提交到创世哈希,并在 rand-bridge-fees-1 之下并入桥根。编码见 fullnode docs/bridge.md §25。

桥加固

按支持资产的 locked 和 InsufficientBacking 保护的是赎回。它们不限制铸造:带有有效守护者法定人数的证明会铸造其所声明的数额。三层机制弥补这一缺口,第四层使代币无需切换链即可上架。它们均不改变源链端点、证明的线路格式或 bridge-codec。

B1:按支持资产的铸造上限与铸造暂停

创世 tokens.mint_cap_per_day 按支持资产适用:chain 21 设为 10000000000000,即每个支持资产每天 100,000 zUSD,与 chain 14 以来的每条链相同。没有 rules v2(chain 14)时,一天为区块时间戳的 timestamp_ms / 86_400_000;在 rules v2 下(chain 15 至 21),同一上限按区块时间的 cap_window_secs 滚动窗口计量,而全注册表范围的全局上限——chain 20 和 21 上为每 86,400 s 4,000 zUSD——是更紧的界限(rules v2)。超出上限的存入在守护者签名验证之前即被拒绝,返回 TokenError::MintCapExceeded,待窗口再次有余量时即可接受。

创世 bridge.pause_key 是一把 Dilithium2 密钥,与每个后量子守护者的密钥均不相同。两个不带捆绑包、不收费用的操作:

  • PauseMints { nonce, signature } ——仅由暂停密钥签名。它只能暂停。
  • UnpauseMints { nonce, pq_signatures } ——需要后量子守护者法定人数,绝不使用暂停密钥。

两者都携带并递增 pause_nonce,因此都无法重放。暂停期间,每个转账型 BridgeAttest 都会被拒绝,返回 BridgeError::MintsPaused;销毁和守护者集合轮换保持开放,因此暂停绝不会困住赎回。在 rules v2 下(chain 15 至 20),暂停期间上架也被拒绝。暂停密钥应由不同于守护者密钥的另一台机器持有。

rand bridge-pause --sig <HEX or @file> [--no-wait]        # no key file and no RAND needed
rand bridge-unpause --pq <JSON or @file> [--no-wait]

暂停所覆盖的情形:守护者守护进程被欺骗或被攻破而共同签署了伪造的铸造,而后量子法定人数的持有者并未如此。它不能防御该法定人数本身被盗:该法定人数可以解除暂停,在一个代币上最多上架 32 个支持资产,每个都有各自的每日上限,并在 rules v2 下替换后量子集合和暂停密钥。

B2:有界的区块时间戳

桥接链的区块时间戳决定守护者集合的到期和铸造上限的计日,因此领导者不得能够使其跳变。在有桥的链上:

  • 一条有效性规则,重放时同样检查:timestamp_ms ≤ parent.timestamp_ms + 60_000,否则为 BlockError::TimestampLeap。结合现有的回退规则(BlockError::TimestampRewind),区块时间只能向前推进,每个区块至多一分钟。
  • 一条投票规则,绝非重放规则:验证者拒绝为比自己时钟超前 15,000 ms 以上的区块投票。

因此桥接链上的每个验证者都需要 NTP:偏差超过 15 秒的验证者,既是有故障的领导者,也是有故障的投票者。在链于停机后追赶期间,区块时间每个区块最多可前进 60 s,因此铸造上限的一天可能比真实的一天翻转得更快——每累计一整天的滞后,至多多出一天的上限。

B3:每次铸造上的 Dilithium2 联合签名

每个 BridgeAttest,无论存入还是轮换,除其 ECDSA 法定人数之外,还必须携带对下列内容的 Dilithium2 签名法定人数:

M = "rand-bridge-pq-cosign-1" || rand_chain_id (u64 BE) || mu        // 63 bytes

mu 是 ECDSA 法定人数所签署的摘要;链 id 使某个 testnet 的联合签名无法在另一条 Rand 链上通过验证。这些签名位于操作的 pq_signatures 中,处于交易绑定之内,因此无法在副本上被剥离或替换。检查按先廉价后昂贵的顺序进行:quorum(n) ≤ len ≤ n(PqNoQuorum),索引严格递增(PqIndexOrder),每个索引 < n(PqIndexOutOfRange),每个签名恰为 2,420 字节(PqBadSignatureLength),最后检查每个签名都能在 pq_guardians[index] 下通过验证(PqBadSignature)。两个法定人数分别独立计数。创世 bridge.pq_guardians 与创世 ECDSA 集合按索引对齐(chain 20 和 21 上为集合 1)。payload-2 轮换只移动 ECDSA 集合;没有 rules v2 时,后量子集合在链的整个生命周期内固定,在 rules v2 下只有 RotatePqGuardians 能替换它(自 chain 20 起为其 V2 形式,带持有证明和一天的延迟)。chain 21 有八把,与 chain 15 至 20 的八把密钥相同;一次铸造需要其中六把(chain 14 有六把,需要五把)。

这份联合签名在链 20 和链 21 上仍然只绑定链 id:与下文的治理消息不同,它没有被移到创世哈希之后,因为生成它的守护者守护进程位于另一个代码仓库,尚未更改。若某条链是同 id 的副本,并且同样复制了守护者集合,它就会接受为原链所作的存款联合签名;存款交易自身的费用包自链 20 起已绑定创世哈希,而其中的证明(attestation)则没有。

B4:创世后列出桥接代币

普通地址永远无法创建 Bridge 权限的代币。有两种操作,每种都使用普通的 RAND 费用包,并且都受后量子法定人数约束:

  • RegisterBridgedToken { name, symbol, salt, chain, token, decimals, nonce, pq_signatures } — 在下一个索引处创建新的桥接代币,在 Rand 上为 8 位小数,受创世铸造上限约束,并带有其首个支撑资产。费用:费用包基础费加上注册表的 registration_fee(链 20 和 21 上为 1 RAND,即 1.001 RAND;自链 20 起,注册费被销毁而不是支付给提议者,在链 21 上基础费也被销毁,受创世 fees.burn_floor 约束)。
  • ListBacking { token_index, chain, token, decimals, nonce, pq_signatures } — 在已注册的桥接代币上增加一个支撑资产,起始为 locked = 0。费用:费用包基础费。

两者都携带并递增 list_nonce(否则返回 BadListNonce { expected, got }),并执行相同的检查:该链已有注册的发射器(BridgeError::NoEmitter),(chain, token) 尚未支撑任何代币,支撑资产少于 32 个,decimals ≤ 18。

rand token register-bridged --name … --symbol … --salt <hex32> --chain N --token <hex32> --decimals N --pq <JSON or @file>
rand token list-backing --asset <index> --chain N --token <hex32> --decimals N --pq <JSON or @file>

两条排序规则。守护者先对注册签名,等待其提交,读回分配到的索引,然后才对引用该索引的列表操作签名——注册原生代币无需许可,因此提前签名的列表操作可能发现其索引已被占用。并且代币要在 Rand 上列出之后,才能通过 setToken 在源端点启用:若顺序相反,存款可能在 Rand 尚无可用于记入的支撑资产时就已锁定,其证明会被以 UnlistedToken 拒绝,直到 Rand 追上。

治理消息

固定字节,绝不使用 bincode;所有整数均为大端序:

M_pause    = "rand-bridge-pause-1"        || chain_id u64 || nonce u64
M_unpause  = "rand-bridge-pq-unpause-1"   || chain_id u64 || nonce u64
M_list     = "rand-bridge-pq-list-1"      || chain_id u64 || nonce u64 || token_index u32 || chain u16 || token [32] || decimals u8
M_register = "rand-bridge-pq-register-1"  || chain_id u64 || nonce u64 || u8 len || name || u8 len || symbol || salt [32] || chain u16 || token [32] || decimals u8
M_rotate_pq    = "rand-bridge-pq-rotate-pq-1"    || chain_id u64 || rotation_nonce u64 || count u32 || key [1312] …   // rules v2
M_rotate_pause = "rand-bridge-pq-rotate-pause-1" || chain_id u64 || rotation_nonce u64 || key [1312]            // rules v2

M_pause/M_unpause 携带 pause_nonce,M_list/M_register 携带 list_nonce,M_rotate_pq/M_rotate_pause 携带 rotation_nonce。count 是密钥的数量,而不是字节长度;每个密钥是 Dilithium2 公钥的原始 1,312 字节,按新集合将采用的顺序排列。

自链 20 起,每条消息都绑定到创世哈希(创世 binding_domain: 1):标签的版本数字变为 2,32 字节的创世哈希位于标签与链 id 之间,其余部分不变——M'_pause = "rand-bridge-pause-2" || genesis [32] || chain_id u64 || nonce u64,rand-bridge-pq-unpause-2、rand-bridge-pq-list-2、rand-bridge-pq-register-2、rand-bridge-pq-rotate-pq-2 和 rand-bridge-pq-rotate-pause-2 同理(bridge/gov.rs)。为某一条链签名的暂停密钥或法定人数,因此不会为与其共享链 id 和密钥的其他任何链签名。以 …-1 形式签名的文件在链 20 和 21 上会被拒绝(BadPauseSignature、PqBadSignature)。链 14 至 19 使用上述 …-1 布局,逐字节一致。

规则 v2:轮换、滚动上限、暂停期间不得列出

创世 bridge 部分中的一个可选分组。链 14 没有它;自链 15 起的每条链都有,链 20 和 21 的取值相同:

"bridge": { …, "rules_v2": { "global_mint_cap_per_window": 400000000000, "cap_window_secs": 86400 } }
  • 后量子法定人数下的两种轮换。 RotatePqGuardians { new_pq_guardians, nonce, pq_signatures } 替换整个后量子集合;RotatePauseKey { new_pause_key, nonce, pq_signatures } 替换暂停密钥。两者与 PauseMints 一样不带费用包、不收费用,均由当前后量子集合签名,并且都携带并递增 rotation_nonce,因此一份已签名的法定人数恰好只授权一次轮换。新集合的长度必须与当前 ECDSA 集合相同,密钥为 1,312 字节,无重复,且不得是暂停密钥。没有宽限期:被取代的集合或密钥此后不再签署任何内容。自链 20 起这些 v1 操作被拒绝,轮换必须证明持有新密钥并等待一天(见下文)。
  • 滚动窗口上限。 每个支撑资产的 mint_cap_per_day 按 cap_window_secs 的滚动窗口计量,而不是按日历日;另有第二个注册表范围的上限 global_mint_cap_per_window,覆盖所有代币的所有支撑资产(TokenError::GlobalMintCapExceeded)。在链 20 和 21 上为 400,000,000,000 个单位,即每 86,400 s 4,000 zUSD。按日规则,23:59:59 与 00:00:01 的上限在两秒内相当于两倍的上限;按窗口规则,第二笔会被拒绝。
  • 暂停期间不得列出。 在 mint_paused 期间,RegisterBridgedToken 和 ListBacking 会被以 MintsPaused 拒绝。销毁和两种轮换保持开放。

轮换:持有证明与一天的延迟

规则 v2 留下了三个缺口:没有任何机制证明新密钥确有人持有(一个笔误,或一个无人持有的密钥,会使后量子法定人数永久失效);轮换在其所在区块内即生效;并且没有任何手段阻止恶意轮换。与 rules_v2(它是前提)并列的第二个可选分组在 v0.6.8 中引入(审计 v6,BRG-14),用于弥补这些缺口。链 20 设置了它,链 21 沿用:

"bridge": { …, "rotation": { "delay_secs": 86400, "needs_possession": true } }
  • 持有证明。 轮换必须是 RotatePqGuardiansV2 { new_pq_guardians, possession, nonce, pq_signatures } 或 RotatePauseKeyV2 { new_pause_key, possession, nonce, pq_signatures }:possession[i] 是新密钥 i 自己对法定人数所签署的同一条消息(M_rotate_pq 或 M_rotate_pause,自链 20 起为其 …-2 形式)所作的 Dilithium2 签名,每个密钥一个,按密钥顺序排列,每个 2,420 字节。v1 操作被以 PossessionRequired 拒绝。数量或长度错误会在验证法定人数之前被拒绝;持有证明签名无效(BadPossession { index })则在其之后被拒绝。
  • 延迟。 被接受的轮换会被记录为待生效,其 effective_at_secs = block time + delay_secs,并在时间戳达到该值的第一个区块末尾生效。在链 20 和 21 上为 86,400 s,即一天。在此之前,旧集合继续为每次铸造联合签名,旧暂停密钥继续负责暂停。接受一次轮换消耗一个 rotation_nonce,使其生效再消耗一个,因此在旧集合下汇集的铸造证明,会在集合变更时离开汇集池。每种轮换同时只能有一个处于待生效状态(RotationPending { effective_at_secs })。
  • 取消。 CancelRotation { kind, nonce, signature }(不带费用包、不收费用)会撤销 kind 类型的待生效轮换(0 = 后量子集合,1 = 暂停密钥)。它由当前暂停密钥签名,即与法定人数分开持有的那把密钥,自链 20 起签署的内容为 "rand-bridge-cancel-rotation-2" || genesis [32] || chain_id u64 || rotation_nonce u64 || kind u8,并消耗该 nonce。因此,被攻破的法定人数无法在一天之内轮换集合,而暂停密钥持有者可以在这一天内阻止它;即将接任的暂停密钥不能取消自己的上任。

签名工具是 rand-node bridge-gov(rotate-pq、rotate-pause、cancel-rotation,每个都有 message、sign 和 submit 步骤;submit 在发送前会重新验证每一行持有证明)。rand_getBridgeState 提供 rotation_rules 和 pending_rotations;在没有该分组的链(链 14 至 19)上,两者均为 null。ECDSA 守护者集合的轮换(payload 2)不受影响:它保留自己的规则和宽限期。链 20 和链 21 上均未执行过任何轮换。

从链 14 沿用

链 15 是从链 14 的桥接状态(相同的发射器和源端点)切出的,链 16、17 和 18 也各自以同样方式从其前一条链切出。链 19 是从链 18 的状态切出的,只有一处改动:其 Ethereum、BSC 和 Tron 发射器是 2026-09-30 重新部署的端点(链 2 和 3 为 0x7af6b17047c1db6cb54347fdea45cf9179075bfa,链 4 为 0x6410797df959987a5baf65b5fab97edeb34d5163)。链 18 从未信任它们,因此那里没有从它们铸造任何内容。链 20 是在先停止桥接之后,从链 19 在高度 7,258 的快照切出的,沿用链 19 的发射器、守护者、后量子密钥和暂停密钥不变;它新增的是费用和轮换分组。链 21 于 2026-10-12 从链 20 在高度 814,702 的桥接状态切出(桥接于 07:40–07:41 UTC 停止,快照于 07:41 生成),并保持其中每一项——发射器、守护者、后量子密钥、暂停密钥、两个分组——不变;中继器和守护者已使用 v0.8.0 的 rand 为链 21 重新启动。Solana 的发射器和 Rand 端发射器自链 14 起未变。链 21 的创世包含:

  • 托管。 100.001999 zUSD,以三张创世 zUSD 票据(10、89.002999 和 0.999 zUSD)的形式存在,对应 Ethereum 上锁定的 22 USDT、BSC 上的 23.001999 USDT、Tron 上的 30 USDT 和 Solana 上的 25 USDT,所有 USDC 支持资产均为 0:这是快照时链 20 的 locked,与各端点的托管量一致。total_supply == Σ locked 在区块 0 时等于各端点的托管量。历史:链 14 上留下的 10 zUSD 在链 15 至链 20 中是一张创世票据;链 18 的创世将其记为 Tron 上锁定的 9 USDT 和 Solana 上的 1 USDT,链 19 的创世则在链 18 上完成再平衡后记为 Solana 上的 10 USDT。链 20 的创世以两张票据(10 和 89,即链 19 的净存入减去其销毁)携带 99 zUSD;链 20 的两笔 BSC 存入和一笔销毁,以及国库从中收取的 10 bps 费用,构成其余部分。
  • 守护者集合索引 1,即源端点当前所处的索引;下一次 ECDSA 轮换必须携带索引 2。八个后量子密钥和暂停密钥沿用链 15 的,未作更改。
  • burn_sequence 14:链 14 的销毁止于 6,链 15 至链 18 的创世从 7 开始,链 18 发生了一次销毁(7),链 19 从 8 开始。链 19 发生了五次销毁,序号为 8 至 12,链 20 从 13 开始并发生一次销毁(13),因此链 21 从 14 开始,没有任何按序号建立索引的端点或守护进程会两次看到同一序号。
  • 重放。 更早一条链的铸造仍能通过 ECDSA 法定多数,但其后量子联合签名绑定了该链的 id,因此在链 21 上会被拒绝。链 15 的创世没有入站重放下限(min_inbound_sequence 为 null),所以当时依靠的是联合签名,以及守护者绝不为新链 id 联合签署旧的锁定。自链 16 起,创世设置了该下限:按源链分别设定,来自该链发射方的转账证明所能携带的最低 sequence。链 21 的下限为 { "2": 3, "3": 5, "4": 3, "5": 7 }:即对链 20 做快照时各端点的下一个出站序号,此前每一笔锁定都已在链 19 或链 20 上完成铸造。(链 20 的为 { "2": 3, "3": 3, "4": 3, "5": 7 };链 18 的为 { "2": 2, "3": 2, "4": 2, "5": 2 };链 19 的为 { "2": 1, "3": 1, "4": 1, "5": 4 },其中每个重新部署的端点上的序号 0 是运营方自己的锁定,绝不能铸造。)低于该下限的转账会被以 BelowReplayFloor 拒绝,无论它携带何种法定多数和联合签名。守护者集合轮换不受其约束。该下限在创世时固定,被纳入创世哈希,并折入桥根。

链 14 上的启动顺序(历史)

2026 年 9 月,桥在早期测试网链 14 上启动的过程如下:

  1. 使用 bridge 部分(含 pq_guardians 和 pause_key)以及设置 registration_fee 和 mint_cap_per_day、且不列出任何代币的 tokens 部分来切出该链。已于 2026-09-20 完成。
  2. 启用水龙头,使部署者钱包可以获得 RAND。已完成。
  3. 部署者在 list_nonce = 0 时,凭法定多数对 M_register 的签名,用其首个支持资产注册 zUSD。已在高度 256 完成。
  4. 守护者读回索引,并对其余六条 ListBacking 消息逐条签名并依次提交。已完成:list_nonce 读数为 7。
  5. 对每个支持资产:先在 Rand 上列出,再在源端点上执行 setToken。
  6. 在四个源端点上进行守护者集合轮换(集合 0 → 集合 1,由当前集合 6 人中的 5 人签名),随后在 Rand 上提交同一份证明。链 14 存续期间 Rand 读到的一直是集合 0;链 15 从集合 1 开始。
  7. 对每条源链用小额资金做一次实际往返——桥入、铸造 zUSD、转账、销毁回桥——并将每个支持资产的 locked(rand_getTokenSupply)与端点的托管量对账。

公开状态

rand_getBridgeState:

{
  "enabled": true,
  "emitter": "01…",  "emitters": { "2": "02…" },
  "guardian_set_index": 0, "guardians": ["aabb…"],
  "pq_guardians": ["…"],
  "mint_paused": false, "pause_nonce": 0, "list_nonce": 0, "pause_key": "…",
  "registration_fee": "1000000000",
  "burn_sequence": 1,
  "rotation_nonce": 0,
  "rules_v2": { "global_mint_cap_per_window": "400000000000", "cap_window_secs": 86400,
                "global_minted_in_window": "…", "global_mint_headroom": "…" },
  "min_inbound_sequence": null,
  "rotation_rules": null, "pending_rotations": null,
  "fees": null,
  "assets": [ … ]
}

pq_guardians 是 Dilithium2 公钥,每个为十六进制形式的 1,312 字节。不再有 next_index:被桥接的代币在可以存入之前,就已在其注册时确定的索引下列出,因此没有需要预测的剩余索引。rotation_nonce 是下一个 M_rotate_pq/M_rotate_pause/M_cancel 必须携带的值;在没有该组的链上(链 14),rules_v2 为 null;在没有重放下限的链上(链 15 及更早),min_inbound_sequence 为 null;在链 21 上它是 { "2": 3, "3": 5, "4": 3, "5": 7 }(链 20 的为 { "2": 3, "3": 3, "4": 3, "5": 7 })。在没有这些组的链上(链 14 至链 19),rotation_rules、pending_rotations 和 fees 为 null。上面的示例展示的是结构,而非链 21 的取值:链 21 以 guardian_set_index 1 和 burn_sequence 14 启动,并返回 "rotation_rules": { "delay_secs": 86400, "needs_possession": true }、"pending_rotations": [] 和 "fees": { "mint_bps": 10, "burn_bps": 10, "recipient": "rand1…" }。没有 bridge 部分的链返回 {"enabled": false}。

不提供余额:被桥接的价值就是票据,钱包通过 rand asset-balance [INDEX] 从自己的票据中读取所持数量。桥根承诺了发射方、守护者集合、已花费摘要、销毁序号、后量子守护者集合,以及暂停密钥、暂停标志和治理 nonce(两个,在规则 v2 下还有 rotation_nonce),在创世设置了重放下限时还包括该下限,自链 20 起还包括轮换规则及任何待处理的轮换和费用组;它仅在有桥的链上才折入状态根。代币注册表及每个支持资产的 locked 则单独承诺于 tokens_root。浏览器的 GET /bridge 和 GET /bridge/assets 提供同一状态,后者附带以 8 位小数字符串表示的各支持资产存入和销毁总额;GET /tokens 和 GET /tokens/:id 提供注册表。

哪些是公开的,哪些是隐藏的

内容谁可以看到
一笔存入的金额、资产索引和接收方地址所有人,仅限那一笔交易
接收方之后花费的是哪张票据无人
一次销毁的金额、资产、币种、目的地和中继者费用所有人——守护者需要这些信息
桥费用票据的金额、资产和所有者,即国库(自链 20 起)所有人:它由公开字段重建
哪些票据为一次销毁提供了资金无人
被桥接的余额仅持有者,或查看密钥持有者
守护者集合、发射方、带有各支持资产 locked 的注册表、销毁日志所有人

已知缺口

来自全节点自身的清单:Rand 上不向任何中继者付费;存入的信封不绑定任何内容,因此恶意中继者可以封入垃圾数据,这也是钱包要根据公开字段重建存入票据的原因;账本层面的共享测试向量尚未在票据池上重建;bridge-burn 要在为证明付费之后才得知目的端点自身的策略;在被桥接的链上允许区块时间戳与父区块相等,因此串通的领导者可以让已被取代的守护者集合在其宽限窗口内继续有效;内存中的销毁日志没有上限;不存在、也未计划提供轻客户端或对源链状态的链上验证。

最近一次核对所依据的 fullnode 提交为 c0878c3f(链 21),日期为 2026-10-12。 若代码与本页不一致,以代码为准;请告知我们。