Rand Protocol
문서
가이드

브리지

구축된 그대로의 guardian 브리지: 소스 체인 잠금, Dilithium2 공동 서명이 포함된 guardian 증명, note를 입금하는 BridgeAttest, 하나의 번들로 출금하는 BridgeBurn을 다루며, 와이어 형식, zUSD의 일곱 가지 담보, mint와 burn 시 부과되는 zUSD 수수료, mint 한도와 일시 중지, 소유 증명과 지연이 적용되는 rules-v2 교체, 그리고 chain 20에서 현재 운영 중인 기능을 설명합니다.

이 페이지는 브리지 연동에 사용합니다. 원본 체인에서 입금을 구성하고, Rand에 어테스테이션을 제출하고, 브리지된 자산을 다시 소각해 내보내고, 브리지의 공개 상태를 조회하는 방법을 다룹니다. 현재 testnet인 체인 20(2026-10-01 06:17 UTC부터 운영, fullnode v0.6.8)은 bridge 섹션과 함께 생성되었습니다. 인덱스 1에서 시작하는 가디언 8명, 양자 내성 가디언 8명, 일시 중지 키, Ethereum·BSC·Tron·Solana의 emitter(앞의 셋은 2026-09-30에 다시 배포된 엔드포인트), 전역 mint 한도가 24시간 롤링 기준 4,000 zUSD인 rules-v2 그룹, 인바운드 리플레이 하한, 그리고 v0.6.8에서 새로 추가된 두 그룹이 포함됩니다. 두 그룹은 모든 mint와 burn에 zUSD로 징수되는 10 bps의 수수료, 그리고 양자 내성 세트 또는 일시 중지 키의 교체 시 새 키의 소유를 증명하고 하루를 대기하도록 하는 교체 규칙입니다. 체인 14와 달리 제네시스에는 일곱 가지 backing을 모두 갖춘 zUSD가 등재되어 있으며, 체인 14에서 이후 각 컷을 거쳐 이어져 온 커스터디도 이월됩니다(체인 14에서 이월된 항목). rand_getAssets와 탐색기의 /bridge/assets는 각 backing의 현재 locked 값을 제공합니다.

흐름

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에 제출하며, 원장은 수신자를 위한 예치 노트 하나를 추가합니다. 이 노트의 asset 워드는 브리지된 토큰의 레지스트리 인덱스입니다. 아웃바운드: Rand 트랜잭션이 해당 자산의 노트를 소각하고 메시지를 내보내면, 가디언이 그 메시지를 어테스트하고, 원본 체인의 컨트랙트가 자산을 해제합니다. Rand는 원본 체인의 헤더나 상태 증명을 검증하지 않습니다. 가디언 위원회의 서명이 신뢰 모델의 전부입니다.

신뢰 모델과 체인 id

컨트랙트와 공유하는 브리지 로컬 체인 id입니다(bridge-codec).

체인id
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)여야 하며, 이로써 가변성(malleability)을 차단합니다. 가디언이 서명하는 다이제스트는 mu = keccak256(keccak256(body_bytes))입니다. 가디언 세트 교체는 거버넌스 emitter (chain 1, keccak256("rand-bridge-governance"))가 발급하는 어테스테이션이며, new_index == current + 1을 담아야 하고, 이전 세트가 아닌 현재 세트가 서명해야 합니다. 교체되는 세트는 처리 중인 전송을 위해 86,400초의 유예 기간을 유지합니다. (이는 ECDSA 세트에 해당합니다. 양자 내성 세트와 일시 중지 키는 각자의 액션으로 교체되며, 체인 20에서는 하루의 대기 시간을 거칩니다: 아래.) n·2/3 + 1명의 가디언이 공모하면 임의의 가치를 mint할 수 있습니다. 브리지 보강은 그 규모와 속도를 제한할 뿐, 이 위험을 없애지는 않습니다.

트랜잭션 바인딩. Rand 트랜잭션에는 서명이 없습니다. burn은 해당 번들의 STARK 증명으로 승인됩니다. 체인 14부터 모든 번들 증명은 Transaction::binding을 대상으로 생성되고 이에 대해 검증됩니다. 이는 트랜잭션 전체(체인 id, 네 개의 봉투를 가진 하나의 번들, 액션)의 해시이며 증명 바이트만 비워 둡니다. 체인 17부터 번들은 번들 증명과 지출 승인 증명이라는 두 개의 증명을 가지며, 바인딩은 둘 다 비우고 번들의 auth_commit은 유지하며, 두 증명 모두 이에 대해 검증됩니다. 대기 중인 burn의 사본에서 to, relayer_fee, (to_chain, token)을 바꾸거나, attest의 time 또는 봉투를 바꾸면 증명이 실패합니다(TxError::InvalidBundleProof). 체인 20에서는 바인딩이 제네시스 해시도 포함하므로(제네시스 binding_domain: 1: 제네시스 해시, 체인 id, 번들, 액션에 대한 rand-tx-bind-2), 체인 id가 같은 다른 체인을 위해 만든 증명도 검증되지 않습니다. 지갑과 릴레이어는 제출 대상 체인에 맞게 빌드된 바이너리를 실행해야 합니다. 체인 20에서는 v0.6.8이며(체인 18과 19에서는 v0.6.7), 이전 버전의 rand는 체인 20이 거부하는 증명과 서명을 만듭니다.

