fuel-core PoA 就绪判定:--max-sync-height-diff高度差检查与--time-until-synced防抖的组合机制
【免费下载链接】fuel-coreRust full node implementation of the Fuel v2 protocol.项目地址: https://gitcode.com/GitHub_Trending/fu/fuel-core
本文围绕 fuel-core 仓库中 PoA 共识模块的“就绪(Synced)判定”展开:PoA Ready 现在将一条实时的保留节点高度差检查(网络高度 − 本地数据库高度 ≤ --max-sync-height-diff,默认 1)与既有的--time-until-synced防抖窗口组合使用——高度差一旦满足阈值即开始计时(差值为 0 且窗口为零时立即置为 Synced),而孤立/开发模式(--min-connected-reserved-peers=0)则直接跳过高度差比较;/v1/health接口的语义保持不变。读完本篇,你将能够准确配置这三项 CLI 参数、理解 PoA 内部状态机如何进入和退出 Synced 状态,并定位到crates/services/consensus_module/poa/src/sync.rs中的对应实现。
三项参数定义与默认值
PoA 的就绪判定由bin/fuel-core的三个 CLI 参数共同控制,均支持通过同名环境变量覆盖(#[clap(..., env)]):
| 参数 | 类型 | 默认值 | 作用 |
|---|---|---|---|
--min-connected-reserved-peers | usize | 0 | 开始同步前需要连接的保留节点数;为0时按孤立/开发模式处理,跳过高度差比较 |
--time-until-synced | humantime::Duration | 0s | 高度差满足--max-sync-height-diff之后、正式宣告 Synced 之前的“静默窗口”;为 0 时立即转换 |
--max-sync-height-diff | u32 | 1 | 仍计为 Ready 的最大保留节点高度差(Height Gap = 保留节点网络高度 − 本地数据库高度) |
这三项参数的 CLI 声明位于 bin/fuel-core/src/cli/run.rs:
/// The number of reserved peers to connect to before starting to sync. #[clap(long = "min-connected-reserved-peers", default_value = "0", env)] pub min_connected_reserved_peers: usize, /// Quiet window after the reserved-peer height gap is within /// `--max-sync-height-diff` before PoA declares Synced. Zero means immediate. #[clap(long = "time-until-synced", default_value = "0s", env)] pub time_until_synced: humantime::Duration, /// Max reserved-peer Height Gap that still counts as Ready. #[clap(long = "max-sync-height-diff", default_value = "1", env)] pub max_sync_height_diff: u32,三者最终汇入 PoA 服务的配置结构体 crates/services/consensus_module/poa/src/config.rs:
pub struct Config { pub trigger: Trigger, pub signer: SignMode, pub metrics: bool, pub min_connected_reserved_peers: usize, /// Quiet window after the height gap is within `--max-sync-height-diff` /// before declaring Synced. Zero transitions immediately once the gap is ok. pub time_until_synced: Duration, /// Largest reserved-peer Height Gap that still counts as Ready. pub max_sync_height_diff: u32, pub production_timeout: Duration, pub chain_id: ChainId, }从配置注释可以直接读出组合语义:高度差检查是 Synced 的前置条件,time_until_synced是在该条件成立之后叠加的防抖窗口,两者是“与”的关系而非二选一。测试场景下Default实现取min_connected_reserved_peers: 0、time_until_synced: Duration::ZERO、max_sync_height_diff: 1(见同文件 config.rs),与 CLI 默认值保持一致。
高度差判定的核心实现
判定逻辑收敛在一个纯函数height_gap_is_ready中,位于 crates/services/consensus_module/poa/src/sync.rs:
/// Height Gap = reserved-peer network height − local DB height. /// Isolated/dev (`min_connected_reserved_peers == 0`) skips the comparison. pub(crate) fn height_gap_is_ready( min_connected_reserved_peers: usize, max_sync_height_diff: u32, reserved_peer_network_height: Option<BlockHeight>, local_height: Option<BlockHeight>, ) -> bool { if min_connected_reserved_peers == 0 { return true; } let Some(network) = reserved_peer_network_height else { return false; }; let local = local_height.unwrap_or(BlockHeight::from(0u32)); let gap = u32::from(network).saturating_sub(u32::from(local)); gap <= max_sync_height_diff }该函数精确对应 changelog 描述的四条行为:
- 孤立/开发模式短路:
min_connected_reserved_peers == 0时直接返回true,不做任何比较——这正是文档中“Isolated/dev(--min-connected-reserved-peers=0)跳过比较”的落点; - 网络侧高度未知则判 Not Ready:保留节点尚未通过心跳上报高度(
reserved_peer_network_height为None)时返回false,节点需要等 P2P 心跳到达后才可能满足高度差条件; - 本地侧高度未知按 0 处理:
local_height.unwrap_or(0)意味着空数据库(刚启动、无块)时高度差等于整个网络高度,只有阈值足够大才会判 Ready; - 有符号差用饱和减法:
saturating_sub保证本地高度超过网络高度(例如主节点领先于个别保留节点心跳)时差距按 0 计,不会下溢,判定仍为 Ready。
SyncTask中的height_gap_ready方法(sync.rs)则是该函数的异步包装:通过p2p.reserved_peer_network_height()取保留节点侧网络高度,通过block_importer.latest_block_height()取本地数据库最新块高度,再交给height_gap_is_ready完成比较。
SyncTask 状态机:如何进入与退出 Synced
PoA 的同步状态由SyncTask维护,其内部状态枚举InnerSyncState(sync.rs)有三个状态,文档中的组合规则直接体现在状态转移注释里:
enum InnerSyncState { /// We are not connected to at least `min_connected_reserved_peers` peers. InsufficientPeers(BlockHeader), /// We are connected to at least `min_connected_reserved_peers` peers. /// Height gap must be within `--max-sync-height-diff`; then `time_until_synced` /// debounce applies (immediate when zero). SufficientPeers(BlockHeader), /// We can go into this state once the height gap is within threshold and /// `time_until_synced` has elapsed (or is zero). Synced { block_header: BlockHeader, has_sufficient_peers: bool }, }状态机在RunnableTask::run的tokio::select!循环中由四条驱动臂推进(sync.rs):
- 保留节点连接数变化(
peer_connections_stream):在InsufficientPeers与SufficientPeers之间迁移,随后触发on_sufficient_peers_activity和recompute_if_synced_lagging; - 块导入事件(
block_stream):更新各状态记录的block_header,并判断是否离开 Synced; - 防抖定时器 tick:仅当
debounce_armed为真时调用advance_to_synced_if_ready——这是“gap 先满足、窗口再计时”的关键护栏; synced_recheck周期重检(每 10 秒,SYNCED_HEIGHT_GAP_RECHECK_INTERVAL,sync.rs):即使没有新的连接或块导入事件,只要保留节点心跳推进了网络高度,状态也能在 10 秒内反映出来。
进入 Synced:gap 门槛 + 一次性武装的防抖
on_sufficient_peers_activity实现了 changelog 所说的组合语义(sync.rs):
async fn on_sufficient_peers_activity(&mut self) { if !matches!(self.inner_state, InnerSyncState::SufficientPeers(_)) { return; } if !self.height_gap_ready().await { self.debounce_armed = false; return; } if self.time_until_synced == Duration::ZERO { self.advance_to_synced_if_ready().await; } else if !self.debounce_armed { self.restart_timer(); self.debounce_armed = true; } }要点有三:
- gap 不满足时立即解除武装(
debounce_armed = false),需要重新进入静默窗口,防止一个早已启动的旧定时器绕过--time-until-synced; - 窗口为 0 时直接转换,即文档中“immediate when zero”的路径;
- 定时器只武装一次(
restart_timer仅在首次满足时调用),10 秒重检不会反复reset定时器,否则一个较大的--time-until-synced永远无法走完——源码注释明确写了这一意图(“Arm once; recheck must not reset_timer every 10s or a long--time-until-syncednever completes”)。
SyncTask::new还做了初始化层面的对应处理:time_until_synced == Duration::ZERO时根本不创建定时器;创建时调用timer.reset()抹掉tokio::time::interval首次立即触发的特性,确保防抖只有在 gap 满足、被显式武装后才会开始计时(sync.rs)。此外SyncState::from_config(sync.rs)在“孤立模式 + 零窗口”时初始即为Synced,与状态机的InnerSyncState::from_config中(0, Duration::ZERO) => Synced分支一致。
离开 Synced:高度差“实时”退出,连接数抖动不翻转
退出侧由recompute_if_synced_lagging负责(sync.rs):只要实时高度差超过--max-sync-height-diff,SyncState就从Synced回落为NotSynced,内部状态退到SufficientPeers或InsufficientPeers(取决于当前连接数)。两条重要细节:
- 块导入本身不代表落后:处理
block_stream时,网络导入的块只更新block_header并标记SyncState::Synced,真正是否离开 Synced 由下一次recompute_if_synced_lagging的高度差检查决定(源码注释:“A network import does not itself mean the node is lagging”); - leader 补账保护:
reconciliation_watermark(一个单调递增的AtomicU32)记录由 leader 通过 reconciliation 补入的水位高度,水位以下的块导入不会触发 Synced → NotSynced 的误翻转(sync.rs); - 连接数抖动保持旧行为:处于 Synced 时,
has_sufficient_peers字段仅随连接数更新,注释明确“Peer-count blips alone do not flip Synced (legacy behaviour preserved)”——即文档所称的/v1/health语义不变:健康状态不再由“是否满足最小保留节点数”单独驱动,而是回到以高度差为核心的定义,对外暴露的SyncState(经watch::Sender<SyncState>共享给查询层)依旧只有NotSynced / Synced两态。
SyncState通过state_sender/state_receiver这对 watch channel 对外发布(RunnableService::shared_data返回接收端),update_sync_state使用send_if_modified避免无意义刷新。
配置组合速查
结合上述实现,几种典型配置对应的行为如下(均以当前仓库源码为准):
--min-connected-reserved-peers | --max-sync-height-diff | --time-until-synced | 实际行为 |
|---|---|---|---|
0 | 任意 | 任意 | 孤立/开发模式:跳过高度差比较;若窗口为 0 则初始即 Synced |
≥1 | 1(默认) | 0s(默认) | 连够保留节点后,只要网络高度 − 本地高度 ≤ 1 立即判 Ready 并即刻 Synced |
≥1 | 调大(如5) | 0s | 允许稍落后(例如正在追赶最后一个块)的节点更快进入 Synced |
≥1 | 任意 | 非零(如30s) | gap 满足后开始一次性计时,持续满足窗口时长才 Synced;期间 gap 再次超标则重新计时 |
运维层面的含义:对被动同步节点,默认配置(阈值 1 + 零窗口)要求本地高度与保留节点网络高度基本齐平才算就绪;如果网络侧心跳延迟较大或希望放宽判定,可以显式调大--max-sync-height-diff;如果希望避免节点在临界高度上反复横跳(Synced ↔ NotSynced 抖动),则配合--time-until-synced引入静默窗口。退出侧则是“实时”的——gap 一旦超过阈值,下一次 10 秒重检或任一事件驱动的检查就会使状态回落。
相关代码索引
- changelog 条目:.changes/changed/3320-sync-height-gap.md
- 核心判定函数与状态机:crates/services/consensus_module/poa/src/sync.rs
- PoA 配置结构体:crates/services/consensus_module/poa/src/config.rs
- CLI 参数声明:bin/fuel-core/src/cli/run.rs
- 同步服务集成与测试入口(可按需深入):crates/services/consensus_module/poa/src/service.rs
【免费下载链接】fuel-coreRust full node implementation of the Fuel v2 protocol.项目地址: https://gitcode.com/GitHub_Trending/fu/fuel-core
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考