본문으로 건너뛰기
Anon Wallet

다이제스트 동기화

첫 실행 시 각 지원 체인에서 발생한 모든 Railgun 쉴드 풀 이벤트를 따라잡아야 합니다. 이더리움만 해도 수백만 블록에 걸친 수십만 커밋먼트, 널리파이어, 언쉴드 이벤트가 있습니다.

RPC로 모두 스캔하면 수십 분이 걸리지만 다이제스트 동기화는 수초로 줄입니다.

Anon SDK에 구현하고 초기화 시 Railgun 엔진에 설치하므로 모든 클라이언트가 같은 구현과 전송 계약을 공유합니다. 이 페이지는 계약을 설명합니다. 속도 외 프라이버시상의 의미는 지갑 동기화를 참고하세요.

문제

엔진은 암호화 노트의 로컬 머클 트리를 유지합니다. 처음부터 만들려면 컨트랙트 배포 이후 모든 커밋먼트 이벤트를 처리해야 합니다.

기본 구현은 Graph/Subgraph를 조회합니다. 이더리움에서는 수십만 이벤트를 페이지별로 가져오고 트리를 만들 동안 전체 데이터를 메모리에 유지해야 합니다. 느리고 작은 메모리에 취약하며 요청마다 클라이언트별 필터가 붙습니다.

작동 방식

인덱서가 온체인 이벤트를 계속 처리해 압축된 분할 파일 청크로 CDN에 올립니다. 클라이언트는 쿼리 대신 파일을 받습니다.

quickSyncEvents, quickSyncRailgunTransactionsV2 콜백을 교체하고 내장 Graph 콜드 동기화를 끕니다. 이미 전체 데이터를 다이제스트로 받으므로 Graph를 남기면 전환 전 실패 시도만 추가됩니다. Graph 폴백은 없습니다. CDN 접근이 복구돼야 동기화가 진행됩니다.

CDN 구조

v4/{chainId}/
  meta.json                              <- root manifest
  chunk-index.json                       <- per-chunk block/sequence ranges
  chunks/
    0/
      meta.json                          <- per-chunk metadata (block range, checksums, counts)
      commitments.json.gz               <- flat array of commitment events
      nullifiers.json.gz                <- flat array of nullifier events
      unshields.json.gz                 <- flat array of unshield events
      transactions.json.gz             <- flat array of transaction events
      tree-utxo-{n}.bin.gz             <- packed merkle tree, present on finalizing chunks
      tree-txid-{n}.bin.gz
    1/
      meta.json
      commitments.json.gz
      ...
  tip/
    meta.json                            <- mutable tip metadata
    commitments.json.gz
    nullifiers.json.gz
    unshields.json.gz
    transactions.json.gz
    tree-utxo-active.bin.gz             <- packed boundary tree
    tree-txid-active.bin.gz

실제 엔드포인트:

  • 루트 매니페스트: https://digest.anon.inc/v4/{chainId}/meta.json
  • 청크 인덱스: https://digest.anon.inc/v4/{chainId}/chunk-index.json
  • 청크 메타데이터: https://digest.anon.inc/v4/{chainId}/chunks/{id}/meta.json
  • 이벤트 파일: https://digest.anon.inc/v4/{chainId}/chunks/{id}/commitments.json.gz

유형별 gzip 파일에 평면 JSON 배열을 저장합니다. 지출 감지용 널리파이어만 필요하면 나머지를 생략하는 등 선택 다운로드가 가능합니다.

경로는 체인 상태만으로 정합니다. 지갑 식별자, 보기 키, 파생 필터가 없어 공통 캐시가 가능하고 해당 정보를 공개하지 않습니다. 프라이버시 참고.

캐시 계층

시간에 따라 세 계층으로 이동합니다.

계층 Cache-Control 대상
실시간 public, max-age=15 루트, chunk-index.json, tip/ 전체
수정 기간 public, max-age=300 가장 최근 봉인 청크
불변 public, max-age=31536000, immutable 이전 봉인 청크 전체

최신 청크의 5분 수정 기간에는 경로 버전을 바꾸지 않고 다시 PUT하여 회귀를 고칠 수 있습니다. 다음 봉인 시 이전 청크의 본문은 그대로 두고 헤더만 불변으로 바꿉니다.

불변 객체를 엣지에서 최대 1년 제공하므로 사용자 수와 관계없이 원본은 위치·객체별 대략 한 번의 캐시 채우기만 담당합니다.

루트 매니페스트

루트 meta.json은 수백 바이트와 스냅샷 레지스트리로 작고 거의 고정된 크기입니다.