가디언 데몬과 릴레이어 데몬은 full node에 포함되지 않습니다. full node는 공유 코덱과 수신 측 절반을 담고 있습니다.

와이어 포맷

모든 정수는 빅엔디언이며, 와이어 버전은 1입니다.

어테스테이션 봉투

오프셋길이필드
01version = 1
14guardian_set_index
51n_sigs
666·n_sigs서명: index (1), r (32), s (32), v (1)
6 + 66·n_sigs…body

본문 헤더 — 51바이트, 이어서 페이로드

오프셋길이필드
04timestamp
44nonce
82emitter_chain
1032emitter_address
428sequence
501consistency_level
51…payload

Transfer 페이로드 — id 1, 133바이트

오프셋길이필드
01id = 1
132amount (u256. Rand는 하위 128비트를 사용하며, 예치금은 노트의 u64에도 들어가야 합니다)
3332token_address
652token_chain
6732to — 수신자 해시, 아래 참조
992to_chain
10132fee (u256): amount 중 릴레이어의 몫

GuardianSetUpgrade 페이로드 — id 2: id (1), new_index (4), n (1), 이어서 n개의 20바이트 키.

Ethereum, BSC, Tron 주소는 20바이트이며 32바이트 슬롯에서 앞쪽이 0 바이트 12개로 채워집니다. Solana 주소는 슬롯을 가득 채웁니다. Transfer는 emitter_chain에 등록된 해당 체인의 emitter에서 와야 하고, token_chain은 발행 체인과 같아야 합니다. 어테스테이션은 최대 16 KiB입니다. 브리지 보강은 와이어 포맷, 코덱, 배포된 원본 체인 엔드포인트를 변경하지 않았습니다. 양자 내성 공동 서명과 거버넌스 메시지는 어테스테이션이 아니라 Rand 트랜잭션에 실려 전달됩니다.

32바이트 수신자

차폐 주소는 1,216바이트인데 와이어에는 32바이트 공간만 있습니다. to 필드는 그 주소의 해시를 담습니다.

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

예치자는 원본 체인에서 이 해시를 지정하고, 가디언이 그것에 서명하며, 제출자는 전체 주소를 BridgeAttest 액션에 넣고, 원장은 해시를 다시 계산해 일치하지 않으면 거부합니다. 그렇지 않으면 제출자가 다른 사람의 예치금을 누가 받을지 선택할 수 있게 됩니다. 원본 체인의 프런트엔드는 이 형식을 받아들여야 하며, 지갑이 이를 출력합니다. 체인 11은 한때 이 슬롯을 수신자 id로 직접 사용했으나, 그 주소 형식은 체인 12에서 되돌려졌습니다(계정과 주소).

하나의 토큰, 여러 backing: zUSD

자산 레지스트리는 더 이상 브리지의 것이 아닙니다. 네이티브 토큰과 브리지된 토큰이 공유하는 원장 수준의 RPL 토큰 레지스트리로 옮겨졌으며, 체인의 상태 루트는 이를 tokens_root로 커밋합니다. 브리지된 토큰은 mint 권한이 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

체인 20에서 zUSD는 인덱스 1, rpl1xtj6k2x8strx8c2d5fjs50ltztck5ykms4ve7nmzmstf6fhn0v0spelqtx, 이름 “Shielded USD”, 소수 자릿수 8이며, 체인 15부터 19까지와 마찬가지로 제네시스 파일에 등재되어 있습니다. 이름, 심볼, 솔트는 체인 14의 것과 같으므로 자산 id(32e5ab28c782c663e14da2650a3feb12f16a12db85599f4f62dc169d26f37b1f)와 rpl1… id도 그대로 이어졌습니다(탐색기: GET /tokens/1). 체인 14에서는 높이 256에서 register_bridged_token 트랜잭션으로 등록되었습니다.

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

(chain, token) 쌍은 전체 레지스트리에서 최대 하나의 토큰만 뒷받침합니다(TokenError::BackingTaken). 토큰은 1개에서 32개의 backing을 가지며, decimals는 브리지된 토큰의 8이 아니라 원본 코인 자체의 자릿수로 0에서 18 사이입니다. 풀링된 backing은 명시된 위험입니다. 일곱 코인은 par로 zUSD와 서로 교환 가능하므로, 어느 한 코인의 디페그는 모든 zUSD 보유자의 가치를 희석하며, 한 backing의 비중에 대한 Rand 측 한도는 없습니다.

