ダイジェスト同期
初回起動では、対応チェーンの Railgun シールドプールで過去に発生した全イベントを取得します。Ethereum だけでも数百万ブロックにわたる数十万のコミットメント、ヌリファイア、アンシールドがあります。
RPC での走査には数十分かかりますが、ダイジェスト同期は数秒に短縮します。
Anon SDK に実装し、初期化時に Railgun エンジンへ組み込むため、全クライアントが同じ実装と転送契約を共有します。本ページはその契約です。速度だけでなくプライバシーに関わる理由はウォレット同期を参照してください。
課題
エンジンは暗号化ノートのローカルマークルツリーを維持します。ゼロからの構築にはコントラクト配置後の全コミットメントが必要です。
既定の同期は Graph/Subgraph を照会します。Ethereum では数十万のイベントをページ単位で読み、構築中は全データをメモリに保持します。遅く、メモリ制約に弱く、各要求にクライアント固有のフィルターが付きます。
仕組み
インデクサーがオンチェーンイベントを継続処理し、圧縮した分割ファイルのチャンクとして 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 配列を格納します。支出検出にヌリファイアだけ必要なら他を省くなど、種類を選んで取得できます。
パスはチェーン状態だけに依存し、ウォレット ID、閲覧鍵、その派生フィルターを含みません。共通キャッシュが可能で、これらを開示しません。プライバシーを参照。
キャッシュの階層
時間に応じて 3 段階を移ります。
| 階層 | Cache-Control |
対象 |
|---|---|---|
| 最新 | public, max-age=15 |
ルート、chunk-index.json、tip/ 内すべて |
| 修正期間 | public, max-age=300 |
最新の封印済みチャンク |
| 不変 | public, max-age=31536000, immutable |
それ以前の封印済みチャンク |
最新チャンクは 5 分間、再 PUT で不具合を修正でき、パスの版変更を避けられます。次が封印されると前のチャンクのヘッダーだけを不変にし、本文は変えません。
不変オブジェクトは最大 1 年エッジから提供し、ウォレット数によらず原点への取得は拠点・オブジェクトごとにほぼ 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 で更新検出に使います。CDN とキャッシュの formatVersion が違うと全消去して再取得します。
live に tip 位置と封印境界を同居させ、2 回の取得で競合せず 1 回で原子的に読みます。
チャンク索引
chunk-index.json は各封印済みチャンクのブロック・シーケンス範囲を小さなオブジェクトに収めます。
{
"chainId": 1,
"chunks": [
{ "id": 0, "startBlock": 15725039, "endBlock": 16234121, "firstSeq": 0, "lastSeq": 799 }
]
}
取引がないチャンクの firstSeq は -1 です。メタデータを巡回せず 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 を超えると新規封印します。境界はブロック単位で、一つのブロックを分割しません。Ethereum は約 200 チャンクに近づき、L2 はかなり小規模です。
クライアント側
初期化時に関数を直接渡し、グローバルフックを使いません。エンジンがイベントを要求すると:
-
ルート取得: 小さな 1 要求でチャンク数、tip チェックサム、現在位置を得ます。
-
不足分だけ取得: 個別にローカル保存し、封印済みは存在すれば永続再利用します。
tipChecksumが変わると tip を再取得。未保存チャンクはmeta.jsonの後、各ファイルを並列取得します。 -
再構成:
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) の呼び出しごとに SDK は:
latestGraphIDを十進数のsequenceラベルとして読みます。欠落、空、旧式の非数値なら先頭より前から始めます。- カーソルのチャンクと検証用ヘッドを含む 1 バッチ分を取得します。追従中は全
tip/、追いついたらシーケンス指定の最近のイベントを使います。 sequence > cursor.sequenceを残し、封印済み優先で重複除去し、sequence順に並べて全ハッシュリンクを検証します。番号の欠落は葉の欠落がないと検証できる場合だけ越えます。壊れた、または検証不能なリンクで止め、バッチ上限内の有効な先頭部分だけを渡します。
エンジンは順に追加し、最後の十進文字列 graphID で再開します。ラベルと正規の葉位置は別の座標であり、カーソルをツリー索引にしてはいけません。
先読み
エンジンのイベント要求前、初期化中に各チェーンのマニフェスト、封印済みチャンク、tip を並列でローカルに取り込みます。要求時には保存済みなので再起動の同期はほぼ即時です。
最新状態のポーリング
初期同期後は 2 モードです。
- 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 秒ごとに次を実行します。
- 状態取得:
GET /api/v1/indexer/head/{chainId}/statusがtipBlock、cdnBlock、reorgEpoch、稼働状態を返します。 - 再編確認:
reorgEpoch増加なら復旧し、通常処理を省きます。 - 初回:
tipBlock、cdnBlockを基準として記録し、イベントは取得しません。 - 変更検出:
tipBlockが同じなら何もしません。 - 新境界の確保:
tipBlockを即座に記録し、並行要求の重複を防ぎます。 - 差分取得:
GET /api/v1/indexer/head/{chainId}/recentがcdnBlockからtipBlockまでを base64 JSON で返し、あればエンジンに渡します。 - 範囲拡張: 封印直後など
/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 と同じ 4 種の配列です。デコード・解析後すぐエンジンに渡し、保存は待たずバックグラウンドで非同期に実行します。
ブロックとシーケンスのカーソル。 ?fromBlock は内容で識別する UTXO に適しますが、境界以下の TXID を落とし得ます。TXID は ?fromSequence とハッシュチェーン検証を使います。旧サーバーはパラメーターを無視する場合があり、必要なら全 tip/ を使います。番号の欠落だけでは葉の欠落を意味せず、ハッシュの連続性が受理条件です。
再編からの復旧
インデクサーは /status でチェーン再編を公開し、クライアントが検出・復旧します。
検出
/status の 3 フィールド:
reorgEpoch— 再編検出ごとに増える単調カウンター。reorgForkBlock— 分岐ブロック番号。reorgTxCount— TXID から戻す Railgun 取引数。
毎回 reorgEpoch を比較し、増加で復旧します。ローカルにも保存し、終了中の再編を次回検出します。
復旧手順
- TXID ロールバック:
clearLeavesForInvalidVerificationHash(txCount)で葉を削除し、invalidateTXOsCacheAllWallets()で DB の再読込を強制します。 - イベント消去:
blockNumber > forkBlockのローカル記録を削除します。 - SyncMeta リセット:
highestBlockがforkBlockを超えればforkBlockに戻します。 - 再同期: CDN tip を取り込み直し、UTXO → TXID 全体で修正済みチェーンを取得します。
UTXO の明示的ロールバックは不要です。ルート検証器が旧コミットメントを飛ばし、次の同期で修正版を受理して自己修復します。
再編処理の延期
UTXO 走査中に即座に復旧すると、読み取り中の保存内容を変更してしまいます。保留として記録し、走査終了後、次のポーリングで新イベント適用前に処理します。
同期不整合の復旧
より難しいのはエラーを示さず tip に近づかない状態です。TXID の穴、後で変わった葉位置、取得済みデータの再配置が、進まない同期として現れます。
SDK は独立した信号を使います。
- 停止検出: カーソルが動かない状態を分類し、無限再試行を避けます。
- 再配置検出: 取り込んだ番号と CDN の不一致から履歴書き換えを知ります。
- 無進捗ブレーカー: 新しい葉がないバッチの反復で停止します。
- スナップショット再読込: スナップショット由来の TXID が詰まったら、再採用とバックフィルで回復します。
TXID の全リセットは 0 から始まり、作成時点に合わせて省いた過去の UTXO データを要求するため、同じ停止を再現します。代わりにスナップショットを再読込します。
自動修復はチェーン・セッションごとに 1 回で、開始前に回数を消費するため失敗も数えます。以降は再有効化せず手動経路に進みます。
イベントファイル形式
チャンクと tip は同じ転送形式です。各ディレクトリと tip/ に 4 つの gzip があり、それぞれ単一種類のフラット JSON 配列です。
| ファイル | 配列要素 | 用途 |
|---|---|---|
commitments.json.gz |
コミットメントバッチ | UTXO マークル葉 |
nullifiers.json.gz |
ヌリファイア | 支出検出 |
unshields.json.gz |
アンシールド | 出金記録 |
transactions.json.gz |
Railgun 取引 | TXID マークル葉 |
同じ要素のファイルはバイト互換で、メタデータの包みとライフサイクルだけが違います。チャンクと tipを参照。同じパーサーを使えます。
エンコード規則
- ハッシュ・体の要素: ゼロ埋めした 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 バイト。 |
utxoTreeIn / utxoTreeOut |
number | 入出力 UTXO ツリー索引。 |
utxoBatchStartPositionOut |
number | 出力ツリーでのコミットメント開始位置。 |
timestamp |
number | Unix 秒。 |
verificationHash |
string | 全取引の累積ハッシュチェーン、接頭辞なし 32 バイト。 |
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 は十進文字列でゼロなら省略。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 付き文字列ではなく配列です。
旧型は Ethereum V2 前の初期チャンクだけに存在します。新チェーンは
ShieldCommitmentとTransactCommitmentV2だけですが、完全なクライアントは 4 種すべてを読む必要があります。
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/ から chunks/{id}/ へ移っても sequence を含む内容は変わりません。
順序と sequence 不変条件
TXID 利用側の契約です。
- 全ファイルの
transactionsは(blockNumber, sequence)昇順です。他の種類は位置、ヌリファイア、(txid, logIndex)で識別するため配列順に依存しません。コミットメントは(treeNumber, startPosition)の挿入順に並べ直します。 sequenceは順序ラベルで、連番索引の保証ではありません。 修正で欠番や重複範囲が残り得ます。ラベルで重複除去し、ハッシュ検証後だけ間隙を越え、葉の欠落を無視しません。- 平坦 TXID 索引(
tree * TREE_MAX_ITEMS + index)は追加位置から決まり、sequenceと異なり得ます。検証済み索引・ツリー長とラベルを分け、比較は実際のヘッドの対応で変換します。 - 再開
graphIDは最後に受理したsequenceの十進文字列です。チャンクとヘッドを検証し、既受理のラベルを除き、検証済み後続を順に取り込みます。欠落や旧非数値はコールド開始です。
ツリースナップショット
生成側はイベントに加え、パックしたマークルツリーそのものを公開します。再構築イベントではなくツリーであり、他と同じ共通のウォレット非依存 CDN パスです。
成果物は gzip バイナリーです。
chunks/{id}/tree-utxo-{n}.bin.gzとchunks/{id}/tree-txid-{n}.bin.gz— 最終葉数に達したツリーを完了チャンクに 1 回記録し、以後不変。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 を並列取得。Ethereum 約 200、L2 は少数 | 5〜15 秒 |
| ウォーム、再利用 | マニフェストを確認し更新 tip だけ取得 | 1 秒未満 |
| CDN 到達不可 | 停止し、インデクサー健全性を示して再試行 | — |
Graph のコールド同期は無効です。CDN 不通時は遅い照会経路へ無言で切り替えず停止します。
完全性
イベントのチェックサムは未圧縮 JSON、スナップショットは提供 gzip バイトが対象です。末尾のルートを登録簿と照合します。Tip は tipChecksum、CDN とローカルの formatVersion 不一致は全消去・再取得で扱います。
5 分の修正期間後の安定性は生成側の性質であり、クライアントの仮定ではありません。中間キャッシュを信じず再取得し、修正を見えるようにします。キャッシュの正しさを参照。
公開かつ不変アドレスなので、誰でも取得・ハッシュ比較・チェーンログや別インデクサーとの照合が可能です。Anon 基盤へのアクセスは不要で、CDN に求める信頼は可用性だけです。
生成側の構成
二つのシステムがあります。
- ダイジェスト生成器: TypeScript の初期データ作成処理です。上流から区切ったラウンドで全履歴を取得し封印・R2 へ配置します。新チェーンの初期化時に手動実行します。
- インデクサー: Go の常駐処理です。CDN から開始し、最新ブロックから tip を作ります。約 6,000 超で封印、前チャンクを不変化、ツリー完了時にスナップショットを公開します。R2 送信は 15 秒間隔で制限します。
上流インデクサーは生成側の依存で、ウォレットは照会しません。別途健全性信号として読み、遅延が上流か Anon 側かを区別します。
プライバシー
性能だけでなくプライバシーの仕組みです。ウォレット同期が一般向け説明で、ここはプロトコルの性質です。
要求はウォレット内容に依存しません。 digest.anon.inc はチャンク ID、ブロック範囲、tip ハッシュなどの状態だけから決まり、閲覧鍵、アドレス、ノートから導きません。同じ位置なら資産に関係なくバイト単位で同じ要求です。解析、保存、開示を求められるウォレットフィルターはありません。
一致と不一致はサーバーから区別できません。 取得後の試行復号は端末内で行い、ノートの有無はクライアントだけが判定します。
共通性は設計上の制約です。 選択が資産に依存しない間だけ成立します。必要な範囲だけ選ぶと照会を再構成してしまうため、ブロック高で境界を決め、ファイル内の選別に範囲要求を使いません。
残る開示:
- CDN は IP、時刻、量、TLS 特性を見ます。 内容は公開でも初回と差分の形は異なり、最後に同期したブロックを推測できます。ノート所有者は分かりません。
- Head API は Anon に達しカーソルを持ちます。
?fromBlock/?fromSequenceは遅れであり、資産ではありません。 - PPOI はウォレット由来データを含みます。 閲覧鍵由来のブラインドコミットメントは個別要求です。現在のプロキシは読み取りとパラメーターを保存でき、保管・関連付けの懸念があります。PPOI プロキシを参照。共通履歴と異なり、別の分析が必要です。
VPN や Tor は観察者を変えますが時刻・量を消しません。経路だけで PPOI データを除けないため、保存、キャッシュ、アクセス制御を確認します。照会型インデクサーのアプリ層の開示は、ネットワーク匿名性だけでは消せないという違いです。
これは秘匿情報検索ではありません。 隠すウォレット条件がないため要求を暗号学的に隠しません。選別片ではなく匿名性集合全体を取得する帯域が代償です。圧縮、分割、エッジキャッシュで抑え、ユーザーごとでなく拠点・オブジェクトごとに 1 回負担するため拡張しやすくなります。
対応チェーン
Ethereum(1)、Arbitrum(42161)、Polygon(137)、BSC(56)に生成します。Ethereum が最大で、L2 は小さく同期も速くなります。