{
  "version": 5,
  "formatVersion": 2,
  "chainId": 1,
  "txidVersion": "V2_PoseidonMerkle",
  "latestSealedChunk": 198,
  "latestBlock": 25725624,
  "tipChecksum": "sha256_of_tip_meta_json",
  "generatedAt": "2026-08-10T15:57:59Z",
  "live": {
    "txidLeafCount": 125336,
    "utxoLeafCount": 252705,
    "endBlock": 25725609
  },
  "treeSnapshot": { /* see Tree Snapshots */ }
}

청크 배열을 포함하지 않습니다. latestSealedChunk는 0부터 N까지의 봉인 범위를 나타냅니다. tipChecksum은 tip의 meta.json SHA-256으로 변경을 감지합니다. formatVersion이 CDN과 로컬 캐시에서 다르면 전체 삭제 후 다시 받습니다.

live가 tip 위치를 봉인 워터마크와 같은 객체에 넣으므로 두 요청의 경쟁 없이 한 번에 원자적으로 읽습니다.

청크 인덱스

chunk-index.json은 각 봉인 청크의 블록·시퀀스 범위를 작은 객체로 제공합니다.

{
  "chainId": 1,
  "chunks": [
    { "id": 0, "startBlock": 15725039, "endBlock": 16234121, "firstSeq": 0, "lastSeq": 799 }
  ]
}

거래 없는 청크는 firstSeq가 -1입니다. 메타데이터를 순회하지 않고 한 요청으로 위치를 찾습니다. 아직 게시하지 않은 체인은 404를 반환하며 전체 청크 순회로 대체합니다.

청크별 메타데이터

각 디렉터리의 meta.json은 블록 범위, 이벤트 수, 파일별 체크섬을 담습니다.

{
  "chunkId": 0,
  "formatVersion": 3,
  "startBlock": 15725039,
  "endBlock": 16234121,
  "counts": { "commitments": 1081, "nullifiers": 1323, "unshields": 595, "transactions": 800 },
  "files": {
    "commitments": { "checksum": "a1b2c3...", "size": 245000, "jsonSize": 720000 },
    "nullifiers": { "checksum": "d4e5f6...", "size": 180000, "jsonSize": 510000 },
    "unshields": { "checksum": "g7h8i9...", "size": 195000, "jsonSize": 580000 },
    "transactions": { "checksum": "j0k1l2...", "size": 91000, "jsonSize": 260000 }
  },
  "sealedAt": "2026-03-15T..."
}

체크섬은 압축 해제 JSON의 SHA-256입니다. size는 gzip 바이트 수, jsonSize는 해제 크기입니다.

청크와 루트의 formatVersion은 독립적입니다. 청크 formatVersion: 3과 루트 formatVersion: 2의 공존은 정상이며 같은 종류끼리 비교해야 합니다.

청크 봉인

청크는 모든 유형 합계 약 3,000개 이벤트를 목표로 하고 tip이 약 6,000개를 넘으면 새 청크를 봉인합니다. 한 블록의 이벤트는 나누지 않고 블록 경계에 맞춥니다. 이더리움은 봉인 청크가 200개에 가까우며 L2는 훨씬 작습니다.

클라이언트

초기화 때 함수를 직접 엔진에 전달하며 전역 훅을 쓰지 않습니다. 체인 이벤트 요청 시:

  1. 루트 가져오기: 작은 요청 하나로 봉인 수, tip 체크섬, 실시간 위치를 얻습니다.

  2. 누락분만 받기: 청크별 로컬 캐시를 쓰고 봉인 청크는 존재하면 영구 재사용합니다. tipChecksum 변경 시 tip을 다시 받습니다. 캐시 미스마다 meta.json을 받은 뒤 이벤트 파일을 병렬로 받습니다.

  3. 재조립: 청크를 AccumulatedEvents에 합치고 커밋먼트를 머클 삽입 순서로 정렬해 엔진에 넘깁니다.

선택은 블록 높이와 캐시에 따르며 지갑 내용과 무관합니다. 같은 동기화 위치면 같은 요청을 합니다.

캐시 정확성

봉인 파일의 불변 URL은 캐시에 좋지만 수정에는 위험합니다. 생산자가 재시퀀싱해 같은 URL의 바이트를 바꾸면 중간 캐시가 이전 파일을 계속 내줄 수 있습니다. 재구축은 오류 없이 tip 전에 멈춥니다.

두 제어로 이를 막습니다.

  • 봉인 파일에 항상 cache: 'reload' 사용. HTTP 캐시를 건너뛰고 CDN에서 다시 받습니다. 정상 웜 상태는 영구 청크 저장소를 쓰므로 추가 요청이 없고, 복구 경로별로 별도 활성화할 필요도 없습니다.
  • Tip에 ?v={tipChecksum} 사용. 내용이 바뀌면 브라우저부터 엣지까지 캐시 키가 바뀌어 옛 항목의 만료를 기다리지 않고 요청 자체를 피합니다.

