投票扩展:CometBFT 让应用在每张预提交票里带一段数据 图 1
投票扩展:CometBFT 让应用在每张预提交票里带一段数据 · 图 1

Tendermint 系的共识消息过去很朴素:节点对一块交易的哈希表态,票上只有高度、轮次和目标。CometBFT 通过 ABCI++ 给表态加了一个口袋:验证者在预提交阶段可以附带一段应用生成的字节——投票扩展(vote extension)——由同一应用的校验函数负责给所有人的扩展打分。价格读数、跨链见证、对上一块的补充证明,这些过去挤在交易通道里的事,从此有了随票传播的专用车道。口子开在共识消息上,规则就一点不能松。

何时开口:只在锁定的那一刻

触发时点写得非常具体:验证者在某轮前投票阶段收齐 2f+1 份对同一区块的前投票、即将锁定并发出预提交时,CometBFT 才调用 ExtendVote;若共识算法此时要投 nil,这个函数根本不会被调用。也就是说,扩展只挂在非 nil 的预提交上,由 CometBFT 连同投票一并签名,作恶者无法篡改别人的扩展内容。响应字段允许长度为零,不用此功能的链照常运行;扩展的生成逻辑也明确允许非确定性——应用读随机数、读本地聚合器都行,反正校验那一头有硬规矩兜底。

预提交票携带扩展片段汇入下一块提案的抽象示意

验算规矩:状态唯一、答案苛刻

对外校验函数 VerifyVoteExtension 的规则与生成端完全相反:实现必须确定性,返回接受还是拒绝,只能取决于请求参数与最近一次已提交的应用状态,不许读时钟、不许依赖本地缓存。返回拒绝的后果是全票作废——共识直接弃掉那张票。规范因此把措辞写得毫不含糊:除非确实清楚拒绝对活性的连锁影响,否则应当总是返回接受。两个容易踩的边角也写在同一页规范里:对 nil 的预提交存在另一条带 CanonicalVoteExtension 字段的通道,与 ExtendVote 产出的扩展不是一回事;本地节点自己发出的预提交票不会再走一遍 VerifyVoteExtension。

扩展怎么回到提案:一处必须补的验算

扩展不会自动进块。它们在下一高度的 PrepareProposal 里,随上一高度的 commit info 回到出块者手上,应用可以读它们来修改提案内容。规范特意留了一句提醒:commit info 里那些在达到最低 +2/3 门槛之后才被补进来的扩展,不会再经过一遍 VerifyVoteExtension——出块者若要消费这些扩展,应当按与校验函数同样的规则自行再验一遍,否则等于给未验算的数据开了后门。把账算全:这条专用车道带来两轮开销,所有验证者要为别人的扩展多跑一次确定性验算,区块本身也要为随票数据付带宽。

不开这个口子会怎样

回头看,投票扩展解决的是一个具体困境:应用想在共识期拿到”每个验证者此刻看到的某个值”,传统做法要么塞进交易——占带宽、要付费、排序时延高;要么各查各的——拿不到多数人签名的同一口径。扩展把这两头都省了:数据随票签名、随 commit info 进块,天然带上”谁在何时对哪个区块表了什么态”的属性。但天下没有白车道——每轮多出一次全体验算、每块多出一份随票数据的带宽,这两笔固定成本对所有链一律征收。典型用法由此可推:链上价格读数、轻客户端见证、跨链轮次证明都装得下;把大体积数据塞进扩展,把验算写成查表之外的任何形态,则是教科书级的反模式。另有一处容易混淆的细节值得单列:响应里除 vote_extension 外还有 non_rp_extension 字段,它同样由 CometBFT 签名并附到预提交消息,但不施加与前者相同的防重放包装,适合需要原始字节直签的场景——两个口袋装的东西都由共识层背书,防重放语义却不一样,选用前应当按所用 CometBFT 版本发布的规范原文逐字段核对。(风险提示:本文仅说明机制,不构成投资建议;节点与应用改动生产环境前以官方版本规范为准。)