Web3
智能合约之外的 Web3 基础设施
在一个生产级区块链系统里,合约是最小的那一部分。大部分工程工作和大部分运维风险都在链下:节点访问、索引、密钥托管、交易管理和监控。
· 阅读约 5 分钟 · Starone 工程团队
关于区块链工程的讨论大多集中在智能合约上,这可以理解:合约一经部署便不可更改,对外公开可查,对错误毫不留情。它们值得接受审计和形式化的严谨对待。但在一个可用的产品里,它们也只占代码的一小部分。
我们的观点是,Web3 系统的可靠性主要是在链下决定的。如果应用从一个落后的节点读到了陈旧状态,把一笔后来因链重组而消失的充值记入了账户,或者因为 nonce 卡住而无法提交交易,那么一份正确的合约也没有多大用处。本文说明围绕合约的那些基础设施,以及它迫使团队做出的决策。
节点与 RPC 访问同样是一项依赖
每一次读写都要经过节点的 RPC 接口,无论节点是自建的还是由服务商提供的。这个接口会以寻常的方式出问题,如限流、超时和宕机,也会以区块链特有的方式出问题:一个节点可以状态健康却落后好几个区块,同一个负载均衡器后面的两个节点,也可能基于不同的链头来回答连续的两次请求。
我们以对待任何关键上游的同等审慎来设计 RPC 访问。请求经过一个内部网关,网关接入多家服务商,跟踪每一家的最新区块高度,把落后者移出轮转,并把幂等的读请求改投到别处重试。必须相互一致的读取会被固定到某个具体的区块号或区块哈希上,而不是 latest。自建节点能换来掌控力和独立性,代价是存储、同步时间以及每次网络分叉时的升级;查询历史状态所需的归档节点则要重得多。
索引:链不是查询引擎
节点只能回答很窄的问题:某个地址上的状态,某个区块范围内的日志。它回答不了产品层面的问题,例如某位用户参与的所有转账,按时间排序并按资产筛选。这些问题需要一个索引器,由它消费区块和事件日志,解码之后写入一个为查询而建的数据库。
索引器是一条数据管道,但有一个不寻常的特性:它的数据源可以收回已经说过的话。稳妥的设计会在每一行派生数据上同时保存区块号和区块哈希,把每个新区块的父哈希与已存的哈希做比对,一旦不一致,就回退到共同祖先并重新处理。处理逻辑必须幂等且具有确定性,这样从创世区块开始的全量重建索引才能重现相同的状态。
最终性与链重组
一笔交易被打包进区块,并不代表已经不可更改。在工作量证明链上,最终性是概率性的,随着后续每一个区块而增强。以太坊这类权益证明网络增加了显式的检查点,通过 safe 和 finalized 区块标签对外暴露,在此之后若要回滚,需要很大比例的验证者受到惩罚。Layer 2 网络则进一步区分了排序器确认与在底层链上的结算。
这在工程上的含义是:确认策略是一项按链、按操作而定的风险决策,应当放在配置里,而不是写在代码里。
- 一旦观察到转入交易,就立即把它显示为待确认,以保证界面响应及时。
- 只有当它达到与其金额相称的确认深度或最终性级别时,才计入可用余额。
- 只有在达到最终性之后,才触发不可逆的链下动作,例如发货或通过银行付款。
- 保留冲正内部账本分录的能力,以应对链重组移除了一笔已入账交易的情况。
密钥管理决定爆炸半径
一把私钥可以授权其地址被允许执行的任何操作,而已签名的交易无法撤回。把密钥放在环境变量或应用内存里,会让任何一次服务器被攻破都变成资金损失。我们把签名放在一个专用服务之后,应用代码只是请求签名,从不持有密钥材料。
- 硬件安全模块(HSM)或云端密钥管理服务可以让密钥不可导出;但必须先确认它支持所需的曲线,通常是 secp256k1 或 ed25519。
- 多方计算(MPC)或门限签名把密钥拆分开来,使任何单台机器或单个人都无法独自签名。
- 链上多签或智能合约账户以透明的方式强制执行审批规则,代价是更高的交易成本和因链而异的行为。
- 置于签名器之前的策略引擎负责执行目标地址白名单、金额上限和频率限制,并在超过阈值时要求人工审批。
Nonce 管理与卡住的交易
在基于账户模型的链上,同一地址发出的每笔交易都带有一个顺序递增的 nonce,网络严格按顺序处理它们。两个读到相同 pending 计数的 worker 会使用同一个 nonce,其中一笔交易会被丢弃。一笔出价低于当前费用水平的交易会滞留在 mempool 里,并阻塞该地址之后的所有交易。
因此,生产级的交易发送器把 nonce 分配视为一种串行化资源:每个地址一个分配器,以持久化存储为后盾,按顺序发放 nonce,并记录每笔交易从提交、上链到最终确定的状态。一个 watcher 负责发现 pending 时间过长的交易,用相同的 nonce 和更高的费用替换它们,并填补因提交失败留下的任何空洞。每笔交易背后的业务操作都需要自己的幂等键,这样崩溃后的重试才不会把同一笔转账发送两次。
监控链不会告诉你的事
常规的服务指标在这里是必要的,但并不充分。能够预示故障的信号都很具体:每家 RPC 服务商的链头与已知最高区块之间的差距,以区块数和秒数衡量的索引器延迟,每个发送地址上最早一笔 pending 交易的存在时长,每个支付手续费的地址的原生代币余额,交易回滚(revert)的比率,以及链重组的深度。具有安全含义的合约事件,例如所有权转移、升级和暂停,应当直接触发告警。
取舍所在
所有这些,都是包裹在去中心化系统外面的中心化基础设施,这种张力应当被正视。索引器和 RPC 网关是用户不得不信任的组件;一个以去除可信中介为宗旨的产品,可能更愿意采用轻客户端、钱包直读和链上验证,并接受更慢、更朴素的界面作为代价。
对大多数把链上结算与传统账户体系、用户体验结合起来的产品而言,务实的立场是:必须可验证的放在链上,必须快速或必须保密的放在链下,并且认真地对这条边界做工程设计。同样值得问一问的是,到底需不需要区块链。如果所有参与方都由同一个组织控制,那么一个带有仅追加审计日志的数据库更简单、更便宜,也更容易运维。
