Ksana
EN

使用网络

运行索引器

索引器只凭比特币重建整条链。它读取结算写进比特币的数据,执行每个主链区块,并核对每个区块的状态根,因此你不必信任节点,也能验证它。

本页目录

ksana-indexer 不与任何 Ksana 节点通信。它只需要同一个比特币网络上的 bitcoind,从中收集结算和补发写进比特币的区块头和区块体。它据此构建出与节点相同的区块 DAG 和主链,执行主链区块,并检查每个区块的 state_root 是否与它自己算出的一致。

钱包与应用signet 钱包、TypeScript SDK、ksana-cli
Ksana 节点检查每个区块,并出新块 节点P2P节点P2P节点
比特币每个节点和索引器都通过 bitcoind 读取
索引器只凭比特币重建整条链状态根一致

节点之间相互通信,并读写比特币;索引器只读比特币,它的状态根必须与节点的一致。

为什么要运行索引器

  • 核对节点。当你的索引器和某个节点在同一高度报告相同的状态根时,那个节点在该高度的状态,恰好就是比特币上的数据所决定的状态。对托管节点也同样适用。
  • 只依赖比特币。索引器用到的一切都在比特币上,因此它证明了:即使所有 Ksana 节点都消失了,这条链也能重建。
  • 核对最终性。它用与节点相同的规则,从比特币判定哪些区块已最终;一旦有对不上的地方,就发出告警。

signet 钱包的验证页面,对托管的索引器和节点做的正是同样的比对。

构建与运行

索引器可以和节点一起构建(见运行节点),也可以单独构建:

Shell
cargo build --release --locked -p ksana-indexer

它需要一个运行在 signet 上、开启 -txindex=1、并已同步过 signet 创世锚定的 bitcoind;怎样启动,见运行节点。然后在仓库目录下运行:

Shell
export KSANA_BTC_RPC_PASS='<your bitcoind RPC password>'
target/release/ksana-indexer --chain signet --data-dir ~/ksana-signet/indexer \
  --genesis signet/genesis.json \
  --btc-rpc-url http://127.0.0.1:38332 --btc-rpc-user ksana

--chain signet 和创世配置必须与节点的相同,数据目录也会记住创建它时所用的创世配置。密码取自 KSANA_BTC_RPC_PASS(或 --btc-rpc-pass),连接 bitcoind 只能用明文的 http://。

选项 默认值 设定的内容
--rpc-addr <IP:PORT> 127.0.0.1:8546 JSON-RPC 的监听地址。
--poll-ms <ms> 500 两轮读取 bitcoind 之间的间隔。
--no-pruning 剪枝开启 把全部历史保留在内存里。

索引器没有 CORS 选项:浏览器页面只能通过与页面同源的代理访问它,托管钱包用 https://signet.ksana.network/indexer 就是这样做的。

启动时,它向标准错误写出 [ksana-indexer] started: verified height …, state root …,向标准输出写出 ksana-indexer rpc=127.0.0.1:8546。追赶进度时,它会记录 verified up to height … (block tree main-chain height …, BTC height …);最终性每向前推进一次,就记录 BTC-final: block … at height …。第一次同步 signet 所需的时间,与读完创世锚定以来的比特币区块所需的时间相当。

它落后节点多少

只有当某笔结算或补发把一个区块的数据带进了比特币区块,索引器才能看到这个区块。之后,一个主链区块要在索引器的主链上有 2 个确认(signet 上的 SETTLEMENT_DEPTH),或者已经最终,索引器才会核对它。所以它已核对的高度落后于节点的链顶,差的正是那些数据还没进入比特币区块的区块,在 signet 上通常是最近几分钟的区块。

JSON-RPC 接口

索引器通过 HTTP 和 WebSocket 应答两个方法。indexer_status 报告它的进度:

Shell
curl -sS https://signet.ksana.network/indexer -H 'content-type: application/json' \
  --data '{"jsonrpc":"2.0","id":1,"method":"indexer_status","params":[]}'
输出
{"jsonrpc":"2.0","id":1,"result":{"height":"282","hash":"76ed3966dec4ddcd40fe31a777add64381a75b0daef81be1780549cf86e5bf7a","state_root":"7f546297e3cdf56a8724539b4930d527550d5cdcda5eee9aa714fe61cf412630","btc_height":326325,"finalized":{"height":"246","hash":"c45e4b58173b529c164e66c61219befe4bf7b3e93c319f9c80dbd98982793071"},"halted":null}}
  • height、hash 和 state_root:它已执行并核对的最高区块。
  • btc_height:它已读到的比特币高度(是一个数字,这一点与快轨高度不同)。
  • finalized:它的最终链顶,按与节点相同的规则从比特币判定。
  • halted:一切正常时为 null;否则是它停止的原因(见下文)。

indexer_stateRoot 接受一个高度、"latest" 或一个区块哈希,只对已核对的区块作答;对其他区块,它返回 -32004,即 the block has not yet been executed and verified by the indexer:

Shell
curl -sS https://signet.ksana.network/indexer -H 'content-type: application/json' \
  --data '{"jsonrpc":"2.0","id":1,"method":"indexer_stateRoot","params":["262"]}'
