跳至正文
Anon Wallet

摘要同步

Anon 首次启动时,需追踪各支持链上自始以来的全部 Railgun 屏蔽池事件。仅以太坊主网就有跨越数百万区块的数十万条承诺、作废标识及解除屏蔽事件。

通过 RPC 扫描需数十分钟,摘要同步可缩短至数秒。

此功能在 Anon SDK 实现,并于初始化时安装到 Railgun 引擎,因此所有客户端共享同一实现和传输契约。本页描述该契约;其隐私意义见钱包同步。

问题

Railgun 引擎在本地维护加密票据的 Merkle 树。从零构建需要处理合约部署以来的所有承诺事件。

默认实现查询 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 更早的封存分块

最新分块保留五分钟窗口,以便发现回归时重新 PUT 修正,无需改变路径版本。下一块封存后,前一块原地改为不可变,仅修改头,不改正文。

不可变对象在边缘保存最长一年,无论多少钱包同步,源站每个对象在每个边缘位置通常仅需填充一次。

根清单

根 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,按 Merkle 插入顺序重排承诺并交给引擎。

下载哪些块取决于区块高度和已有缓存,而非钱包内容。同一同步位置的钱包发出相同请求。

缓存正确性

封存文件使用不可变 URL,便于缓存却难以修正。生产端重排后,同一 URL 内容被重写,中间缓存可能仍返回旧数据,使重建无报错却停在 tip 之前。

两项控制解决此问题:

  • 封存文件始终使用 cache: 'reload'。 绕过本地 HTTP 缓存重取 CDN;稳定运行时仍直接使用持久分块存储,无额外获取成本。规则不依赖路径,恢复流程无需单独选择启用。
  • Tip 文件附带 ?v={tipChecksum}。 内容改变即轮换浏览器至边缘的缓存键,不再请求旧条目,无需等待过期。

后者是正确性控制。尽管 tip 设计为短 TTL,任一错误策略仍可静默固定旧数据。2026 年 8 月,CDN 覆盖浏览器 TTL 为 max-age=14400,钱包数小时重读旧 tip,甚至继续读取生产端已修复的错误验证哈希链。内容驱动的缓存键避免客户端再次命中此类旧条目,不依赖中间层配置。

TXID 交易同步

交易事件加速 TXID Merkle 树同步。UTXO 事件(承诺、作废标识、解除屏蔽)合并为内存对象,交易则按游标分批流入引擎,对应事件文件格式中的 transactions.json.gz。

引擎反复调用 QuickSyncRailgunTransactionsV2(chain, latestGraphID),SDK 每次:

  1. 将 latestGraphID 解析为十进制 sequence 标签。缺失、空或旧非数字游标从首个序列之前开始。
  2. 定位游标所在块,获取足够一批的封存数据及用于验证的当前头。追赶时用完整 tip/,实时阶段用按序列获取的近期事件。
  3. 保留 sequence > cursor.sequence,去重重叠数据(封存数据优先),按 sequence 排序并验证每个哈希链接。仅当哈希链证明没有缺叶时跨越编号间隙。损坏或不可验证则停止,仅交付批量上限内的有效前缀。

引擎按顺序追加,以末笔交易的十进制字符串 graphID 继续。序列标签和规范叶位置是不同坐标,不能把游标当树索引。

预取

初始化期间、引擎请求事件前,客户端下载各链清单并并行导入全部封存块及 tip。引擎请求时数据已在本地,重复启动的同步几乎立即完成。

实时轮询

初始同步后有两种模式:

  • API 模式:主要模式,每 5 秒查 /api/v1/indexer/head/{chainId}/status。tipBlock 改变后从 /recent 获取增量;响应为空且本地落后时扩大范围。
  • CDN 模式:回退模式,每 30 秒执行;API 返回 status: "not_configured" 时启用,轮询根 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(例如事件刚封存至 CDN)且本地落后于 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() 强制钱包重读数据库。
  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 Merkle 叶
nullifiers.json.gz 作废标识事件 检测支出
unshields.json.gz 解除屏蔽事件 提款记录
transactions.json.gz Railgun 交易 TXID Merkle 叶