두 번째는 최적화가 아닌 정확성 장치입니다. 짧은 TTL 설계라도 잘못된 캐시 정책 하나로 조용히 고정될 수 있습니다. 2026년 8월 CDN이 max-age=14400을 적용하여 지갑이 몇 시간 동안 이전 tip을 읽었고, 생산자가 검증 해시 체인을 고친 뒤에도 문제 데이터를 받았습니다. 내용별 키 회전은 중간 설정과 무관하게 클라이언트의 이런 오래된 조회를 막습니다.

TXID 거래 동기화

거래 이벤트는 TXID 머클 트리 동기화를 가속합니다. UTXO 이벤트는 하나의 메모리 객체로 합치지만, 거래는 이벤트 파일 형식의 transactions.json.gz에 대응하는 커서 기반 배치로 전달합니다.

엔진이 QuickSyncRailgunTransactionsV2(chain, latestGraphID)를 반복 호출하면:

  1. latestGraphID를 십진수 sequence 라벨로 읽습니다. 누락·빈 값·예전 비숫자 커서는 첫 시퀀스 이전에서 시작합니다.
  2. 커서 청크를 찾아 검증용 현재 헤드를 포함한 한 배치 분량을 가져옵니다. 따라잡는 동안 전체 tip/, 실시간에는 시퀀스별 최근 이벤트를 씁니다.
  3. sequence > cursor.sequence를 남기고 중복 제거(봉인 우선), sequence 정렬 후 모든 해시 링크를 검증합니다. 누락 잎이 없음을 해시로 입증할 때만 번호 간격을 건넙니다. 깨지거나 검증 불가면 멈추고 배치 크기 내 유효 접두부만 전달합니다.

엔진은 순서대로 추가하고 마지막 거래의 십진 문자열 graphID로 계속합니다. 라벨과 정규 잎 위치는 다른 좌표이므로 커서를 트리 인덱스로 쓰면 안 됩니다.

미리 가져오기

엔진 초기화 중 이벤트 요청 전에 체인별 매니페스트와 봉인 청크·tip을 병렬로 로컬에 넣습니다. 요청 시 이미 캐시돼 있어 반복 실행은 거의 즉시 동기화됩니다.

실시간 폴링

초기 동기화 후 두 모드를 사용합니다.

  • API 모드: 기본 5초 간격으로 /api/v1/indexer/head/{chainId}/status를 확인합니다. tipBlock이 변하면 /recent 증분을 받고, 빈 응답에 로컬이 뒤처지면 범위를 넓힙니다.
  • CDN 모드: status: "not_configured"일 때 30초 간격 폴백으로 루트 meta.json의 tipChecksum을 비교합니다.

새 이벤트가 오면 둘 다 가벼운 잔액 갱신을 실행합니다.

마우스·키보드·스크롤·터치 활동이 10분 없으면 멈추고 활동 시 재개합니다.

API만 Anon 운영 인프라에 도달하며 클라이언트 상태를 전달합니다. ?fromBlock / ?fromSequence는 체인 위치이지 지갑 조건이 아닙니다. 프라이버시 참고.

API 폴링 흐름

5초마다 다음 순서로 실행합니다.

  1. 상태 조회: GET /api/v1/indexer/head/{chainId}/status에서 tipBlock, cdnBlock, reorgEpoch, 활성 상태를 받습니다.
  2. 재구성 확인: reorgEpoch 증가 시 복구하고 일반 처리를 건너뜁니다.
  3. 첫 폴링: tipBlock, cdnBlock을 기준으로 기록하고 이벤트는 받지 않습니다.
  4. 변경 감지: tipBlock이 같으면 아무 작업도 하지 않습니다.
  5. 워터마크 확보: 새 tipBlock을 즉시 기록해 동시 요청 중복을 막습니다.
  6. 증분 조회: GET /api/v1/indexer/head/{chainId}/recent가 cdnBlock과 tipBlock 사이를 base64 JSON으로 반환합니다. 있으면 엔진에 전달합니다.
  7. 넓은 폴백: 봉인 직후 등 /recent가 0개이고 로컬이 tipBlock보다 뒤면 ?fromBlock={clientBlock}으로 마지막 블록부터 넓게 요청합니다.

최근 이벤트 엔드포인트

GET /api/v1/indexer/head/{chainId}/recent[?fromBlock=N | ?fromSequence=N]

cdnBlock 또는 지정 커서 이후 증분을 반환합니다.

