首页
/ Bitcoin Core 私有广播:每笔交易 1000 次发送尝试上限与 attempts_remaining 观测字段

Bitcoin Core 私有广播:每笔交易 1000 次发送尝试上限与 attempts_remaining 观测字段

2026-09-06 13:58:46作者:昌雅子Ethen

本篇基于 Bitcoin Core 的发布说明 release-notes-35680.md(PR #35680)展开,讲清 -privatebroadcast 私有广播机制中两个具体变化:每笔交易被限制为最多 1000 次发送尝试、达到上限后的停播与重试方式,以及 getprivatebroadcastinfo 新增的 attempts_remaining 字段。读完后你将能理解私有广播队列的发送预算如何计算、耗尽后交易的状态如何查询与恢复,并能用仓库中的单元测试和功能测试自行验证这些行为。

发布说明原文的两项核心变更

release-notes-35680.md 记录了两点:

  1. P2P 与网络变更:每笔通过私有广播(-privatebroadcast)发送的交易限制为 1,000 次发送尝试。达到上限后停止广播;再次调用 sendrawtransaction 即可重试。达到上限的交易仍然可以通过 getprivatebroadcastinfo 查询,也可以被 abortprivatebroadcast 移除。
  2. RPC 更新getprivatebroadcastinfo 现在为每笔交易额外报告一个 attempts_remaining 字段(该交易剩余可用的发送尝试次数)。

下面结合当前仓库源码逐项核实其实现。

私有广播是什么、何时生效

私有广播由启动参数 -privatebroadcast 控制。参数在 src/init.cpp 中的定义说明其语义:

通过 Tor 或 I2P 网络,使用短生命周期连接广播经 sendrawtransaction RPC 提交的交易,且不先进入 mempool;通过钱包提交的交易不受此选项影响。

因此该功能有两个前提,均可在 sendrawtransaction 的 RPC 实现 中直接看到校验:

  • 必须开启 -privatebroadcast,且 Tor(NET_ONION)或 I2P 网络可达,否则 sendrawtransaction 会直接报错并提示检查 Tor daemon、-torcontrol-torpassword-i2psam 配置;
  • 开启私有广播后,sendrawtransactionNO_MEMPOOL_PRIVATE_BROADCAST 广播路径;未开启时走常规的 MEMPOOL_AND_BROADCAST_TO_ALL 路径(src/rpc/mempool.cpp)。

启动阶段还有一组兼容性检查:若 Tor 与 I2P 都不可达则初始化失败;若同时配置了 -connect(禁用 addrman 出站连接)也会报错,因为私有广播需要向随机选择的 Tor/I2P 节点建立新连接;另外若未开启 -proxyrandomize,会给出隐私相关警告(src/init.cpp)。

变更一:每笔交易 1000 次发送尝试上限

上限常量与"一次尝试"的定义

上限常量定义在 src/private_broadcast.h

/// Maximum number of send attempts for a transaction. Once this limit is
/// reached, the transaction remains tracked but is not sent again unless
/// explicitly re-added.
static constexpr size_t MAX_SEND_ATTEMPTS{1'000};

从源码结构看,"一次发送尝试"对应 PrivateBroadcast::PickTxForSend() 每被调用一次并为某个节点选中的交易追加一条 SendStatus(即向一个对等节点发一次)。是否还有尝试剩余由 IsPending() 判定:

bool PrivateBroadcast::IsPending(const TxSendStatus& status) const
{
    return status.send_statuses.size() < m_max_send_attempts;
}

选取下一个待发送交易时,PickTxForSend() 先按 IsPending 过滤掉已耗尽交易,再从剩余交易中按优先级(被选次数最少、确认数最少、时间最早者优先,见 Priority 比较逻辑)选出最紧急的一笔。这意味着某笔交易耗尽预算后,不会阻塞队列中其他交易的发送——这一点在单元测试中有专门断言(见下文"行为验证"一节)。

达到上限后发生了什么

与"直接丢弃"不同,达到上限的交易仍保留在私有广播队列中,只是不再被发送:

  • HavePendingTransactions() 返回 false,PickTxForSend() 不会返回它;
  • 它仍然出现在 getprivatebroadcastinfo 的结果中,且 attempts_remaining 为 0;
  • 可以随时用 abortprivatebroadcast 将其从队列中移除(RPC 定义见 src/rpc/mempool.cpp,说明为 "Abort private broadcast attempts for a transaction currently being privately broadcast. The transaction will be removed from the private broadcast queue.")。

发布说明中"call sendrawtransaction again to retry"对应的是 Add() 的重置语义。PrivateBroadcast::Add() 对"已存在且已耗尽"的交易不是简单拒绝,而是重置其状态:

// An exhausted transaction can be explicitly retried by adding it again.
it->second.time_added = NodeClock::now();
it->second.send_statuses.clear();
return AddResult::Added;

即重新提交同一笔交易(wtxid 相同)会清空全部发送历史、刷新入队时间,让它获得全新的 1000 次发送预算;若该交易尚未耗尽预算,则返回 AddResult::AlreadyPresent 不做任何改动。返回值的三种语义定义在 AddResult 枚举Added(新加入或耗尽后重置)、AlreadyPresent(仍在预算内)、QueueFull(队列达到 MAX_TRANSACTIONS,即同时最多跟踪 10,000 笔,见 src/private_broadcast.h)。

需要说明的是,1,000 是主线路径上的编译期常量;构造函数虽然接受 max_send_attempts 参数(默认即 MAX_SEND_ATTEMPTS,见 构造函数),但该可配置性目前仅被测试代码利用,用户侧并无对应的启动参数。

变更二:getprivatebroadcastinfo 报告 attempts_remaining

attempts_remaining 字段在 TxBroadcastInfo 结构中声明(src/private_broadcast.h),其值在 GetBroadcastInfo() 中计算:

const size_t attempts_remaining{m_max_send_attempts - std::min(state.send_statuses.size(), m_max_send_attempts)};

即"上限减去已向节点发送的次数",且用 std::min 钳制保证结果不会为负。RPC 侧在 getprivatebroadcastinfo 方法定义 中将该字段加入结果描述,并把文档说明一并更新为"达到发送尝试上限的交易会以 attempts_remaining=0 留在结果中"(src/rpc/mempool.cpp)。

该 RPC 仅在以 -privatebroadcast 运行时可用,否则抛出 Method not foundsrc/rpc/mempool.cpp)。其完整返回结构为:

{
  "transactions": [
    {
      "txid": "0a1b…",
      "wtxid": "1c2d…",
      "hex": "020000000001…",
      "time_added": 1730000000,
      "attempts_remaining": 997,
      "peers": [
        { "address": "onion:…:8333", "sent": 1730000010 },
        { "address": "i2p:…:11337", "sent": 1730000020, "received": 1730000021 }
      ]
    }
  ]
}

(以上为按 RPC 字段定义 组织的示例结构。)其中 peers 数组逐项记录每个接收节点:address(发送目标地址)、sent(被选中发送的时间)、received(可选,对端以 PONG 回应 PING 确认收到的时间,见 NodeConfirmedReception 的注释)。达到上限时该交易 attempts_remaining 为 0、peers 中最多有 1000 条记录。

对运维者的实际意义是:可以持续轮询该 RPC,观察 attempts_remaining 是否单调下降到 0,从而判断私有广播是否长期无法触达对端(例如 Tor/I2P 路由异常),并据此决定重试(重新 sendrawtransaction)还是放弃(abortprivatebroadcast)。

行为验证:单元测试、模糊测试与功能测试

仓库中三处测试与本次变更直接对应,可在本地构建后运行核对行为。

  1. 单元测试 src/test/private_broadcast_tests.cppsend_attempt_limit 用例用 max_attempts=5 构造队列,验证了完整生命周期:
    • 连续 5 次 PickTxForSend 都能选中该交易;
    • 第 6 次起 HavePendingTransactions() 为 false、PickTxForSend() 返回 nullopt;
    • GetBroadcastInfo()peers.size()==5attempts_remaining==0
    • 耗尽的交易不会使队列过期(GetStale() 为空),也不会阻止其他新交易被发送;
    • 再次 Add 同一交易后 peers 被清空、attempts_remaining 恢复为 5。
  2. 模糊测试 src/test/fuzz/private_broadcast.cpp 在 1~12 的随机 max_send_attempts 范围内构造队列,并断言 attempts_remaining == max_send_attempts - 已发送次数src/test/fuzz/private_broadcast.cpp),保证该字段在各种乱序操作下始终与内部计数一致。
  3. 功能测试 test/functional/p2p_private_broadcast.py 在真实 P2P 环境下断言 attempts_remaining == MAX_PRIVATE_BROADCAST_ATTEMPTS - len(peers),并与实际收到的 peers 条目、确认收到的数量做交叉校验。

另外 test/functional/p2p_private_broadcast_retry_v1.py 覆盖了重试相关路径,可作为"重新提交后重新开始发送预算"这一行为的端到端佐证。

小结

PR #35680 为私有广播增加了"发送预算"这一确定性边界:每笔交易最多 1000 次逐节点发送尝试(MAX_SEND_ATTEMPTSsrc/private_broadcast.h),耗尽后交易滞留队列、attempts_remaining 归零(src/rpc/mempool.cpp 上报),既不自动消失也不阻塞其他交易;用户重新调用 sendrawtransaction 即通过 Add() 的重置分支(src/private_broadcast.cpp)恢复预算,或用 abortprivatebroadcast 明确放弃。适用前提与限制:功能仅对 sendrawtransaction 提交的交易生效,要求 Tor 或 I2P 可达,且与 -connect 配置互斥;上限值在主线路径上是固定的 1000,没有运行时配置项。

登录后查看全文
热门项目推荐
相关项目推荐