同类元素字节兼容,只有元数据封装和生命周期不同,见分块与 tip。可用相同代码解析。

编码约定

  • 哈希与域元素:固定 64 字符、32 字节、补零十六进制。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 字节十六进制。
nullifiers string[] 带 0x 的 32 字节十六进制。
boundParamsHash string 32 字节十六进制,无前缀。
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,但以 encryptedRandom: [string, string] 替代 encryptedBundle/shieldKey/fee/from。

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 字节十六进制,以 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
可变性 晋级后不可变 每次索引器刷新重写
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。标签游标与已验证索引、树长等索引坐标应分开,比较时用实际头的对应关系换算。
  • 恢复 graphID 是最后接受的 sequence 十进制字符串。定位游标块、验证头、丢弃已接受标签,按序导入验证后的后缀。缺失或旧非数字游标视为冷启动。

树快照

生产端还发布打包的 Merkle 树本身,而非重建事件。它们是普通 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 字符无前缀十六进制,需与产物尾部记录比较。
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 验证。CDN 与本地 formatVersion 不符时清空重下。

五分钟修正窗口后封存内容稳定,但这是生产端属性,客户端不应假定中间缓存正确,而应重新获取以看见修正。见缓存正确性。

产物公开且按不可变地址提供,可独立验证:任何人可下载、比较已发布校验和,并与链上日志或其他索引器核对,无需访问 Anon 基础设施;除可用性外无需信任 CDN。

生产端架构

由两套系统生成摘要:

  • 摘要生成器:TypeScript 一次性初始化任务,分轮从上游索引器获取完整历史、封存并上传 R2,用于新链,由人工运行。
  • 索引器:Go 常驻系统,启动从 CDN 引导,持续处理新区块形成 tip。超过约 6,000 事件时封存、晋级前块并在树完成时发布快照,以 15 秒节流上传 R2。

上游索引器是生产端依赖,客户端不查询它。另作健康信号读取,以区分上游落后与 Anon 索引器问题。

隐私

摘要同步也保护隐私。钱包同步面向一般读者,本节描述协议性质。

请求独立于钱包内容。 digest.anon.inc 路径来自分块 ID、区块范围、tip 校验和等链状态,不来自查看密钥、地址或票据。相同同步位置的请求字节一致,与持有资产无关。服务器没有钱包筛选查询可解析、记录、保留或被要求交出。

服务器无法区分命中与未命中。 下载后在设备上尝试解密,只有客户端能判断分块是否含自己的票据。

统一性是设计约束。 仅在选块不依赖钱包内容时成立。按资产只取所需范围会变相重建查询,因此边界由区块高度决定,不以范围请求挑选文件内部。

仍暴露的信息:

  • CDN 可见 IP、时序、流量和 TLS 特征。 文件本身公开,但冷同步与追赶同步形态不同,可推测某 IP 上次同步到哪个区块,不能识别票据归属。
  • Head API 到达 Anon 且携带游标。 ?fromBlock / ?fromSequence 暴露落后程度,而非资产。
  • PPOI 携带钱包派生数据。 盲化承诺由查看密钥派生,构成真正的逐钱包查询。当前代理可缓存并保留参数,产生保留与关联风险,见 PPOI 代理。批量历史无此查询,合规路径则有,需分别分析。

VPN 或 Tor 改变观察方,不抹去时序和流量;路由不移除 PPOI 中的钱包数据,运营方须审查缓存、保留与访问控制。这与在应用层披露钱包筛选条件的查询型索引器不同,网络匿名本身无法去除应用层披露。

这不是私密信息检索。 请求没有使用密码学隐匿钱包筛选条件,因为本来就没有此类条件。代价是下载整个匿名集而非筛选片段的带宽,通过压缩、分块、边缘缓存限制;每个边缘位置每对象承担一次成本,而非每用户一次,更易扩展。

支持的链

为 Railgun 支持的 Ethereum(1)、Arbitrum(42161)、Polygon(137)、BSC(56)生成摘要。以太坊主网数据量最大,L2 更小、同步更快。