{
  "chainId": "1",
  "tipBlock": "24729300",
  "cdnBlock": "24729271",
  "recentEvents": 3,
  "eventsJson": "<base64-encoded JSON>"
}

eventsJson은 CDN tip과 같은 네 유형 배열입니다. 디코딩·파싱 후 로컬 저장 완료를 기다리지 않고 엔진에 전달하며, 영구 저장은 백그라운드에서 비동기로 합니다.

블록·시퀀스 커서. ?fromBlock은 내용 기반 UTXO에 맞지만 워터마크 아래 TXID를 빠뜨릴 수 있습니다. TXID는 ?fromSequence와 해시 체인 검증을 씁니다. 구 서버가 매개변수를 무시할 수 있어 필요하면 전체 tip/를 사용합니다. 번호 간격만으로 잎 누락을 판단하지 않으며 해시 연속성이 기준입니다.

체인 재구성 복구

인덱서는 /status로 재구성을 알립니다. 클라이언트가 감지·복구합니다.

감지

/status의 세 필드:

  • reorgEpoch — 재구성마다 증가하는 단조 카운터.
  • reorgForkBlock — 분기 블록 번호.
  • reorgTxCount — TXID에서 되돌릴 Railgun 거래 수.

매 틱 reorgEpoch를 비교해 증가하면 복구합니다. 로컬에도 보존해 종료 중 재구성을 다음 실행에서 감지합니다.

복구 단계

  1. TXID 롤백: clearLeavesForInvalidVerificationHash(txCount)로 잎을 지우고 invalidateTXOsCacheAllWallets()로 DB 재읽기를 강제합니다.
  2. 이벤트 삭제: blockNumber > forkBlock인 로컬 이벤트를 지웁니다.
  3. SyncMeta 초기화: highestBlock이 forkBlock을 넘으면 forkBlock으로 되돌립니다.
  4. 재동기화: CDN tip을 다시 넣고 UTXO → TXID 전체 파이프라인으로 수정된 체인을 받습니다.

UTXO는 명시적 롤백이 필요 없습니다. 루트 검증기가 오래된 커밋먼트를 건너뛰고 다음 동기화의 수정본을 받아 복구합니다.

지연 재구성

UTXO 스캔 중 바로 복구하면 읽는 저장소를 바꾸게 됩니다. 따라서 대기 작업으로 보관하고 다음 폴링에서 새 이벤트 적용 전에 처리해 스캔이 먼저 끝나게 합니다.

동기화 불일치 복구

더 어려운 문제는 명시적 오류 없이 tip에 도달하지 못하는 경우입니다. TXID 구멍, 나중에 바뀐 잎 위치, 이미 읽은 데이터의 재시퀀싱은 무한 진행처럼 보입니다.

SDK는 독립 신호로 감지합니다.

  • 정체 감지: 커서가 움직이지 않으면 무한 재시도 대신 분류합니다.
  • 재시퀀싱 감지: 로컬 시퀀스와 현재 CDN의 차이로 이력 재작성을 파악합니다.
  • 무진행 차단: 새 잎 없는 배치가 반복되면 루프를 끊습니다.
  • 스냅샷 재수화: 스냅샷 기반 체인의 TXID 정체는 스냅샷 재적용과 백필로 복구합니다.

전체 TXID 초기화는 0부터 시작해 생성 시점 이전의 UTXO 데이터를 요구합니다. 생성 시점에 맞춰 줄인 스냅샷은 이를 의도적으로 저장하지 않았으므로 전체 재구축이 같은 정체를 재현합니다. 대신 스냅샷을 다시 적용합니다.

자동 복구는 체인·세션당 한 번이며 시작 전에 횟수를 소모해 실패도 계산합니다. 이후 자동 재활성화 대신 수동 경로로 올립니다.

이벤트 파일 형식

청크와 tip은 하나의 전송 형식입니다. 각 청크와 tip/에는 네 gzip 파일이 있고 유형별 평면 JSON 배열을 담습니다.

파일 배열 요소 용도
commitments.json.gz 커밋먼트 배치 UTXO 머클 잎
nullifiers.json.gz 널리파이어 지출 감지
unshields.json.gz 언쉴드 출금 기록
transactions.json.gz Railgun 거래 TXID 머클 잎

같은 요소의 바이트 형식은 호환됩니다. 메타데이터 래퍼와 수명 주기만 달라 같은 코드로 파싱합니다. 청크와 tip 참고.

