Solana 共识层 Leader 重复区块惩罚机制(Duplicate Block Slashing)设计解析
2026/9/15 1:56:49 网站建设 项目流程

Solana 共识层 Leader 重复区块惩罚机制(Duplicate Block Slashing)设计解析

【免费下载链接】solanaWeb-Scale Blockchain for fast, secure, scalable, decentralized apps and marketplaces.项目地址: https://gitcode.com/GitHub_Trending/so/solana

本设计文档(对应仓库中的 docs/src/proposals/handle-duplicate-block.md)描述了 Solana 集群如何对产生**重复区块(duplicate block)**的 Leader 进行惩罚与处理:同一槽位(slot)被 Leader 产出多个版本,会急剧增加集群需要解决的分叉数量。本文以该提案为骨架,结合coregossipledger等模块的源码实现,完整讲解从"重复区块的检测与广播"到"duplicate_confirmed 判定"再到"死区块修复(repair)"的整条链路,并给出各阈值的精确来源与安全边界分析。

问题背景:为什么重复区块必须被处理

在 Solana 的流水线共识模型中,每个 slot 由调度表指定的 Leader 负责打包交易并生成 PoH 条目。正常运行时,一个 slot 只对应一个区块;一旦 Leader 行为异常(作恶或被分区),同一个 slot 可能出现多个不同哈希(hash)的区块版本。从源码结构看,这会产生两种后果:

  • 每个重复版本都构成一条潜在分叉,集群的 fork choice 需要解析的候选树规模翻倍,甚至是指数增长;
  • 由于一个 slot 的多个版本无法同时在 BankForks 中共存(见下文切换证明一节),验证者容易在错误版本上停滞,最终导致网络失去活性(liveness)。

因此提案的核心目标是:让整个集群对"某个 slot 是重复的"达成共识,并据此从 fork choice 中剔除未确认的重复版本,同时对已被多数权益确认(duplicate_confirmed)的正确版本进行修复与对齐

三个基础原语(Primitives)

提案首先定义了三个基础原语,它们分别在 gossip 与 replay 层承担职责:

原语含义仓库中的对应实现
gossip_root节点现在会向集群 gossip 自己当前的root(已确认并剪枝的最深槽位)参见 gossip/src/duplicate_shred_handler.rs 及 cluster info 的 root 广播逻辑
gossip_duplicate_slots节点可以向集群 gossip 最多N重复槽证明(duplicate slot proofs)core/src/window_service.rs 检测到重复后经duplicate_slots_sender上报
DUPLICATE_THRESHOLD一个重复槽的某个版本要变为可投票(votable),所需的最低权益投票百分比常量定义于 core/src/replay_stage.rs

DUPLICATE_THRESHOLD 的精确取值:52%

提案正文以DUPLICATE_THRESHOLD = 52为例进行分析,而这一数值并非随意选取——它由两个已确定的共识常量推导而来。在 core/src/replay_stage.rs 中:

pub const DUPLICATE_LIVENESS_THRESHOLD: f64 = 0.1; pub const DUPLICATE_THRESHOLD: f64 = 1.0 - SWITCH_FORK_THRESHOLD - DUPLICATE_LIVENESS_THRESHOLD;

其中SWITCH_FORK_THRESHOLD定义在 core/src/consensus.rs:

pub const SWITCH_FORK_THRESHOLD: f64 = 0.38;

因此DUPLICATE_THRESHOLD = 1.0 - 0.38 - 0.1 = 0.52,即52%。这三个常量共同界定了系统的安全与活性窗口(详见下文"阈值的安全与活性分析"一节)。

在共识逻辑中,DUPLICATE_THRESHOLDHeaviestSubtreeForkChoiceis_slot_duplicate_confirmed方法直接引用(core/src/consensus.rs):

pub(crate) fn is_slot_duplicate_confirmed( &self, slot: Slot, voted_stakes: &VotedStakes, total_stake: Stake, ) -> bool { voted_stakes .get(&slot) .map(|stake| (*stake as f64 / total_stake as f64) > DUPLICATE_THRESHOLD) .unwrap_or(false) }

即:当投在某一槽某版本上的权益占总权益的比例严格大于 52% 时,该槽的该版本被判定为duplicate_confirmed

协议流程:从检测到确认再到恢复

第一步:WindowStage 检测重复槽并广播证明