zUSD는 바스켓입니다. 보유자는 자신이 예치한 코인이 아니라 목록에 있는 모든 담보의 위험을 부담합니다. zUSD 1개는 풀에 대한 청구권이며, locked가 충분한 어떤 담보에 대해서도 행사할 수 있습니다. 어떤 담보에도 가격을 매기지 않습니다. 목록에 있는 코인 1단위를 잠그면 zUSD 1개가 발행되고, 소각은 locked가 그 금액을 충당하는 어떤 담보든 지정할 수 있습니다. 따라서 페그를 잃은 코인을 액면가로 예치하고, 페그를 유지한 코인으로 zUSD를 소각할 수 있으며, 이는 건전한 담보가 모두 소진될 때까지 이어질 수 있습니다. 손실은 롤링 발행 윈도와 실제로 잠긴 보관 자산으로 제한됩니다. chain 20에서는 24시간 동안 모든 담보를 합쳐 최대 4,000 zUSD, 담보별로는 100,000 zUSD까지 발행할 수 있으며(rules v2), 건전한 담보를 통해 빠져나갈 수 있는 금액은 그 담보가 보유한 양을 넘을 수 없습니다. 윈도는 총량이 아니라 속도를 제한합니다. 윈도가 벌어 주는 시간은 발행을 일시 중지하는 데 쓰이며, 일시 중지하면 취약한 코인의 신규 예치는 막히지만 소각은 계속 열려 있습니다. 담보별 상환 한도도, 담보별 일시 중지도 없습니다. rules v2 이전의 chain 14에서는 소스 엔드포인트 자체의 pauser와 토큰별 속도 상한이 유일한 제한이었습니다.

불변식: total_supply == Σ backings.locked. TokenRegistry::lock(BridgeAttest)은 증명된 금액을 한 번의 호출로 담보의 locked와 토큰의 total_supply에 더하고, TokenRegistry::release(BridgeBurn)는 해제된 금액을 양쪽에서 뺍니다. 그 밖에는 어떤 것도 두 필드에 쓰지 않으며, 토큰 리프가 모든 담보의 locked를 tokens_root에 커밋하므로, 둘 중 하나에 대해 의견이 다른 두 노드는 상태 루트에서 갈라집니다. 소스 엔드포인트는 잠금 시 수수료를 받지 않고 해제 시 증명된 금액 전체를 차감하므로, 처리 중인 메시지를 제외하면 각 담보의 locked는 해당 엔드포인트의 보관 자산과 같습니다. chain 20의 수수료는 이를 깨뜨리지 않습니다. 수수료는 발행된 채 담보가 유지되는 zUSD 노트이므로, 예치는 여전히 총액을 잠그고 소각은 amount − fee만 해제합니다. chain 20에서 엔드포인트 자체의 해제 시 10 bps USDT 공제는 컷오버 시점에 0으로 설정되었으며, 수수료는 이제 Rand에서 부과됩니다.

금액은 와이어상에서 8자리 소수 단위입니다. 소스 코인 자체의 소수 자릿수와 무관하며, Ethereum의 1 USDT(소수 6자리)를 예치하면 100_000_000 단위로 잠깁니다. 인덱스 0인 RAND만 이 체인의 9자리 소수를 가집니다.

토큰은 첫 예치 이전에 목록에 올라 있어야 합니다. 더 이상 최초 확인 시 등록은 없습니다. 아무도 목록에 올리지 않은 (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자리 소수 단위의 십진 문자열이며, chain 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

일반적인 shielded 트랜잭션으로 전달되며, 이 트랜잭션의 숨겨진 자산 번들 하나가 0.001 RAND의 기본 수수료만 지불하고 다른 것은 지불하지 않습니다. 릴레이어가 아직 RAND를 보유하지 않은 예치자를 대신해 비용을 내기 때문입니다. 예치 노트 { pk: recipient.pk, from: 0, amount, asset: <index>, time, r }는 제출자가 아니라 체인이 가디언이 서명한 금액으로 계산하므로, 릴레이어는 발행량을 부풀리거나 방향을 바꿀 수 없습니다. 이 노트의 커밋먼트는 트랜잭션이 와이어상에 담지 않는 유일한 리프입니다. chain 20에서 노트의 amount는 총액에서 발행 수수료를 뺀 금액이며, 체인은 수수료인 두 번째 노트를 트레저리에 추가합니다(발행 및 소각 수수료).

블라인딩은 선택하는 것이 아니라 도출됩니다. r은 다음과 같아야 합니다.

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

이는 가디언 정족수가 서명한 다이제스트입니다. 그렇지 않으면 BridgeError::WrongDepositBlinding이며, 서명 검증 이전에 거부됩니다. 이 필드는 와이어에 그대로 남아 있으며(지갑의 공개 필드 재구성이 여기서 읽습니다), 제약만 추가되었고, 이 규칙은 로테이션의 attest에도 적용됩니다. 이 규칙 이전에는 릴레이어의 대기 중 트랜잭션을 본 누구든 자신의 블라인딩으로 같은 증명을 제출할 수 있었습니다. 수신자는 여전히 노트를 받았지만, 릴레이어의 트랜잭션은 증명 비용을 이미 치른 뒤에 무효가 되었습니다. r이 고정되면 같은 time에 같은 증명을 제출한 두 제출자는 같은 노트를 지정하며, 풀은 커밋먼트를 기준으로 둘을 충돌 처리합니다.

time과 asset은 트랜잭션이 존재하기 전에 봉투를 봉인할 수 있도록 예치자가 하는 두 가지 예측입니다. time은 번들의 time과 같은 윈도로 제한되며(chain 20에서는 제네시스 proof_window_blocks에 따라 1,024 블록, 그 이전에는 256), 의도적으로 도출되지 않습니다. 모든 제출자가 합의할 수 있는 time을 정하는 규칙이 없으므로, 선행 거래자는 윈도 안에서 다른 time을 고를 수 있습니다. 이는 수신자, 금액, 자산이 같은 다른 노트이며 수신자의 지갑이 같은 방식으로 찾아내지만, 두 제출은 대신 증명 다이제스트에서 충돌합니다. asset은 레지스트리가 이미 (chain, token)에 부여한 인덱스와 같아야 하며, 그렇지 않으면 TxError::AttestAssetMismatch입니다.

Transfer 페이로드의 fee는 담기기만 하고 누구에게도 지급되지 않습니다. 지급할 제출자 신원이 없으므로, 예치는 총액이며 제네시스가 설정한 경우(chain 20)에만 체인 자체의 발행 수수료가 차감됩니다. 재전송 방지는 다이제스트로 이루어집니다. 소비된 모든 mu는 브리지의 spent 집합에 들어가며, 멤풀은 같은 증명을 두고 경쟁하는 두 번째 릴레이어를 거부합니다. 예치는 담보의 발행 상한에도 적용되며, chain 20에서는 전역 상한에도 적용되고, 발행이 일시 중지된 동안에는 거부됩니다(B1, rules v2). 두 상한 모두 총액을 기준으로 계산합니다. 제네시스가 재전송 하한을 설정한 체인(chain 20 포함)에서는 sequence가 소스 체인의 하한보다 낮은 전송이 서명 검증 이전에 BelowReplayFloor로 거부됩니다(chain 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 }
}

