摘要同步
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 数据量明显更小。
客户端
初始化时直接把同步函数交给引擎,不使用全局钩子。引擎请求某链事件时:
-
获取根清单:一次小请求取得封存块数、tip 校验和和实时位置。
-
仅下载缺失数据:本地逐块缓存,封存块存在即永久复用;
tipChecksum改变时重下 tip。未命中的块先取meta.json,再并行下载事件文件。 -
重组:合并成
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 每次:
- 将
latestGraphID解析为十进制sequence标签。缺失、空或旧非数字游标从首个序列之前开始。 - 定位游标所在块,获取足够一批的封存数据及用于验证的当前头。追赶时用完整
tip/,实时阶段用按序列获取的近期事件。 - 保留
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 秒执行:
- 取状态:
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(例如事件刚封存至 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,增长即恢复。该值也持久保存,以便下次启动发现关闭期间的重组。
恢复步骤
- TXID 回滚:调用
clearLeavesForInvalidVerificationHash(txCount)删除受影响叶,再调用invalidateTXOsCacheAllWallets()强制钱包重读数据库。 - 清除事件存储:删除
blockNumber > forkBlock的本地事件。 - 重置 SyncMeta:
highestBlock超过forkBlock时重置为forkBlock。 - 重新同步:从 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 更小、同步更快。