이 페이지는 풀을 읽는 모든 도구를 만들거나 감사할 때 사용합니다. 자신의 노트를 스캔하는 지갑, 커미트먼트와 널리파이어를 추적하는 인덱서, 트랜잭션이 공개하는 내용을 검사하는 도구가 그 예입니다. 이 체인에서 가치는 노트의 집합이며, 체인은 노트의 커미트먼트를 보관할 뿐 내용은 보관하지 않습니다.
노트
노트는 { pk, from, amount, asset, time, r }이며, 32비트 워드 28개, 즉 112바이트로 pk(8) from(8) amount_lo amount_hi asset time r(8) 순서로 배치됩니다. pk는 소유자의 노트 공개값(뷰잉 키에서 유도)이고, from은 발신자의 공개값입니다. amount는 단위(unit) 기준의 u64(1 RAND = 10⁹ units)이고, asset은 u32로 RAND는 0, 그 외에는 브리지된 토큰이든 네이티브 토큰이든 RPL 토큰에 대한 토큰 레지스트리의 밀집 인덱스입니다(RPL tokens 참조). time은 발신자가 목표로 한 블록 높이이고, r은 블라인딩용 무작위 워드 8개입니다.
평문은 체인에 나타나지 않습니다. 체인에 나타나는 것은 다음과 같습니다.
- 커미트먼트
cm = H(CM, words): 도메인 태그 4 아래에서 28개 워드에 대해 계산한 Poseidon2 해시이며, 트리의 리프로 추가됩니다. - 봉투(envelope): 소유자의 주소로 봉인된 평문이며, 소유자 또는 아웃고잉 뷰잉 키를 가진 발신자만 읽을 수 있습니다. chain 18부터(chain 20 포함) 봉투에는 암호화된 메모도 담깁니다. 체인이 공개 필드로부터 계산하는 일부 노트에는 봉투가 전혀 없습니다(아래 참조).
노트를 사용하면 널리파이어 H(NF, nk, cm)가 공개됩니다. 이는 소유자의 뷰잉 키와 커미트먼트에 대해 도메인 태그 3 아래에서 계산한 Poseidon2 해시입니다. nk 없이는 널리파이어를 그 커미트먼트와 연결할 수 없습니다. 이중 지불을 막는 것은 널리파이어 집합입니다.
봉투
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바이트) |
모든 부분은 앞에 무작위 12바이트 nonce를, 뒤에 16바이트 태그를 붙인 ChaCha20-Poly1305이며, 연관 데이터(associated data)는 태그(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 20의 모든 노트 봉투에는 메모의 유무와 관계없이 이 필드가 있습니다. 거스름돈 출력, 더미 출력, 메모 없는 지불은 모두 같은 방식으로 패딩되며, 체인에서는 메모, 메모의 길이, 메모의 부재를 구분할 수 없습니다. 메모는 노트, 커미트먼트, 어떤 증명에도 포함되지 않습니다. 메모는 이미 노트를 읽을 수 있는 사람, 즉 소유자, ovk를 가진 발신자, 또는 해당 출력의 TxKey를 건네받은 누구든지 읽을 수 있습니다. 형식이 잘못된 메모 필드(잘못된 길이, 유효하지 않은 UTF-8, 0이 아닌 패딩)는 메모 없음으로 열리며, 소유자가 노트를 잃는 일은 없습니다. 누구든 공개 주소로 먼지(dust) 노트를 보낼 수 있으므로 메모는 누구의 텍스트일 수도 있습니다. rand 지갑처럼 신뢰할 수 없는 내용으로 표시하고, 제어 문자와 유니코드 서식 문자는 치환하십시오. rand send --memo는 지불 출력에 메모를 봉인하며, rand notes --memo와 rand history --memo는 이를 보여 줍니다.
트리, 앵커, 윈도
커미트먼트는 하나의 깊이 32 Poseidon2 트리에 오래된 순서대로 추가되며 제거되지 않습니다. 리프 i는 체인이 만든 i번째 노트이고, 제네시스 입금 노트가 첫 리프입니다. 번들은 무엇을 옮기든 더미를 포함해 리프 4개와 널리파이어 4개를 추가합니다. 빈 리프는 모두 0이며, 노드는 H(NODE, left, right)입니다(도메인 태그 7). 노드는 프런티어만 보관하므로 합의 상태에는 리프가 남지 않습니다. rand_getWitness는 필요할 때 저장된 리프로 전체 트리를 재구성하며, 그래서 노드가 제공하는 읽기 중 가장 비용이 큽니다.
번들은 앵커, 즉 트리 루트에 대해 입력의 멤버십을 증명합니다. 두 가지 규칙이 앵커의 정직성을 지킵니다.
| 상수 | 값 | 규칙 |
|---|---|---|
ANCHOR_WINDOW | 256블록, chain 20에서는 1,024 | 앵커는 최근 window개의 블록 끝 루트 중 하나여야 하며, 블록 중간에 트리가 지나가는 루트는 앵커가 될 수 없음 |
TIME_WINDOW | 256블록, chain 20에서는 1,024 | 번들의 time은 [height − window, height] 안에 있어야 함 |
제네시스가 proof_window_blocks(v0.6.8, 256에서 4,096)를 설정하지 않으면 두 윈도는 모두 256블록이며, 이 필드는 두 윈도를 함께 대체합니다. chain 20은 1,024로 설정하며, chain 14부터 19까지는 이 필드가 없었습니다.
윈도가 있는 것은 증명에 실제로 시간이 걸리기 때문입니다. tier-14 번들 증명은 노트북에서 약 100초가 걸립니다(chain 17부터 지갑은 약 7초가 걸리는 두 번째 tier-10 인증 증명도 만듭니다). 256블록은 chain 18의 1.17초 블록에서 약 5분이었고, 느린 증명자에게 위임한 증명이 294초에 거부된 적이 있습니다. chain 20의 1,024블록은 탐색기가 보고한 1.34초 기준으로 약 23분입니다. 경쟁에서 진 지갑에는 anchor is not one of the last 1024 roots라는 응답이 돌아오며, 다시 증명해야 합니다. 매개변수 없는 rand_getAnchor는 헤드의 블록 끝 루트를 반환하며, 증명자가 기준으로 삼아야 하는 앵커는 이것뿐입니다.
번들
가치를 옮기는 모든 트랜잭션에는 정확히 하나의 고정된 4-in-4-out 히든 에셋 번들이 포함됩니다(chain 14부터이며, chain 13까지는 2-in-2-out이었습니다). 이 번들은 히든 에셋 zkVM 게스트가 증명하며, 게스트의 다이제스트 hc_bundle은 제네시스 파일에 고정됩니다(chain 17부터 20까지는 60af094acfe65d85fdb18fb3d06cf9085dcf28c96e59e87f1ee527226e6e3fce, 번들 게스트 v3이며, chain 14와 15에서는 게스트 v1인 83d3a3704a1fcdb9bae7136c0a947ffa34bd53f055388395ed705fe8cacd0ef8이었습니다).
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 토큰의 전송은 burn_a = burn_r = burn_asset = 0인 Action::None이고 필드 하나하나가 RAND 지불과 같은 형태입니다.
증명은 모든 실제 입력이 anchor 아래의 리프이며 해당 슬롯의 에셋을 담고 있다는 것, 네 개의 널리파이어가 공개된 것과 같다는 것, 각 그룹의 수지가 맞는다는 것(비공개 에셋에서는 in0 + in1 = out0 + out1 + burn_a, RAND에서는 in2 + in3 = out2 + out3 + fee + burn_r), 그리고 지출자가 키를 보유한다는 것을 보증하며, 어떤 리프인지, 어떤 금액인지, 어떤 에셋인지, 누구인지는 드러내지 않습니다. 세 개의 burn 필드는 풀에서 가치가 빠져나가는 것에 대해 번들이 공개하는 유일한 진술입니다. burn_a와 burn_asset은 TokenBurn 또는 BridgeBurn에서만 0이 아니며 이때 액션의 금액 및 에셋과 같습니다. burn_r은 소각된 RAND이며 Bond(스테이크)와 RegisterAggregator(본드)에서만 0이 아닙니다. 수수료는 항상 RAND입니다.
chain 17부터 지출 권한은 두 개의 증명으로 나뉩니다(분할 인증). 번들 게스트 v3은 이전 게스트가 지출 키를 받던 자리에서 뷰잉 키 nk와 솔트를 받아 pk_self = H(PK, nk)를 유도하고, 모든 입력이 이 값의 소유일 것을 요구하며, 공개하는 다이제스트에 c = H(AUTH, nk ‖ salt)를 포함시킵니다. 두 번째 게스트인 인증 게스트(제네시스 hc_auth, 1e4e347f44cf86750b30a9a4bdf9ec9256efe353d4ff8017451eca7d195639c1)는 지출 키 sk와 같은 솔트를 받아 nk = H(NK, sk)를 유도하고 같은 c를 공개합니다. 번들은 c를 auth_commit으로, 인증 게스트의 tier-10 증명을 auth_proof로 담고 있으며, 원장은 두 증명이 같은 트랜잭션 바인딩에 대해 검증되고 두 c 값이 일치할 것을 요구합니다. 따라서 nk와 솔트를 가진 증명자는 번들 증명은 만들 수 있지만 인증 증명은 만들 수 없으며, 위임 증명은 이 점에 근거합니다. 솔트는 번들마다 새로 뽑은 256비트 난수이므로 auth_commit은 공개되어 있어도 아무것도 연결하지 않습니다. 스스로 증명하는 지갑은 두 증명을 모두 만들기 때문에, 자체 증명한 트랜잭션과 위임 증명한 트랜잭션은 체인에서 똑같아 보입니다.
고정된 구조 때문에 슬롯은 흔히 더미입니다. 더미 입력은 지출하는 사람이 소유한 값이 0인 노트이며(널리파이어는 그대로 공개됩니다), 더미 출력은 금액이 0인 노트이고, 거스름돈 출력은 거스름돈이 0이어도 존재합니다. 거스름돈이 없을 때 모양이 달라지는 트랜잭션은 거스름돈이 없었다는 사실을 드러내게 됩니다. 지갑에 미치는 결과는 다음과 같습니다. 번들의 두 그룹은 각각 최대 두 개의 노트만 지출합니다. RAND 결제, 본드, 배포, 호출은 최대 두 개의 RAND 노트를 지출하며, 토큰 전송이나 소각은 해당 토큰의 노트를 최대 두 개, 수수료용 RAND 노트를 최대 두 개 지출합니다. 한 그룹에서 세 개 이상의 노트에 나뉜 금액은 먼저 자신의 주소로 보내 하나로 통합해야 합니다.
체인 14부터 증명은 해당 트랜잭션에도 묶입니다. 증명은 증명 부분을 비운 트랜잭션 전체의 해시에 대해 만들어지고 검증되므로, 다른 행위, 다른 봉투, 다른 체인에 복사할 수 없습니다. 체인 20에서는 해시가 제네시스 해시까지 포함하므로, 체인 id가 같은 다른 체인에도 복사할 수 없습니다(트랜잭션).
공개되는 것과 숨겨지는 것
| 체인에 공개 | 숨겨짐 | |
|---|---|---|
| RAND 또는 임의의 RPL 토큰의 전송 | 앵커, 네 개의 널리파이어 전부, 네 개의 커밋먼트 전부, 수수료, burn_a = burn_r = burn_asset = 0, time, 네 개의 봉투 암호문, 증명, auth_commit과 인증 증명 | 보낸 사람, 받는 사람, 금액, 메모, 이동한 자산, 거스름돈, 지출된 리프, 더미였던 슬롯 |
| 토큰 소각 또는 브리지 소각 | 전송이 공개하는 모든 것, 그리고 해당 행위의 자산과 금액에 같은 burn_asset과 burn_a(브리지 소각의 경우 릴레이어 수수료, 담보 토큰, 목적지 포함) | 소각된 노트, 소각한 사람 |
| 파우셋 발행 | 커밋먼트, 봉투, 금액, 발행한 밸리데이터, 그리고 체인 14부터 노트의 오프닝(pk, time, r)이며, 승인 과정에서 이 오프닝으로 커밋먼트를 다시 계산합니다 | 이후 노트의 모든 지출 |
| 제네시스 예치 | 제네시스 파일의 { cm, envelope, amount, opening: { pk, time, r } }이며, 등재된 토큰의 노트에는 오프닝에 asset이 들어갑니다(체인 20의 제네시스에는 두 개의 zUSD 노트가 있습니다). 소유자의 pk, 금액, 자산은 설계상 공개입니다 | 이후 지출되는 시점과 용도 |
| 출금, 브리지 예치, 토큰 발행 또는 등록의 최초 발행 | 금액, 수취인, 노트의 time과 블라인딩 r(커밋먼트는 체인이 직접 계산합니다) | 이후 노트의 모든 지출 |
| invoke의 지급 또는 프로그램 발행(체인 20) | 금액, 자산, 수취인, r이며, 노트의 time은 번들의 것입니다 | 이후 노트의 모든 지출 |
| 브리지 수수료 노트(체인 20) | 금액, 자산, time, 파생된 블라인딩, 소유자인 제네시스 트레저리이며, 봉투는 없습니다 | 이후 노트의 모든 지출 |
가치는 공개된 금액과 함께 풀에 들어오며(파우셋 발행, 제네시스 예치, 밸리데이터의 출금, 브리지 증명과 그 수수료 노트, 토큰 발행, 프로그램 금고의 지급), 공개된 수수료나 소각으로 풀을 떠납니다. 그 사이의 모든 것은 노트입니다.
이 표는 체인 20의 증명이 숨기는 내용이며, 항상 그랬던 것은 아닙니다. 체인 14와 15에서는 번들 증명 자체가 더 많은 것을 드러냈습니다(2026-09-27 zkVM 검토, INT-2 / GV-1). 각 테이블의 LogUp 누적합이 블라인딩 없이 공개되었고, v1 게스트는 더미 입력에 대해 머클 검사와 자산 검사를 건너뛰었으며 모든 머클 경로 비트에서 분기했습니다. 그래서 누구든 번들 증명만 보고 네 개의 입력 슬롯 중 어느 것이 실제인지, 따라서 토큰을 옮겼는지 RAND만 옮겼는지, 그리고 지출된 각 리프 인덱스에서 1인 비트의 개수를 읽어낼 수 있었습니다. 두 가지 변경으로 이를 막았습니다. 체인 16은 모든 LogUp 종단 값에 블라인딩을 적용하고 선언된 모든 테이블을 2^7행 이상으로 하한을 두는 제약 집합 7로, 그리고 더미든 실제든 모든 슬롯이 같은 명령으로 전체 멤버십 검사와 자산 검사를 수행하는 분기 없는 번들 게스트 v2로 전환했습니다. 체인 17부터 20까지 실행하는 게스트 v3는 지출 키 대신 nk와 솔트를 쓰는 v2의 관계이며, 역시 분기가 없습니다. 체인 14와 15에서 커밋된 번들은 이미 드러난 정보를 그대로 갖고 있습니다. 체인 20에서 증명이 여전히 보여 주는 것은 헤더입니다. 번들의 경우 고정된 하나의 형태(티어 14, 모든 번들에서 선언된 가스 한도 20,479)이고, Call이나 Invoke의 경우 티어, 선언된 가스 한도, 그리고 호출이 keccak 또는 sha256 프리컴파일을 사용했는지와 대략 얼마나 자주 사용했는지입니다(증명).
봉투가 없는 노트
대부분의 노트는 봉투를 열어 찾습니다. 일부 노트는 체인이 공개 필드로부터 계산하며, 그런 노트의 봉투는 없거나 신뢰해서는 안 됩니다.
- 브리지 수수료 노트(체인 20).
bridge.fees아래에서 체인은 모든 예치와 모든 소각마다 제네시스 트레저리에 zUSD 노트를 추가합니다(RPL 토큰). 이 노트에는 봉투가 없습니다. 저장소는 누구에게도 봉인되지 않은 빈 봉투로 색인하며,rand_getCompactBlocks와rand_getCommitments도 그렇게 보여 줍니다. 예치의 수수료 노트는 예치 노트 뒤에, 소각의 수수료 노트는 번들의 네 노트 뒤에 나옵니다. 노트의 모든 워드는 공개입니다. 금액은 트랜잭션과 제네시스 비율에서, 자산과time은 트랜잭션에서 나오고,from = 0이며, 블라인딩은 예치의 경우blake3("rand-bridge-fee-r-1" ‖ mu), 소각의 경우blake3("rand-bridge-burn-fee-r-1" ‖ txid)이고, 소유자는 제네시스의recipient입니다. - 봉투가 아무것도 구속하지 않는 노트. 브리지 예치, 토큰 발행, 등록의 최초 발행, 그리고 체인 20에서 invoke의 지급과 프로그램 발행은 트랜잭션을 제출한 쪽이 봉인한 봉투를 담고 있지만, 체인은 행위의 공개 필드로 노트를 계산할 뿐 봉투를 그 노트와 대조하지 않습니다. 부주의하거나 악의적인 제출자가 의미 없는 내용을 봉인할 수 있으므로, 수취인은 이에 의존해서는 안 됩니다.
따라서 rand 지갑은 이러한 노트를 스스로 재구성합니다(wallet::rebuilt_notes_with). 시험 복호화 옆에서 공개 단계로 수행하며, 자신의 pk를 지정하는 모든 해당 트랜잭션과, 자신의 주소가 트레저리일 때의 수수료 노트에 대해 노트를 계산하여 그 커밋먼트로 해시되는 리프에 기록합니다. 재구성된 노트는 후보일 뿐입니다. 트랜잭션, 수수료율, 해시에 대해 거짓말하는 노드는 노트를 찾지 못하게 만들 수는 있어도 노트를 입금된 것으로 만들 수는 없습니다. v0.6.8부터 rand_getBlocks가 반환하는 헤더는 예치, 발행, 등록 트랜잭션을 public_notes에 실제 id와 함께 증명을 제거한 채 담고, 브리지 수수료가 있는 체인에서는 소각도 담으므로, 이 단계는 헤더 페이지만 읽습니다. v0.6.8에서 invoke는 public_notes에 없으므로, v0.6.8 노드에 대해서는 지갑이 invoke의 지급을 봉투를 통해서만 찾으며, 이 봉투는 invoke한 쪽이 봉인하고 아무도 검사하지 않습니다. Fullnode main은 d0bb73fb 이후 invoke도 증명을 제거한 채 여기에 담습니다. 이는 노드 전용 변경이며 아직 체인 20의 플릿에 적용되지 않았고, 지갑은 이를 위해 변경할 필요가 없습니다.
스캔: 지갑이 자신의 노트를 찾는 방법
노드는 “여기 트리 전체가 있습니다”라고 답할 뿐, “여기 당신의 잔액이 있습니다”라고 답하지 않습니다. 지갑은 다음을 수행합니다.
rand_getCommitments(from_index, limit)(호출당 최대 1,000개) 또는 블록별 스트림rand_getCompactBlocks(from_height, to_height)(호출당 최대 128블록)로 리프를 페이지 단위로 읽습니다. 번들을 담은 모든 트랜잭션은 여기에 네 개의 노트와 네 개의 널리파이어를 나열하며, 어떤 독자도 더미 슬롯과 실제 슬롯을 구별할 수 없습니다.- 모든 봉투를 시험 복호화합니다. 수신자로서는 ML-KEM 역캡슐화 키로, 발신자로서는
ovk로 복호화하고,pk가 자신의 것인 노트를 보관하며, 공개 금액을 가진 노트는 해당 트랜잭션으로부터 재구성합니다(위). - 보관한 각 노트의 널리파이어를 계산하고,
rand_getNullifiers(from_height, limit)에 나타나는 것을 표시합니다. - 나머지를 합산합니다.
rand_getTreeInfo는 스캔해야 할 범위를 알려 줍니다: { "next_index", "root", "nullifiers" }. 지출을 위해 지갑은 각 실제 입력의 32단계 머클 경로가 필요합니다. 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 이후부터, 해당 노트의 nullifier가 확인되거나 time + window를 넘어서는 블록이 확인되는 다음 sync까지 pending 상태입니다. 이 window는 chain 20에서 1,024이며 rand_getLimits.proof_window_blocks에서 읽고, 이 값이 없으면 256입니다. 이후에는 해당 번들이 더 이상 승인될 수 없으며 노트를 다시 사용할 수 있게 됩니다.