chain 14의 숨겨진 자산 번들 이후 소각은 번들 하나입니다(이전에는 둘이었습니다). 슬롯 0–1은 소각자가 합계가 amount 이상인 해당 자산의 노트를 소유했음을 증명하며, burn_a는 풀을 떠나는 값입니다. 슬롯 2–3은 RAND로 수수료를 지불합니다. 수수료는 BRIDGE_BURN_FEE로 0.01 RAND이며 번들의 기본 수수료를 포함합니다. relayer_fee는 amount의 일부(fee ≤ amount)로서 목적지 체인의 해제 컨트랙트가 떼어 내는 것이며 추가 비용이 아니므로, burn_a는 정확히 amount입니다. chain 20에서는 체인이 amount의 10 bps에 해당하는 소각 수수료도 treasury로 가는 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, chain 20에서는 amount − fee입니다.

to는 올바른 형식이어야 합니다(체인 2, 3, 4에서는 0으로 채운 20바이트이며, 모두 0이어서는 안 됩니다). 원장은 BridgeBurnRecord를 엄격히 증가하는 burn_sequence로 기록하며 전송자 슬롯에 트랜잭션 해시를 담고, 가디언은 소각 로그를 읽습니다.

→ {"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에서 0.999 zUSD를 BSC USDT 쪽으로 소각했고, 그중 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 20에서는 chain 17 이후의 모든 번들과 마찬가지로 그 옆에 지출 승인 증명(수 초, 약 1.37 MB)이 함께 붙습니다.

민트 및 소각 수수료

fullnode v0.6.8에서 새로 도입되었으며 chain 20에서 제네시스 그룹이 설정했습니다.

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

체인은 브리지를 통한 모든 민트와 모든 소각에 수수료를 부과합니다. 수수료는 브리지된 토큰으로, Rand 위에서, 차폐 노트로 트레저리 주소 recipient에 지급됩니다. 각 수수료율은 최대 100 bps(1 %)이며, chain 20에서는 두 값 모두 10 bps, 즉 0.1 %입니다. 수수료는 ⌊amount · bps / 10⁴⌋를 담보 자산의 정수 인출 단위(10^(8 − source decimals): 소수점 6자리 USDT는 100 단위, 소수점 18자리 코인은 1 단위)로 내림한 값입니다. 따라서 수수료와 나머지는 모두 정수 단위이며, 수수료가 0으로 내림되면 노트는 만들어지지 않습니다(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_supplyg만큼 증가release_amount만큼 감소
검사 대상g에 대한 백킹별 및 전역 민트 한도release_amount에 대한 InsufficientBacking 및 NotReleasable. 릴레이어의 몫은 그 안에 들어가야 합니다(FeeExceedsAmount)

두 노트 모두 발행되고 담보되므로 total_supply == Σ locked == custody는 여전히 성립합니다. 수수료 노트도 다른 노트와 같은 zUSD이며, 그 가치는 누군가 소각할 때까지 원천 체인에 잠겨 있습니다. 소각이 RAND 슬롯에서 지불하는 0.01 RAND인 BRIDGE_BURN_FEE는 변함이 없으며 별개입니다.

chain 20의 첫 왕복을 노드에서 읽은 결과는 다음과 같습니다. 높이 1,664에서 BSC로부터 1 zUSD를 입금하자(4f6e6c54…1a64) 입금자는 0.999 zUSD를, 트레저리는 0.001을 받았습니다. 높이 2,899에서 BSC USDT로 0.999 zUSD를 소각하자(시퀀스 13) 0.000999 zUSD가 남고 0.998001이 인출되었습니다. 그 뒤(2026-10-01에 읽음) BSC USDT 백킹은 22.001999 zUSD를 보유했고, 토큰의 공급량 99.001999 zUSD는 백킹의 합과 같았습니다.

트레저리가 자신의 노트를 찾는 방법. 수수료 노트에는 봉투(envelope)가 없습니다. 노트의 모든 워드가 공개되어 있습니다(금액은 트랜잭션과 제네시스 수수료율에서, 자산과 time은 트랜잭션에서 얻고, 블라인딩은 유도되며, 소유자는 제네시스의 recipient입니다). 주소가 recipient인 지갑은 입금 노트를 재구성하는 것과 같은 공개 패스에서 수수료 노트를 재구성하며, 동기화마다 한 번 rand_getBridgeState.fees와 백킹의 소수점 자릿수를 읽습니다. 수수료에 대해 거짓말을 하는 노드는 노트를 찾지 못하게 만들 수 있을 뿐, 노트를 잘못 입금 처리하게 만들 수는 없습니다. 입금자의 지갑도 같은 방식으로 자신의 입금 가치, 즉 총액에서 수수료를 뺀 값을 계산합니다. 이 방식은 v0.6.8 지갑만 수행하며, 이전 지갑은 어차피 chain 20에서 전송할 수 없습니다.

그룹이 없는 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 14 이후 모든 체인과 마찬가지로 chain 20도 10000000000000, 즉 백킹당 하루 100,000 zUSD로 설정합니다. rules v2(chain 14) 이전에는 하루가 블록 타임스탬프의 timestamp_ms / 86_400_000이며, rules v2(chain 15~20)에서는 같은 한도를 블록 시간 기준 cap_window_secs 길이의 롤링 윈도우로 측정하고, 레지스트리 전체의 전역 한도(chain 20에서 86,400초당 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)과 함께, 블록 시간은 앞으로만, 블록당 최대 1분씩 움직입니다.
  • 투표 규칙(리플레이 규칙은 결코 아님): 밸리데이터는 자신의 시계보다 15,000 ms를 넘게 앞선 블록에는 투표하지 않습니다.

따라서 브리지가 있는 체인의 모든 밸리데이터에는 NTP가 필요합니다. 시계가 15초보다 더 어긋나 있으면 결함 있는 리더이자 결함 있는 투표자입니다. 체인이 중단 후 따라잡는 동안에는 블록 시간이 블록당 최대 60초까지 진행될 수 있으므로, 민트 한도의 하루가 실제 하루보다 빨리 넘어갈 수 있습니다. 누적된 지연 하루당 최대 하루치 한도가 추가됩니다.

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에서는 세트 1)와 인덱스가 정렬되어 있습니다. payload-2 교체는 ECDSA 세트만 옮깁니다. rules v2 이전에는 포스트 퀀텀 세트가 체인 수명 동안 고정이고, rules v2에서는 RotatePqGuardians만 이를 교체합니다(chain 20에서는 소유 증명과 하루의 지연이 있는 V2 형태). chain 20에는 chain 15~19와 같은 여덟 개의 키가 있으며, 민트에는 그중 여섯 개가 필요합니다(chain 14에는 여섯 개가 있었고 다섯 개가 필요했습니다).

이 공동 서명은 chain 20에서도 여전히 체인 id에만 바인딩됩니다. 아래의 거버넌스 메시지와 달리 제네시스 해시 뒤로 옮기지 않았는데, 이를 생성하는 가디언 데몬이 다른 저장소에 있고 변경되지 않았기 때문입니다. 가디언 집합까지 복사한 동일 id의 체인 사본은 원래 체인을 위해 만들어진 입금의 공동 서명을 받아들일 수 있습니다. chain 20에서 입금 트랜잭션 자체의 수수료 번들은 제네시스에 바인딩되어 있지만, 그 안에 포함된 증명(attestation)은 그렇지 않습니다.

B4: 제네시스 이후 브리지 토큰 등록

일반 주소는 Bridge 권한 토큰을 만들 수 없습니다. 두 가지 동작이 있으며, 각각 일반 RAND 수수료 번들로 처리되고 양자 내성 정족수의 서명을 받아야 합니다.

  • RegisterBridgedToken { name, symbol, salt, chain, token, decimals, nonce, pq_signatures } — 다음 인덱스에 새 브리지 토큰을 만듭니다. Rand에서 소수 자릿수는 8이며, 제네시스 발행 한도 아래에서 첫 번째 담보(backing)와 함께 등록됩니다. 수수료: 번들 기본 수수료에 레지스트리의 registration_fee를 더한 금액입니다(chain 20에서는 1 RAND이므로 1.001 RAND이며, chain 20은 등록 수수료를 제안자에게 지급하지 않고 소각합니다).
  • 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>

순서 규칙이 두 가지 있습니다. 가디언은 먼저 등록에 서명하고, 그것이 커밋되기를 기다려 부여된 인덱스를 읽어 온 다음, 그 인덱스를 지정하는 등록 목록(listing)에 서명합니다. 네이티브 토큰의 등록은 허가가 필요 없으므로, 먼저 서명해 둔 목록은 인덱스를 다른 토큰에 빼앗길 수 있기 때문입니다. 또한 코인은 소스 엔드포인트에서 setToken으로 활성화하기 전에 Rand에 등록해야 합니다. 순서가 반대이면 Rand에 크레딧할 담보가 생기기 전에 입금이 잠길 수 있고, 그 증명은 Rand가 따라잡을 때까지 UnlistedToken으로 거부됩니다.

거버넌스 메시지

고정 바이트이며 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바이트이고 새 집합이 갖게 될 순서대로 배열됩니다.

chain 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). 따라서 한 체인에 서명하는 pause 키나 정족수는 체인 id와 키를 공유하는 다른 어떤 체인에도 서명하지 않게 됩니다. …-1 형식으로 서명된 파일은 chain 20에서 거부됩니다(BadPauseSignature, PqBadSignature). chain 14부터 19까지는 위의 …-1 레이아웃을 바이트 단위까지 그대로 사용합니다.