当网络中的某个 slot 出现多个版本的 shred 时,WindowStage会先通过Blockstore的重复检测路径发现它。在 core/src/window_service.rs 中,处理PossibleDuplicateShred的逻辑区分了多种冲突来源:

  • LastIndexConflict:同一 shred 索引出现了不同的内容;
  • ErasureConflict:纠删码(erasure coding)碎片冲突;
  • MerkleRootConflict:Merkle 根冲突;
  • Exists:已存在相同索引的 shred。

一旦确认冲突,节点调用blockstore.store_duplicate_slot(...)落盘记录证明,并通过duplicate_slots_sendershred_slot发送给 ReplayStage(core/src/window_service.rs)。对应地,ReplayStage 在 core/src/replay_stage.rs 的process_duplicate_slots中消费这些信号,构建DuplicateState并交给check_slot_agrees_with_cluster处理。

提案对广播条件做了明确约束:

  1. WindowStage 检测到重复槽证明P后,先检查新的gossip_root如果已有不超过 1/3 的节点对某个S >= P的槽位完成了 rooting,才将该证明推入gossip_duplicate_slots进行广播——这是为了防止网络刚启动或多数节点尚未稳定时过早扩散不可靠信息;
  2. 随后 WindowStage 将重复槽S的信号发送给 ReplayStage;
  3. 这些证明的生命周期:当验证者观察到超过 2/3 的节点 gossip 的 rootR > S时,说明该重复槽已经远离共识前沿,证明可以从 gossip 中清除(purge)。

第二步:ReplayStage 的 duplicate_confirmed 判定

当 ReplayStage 收到来自第一步的重复槽S信号后,会持续监控 gossip 与 replay 中对该槽的投票:

  • 若某个版本(哈希H)累积的投票达到>= DUPLICATE_THRESHOLD(即 52%),该版本被称为duplicate_confirmed版本
  • Sduplicate_confirmed之前,它被排除在 fork choice 的投票候选集之外(从 fork choice 中移除,避免验证者继续在未确认的重复版本上投票);
  • 同时,ReplayStage 将PoH 重置到"最早的non-duplicate/已确认duplicate槽位"的最新祖先reset_poh_recorder,见 core/src/replay_stage.rs),从而让区块生成能够在最早已知的安全区块上继续,而不是基于可能存在重复的分叉头继续出块。

在 ReplayStage 内部,duplicate_confirmed信息经由DuplicateConfirmedSlotsReceiver汇入process_duplicate_confirmed_slots(core/src/replay_stage.rs)。该函数会对每个新确认的槽位构建DuplicateConfirmedState,并调用check_slot_agrees_with_cluster(实现于 core/src/repair/cluster_slot_state_verifier.rs),将本地状态与集群确认的哈希进行比对,据此决定是否触发后续的修复流程。

ReplayStage 还区分了两种确认类型(core/src/replay_stage.rs):

enum ConfirmationType { SupermajorityVoted, DuplicateConfirmed, }

普通槽通过SupermajorityVoted(超级多数投票)确认,而重复槽必须通过DuplicateConfirmed路径确认,两者在ConfirmedSlot中分别用new_supermajority_votednew_duplicate_confirmed_slot构造。

DUPLICATE_THRESHOLD 的安全与活性分析

提案用DUPLICATE_THRESHOLD = 52推导出两个关键边界:

a) 安全性:恶意容限为 4%

如果网络中作恶(恶意)的权益占比小于2 * DUPLICATE_THRESHOLD - 1的百分比,那么整个网络至多只能产生一个duplicate_confirmed版本的 slot

  • 代入DUPLICATE_THRESHOLD = 522 × 52% - 1 = 4%,即恶意容限为4%
  • 直觉理解:52% 的确认线意味着要同时让两个不同版本都达到 52% 的投票,总共需要 104% 的权益;而104% - 100%的差额正是作恶者可以利用的"多出"的权益,因此只要恶意权益低于 4%,双确认就不可能发生。

b) 活性:网络活性容限为 10%

网络的活性上限为1 - DUPLICATE_THRESHOLD - SWITCH_THRESHOLD

  • 当某重复分叉上只有< DUPLICATE_THRESHOLD(52%)的权益在投票、且尚未duplicate_confirmed时,验证者要切换到另一条分叉,至少需要SWITCH_THRESHOLD(38%)的权益投票(即满足切换证明);
  • 代入DUPLICATE_THRESHOLD = 52SWITCH_THRESHOLD = 381 - 0.52 - 0.38 = 10%,即活性容限为10%。也就是说,只要诚实验证者的权益不低于 90%,系统在最坏情况下仍能维持出块活性。

