钱包重发同一笔交易,节点回了三个字的代号:txn-already-known。它和”钱已经花掉了”听起来像一回事,在源码里却挂在完全不同的判定分支上。看懂这一小段代码,能省掉很多重复广播的无用功。
判定的起点是”输入找不到”
比特币核心 v31.0 的交易检查流程里,有一步是逐个输入去问当前视图:这个输出点现在还有对应的未花费输出吗。这一问用 m_view.HaveCoin 完成,答案是否,才进入分岔。
关键在这个分岔里做了什么。源码先做了一件看起来绕远的事:不去追查到底是哪种情况,而是拿这笔交易自己的交易哈希,配上它每一个输出序号,组成一组”这笔交易会产生的输出点”,然后逐个问一句:这些输出点,我的缓存里有吗。
只要有任何一个命中,判定立刻结束,返回内部结果 TX_CONFLICT,拒绝代号就是 txn-already-known。这里的思路很朴素:如果这笔交易自己造出来的输出,我这儿已经存着了,那说明这笔交易我早就收过甚至确认过,现在又原样递上来一次,那它的输入当然”找不到”——不是缺父交易,是被它自己花掉了。
只查缓存,是一条有意的省事路线
注释把这一点写得很清楚:这一步是乐观地做一次高效缓存检查,只查内存里的未花费输出缓存。也就是说,它不去翻磁盘上完整的状态数据库。
这条取舍带来两个直接后果。一是快,重复提交的判定不需要任何磁盘往返,一次哈希查表就出结论,适合被反复广播打过来的场景。二是它有覆盖面缺口:如果那笔交易是很早以前确认的、相关输出早就被挤出内存缓存,这步就命中不了。命中不了不等于节点认为交易合法,只是这条快路径没起作用,流程会继续往下走到完整的状态查询上去——那里给出的是另一个代号,通常是我们更熟悉的输入缺失或已被花费那一类。
同一段代码还留了个尾巴值得注意:前面那次逐输入查询,可能顺手把被查询的输出点塞进了缓存,注释明确说,如果这笔交易最后被判定无效,这些临时进缓存的项要由 coins_to_uncache 清掉。也就是说,判定路径本身会留下待清理的痕迹,快路径的”便宜”是相对于完整查询而言,不是零成本。
与另一条分支的分工
输入找不到时,缓存里也没有这笔交易自己的输出,源码给出的结论是:这可能是一笔孤儿交易,只是我们还没见到它的父交易,于是返回 TX_MISSING_INPUTS,拒绝代号为 bad-txns-inputs-missingorspent。
两个代号的分工因此非常清楚。
txn-already-known:内部结果属于冲突类,含义是”这笔我认得,别重复递了”。父交易到不到齐都不影响这个结论。bad-txns-inputs-missingorspent:内部结果属于缺输入类,节点保留了一个假设——也许父交易马上就到。
这个分工决定了节点后续的行为差异。前者不会让交易获得任何等待资格,重复广播只会 repeatedly 撞回同一句话;后者才让交易进入可能被延后处理的通道,一旦父交易补齐,节点会重新拿它试一次。
实操上怎么用这两个代号
第一,钱包或者自建服务在收到 txn-already-known 时,正确的动作是去查状态,不是再发一次。这个代号说明节点手里已经有这笔交易的痕迹,重发只是浪费一次往返。要确认的是它在哪个高度被收录、被哪一笔花了,而不是继续点击发送。
第二,收到输入缺失那一类时,先分清是”父还没到”还是”父的输出被别人花掉了”。前者是时序问题,稍等或让节点补齐即可;后者是双花,重发没有意义。这两个可能共用一个代号,源码本身不做区分,区分要靠对输出点状态的查询。
第三,不要把 txn-already-known 当成对端在指责你作弊。它是个纯策略层的重复识别,不会像共识层无效那样触发对邻居的处罚。节点只是不想为同一笔交易做第二遍账。
第四,如果你的系统会主动重放交易——例如批量补发、失败重试队列——建议按内部结果分类做退避:冲突类直接终止重放,缺输入类才允许有限次重试,并且每次重试前重新查询输出点状态。把两个代号混成”广播失败”一种状态处理,是很多钱包重放队列越堆越长的根源。
风险提示:本文依据比特币核心 v31.0 源码的实现细节撰写,代号与判定路径可能随版本调整;内容为技术说明,不构成任何投资建议。
发表评论
还没有评论,来说两句吧。
评论区为展示样式,提交不会被处理。