带相对时间锁的交易,在内存池门口常撞上一个生僻代号:non-BIP68-final。它不是说你的锁写错了,而是说按”下一个块”这个尺度算,你的锁还没到点。这个尺度的选择,本身就是一段值得讲清楚的实现细节。
内存池用的是”下一块”的视角
比特币核心 v31.0 里负责这项判定的函数是 CheckSequenceLocksAtTip。它的注释开门见山:这个函数用链顶高度加一来评估相对时间锁,因为内存池里的交易是准备进下一个块的。
这个”加一”很关键。节点不会去猜矿工多久之后打包,也不按当前时间做浮动的推算,它直接构造一个假想的下一个区块高度,问一句:如果这笔交易进的是那个高度,它的相对锁满足了吗。满足才让进池,不满足就返回内部结果 TX_PREMATURE_SPEND,代号 non-BIP68-final。
用固定尺度而不是浮动时钟,好处是可复现:同一高度下所有节点对同一笔交易的判定必然一致。代价则是提前量必须被精确计算——如果相对锁的起算高度是最近才确认的输出,那按”下一块”衡量就差一格,节点会明确拒收,而不是”先进池子等着”。内存池不做等待队列,等待是孤儿交易库的职责,两者不是一回事。
绝对锁与相对锁分两个代号
同一个内部结果 TX_PREMATURE_SPEND 还挂着另一个代号:non-final,那是更早的整笔锁(nLockTime)检查失败时给的。两个代号的分工是:
non-final:交易整体的绝对锁时间点还没过。non-BIP68-final:输入级别的相对时间锁按下一块尺度还没到期。
钱包侧如果只按”锁还没生效”这一类做统一退避,很容易把两件事混一起:绝对锁是墙钟高度问题,只能等;相对锁则牵涉被花输出的确认位置,你引用的输出越新,差得越多。
区块层还有一个同名报错
内存池只是第一道。矿工真把带相对锁的交易塞进块里时,节点在执行区块的环节还要再查一遍,用的代号是 bad-txns-nonfinal,附带的调试信息是 contains a non-BIP68-final transaction 加上具体交易哈希。
这一层的注释解释了为什么不能省:相对时间锁的检查需要完整的未花费输出集合,也就是必须知道每个被引用输出是在哪个高度、哪个时间被确认的,这类信息只有在连接区块、持有状态时才能可靠拿到。所以内存池做的是”要不要收”的判断,区块层做的是”这个块是否合法”的判断,两处逻辑相似但不能互相替代。
一个现实推论是:如果你的交易在内存池里活得好好的,最后却因为相对锁判定被区块拒收,责任在打包它的矿工,而不是你的交易本身忽然变非法。此时链上会出现一个无效块,节点会拒绝该块并按需处罚送块的邻居。
写相对锁时该注意什么
第一,别贴着边界写。相对锁以”被引用输出所在块”的下一个块为零点,节点判定用的高度是链顶加一。你要留出足够的余量,不要指望差一格能蒙混过关。
第二,注意计数单位的换算。相对时间锁的高度模式以块为单位,时间模式以固定的时间颗粒为单位,两种模式由序号字段里的高位标志区分。写错标志位,锁会被按另一个尺度解释,判定点就完全偏了。
第三,链分叉时高度模式更稳。时间模式依赖块时间戳,而时间戳本身允许一定偏移;高度模式只看确认块数,跨节点一致性更强。这也是多数钱包默认走高度模式的原因。
第四,排查时先确认自己拿到的是内存池层的代号还是区块层的代号。前者说明这笔交易根本没进池,广播方要等;后者说明块被打废了,问题在矿工的打包校验。
风险提示:本文依据比特币核心 v31.0 源码撰写,函数名与拒绝代号可能随版本调整;本文只讲机制与排查顺序,不构成任何投资建议。
发表评论
还没有评论,来说两句吧。
评论区为展示样式,提交不会被处理。