9 月 11 日,比特币在高度 966,500 附近又发生了一次单块重组。SpiderPool 和 AntPool 几乎同时挖出有效块,两个块都指向同一个父块。Galaxy Research 的节点先看到了 SpiderPool 的版本,随后 AntPool 的块到达,节点最终切到了 AntPool 那条链。SpiderPool 的块被孤立,大约 3.1389 BTC 的区块奖励没了。
这不是传统意义上的 bug。它是分布式共识在真实网络延迟下必然会出现的现象。但“必然”不等于“最优”。每一次单块重组,都意味着算力浪费、矿池收入损失,以及商户和交易所在一小段时间里无法确定自己看到的确认到底算不算数。
真正的问题不是“会不会再发生”。它一定还会发生。真正的问题是:代码在重组发生时到底做了什么,以及有没有空间在不碰共识规则的前提下做得更好。
事情是怎么一步步发生的
整个过程拆开看,其实很直白。
- SpiderPool 和 AntPool 在高度 966,500 同时找到有效块,两个块都建立在高度 966,499 之上。
- 两个块都通过验证,都有合法的工作量证明。
- 两个矿池各自开始广播自己的块。SpiderPool 发给它的对等节点,AntPool 也发给它的对等节点。
- 两个块在 P2P 网络里传播的速度不一样,取决于对等节点拓扑、地理分布和网络状况。
- Galaxy Research 的节点先收到 SpiderPool 的块。它验证、连接、更新链尖。此时这个节点认为高度 966,500 是 SpiderPool 的块,连在这个节点上的商户和交易所会看到一个新的确认。
- 过了一会儿,AntPool 的块也到了同一个节点。节点现在有两个高度相同、都有效的块,都是链尖候选。
- 节点按比特币的分叉选择规则处理:累计工作量更大的那条链赢。
- AntPool 的块包含 4,359 笔交易,网络也开始从它继续扩展,所以这条分支累计了更多工作。节点执行重组:断开 SpiderPool 的块,把它放进孤块池,连接 AntPool 的块。
- SpiderPool 块里那些不在 AntPool 块里的交易回到内存池。SpiderPool 的 3.1389 BTC 区块奖励丢失。从 SpiderPool 的角度看,他们没做错任何事,只是输掉了传播竞赛。
一旦两个块被挖出来,后面的过程基本是确定的。唯一的变量是传播时序。
代码路径:重组在哪里触发,在哪里解决
比特币核心的区块处理逻辑主要在 validation.cpp。关键函数是 ConnectBlock(),它把一个块的效果应用到 UTXO 集,并验证块内所有交易。当两个块在同一高度竞争时,节点会调用 ActivateBestChain(),判断新块所在的链是否比当前链尖有更多工作。
核心逻辑可以简化成这样:
// validation.cpp 简化
bool CChainState::ActivateBestChain(BlockValidationState& state, ...) {
// …
while (pindexMostWork->nChainWork > pindexBest->nChainWork) {
// 新链有更多工作,执行重组
if (!Reorganize(pindexMostWork, state)) {
return false;
}
}
}
真正切换链的地方在 Reorganize()。它会断开旧链上的块,再连接新链上的块。对于单块重组,就是断开 SpiderPool 的块,连接 AntPool 的块。
// validation.cpp 简化
bool CChainState::Reorganize(const CBlockIndex* pindexNew, ...) {
// 找分叉点
CBlockIndex* pfork = pindexNew;
CBlockIndex* plonger = chainActive.Tip();
while (pfork != plonger) {
// 一直往回走,直到找到共同祖先
}
// 断开旧链上的块
for (CBlockIndex* pindex : vDisconnect) {
if (!DisconnectBlock(pindex, state, ...)) {
return false;
}
}
// 连接新链上的块
for (CBlockIndex* pindex : vConnect) {
if (!ConnectBlock(block, state, pindex, ...)) {
return false;
}
}
}
这段代码本身没问题。它忠实实现了共识规则。问题不在这里。等 Reorganize() 被调用时,竞赛其实已经输了。
真正的问题:传播不对称和孤块处理
更深层的问题是,比特币的 P2P 层不会根据矿池身份、权益或者任何“重要性”来给区块排序。一个块就是一个块。它通过 TCP 和对等连接在网络里传播,能多快就多快。两个块竞争时,先到达更多算力那边的块赢。这不是代码 bug,而是去中心化系统没有中心协调时自然涌现出来的性质。
但有两个具体方向,可以在不碰共识的前提下降低孤块率。
改动一:在 net_processing.cpp 里更聪明地处理孤块
现在比特币核心对孤块的处理基本是丢弃。块里的交易回到内存池,但块头和元数据丢了。如果被孤立的块所在链后来又被矿池扩展,节点必须重新下载、重新验证一遍。
更好的做法是:把孤块保留在一个单独索引里,给它一个短窗口,比如 100 个块或者 1 小时。把里面的交易标记为“可能因重组回来”。如果竞争链后来从孤块继续扩展,节点可以立刻重连,不需要完整重验证。
一个最小实现大概是这样:
// net_processing.cpp 增加
struct OrphanBlockIndex {
CBlock block;
CBlockIndex* pindex;
int64_t nTimeReceived;
};
std::map<uint256, OrphanBlockIndex> m_orphan_blocks GUARDED_BY(cs_main);
void PeerManagerImpl::BlockConnected(
const std::shared_ptr<const CBlock>& pblock,
const CBlockIndex* pindexConnected,
const std::vector<CTransactionRef>& vtxConflicted)
{
// 当有块被连接时,检查是否有孤块从它扩展。
// 如果有,就保留一段时间,方便以后重连。
for (auto it = m_orphan_blocks.begin(); it != m_orphan_blocks.end(); ) {
if (it->second.block.hashPrevBlock == pindexConnected->GetBlockHash()) {
// 这个孤块从刚连接的块扩展而来。
// 留在索引里,等待可能的重连。
it->second.nTimeReceived = GetTime();
++it;
} else if (GetTime() – it->second.nTimeReceived > 3600) {
// 超过一小时的孤块过期清理
it = m_orphan_blocks.erase(it);
} else {
++it;
}
}
}
效果:单块重组发生后,如果孤块那条链后来获得更多工作,节点可以直接从本地索引重连孤块,不用再向对等节点重新请求。这样能降低重组延迟,也能减少链切换的带宽消耗。它不改共识,也不会给任何矿池特殊优势。
改动二:通过矿池级区块中继协调减少传播不对称
现在的 CConnman 和对等节点选择逻辑对所有出站节点一视同仁。一个节点连了十个随机对等节点,可能因为地理距离近,先收到 SpiderPool 的块。它没有机制说“我想更快收到 AntPool 中继网络的块”,也没有机制说“我想优先处理从当前链尖扩展的块”。
一个有针对性的改进是:允许节点维护一小批“优先中继”对等节点,根据历史区块传播表现来选。当新块头通过 cmpctblock 或 headers 公告时,节点可以向历史上延迟更低、有效块率更高的对等节点请求完整块。
// net_processing.cpp 对等节点评分增加
struct PeerBlockRelayStats {
double dAvgLatency; // 滚动平均区块投递延迟
int nValidBlocks; // 投递并通过验证的块数
int nInvalidBlocks; // 投递但验证失败的块数
};
// 在 ProcessMessage 里处理新区块公告时:
if (msg_type == NetMsgType::CMPCTBLOCK) {
auto& stats = m_peer_relay_stats[pfrom->GetId()];
stats.dAvgLatency = (stats.dAvgLatency * 0.9) + (latency * 0.1);
// 优先向延迟更低、有效率更高的对等节点请求完整块
}
效果:节点能更快收敛到主链。两个块竞争时,节点更可能先收到最终赢的那个块,减少跟着错误链尖走的窗口。这能直接降低接收节点发生重组的概率。
为什么这不只是丢一个块
单块重组不算罕见,通常几周一次。但频率在增加,四周三次就值得注意了。原因不是网络故障,而是矿池更多、地理分布更散、算力增长下出块更快。每一次单块重组都有三个成本。
- 输的矿池损失收入。SpiderPool 在 9 月 11 日丢了 3.1389 BTC。对中型矿池来说,这不是小数。
- 商户和交易所的确认变得不确定。重组窗口里,孤块里看起来已经确认的交易不再确认。接受支付的商户可能面临回滚。少见,但真实。
- 全网带宽和 CPU 浪费。每个跟错链的节点都要断开块、重连新块、重新验证交易。几万个节点加起来,成本不小。
上面这些改动不会消灭重组,只会降低影响。这才是现实目标。比特币共识层故意抗变,P2P 层不是。下一轮改进应该聚焦在这里。
普通用户该明白什么
- 单块重组不是黑客攻击,也不是安全漏洞。它是网络在解决“哪个有效块先来”的临时分歧,设计上就是这样。
- 交易有 6 个确认就安全。多数场景 3 个确认够用。高价值交易等 6 个确认。这不是新建议,9 月 11 日重组只是提醒大家为什么会有这条建议。
- 真正的问题不是重组发生,而是代码没有从重组里学习。每个孤块都被当成孤立事件。通过孤块处理和 peer 中继评分的小改动,可以降低孤块率,同时不碰保护比特币安全的共识规则。
比特币共识层像坦克,慢、难改、不在乎你的感受。P2P 层像跑车,快、可调,下一波改进会从这里来。9 月 11 日这次重组是一个信号:跑车该保养了。