Rules v2: 교체, 롤링 한도, 일시 중지 중 등록 금지

제네시스 bridge 섹션 안의 선택적 그룹입니다. chain 14에는 없었고, 15 이후의 모든 체인에는 있으며, chain 20도 같은 값을 사용합니다.

"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 }는 pause 키를 교체합니다. 둘 다 PauseMints처럼 번들과 수수료가 없으며, 현재 양자 내성 집합이 서명하고, rotation_nonce를 포함하고 증가시키므로 서명된 정족수 하나는 정확히 한 번의 교체만 승인합니다. 새 집합은 현재 ECDSA 집합과 길이가 같아야 하고, 키는 1,312바이트여야 하며, 중복이 없어야 하고 pause 키와도 달라야 합니다. 유예 기간은 없으며, 대체된 집합이나 키는 그 이후 아무것도 서명하지 못합니다. chain 20에서는 이 v1 동작이 거부되며, 교체 시에는 새 키의 소유를 증명하고 하루를 기다려야 합니다(아래).
  • 롤링 윈도우 한도. 담보별 mint_cap_per_day는 달력상의 하루가 아니라 cap_window_secs의 롤링 윈도우 기준으로 측정되며, 모든 토큰의 모든 담보를 합산하는 두 번째 레지스트리 전체 한도 global_mint_cap_per_window가 추가됩니다(TokenError::GlobalMintCapExceeded). chain 20에서 이 값은 400,000,000,000 단위, 즉 86,400 s당 4,000 zUSD입니다. 일 단위 규칙에서는 23:59:59의 한도와 00:00:01의 한도를 합치면 2초 만에 한도의 두 배가 되었지만, 윈도우 방식에서는 두 번째 발행이 거부됩니다.
  • 일시 중지 중 등록 금지. mint_paused 상태에서는 RegisterBridgedToken과 ListBacking이 MintsPaused로 거부됩니다. 소각과 두 가지 교체는 계속 열려 있습니다.