提案给出了一个直观的反例(原文档 ASCII 图):

|-------- 2 (51% voted, then detected this slot was a duplicate and removed this slot from fork choice) 0---| |---------- 6 (39%)

在槽 2 上投过票的验证者(51%)不能再继续在分叉 2 上投票,因为该槽已被从 fork choice 中移除;此时槽 6 必须累积足够的权益以构成切换证明(至少 38%),否则网络将停滞。本例中槽 6 有 39%,恰好满足切换阈值。

切换证明(Switching Proofs)需要扩展:跨重复版本投票

Solana 的投票机制要求验证者在切换分叉时提供切换证明(switching proof),即"我在旧分叉上投票的槽位是这条新分叉的严格祖先或与其兼容"。提案指出:现有切换证明必须扩展,以允许包含同一 slot 不同版本的投票哈希(通过第一步的重复检测获得)

当前实现不支持这一点的根本原因:切换证明只能使用 BankForks 中存在的 bank 的投票来构建,而同一个 slot 的两个不同版本无法同时存在于 BankForks 中(bank 以 slot 为键,一个 slot 只有一个 bank)。

反例场景(原文档 ASCII 图):

|-------- 2 | 0------------- 1 ------ 2' | |---------- 6
  • 槽 2 与槽 2' 是同一 slot 的两个重复版本,各自获得了DUPLICATE_THRESHOLD / 2(约 26%)的投票,因此两者都无法被单独确认
  • 槽 6 至多获得1 - DUPLICATE_THRESHOLD / 2 ≈ 74%的投票,但仍低于切换阈值所需的证明强度——因为投在 2 上的验证者无法用 2' 的投票构造切换证明,反之亦然;
  • 结论:为了让投票在 2 或 2' 上的验证者能够切换到槽 6 并继续前进,必须把另一个重复版本的投票也纳入切换证明。这正是提案要求扩展切换证明数据结构的原因。

修复问题(The Repair Problem)

三种触发修复的异常情形

即使重复检测与确认机制正常工作,集群仍可能因以下情况出现"部分验证者无法前进":

  1. 错过 gossip 投票:由于网络抖动/延迟,部分验证者未能在旧投票被新投票覆盖前观察到 gossip 中的投票,导致他们对某个 slotS是否duplicate_confirmed的判断与其他人不一致;
  2. lockout 导致延迟确认:由于投票锁定期(lockout),重复槽S的任何版本都未能直接达到duplicate_confirmed,但它的某个后代槽位可能在 lockout 到期后达到duplicate_confirmed——而根据定义,一旦后代被确认,S也随之被确认
  3. 追赶中的节点:正在追赶网络状态的验证者没有看到 gossip 中的投票,遇到重复区块(dup block)后无法继续前进。

提案给出一个前提假设:只要网络最终稳定,且至少有一个诚实验证者观察到Sduplicate_confirmed,那么只要S属于最重分叉(heaviest fork),最终所有验证者都会观察到S的某个后代被 duplicate confirmed

核心场景模型

提案将待解决的问题抽象为如下链:

1 -> 2 (duplicate) -> 3 -> 4 (duplicate)

并假设三种情况同时成立:

  1. 通过 gossip 重复证明,所有人最终都会看到槽 2 和槽 4 的重复证明,并一致同意将它们从 fork choice 中移除,直到它们被duplicate_confirmed
  2. 由于 lockout,超过DUPLICATE_THRESHOLD(52%)的权益投在了槽 4 上(而非槽 2),这意味着至少 52% 的人持有槽 2 与槽 4 的正确版本
  3. 但剩余1 - DUPLICATE_THRESHOLD(48%)的人持有的是槽 2 的错误版本,于是这些人在重放(replay)时会把槽 3 标记为死区块(dead)——即使出错的槽是 2 而不是 3。目标就是把这些验证者重新拉回正确的分叉。

修复方案的四步流程

提案给出的解决方案在源码中已有对应实现(核心校验函数位于 core/src/repair/cluster_slot_state_verifier.rs):