인코딩 규칙

  • 해시·필드 요소: 0 패딩한 64자(32바이트) hex입니다. 0x 여부는 필드별이며 틀리면 Poseidon이 맞지 않습니다.
  • 2^53을 넘을 수 있는 정수(value, amount, fee, tokenSubID)는 십진 문자열입니다.
  • 블록 번호·타임스탬프·트리 및 인덱스 위치는 JSON 숫자입니다.
  • 주소(toAddress, tokenAddress)는 0x 접두사가 있는 EIP-55 체크섬 형식입니다.
  • tokenType: 0 = ERC20, 1 = ERC721, 2 = ERC1155.
필드 0x 여부
transaction.commitments[], transaction.nullifiers[] 있음
transaction.txid / boundParamsHash / verificationHash / railgunTxid 없음
commitment.hash / txid / npk / shieldKey / encryptedBundle[] / encryptedRandom[] 없음
preImage.value 있음
TransactCommitmentV2 ciphertext.memo 있음
TransactCommitmentV2 ciphertext.iv / tag / data[] / annotationData / 보기 키 없음
nullifier.nullifier / nullifier.txid 없음
unshield.txid 없음
주소 있음, EIP-55

transactions.json.gz

TXID 입력이며 각 요소는 내부 Railgun 거래입니다. 하나의 온체인 거래에 여러 내부 거래가 있으면 txid는 같지만 sequence, commitments, nullifiers, boundParamsHash, railgunTxid, utxoBatchStartPositionOut은 다릅니다.