교체: 소유 증명과 하루의 지연

Rules v2에는 세 가지가 남아 있었습니다. 새 키를 누군가 보유하고 있다는 증명이 없었고(오타가 나거나 아무도 가지고 있지 않은 키라면 양자 내성 정족수가 영구히 사용 불능이 될 수 있었습니다), 교체는 포함된 블록에서 즉시 효력이 생겼으며, 악의적인 교체를 막을 방법이 없었습니다. rules_v2와 나란히 있는(그리고 이를 필요로 하는) 두 번째 선택적 그룹이 v0.6.8에서 새로 추가되어(audit v6, BRG-14) 이를 해결합니다. chain 20이 이를 설정합니다.

"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이며, chain 20에서는 …-2 형식)에 대해 만든 자신의 Dilithium2 서명이고, 키 순서대로 키당 하나씩이며 각각 2,420바이트입니다. v1 동작은 PossessionRequired로 거부됩니다. 개수나 길이가 잘못되면 정족수를 검증하기 전에 거부되고, 잘못된 소유 증명 서명(BadPossession { index })은 정족수 검증 이후에 거부됩니다.
  • 지연. 승인된 교체는 effective_at_secs = 블록 시간 + delay_secs와 함께 **대기 중(pending)**으로 기록되며, 타임스탬프가 그 값에 도달한 첫 번째 블록이 끝날 때 효력이 생깁니다. chain 20에서는 86,400 s, 즉 하루입니다. 그때까지는 기존 집합이 모든 발행에 공동 서명하고 기존 pause 키가 일시 중지를 수행합니다. 교체를 승인할 때 rotation_nonce 하나가 소모되고 활성화할 때 또 하나가 소모되므로, 기존 집합 아래에서 풀링된 발행 증명은 집합이 바뀌면 풀에서 빠집니다. 종류별로 한 번에 하나의 교체만 대기할 수 있습니다(RotationPending { effective_at_secs }).
  • 취소. CancelRotation { kind, nonce, signature }(번들과 수수료 없음)는 kind(0 = 양자 내성 집합, 1 = pause 키)의 대기 중인 교체를 폐기합니다. 정족수와 별도로 보관되는 유일한 키인 현재 pause 키가 서명하며, chain 20에서는 "rand-bridge-cancel-rotation-2" || genesis [32] || chain_id u64 || rotation_nonce u64 || kind u8에 대해 서명하고 nonce를 소모합니다. 따라서 탈취된 정족수는 하루 안에 집합을 교체할 수 없고, pause 키 보유자는 그 하루 동안 교체를 막을 수 있습니다. 새로 들어오는 pause 키는 자신의 도입을 취소할 수 없습니다.

서명 도구는 rand-node bridge-gov입니다(rotate-pq, rotate-pause, cancel-rotation이 있으며, 각각 message, sign, submit 단계를 가집니다. submit은 전송 전에 모든 소유 증명 행을 다시 검증합니다). rand_getBridgeState는 rotation_rules와 pending_rotations를 제공하며, 이 그룹이 없는 체인(chain 14부터 19)에서는 둘 다 null입니다. ECDSA 가디언 집합의 교체(payload 2)는 영향을 받지 않으며, 자체 규칙과 유예 기간을 유지합니다. chain 20에서는 아직 교체가 실행된 적이 없습니다.