1. 改变EpochSlots的信号语义:从"slot 完成"改为"bank 冻结(frozen)"

  • 当观察到超过DUPLICATE_THRESHOLD的验证者冻结了死槽 3 时,才发起恢复尝试;
  • 注意:这不代表所有这些验证者冻结的是同一版本的 bank——它只是一个信号,提示"我们的 bank 版本可能有问题"。

2. 发起特殊修复请求RepairDuplicateConfirmed

  • 请求格式为RepairDuplicateConfirmed(dead_slot, Vec<(Slot, Hash)>):指定一个死槽位,以及它的N个最新祖先的(slot, hash)向量;
  • 该请求让修复方(repairer)可以校验请求方的祖先哈希是否正确。

3. 修复者(repairer)的响应条件

  • 只有当(slot, hash)向量中的任意一个元素同时满足两个条件时,修复者才回复"正确的哈希":
    • 该槽位是duplicate_confirmed
    • 该哈希与请求方向量中提供的哈希不一致

4. 请求方的自愈流程

  • 请求方一旦发现"正确哈希"与自己冻结的哈希不同,就dump(丢弃)本地该区块,以便接受新区块,并向网络请求正确哈希对应的区块;
  • 在 core/src/replay_stage.rs 的process_duplicate_slots_to_repair中可以看到完整实现:本地节点会检查duplicate_slots_to_repair中记录的待修复槽,将其与bank_forks.bank_hash对比,若哈希不一致则 dump 该槽及其后代,并通过MAX_REPAIR_RETRY_LOOP_ATTEMPTS(定义为 10,见 core/src/replay_stage.rs)限制重试次数,防止无限循环。

关于"修复者可能说谎"的说明

提案在最后明确指出:修复者可能会欺骗你,给你错误版本的区块,此时你最终会再次得到一个死区块并重复整个流程。这一"信任但验证"的循环正是修复机制能够在拜占庭环境中收敛的基础——因为正确的哈希最终会由超过 52% 的权益投票所锚定,错误版本的修复请求会在后续轮次中再次被纠正。

源码中的落地实现速览

以下是本文涉及的核心实现位置,供深入阅读:

功能文件与关键位置
DUPLICATE_THRESHOLD等阈值定义core/src/replay_stage.rs、core/src/consensus.rs
duplicate_confirmed 判定core/src/consensus.rs
重复槽信号处理core/src/replay_stage.rs
duplicate_confirmed 信号处理core/src/replay_stage.rs
重复槽检测与上报core/src/window_service.rs
集群状态一致性校验(含死槽修复判定)core/src/repair/cluster_slot_state_verifier.rs
重复槽修复执行core/src/replay_stage.rs
ancestor hashes 服务中的正确哈希下发core/src/repair/ancestor_hashes_service.rs
gossip 层重复 shred 处理gossip/src/duplicate_shred_handler.rs
Blockstore 中重复槽证明的存取ledger/src/blockstore.rs、ledger/src/blockstore_db.rs
集群级集成测试local-cluster/tests/local_cluster.rs

总结

Solana 的重复区块惩罚机制是一套"检测 → 广播 → 排除 → 确认 → 修复"的完整闭环:

  • 检测层(WindowStage + Blockstore)识别同一 slot 的多个 shred 版本并落盘证明;
  • 共识层(ReplayStage + HeaviestSubtreeForkChoice)通过DUPLICATE_THRESHOLD = 52%将重复槽的某个版本确定为duplicate_confirmed,在此之前将重复槽移出投票候选集,并把 PoH 重置到最早安全祖先;
  • 阈值设计同时保证了 4% 的恶意容限与 10% 的活性容限;
  • 修复层通过扩展切换证明、改变 EpochSlots 信号语义以及RepairDuplicateConfirmed特殊修复请求,把持有错误重复版本的验证者拉回正确分叉,并容忍修复者说谎后再次修复的循环。

这套设计的关键在于:所有安全与活性边界都由DUPLICATE_THRESHOLD(52%)、SWITCH_THRESHOLD(38%)与活性容限(10%)三个常量共同锁定,它们不是经验参数,而是从数学上可推导的共识保证,这一严谨性正是 Solana 在面对恶意 Leader 产出重复区块时仍能维持最终一致性的基础。

【免费下载链接】solanaWeb-Scale Blockchain for fast, secure, scalable, decentralized apps and marketplaces.项目地址: https://gitcode.com/GitHub_Trending/so/solana

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询