{
  "version": 2,
  "sequence": 54307,
  "commitments": ["0x1b02b669258e874b2a3d3e2c58e5b4ee7f38dbb8470766a89a119db47eaf4d9b"],
  "nullifiers": ["0x298937ed3be6af8bf16ef3e0a70212e1012ce3902a15bfe2f23eae05df3f27ec"],
  "boundParamsHash": "129bda4babbc749540d03d7c4514c5ecda06009a984ee7dbf653e6ca4fb762ac",
  "blockNumber": 449432836,
  "txid": "63459180506a7477924d1217245f967f22c32a2dbb0094c374a4d66eb236be5d",
  "utxoTreeIn": 1,
  "utxoTreeOut": 1,
  "utxoBatchStartPositionOut": 35669,
  "timestamp": 1775426303,
  "verificationHash": "2395448c1a31f6b4e018c213f27801421f13d71d5bd7c94d7fbbe30a13c3b15f",
  "railgunTxid": "2a79bc02e174d1ff5b6675b04abe3fde748d47fc40628de8fa1d21e9d7c09137",
  "unshield": {
    "tokenData": { "tokenAddress": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", "tokenType": 0, "tokenSubID": "0" },
    "toAddress": "0x5aD95C537b002770a39dea342c4bb2b68B1497aA",
    "value": "1000000000000000"
  }
}
필드 타입 설명
version number 항상 2, V2_PoseidonMerkle.
sequence number 체인별 단조 순서 키. 순서와 sequence 불변식 참고.
commitments string[] 0x 포함 32바이트 hex.
nullifiers string[] 0x 포함 32바이트 hex.
boundParamsHash string 접두사 없는 32바이트 hex.
blockNumber number
txid string 온체인 거래 해시, 접두사 없는 32바이트 hex.
utxoTreeIn / utxoTreeOut number 입력·출력 UTXO 트리 인덱스.
utxoBatchStartPositionOut number 출력 트리의 커밋먼트 시작 위치.
timestamp number Unix 초.
verificationHash string 전체 거래의 누적 해시 체인, 접두사 없는 32바이트 hex.
railgunTxid string poseidon(nullifiers, commitments, boundParamsHash), 접두사 없음. 인덱서 계산, 비면 생략.
unshield object 언쉴드 거래만 포함.

commitments.json.gz

배치 이벤트 배열입니다. 한 온체인 로그의 커밋먼트를 startPosition부터 연속 트리 위치로 묶습니다.

{
  "txid": "63459180506a7477924d1217245f967f22c32a2dbb0094c374a4d66eb236be5d",
  "treeNumber": 1,
  "startPosition": 35669,
  "blockNumber": 449432836,
  "commitments": [ /* 1+ commitment objects, see types below */ ]
}

각 객체는 commitmentType, txid, timestamp, hash, blockNumber, utxoTree, utxoIndex와 유형별 필드를 가집니다.

ShieldCommitment(타입 2) — 풀 입금:

{
  "commitmentType": "ShieldCommitment",
  "txid": "…", "timestamp": 1775426303, "hash": "…", "blockNumber": 449432836, "utxoTree": 1, "utxoIndex": 35669,
  "preImage": { "npk": "…", "token": { "tokenAddress": "0x82aF…", "tokenType": 0, "tokenSubID": "0" }, "value": "0x…" },
  "encryptedBundle": ["…", "…", "…"],
  "shieldKey": "…",
  "fee": "10000000000000",
  "from": null
}

preImage.value에는 0x가 있고 fee는 십진 문자열로 0이면 생략합니다. from은 항상 null입니다.

TransactCommitmentV2(타입 3) — 비공개 전송의 출력 노트:

{
  "commitmentType": "TransactCommitmentV2",
  "txid": "…", "timestamp": 1775426303, "hash": "…", "blockNumber": 449432836, "utxoTree": 1, "utxoIndex": 35670,
  "ciphertext": {
    "ciphertext": { "iv": "…(16-byte hex)", "tag": "…(16-byte hex)", "data": ["…", "…"] },
    "blindedReceiverViewingKey": "…",
    "blindedSenderViewingKey": "…",
    "memo": "0x…",
    "annotationData": "…"
  },
  "railgunTxid": "2a79bc02…"
}

ciphertext.memo에는 0x가 있고 iv/tag/data[]/annotationData/보기 키에는 없습니다. 일치 거래가 없으면 railgunTxid는 null입니다.

LegacyGeneratedCommitment(타입 0) — ShieldCommitment와 같지만 encryptedBundle/shieldKey/fee/from 대신 encryptedRandom: [string, string]를 씁니다.

LegacyEncryptedCommitment(타입 1) — TransactCommitmentV2와 유사하나 ciphertext.ephemeralKeys: string[], ciphertext.memo: string[]를 씁니다. 후자는 0x 문자열이 아닌 배열입니다.

레거시 유형은 이더리움 V2 이전 청크에만 있습니다. 새 체인은 ShieldCommitment, TransactCommitmentV2만 있지만 전체 소비자는 네 유형 모두 파싱해야 합니다.

nullifiers.json.gz

{
  "nullifier": "298937ed3be6af8bf16ef3e0a70212e1012ce3902a15bfe2f23eae05df3f27ec",
  "treeNumber": 1,
  "txid": "63459180506a7477924d1217245f967f22c32a2dbb0094c374a4d66eb236be5d",
  "blockNumber": 449432836
}

nullifier, txid는 접두사 없는 32바이트 hex이며 nullifier가 전역 고유 키입니다.

unshields.json.gz

{
  "txid": "cb4293e3a81241ef8b6c48285e4860222d9c147da8583aa1d53a7117f230c368",
  "timestamp": 1676326168,
  "toAddress": "0x5aD95C537b002770a39dea342c4bb2b68B1497aA",
  "tokenType": 0,
  "tokenAddress": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
  "tokenSubID": "0",
  "amount": "1000000000000000",
  "fee": "10000000000000",
  "blockNumber": 60674448,
  "eventLogIndex": 5,
  "railgunTxid": "2a79bc02…",
  "poisPerList": null
}

amount/fee/tokenSubID는 십진 문자열입니다. timestamp, eventLogIndex, railgunTxid, poisPerList는 null일 수 있습니다. (txid, eventLogIndex)로 고유하며 poisPerList는 현재 항상 null입니다.

청크와 tip

요소 형식은 같고 메타데이터와 수명 주기만 다릅니다.

봉인 청크 Tip
경로 chunks/{id}/ tip/
메타데이터 chunkId, startBlock, endBlock, counts, files, sealedAt startBlock, endBlock, counts, files, generatedAt, chunkId 없음
변경 승격 후 불변 인덱서 flush마다 재작성
Cache-Control 최신은 max-age=300, 이후 max-age=31536000, immutable max-age=15
변경 감지 청크 id 존재, latestSealedChunk 루트의 tipChecksum

tip/meta.json:

{
  "formatVersion": 2,
  "startBlock": 449762846,
  "endBlock": 449765001,
  "counts": { "commitments": 412, "nullifiers": 388, "unshields": 21, "transactions": 140 },
  "files": {
    "commitments": { "checksum": "…", "size": 12345, "jsonSize": 45678 },
    "nullifiers": { "checksum": "…", "size": 9876, "jsonSize": 23456 },
    "unshields": { "checksum": "…", "size": 1234, "jsonSize": 4567 },
    "transactions": { "checksum": "…", "size": 5678, "jsonSize": 12345 }
  },
  "generatedAt": "2026-04-06T12:34:56Z"
}

Tip은 미봉인 실시간 창입니다. 약 6,000개를 넘으면 오래된 약 3,000개를 블록 경계에 맞춰 봉인하고 tip을 줄입니다. 이벤트가 tip/에서 chunks/{id}/로 옮겨도 sequence를 포함한 내용은 유지됩니다.

순서와 sequence 불변식

TXID 소비자의 계약:

  • 모든 파일에서 transactions는 (blockNumber, sequence) 오름차순입니다. 나머지는 위치, 널리파이어, (txid, logIndex)로 식별하므로 배열 순서에 의존하지 마세요. 커밋먼트는 (treeNumber, startPosition) 삽입 순서로 재정렬합니다.
  • sequence는 체인별 순서 라벨이며 빈틈 없는 인덱스가 아닙니다. 수정으로 누락 번호·중복 범위가 생길 수 있습니다. 라벨 중복 제거 후 해시 검증된 간격만 건너며 누락 잎을 조용히 건너뛰면 안 됩니다.
  • 평면 TXID 인덱스(tree * TREE_MAX_ITEMS + index)는 추가 위치에서 나오며 sequence와 다를 수 있습니다. 검증 TXID 인덱스·트리 길이와 라벨 커서를 구분하고 실제 헤드 관계로 변환합니다.
  • 재개 graphID는 마지막 수락 sequence의 십진 문자열입니다. 커서 청크와 헤드를 검증하고 이미 받은 라벨을 제거한 뒤 검증된 나머지를 순서대로 넣습니다. 누락·옛 비숫자 커서는 콜드 시작입니다.

트리 스냅샷

생산자는 이벤트와 함께 패킹한 머클 트리 자체도 게시합니다. 재구축용 이벤트가 아니라 트리이며 다른 파일처럼 지갑과 무관한 공통 CDN 경로입니다.

gzip으로 압축한 바이너리입니다.

  • chunks/{id}/tree-utxo-{n}.bin.gz, chunks/{id}/tree-txid-{n}.bin.gz — 최종 잎 수에 도달한 트리, 완료 청크에 한 번 쓰고 불변.
  • tip/tree-utxo-active.bin.gz, tip/tree-txid-active.bin.gz — 봉인 데이터만 포함하는 현재 경계 트리.

루트의 treeSnapshot이 위치·검증 레지스트리입니다.

{
  "sealedEndBlock": 25684485,
  "chunkId": 198,
  "treeDepth": 16,
  "finalized": {
    "utxo": [
      { "tree": 0, "chunkId": 53, "leafCount": 65536, "finalizedAtBlock": 21332114, "root": "02854cff…" },
      { "tree": 1, "chunkId": 106, "leafCount": 65535, "finalizedAtBlock": 23461913, "root": "23699538…" }
    ],
    "txid": [
      { "tree": 0, "chunkId": 107, "leafCount": 65536, "finalizedAtBlock": 23481977, "root": "21efb8c0…" }
    ]
  },
  "active": {
    "utxo": { "tree": 3, "leafCount": 54733, "root": "2885ae14…", "checksum": "8f68e1ac…", "bytes": 3504154 },
    "txid": { "tree": 1, "leafCount": 58775, "root": "108cf0ac…", "checksum": "fa4c125d…", "bytes": 3762975 }
  },
  "txidState": { "leafCount": 124311, "cursorSeq": 124310, "verificationHash": "30c7d5e1…" }
}
필드 설명
treeDepth 잎 용량 지수, 2treeDepth개. 프로토콜이 공지 후 변경할 수 있으므로 16·65536을 고정하지 말고 읽어야 합니다.
finalized[].leafCount 최종 잎 수. 보통 2treeDepth이나 배치가 남은 공간에 안 맞으면 조기 완료합니다. 메인넷 UTXO 트리 1은 65,535개입니다.
finalized[].finalizedAtBlock 완료 잎의 블록 또는 그 이후 경계. 이후 생성 지갑은 소유 노트가 없으므로 잎 데이터는 생략하되 노드는 수화합니다.
finalized[].root / active.root 접두사 없는 64자 hex. 바이너리 마지막 기록과 비교합니다.
active.checksum 제공된 gzip 바이트의 SHA-256.
txidState 경계의 전체 TXID 잎 수, 마지막 봉인 잎의 시퀀스 라벨, 검증 해시. 번호는 비어도 잎 순서는 서수입니다.

leafCount는 항상 2의 거듭제곱이 아니며 cursorSeq는 위치가 아닌 라벨입니다. 인덱스로 사용하면 어긋납니다.

콜드 시작과 웜 시작

상황 처리 일반 시간
콜드 시작, 첫 설치 모든 봉인 청크와 tip 병렬 다운로드. 이더리움 약 200개, L2는 더 적음 5–15초
웜 시작, 재방문 매니페스트 확인 후 변경 tip만 다운로드 1초 미만
CDN 접근 불가 진행 중단, 인덱서 상태 표시 및 재시도 —

Graph 폴백은 없습니다. 내장 Graph 콜드 동기화가 꺼져 있으므로 CDN 장애 시 느린 쿼리 경로로 전환하지 않고 멈춥니다.

무결성

이벤트 체크섬은 압축 전 JSON, 스냅샷은 제공된 gzip 바이트를 대상으로 합니다. 스냅샷 끝의 루트를 레지스트리와 직접 비교합니다. Tip은 루트 tipChecksum으로 검증하며 formatVersion 불일치 시 전체 삭제·재다운로드합니다.

5분 수정 기간 후 봉인 내용은 안정적이지만 이는 생산자의 속성입니다. 클라이언트는 수정이 보이도록 중간 캐시를 맹신하지 않고 다시 가져옵니다. 캐시 정확성 참고.

공개·불변 주소이므로 누구나 청크 해시를 대조하고 온체인 로그나 다른 인덱서와 비교할 수 있습니다. Anon 인프라 접근 없이 검증하며 CDN은 가용성 외에는 신뢰할 필요가 없습니다.

생산자 구조

두 시스템이 만듭니다.

  • 다이제스트 생성기: TypeScript 일회성 시드 작업으로 상위 이력을 제한된 라운드로 받고 봉인·R2 업로드합니다. 새 체인 초기화 시 수동 실행합니다.
  • 인덱서: Go 상시 프로세스로 CDN에서 시작해 실시간 블록으로 tip을 만듭니다. 약 6,000개 초과 시 봉인하고 이전 청크를 불변으로 바꾸며 트리 완료 시 스냅샷을 게시합니다. R2 업로드는 15초 간격으로 제한합니다.

상위 인덱서는 생산자 의존성이지 지갑 의존성이 아닙니다. 클라이언트는 조회하지 않습니다. 별도 상태 신호로 읽어 지연 원인이 상위인지 Anon인지 구분합니다.

프라이버시

속도뿐 아니라 프라이버시 메커니즘입니다. 일반 설명은 지갑 동기화, 여기서는 프로토콜 속성을 설명합니다.

요청은 지갑 내용과 무관합니다. digest.anon.inc 경로는 청크, 블록 범위, tip 체크섬 같은 체인 상태에서 나옵니다. 키·주소·노트에서 파생하지 않습니다. 같은 위치면 보유 자산과 관계없이 같은 바이트 요청을 합니다. 서버가 해석·기록·보관·제출할 지갑 필터 쿼리는 없습니다.

일치 여부는 서버가 구분 못 합니다. 기기에서 받은 뒤 시험 복호화하므로 해당 청크에 자기 노트가 있는지는 클라이언트만 판단합니다.

균일성은 설계 제약입니다. 선택이 지갑 내용과 무관할 때만 성립합니다. 자산에 따라 필요한 범위만 받으면 쿼리를 재구성하는 셈이므로 블록 높이로 경계를 정하고 파일 내부 선택용 범위 요청을 쓰지 않습니다.

남는 노출:

  • CDN은 IP, 시간, 양, TLS 특성을 봅니다. 데이터는 공개지만 콜드·꼬리 동기화 형태가 달라 대략 마지막 동기화 블록을 추측할 수 있습니다. 노트 소유자는 알 수 없습니다.
  • Head API는 Anon에 도달하고 커서를 담습니다. ?fromBlock / ?fromSequence는 뒤처진 정도이지 자산 정보가 아닙니다.
  • PPOI는 지갑 파생 데이터입니다. 보기 키의 블라인드 커밋먼트를 조회하는 실제 지갑별 경로입니다. 현재 프록시는 읽기와 매개변수를 보관할 수 있어 보존·연결 위험이 있습니다. PPOI 프록시 참고. 대량 이력과 달리 별도 분석이 필요합니다.

VPN·Tor는 관찰자를 바꾸지만 시간과 양을 지우지 않습니다. 라우팅만으로 PPOI 지갑 데이터를 없앨 수 없어 캐시, 보존, 접근 통제를 검토해야 합니다. 쿼리 인덱서의 앱 계층 공개는 네트워크 익명성만으로 제거되지 않는다는 차이가 있습니다.

비공개 정보 검색은 아닙니다. 숨길 지갑 필터가 없어 암호학적으로 요청을 감추지 않습니다. 필터된 조각 대신 익명성 집합 전체를 받는 대역폭이 비용이며 압축·청크·엣지 캐시로 줄입니다. 사용자마다 아닌 엣지 위치·객체별로 한 번 부담하므로 사용자 수에 더 잘 확장됩니다.

지원 체인

Ethereum(1), Arbitrum(42161), Polygon(137), BSC(56) 등 Railgun 지원 체인에 생성합니다. 이더리움 데이터가 가장 크고 L2는 작아 더 빨리 동기화합니다.