chain 14에서 이어받은 것

chain 15는 chain 14의 브리지 위치(같은 에미터와 소스 엔드포인트)에서 만들어졌고, chain 16, 17, 18도 각각 직전 체인의 위치에서 같은 방식으로 만들어졌습니다. chain 19는 chain 18의 위치에서 한 가지만 바꾸어 만들어졌는데, Ethereum, BSC, Tron 에미터가 2026-09-30에 다시 배포된 엔드포인트라는 점입니다(체인 2와 3은 0x7af6b17047c1db6cb54347fdea45cf9179075bfa, 체인 4는 0x6410797df959987a5baf65b5fab97edeb34d5163). chain 18은 이를 신뢰한 적이 없으므로 그곳에서는 이 엔드포인트로부터 아무것도 발행되지 않았습니다. chain 20은 먼저 브리지를 중지한 상태에서 높이 7,258의 chain 19 스냅샷으로부터 만들어졌으며, chain 19의 에미터, 가디언, 양자 내성 키, pause 키를 그대로 유지합니다. 추가된 것은 수수료 그룹과 교체 그룹입니다. Solana의 에미터와 Rand 측 에미터는 chain 14 이후 바뀌지 않았습니다. chain 20의 제네시스가 담고 있는 내용은 다음과 같습니다.

  • 수탁. 제네시스 zUSD 노트 두 개(10 zUSD와 89 zUSD)로 이루어진 99 zUSD는 Ethereum에 잠긴 22 USDT, BSC의 22 USDT, Tron의 30 USDT, Solana의 25 USDT에 대응하며, 모든 USDC 담보의 잠금량은 0입니다. 이는 스냅샷 시점 체인 19의 locked 값이며, 각 엔드포인트의 수탁량과 일치했습니다. total_supply == Σ locked는 블록 0에서 각 엔드포인트의 수탁량과 같습니다. 이력: 체인 14에 남아 있던 10 zUSD는 체인 15~19에서 하나의 제네시스 노트였습니다. 체인 18의 제네시스에서는 이를 Tron에 잠긴 9 USDT와 Solana의 1 USDT로 보유했고, 체인 19의 제네시스에서는 체인 18에서 이루어진 재조정 이후 Solana의 10 USDT로 보유했습니다. 나머지 89는 체인 19의 순입금에서 소각분을 뺀 값입니다.
  • 가디언 세트 인덱스 1: 소스 엔드포인트가 이미 있는 인덱스입니다. 다음 ECDSA 교체는 인덱스 2를 담아야 합니다. 여덟 개의 양자 내성 키와 일시 중지 키는 체인 15의 것 그대로입니다.
  • burn_sequence 13: 체인 14의 소각은 6에서 끝났고, 체인 1518의 제네시스는 7에서 시작했으며, 체인 18은 소각을 한 번(7) 수행했고 체인 19는 8에서 시작했습니다. 체인 19는 시퀀스 812의 소각을 다섯 번 수행했으므로 체인 20은 13에서 시작하며, 시퀀스를 키로 사용하는 어떤 엔드포인트나 데몬도 같은 시퀀스를 두 번 보지 않습니다. 체인 20의 첫 소각은 시퀀스 13을 사용했습니다.
  • 리플레이. 이전 체인의 민트는 여전히 ECDSA 쿼럼을 통과하지만, 그 양자 내성 공동 서명이 해당 체인의 id에 묶여 있으므로 체인 20에서는 거부됩니다. 체인 15의 제네시스에는 인바운드 리플레이 하한이 없었기(min_inbound_sequence가 null이었습니다) 때문에, 그곳에서는 공동 서명과, 가디언이 오래된 잠금을 새 체인 id에 대해 공동 서명하지 않는다는 점에 의존했습니다. 체인 16부터 제네시스가 하한을 설정합니다. 소스 체인별로, 해당 체인의 emitter가 보낸 전송 증명이 가질 수 있는 가장 낮은 sequence입니다. 체인 20의 하한은 { "2": 3, "3": 3, "4": 3, "5": 7 }이며, 체인 19를 스냅샷했을 때 각 엔드포인트의 다음 아웃바운드 시퀀스입니다. 그 이전의 모든 잠금은 체인 19에서 민트되었습니다. (체인 18의 하한은 { "2": 2, "3": 2, "4": 2, "5": 2 }였고, 체인 19의 하한은 { "2": 1, "3": 1, "4": 1, "5": 4 }였습니다. 재배포된 각 엔드포인트의 시퀀스 0은 운영자 본인의 잠금이었으며 절대 민트되어서는 안 됩니다.) 하한 미만의 전송은 쿼럼과 공동 서명이 있더라도 BelowReplayFloor로 거부됩니다. 가디언 세트 교체는 이 하한의 판정 대상이 아닙니다. 하한은 제네시스에서 고정되고 제네시스 해시에 커밋되며 브리지 루트에 포함됩니다.

체인 14의 시작 순서 (이력)