输出
{"jsonrpc":"2.0","id":1,"result":{"height":"262","hash":"2f22a435469f702c2d23b1142ac39034d019fd406331a094485a789a1cc7d7b3","state_root":"e49f287ddfd7c232e655e8b9ae42626e4c745b5fcb26315dfa8ab2844f6e5e3e"}}

ksana-cli 不能和索引器通信,因为它需要节点的方法;请改用 curl 或 TypeScript SDK 的索引器客户端。

与节点比对状态根

这个脚本把索引器最近核对的五个高度,与节点在同样高度上的区块逐一比较。把 NODE 和 INDEXER 设成你自己的,或者沿用托管的;它需要 curl 和 jq。

compare-roots.sh
NODE=https://signet.ksana.network/rpc
INDEXER=https://signet.ksana.network/indexer
call() {  # 参数依次为 url、method、params
  curl -sS "$1" -H 'content-type: application/json' \
    --data "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"$2\",\"params\":$3}" | jq -c .result
}
top=$(call "$INDEXER" indexer_status '[]' | jq -r .height)
for h in $(seq $((top - 4)) "$top"); do
  a=$(call "$INDEXER" indexer_stateRoot "[\"$h\"]" | jq -c '[.hash, .state_root]')
  b=$(call "$NODE" ksana_getBlock "[\"$h\"]" | jq -c '[.hash, .header.state_root]')
  if [ "$a" = "$b" ]; then echo "$h same $a"; else echo "$h DIFFERENT indexer $a node $b"; fi
done
输出
278 same ["d5e70875e331fb4093b9f8d5c1d460f1f6b557f063d85e93b5a0fa8fbf437d01","7f546297e3cdf56a8724539b4930d527550d5cdcda5eee9aa714fe61cf412630"]
279 same ["ca81971355fdbae2693c4fe0b8a31670c5c410314524fd9e01dbd97aa01b5fe2","7f546297e3cdf56a8724539b4930d527550d5cdcda5eee9aa714fe61cf412630"]
280 same ["792fd0dbb60d85f8195326e0ac83847c10d031ccb8b225a6fa64f3764289fee2","7f546297e3cdf56a8724539b4930d527550d5cdcda5eee9aa714fe61cf412630"]
281 same ["d18e91c9383400d27166695177decadb89399456e55d79eb6e6164d625edaf95","7f546297e3cdf56a8724539b4930d527550d5cdcda5eee9aa714fe61cf412630"]
282 same ["76ed3966dec4ddcd40fe31a777add64381a75b0daef81be1780549cf86e5bf7a","7f546297e3cdf56a8724539b4930d527550d5cdcda5eee9aa714fe61cf412630"]

区块哈希和状态根都必须一致。这里每个状态根都相同,因为这些都是空块,空块不改变状态。

刚发生比特币重组或主链切换时出现的差异,可能会持续到双方都跟上为止。如果差异停留在双方早已越过的高度上,就说明节点和索引器对同一个区块的执行结果不同:请保留双方的数据目录,并按安全页面的说明报告。

告警

索引器把问题写到标准错误,每行以 [ksana-indexer] alarm 开头。大多数告警描述的是它自己就能处理的情况:

告警 索引器的处理
block … at height … is invalid (…) 节点也会拒绝这个区块;索引器把它排除在外,并重新选择主链。
block … cannot land (…) 把这个区块排除在外,并重新选择主链。
a BTC reorg undid the finality decision, the final tip fell back to height … 在新的比特币链上重新判定最终性。
a BTC reorg took the anchor of main-chain block … off the BTC best chain 退到这个区块之下,并重新选择主链。
block … met the BTC finality condition but is invalid 把这个区块以及建立在它之上的每个区块,都排除在最终性之外。

有三种最高级别的告警需要你来处理:

  • alarm (highest level): … processing halted 会让索引器停止,同时在 indexer_status 里显示为 halted。一个被比特币判为最终的区块,结果却是无效的:要么是快轨让一个无效区块成了最终区块,要么是 Ksana 的软件在不同机器上算出了不同的结果。请保留数据目录并报告。
  • the payload contains a block header of protocol version …, but this indexer only supports up to … 表示网络已经升级了规则。索引器会在变更之前停下;请从仓库更新它。
  • cannot check block header … in the payload 表示这台机器没能算出某个区块头的工作量证明,这是本地故障,比如内存分配失败。索引器不会据此判定任何区块,它会记录 sync failed (will retry later) 并重试;如果这条告警反复出现,请检查这台机器。

重启与剪枝

索引器把工作成果保存在数据目录里(state/、index/、indexer.json 和 pruning.ckpt)。重启后,它先把存下的状态与比特币上的数据核对一遍,bitcoind 落后时就等着,然后记录 verification against the BTC data complete: continuing from height …。

剪枝默认开启,做法与节点相同:内存只保留它仍可能跟随的那些主链所需的数据,在此之下的已核对主链由磁盘上的索引来应答,每次剪枝都写下一个检查点,下次启动就从那里继续。在一次 10 小时的 devnet 浸泡测试中,索引器跑完 7,308 个区块后,常驻内存为 67 MiB,数据目录约 45 MB。

如果无法从保留的数据继续,索引器会自己从创世锚定重新开始:比特币重组深过它保留的范围、主链离开它的剪枝根,或者索引丢失时,都是如此。它会记录一条告警,然后从 bitcoind 重建全部数据,耗时与第一次同步相当。signet 从创世重启后,给它新的创世配置和新的数据目录。