2026년 9월, 이전 testnet인 체인 14에서 브리지가 가동된 과정입니다.

  1. bridge 섹션(pq_guardians와 pause_key 포함)과, registration_fee와 mint_cap_per_day를 설정하되 토큰은 나열하지 않는 tokens 섹션으로 체인을 생성합니다. 2026-09-20에 완료했습니다.
  2. 배포자 지갑이 RAND를 얻을 수 있도록 faucet을 활성화합니다. 완료했습니다.
  3. 배포자가 list_nonce = 0에서 M_register에 대한 쿼럼의 서명으로 첫 번째 담보와 함께 zUSD를 등록합니다. 높이 256에서 완료했습니다.
  4. 가디언이 인덱스를 다시 읽어 확인하고, 남은 여섯 개의 ListBacking 메시지에 서명하며, 이를 한 번에 하나씩 제출합니다. 완료했습니다. list_nonce는 7을 가리킵니다.
  5. 각 담보에 대해: 소스 엔드포인트에서 setToken을 실행하기 전에 Rand에 먼저 등록합니다.
  6. 네 개의 소스 엔드포인트에서 가디언 세트 교체(세트 0 → 세트 1, 현재 세트의 6명 중 5명이 서명)를 수행한 다음, 같은 증명을 Rand에서도 수행합니다. Rand는 체인 14가 존속하는 동안 세트 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 공개 키이며, 각각 16진수 문자열로 1,312바이트입니다. next_index는 더 이상 없습니다. 브리지된 토큰은 입금되기 전에 등록 시 확정된 인덱스로 등재되므로, 예측해야 할 인덱스가 남아 있지 않습니다. rotation_nonce는 다음 M_rotate_pq/M_rotate_pause/M_cancel이 담아야 하는 값입니다. rules_v2는 해당 그룹이 없는 체인(체인 14)에서는 null이고, min_inbound_sequence는 리플레이 하한이 없는 체인(체인 15 이전)에서는 null이며, 체인 20에서는 { "2": 3, "3": 3, "4": 3, "5": 7 }입니다. rotation_rules, pending_rotations, fees는 해당 그룹이 없는 체인(체인 14~19)에서는 null입니다. 위의 예시는 체인 20의 값이 아니라 형태를 보여 줍니다. 체인 20은 guardian_set_index 1과 burn_sequence 13에서 시작했으며, "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]로 보유량을 읽습니다. 브리지 루트는 emitter, 가디언 세트, 사용된 다이제스트, 소각 시퀀스, 양자 내성 가디언 세트, 일시 중지 키, 일시 중지 플래그와 거버넌스 논스(둘이며, rules v2에서는 rotation_nonce)에 커밋하고, 제네시스가 설정한 경우 리플레이 하한에, 체인 20에서는 대기 중인 교체를 포함한 교체 규칙과 수수료 그룹에도 커밋합니다. 이 루트는 브리지가 있는 체인에서만 상태 루트에 합쳐집니다. 각 담보의 locked를 포함한 토큰 레지스트리는 tokens_root로 별도로 커밋됩니다. 탐색기의 GET /bridge와 GET /bridge/assets는 같은 상태를 제공하며, 후자는 담보별 입금 및 소각 합계를 소수점 8자리 문자열로 제공합니다. GET /tokens와 GET /tokens/:id는 레지스트리를 제공합니다.

공개되는 것과 숨겨지는 것

항목볼 수 있는 대상
입금의 금액, 자산 인덱스, 수신자 주소모든 사람, 해당 트랜잭션 한 건에서
수신자가 이후에 사용하는 노트아무도 볼 수 없음
소각의 금액, 자산, 코인, 목적지, 릴레이어 수수료모든 사람 — 가디언에게 필요하기 때문입니다
브리지 수수료 노트의 금액, 자산, 소유자, 트레저리(체인 20)모든 사람: 공개 필드로부터 재구성됩니다
소각에 자금을 댄 노트아무도 볼 수 없음
브리지된 잔액보유자 본인 또는 조회 키(viewing key) 보유자만
가디언 세트, emitter, 각 담보의 locked를 포함한 레지스트리, 소각 로그모든 사람

알려진 한계

fullnode 자체 목록에 따르면 다음과 같습니다. Rand에서는 릴레이어에게 보수가 지급되지 않습니다. 입금의 봉투(envelope)는 어떤 것에도 묶여 있지 않으므로 악의적인 릴레이어가 임의의 데이터를 봉인할 수 있으며, 그래서 지갑은 대신 공개 필드로부터 입금 노트를 재구성합니다. 원장 수준의 공용 테스트 벡터는 노트 풀 기준으로 다시 만들어지지 않았습니다. bridge-burn은 증명 비용을 지불한 뒤에야 목적지 엔드포인트 자체의 정책을 알게 됩니다. 브리지된 체인에서는 부모와 같은 블록 타임스탬프가 허용되므로, 공모한 리더들이 대체된 가디언 세트를 유예 기간 안에 유지할 수 있습니다. 메모리 내 소각 로그는 크기 제한이 없습니다. 소스 체인 상태에 대한 라이트 클라이언트나 온체인 검증은 존재하지 않으며 계획되어 있지도 않습니다.

fullnode 커밋 c0878c3f(chain 21) 기준으로 2026-10-12에 마지막으로 검증되었습니다. 코드와 이 페이지의 내용이 다를 경우 코드가 옳습니다. 알려 주시기 바랍니다.