Skip to content

DLT P2P 架构

VIZ Ledger 的 P2P 层已从基于 Graphene 摘要的旧协议(node.cpp)重新设计为专用的 DLT 原生协议(dlt_p2p_node.cpp)。公共插件 API 保持不变——只替换了内部实现。


概述

之前:  p2p_plugin → graphene::network::node    (node.cpp, 6978 行, STCP, 库存 gossip)
之后:  p2p_plugin → dlt_p2p_node               (dlt_p2p_node.cpp, 2627 行, 原始 TCP, 范围式同步)

替换就地完成:相同的插件名称 "p2p"、相同端口(2001/4243)、相同的公共 API。所有依赖插件(验证者、snapshot 等)无需任何更改。


线格协议

原始 TCP——无 STCP 加密层。线格上的每条消息:

[4 字节: 数据大小 (uint32_t)] [4 字节: msg_type (uint32_t)] [N 字节: fc::raw::pack(T)]

消息类型(5100–5116)

类型ID描述
dlt_hello_message5100握手:协议版本、head/LIB、DLT 范围、节点/分叉状态
dlt_hello_reply_message5101握手回复:exchange_enabled、fork_alignment
dlt_range_request_message5102请求区块 ID 范围
dlt_range_reply_message5103回复可用区块范围
dlt_get_block_range_message5104获取 start..end 的区块,带 prev_block_id 检查
dlt_block_range_reply_message5105回复:区块向量 + is_last 标志
dlt_get_block_message5106按 ID 获取单个区块
dlt_block_reply_message5107回复:区块 + next_available + is_last
dlt_not_available_message5108区块不可用
dlt_fork_status_message5109向对等节点广播当前分叉/节点状态
dlt_peer_exchange_request5110请求已知对等节点列表
dlt_peer_exchange_reply5111回复对等节点列表
dlt_peer_exchange_rate_limited5112速率限制通知:等待 N 秒
dlt_transaction_message5113广播已签名交易
dlt_soft_ban_message5114断开封禁对等节点前的通知
dlt_gap_fill_request5115请求特定区块号以填补空缺
dlt_gap_fill_reply5116回复请求的区块

Fiber 架构

所有 I/O 在单个 fc::thread 上使用协作式 fiber 运行——共享状态无需互斥锁:

Fiber角色
接受循环等待传入连接;拒绝重复 IP
读取循环(每对等节点)读取消息;分发到 on_message()
周期性任务重新连接、检查停滞、对等节点统计、mempool 清理

Fiber 在阻塞 I/O(readsome()writesome())时让出控制权,允许在单线程上处理多个对等节点而无需争用。


节点状态和对等节点生命周期

节点状态: SYNC(追赶中)/ FORWARD(实时,交换区块)

对等节点生命周期状态:

CONNECTING → HANDSHAKING → SYNCING → ACTIVE → DISCONNECTED → BANNED

超时:connecting=5s,handshaking=10s。重连退避:30s → 60s → … → 3600s,带 ±25% 抖动,稳定运行 5 分钟后重置。8 小时无响应后移除对等节点。


区块同步:SYNC 模式

处于 SYNC 模式的节点从 head 更高的对等节点顺序获取区块:

  1. request_blocks_from_peer() — 发送 dlt_get_block_range_message,请求我们 head 之后最多 200 个区块。
  2. on_dlt_block_range_reply() — 验证 prev_block_id 哈希链,应用每个区块。
  3. check_sync_catchup() — 将我们的 head 与所有对等节点的 head 比较;赶上后切换到 FORWARD。
  4. sync_stagnation_check() — 30s 无新区块后最多重试 3 次,然后切换到 FORWARD 并发出警告。

空缺填补

当我们的 head 与同步对等节点最早可用区块之间存在连续空缺时,request_gap_fill() 向 DLT 范围覆盖该空缺的任意对等节点发送 dlt_gap_fill_request(每次请求最多 100 个区块)。空缺填补在 SYNC 和 FORWARD 模式下均可工作:

  • on_dlt_block_reply()(检测到乱序区块)和 periodic_task()(每 5s)触发。
  • 从启用交换的对等节点回退到任何 head 更高的活跃对等节点。
  • 如果没有对等节点拥有所需区块,回退到 SYNC 模式。
  • 大空缺以 100 个区块为块处理,请求间有 5s 冷却时间。

区块交换:FORWARD 模式

在 FORWARD 模式下,对等节点交换实时区块和交易:

  • exchange_enabled 标志控制对等节点是否从我们这里接收新区块。
  • 切换到 FORWARD 时,dlt_fork_status_message 发送到所有对等节点(不仅仅是启用交换的),通知它们我们已就绪。
  • on_dlt_fork_status() 在对等节点从 SYNC 切换到 FORWARD 时重新评估 exchange_enabled
  • check_forward_stagnation() — 如果 head 30s 未推进且至少一个对等节点领先,切换到 SYNC。

分叉对齐和交换资格

在握手期间,check_fork_alignment() 执行多层区块 ID 匹配以确定对等节点是否在同一分叉:

检查条件
空对等节点head_block_num == 0 → 已对齐(新节点)
范围重叠我们的 DLT 日志覆盖对等节点的 head → is_block_known(head_id)
边界链接peer_head + 1 == our_earliest → 检查我们最早区块的 previous == peer_head_id
LIB 回退始终检查 is_block_known(lib_id)

这种多层检查防止了 DLT 模式下的误"不同分叉"断连,在 DLT 模式中旧区块已被修剪,旧的单 ID 检查对同一链上的对等节点会失败。


分叉解决

分叉解决子系统跟踪竞争链的顶端:

  • 阈值: 42 个区块的分歧触发 resolve_fork()(= CHAIN_MAX_VALIDATORS × 2,一个完整的调度轮次)。
  • 选择: 按投票权重最重的分支。
  • 迟滞: 切换前需要连续 6 个区块作为赢家(CONFIRMATION_BLOCKS)。
  • 状态: _fork_status 通过 is_on_majority_fork() 暴露,供验证者插件在生产区块前检查。

反垃圾

机制描述
spam_strikes 计数器每个对等节点一个计数器;好数据包时重置;阈值=10 时软封禁
软封禁设置 BANNED 状态持续 3600s;关闭前发送 dlt_soft_ban_message
按 IP 去重拒绝来自同一 IP 的重复连接(入站和出站均适用)
广播去重send_to_all_our_fork_peers() 跟踪 std::set<ip::address> 跳过重复 IP

来自范围回复的重复区块和乱序区块会被静默跳过——不计为垃圾。反序列化错误不增加 spam strikes 计数。


P2P Mempool

独立的进程内 mempool(区别于链的 _pending_tx),在链接受前提供早期交易过滤:

  • tx_id 去重
  • 达到限制时按最旧到期驱逐
  • 限制(可配置):最多 10,000 条、总计 100 MB、每笔交易 64 KB。
  • 临时条目在 SYNC 模式下标记;切换到 FORWARD 时重新验证。
  • 收到区块时(remove_transactions_in_block)和分叉切换时(prune_mempool_on_fork_switch清理

对等节点交换

速率受限的对等节点发现:

  • 每个对等节点每 5 分钟窗口最多 3 次请求。
  • 子网多样性过滤器:每个回复中每个 /24 前缀最多 2 个对等节点。
  • 只共享运行时间 ≥600s 的对等节点。
  • 入站对等节点(临时端口)从交换回复中排除。

恢复机制

对等节点隔离(P53)

当 60 秒内没有活跃对等节点时,emergency_peer_reset()

  • 清除所有软封禁(BANNED → DISCONNECTED,重置 spam strikes)。
  • 将所有断开连接的对等节点退避重置为最小值,立即重连。

区块处理暂停/恢复

pause_block_processing() / resume_block_processing() 允许 snapshot 插件在状态序列化期间暂停 P2P 区块接收。周期性任务在暂停时跳过访问数据库的操作。

启动宽限期(P22)

启动后前 60 秒内,head 10 个区块以内的区块被视为 FORK_DB_ONLY 而非 DEAD_FORK——防止 fork_db 从区块日志重建时的级联拒绝。


区块接受结果

dlt_block_accept_result 枚举替代旧的布尔返回:

含义
ACCEPTED区块已应用到链(成为新 head)
FORK_DB_ONLY存储在 fork_db 但未应用(无法链接、竞争分叉)
DEAD_FORK死分叉中处于/低于 head 的区块——对等节点被软封禁
ALREADY_KNOWN已有此区块(重复、block_too_old_exception
REJECTED验证完全失败

配置参考

选项默认值描述
dlt-block-log-max-blocks100,000DLT 滚动区块日志中的最大区块数
dlt-peer-max-disconnect-hours8无响应后多少小时移除对等节点
dlt-mempool-max-tx10,000Mempool 条目硬限制
dlt-mempool-max-bytes100 MBMempool 总内存硬限制
dlt-mempool-max-tx-size64 KB拒绝超大交易
dlt-mempool-max-expiration-hours24拒绝远期到期交易
dlt-peer-exchange-max-per-reply10每次交换回复的最大对等节点数
dlt-peer-exchange-max-per-subnet2反 Sybil:每 /24 最多 2 个对等节点
dlt-peer-exchange-min-uptime-sec600共享对等节点前的最小运行时间
dlt-stats-interval-sec300对等节点统计日志间隔(最小 30)

彩色日志

颜色含义
绿色同步进度和区块生产
白色正常区块交换
红色分叉事件
深灰色交易处理
橙色警告(软封禁、停滞、空缺)
青色对等节点统计输出

委托模式

网络库只链接 fcgraphene_protocol——不链接 graphene_chaindlt_p2p_delegate 抽象接口弥补了这一差距:

dlt_p2p_node(网络库)  ←→  dlt_p2p_delegate(接口)  ←→  dlt_delegate(p2p_plugin)

p2p_plugin.cpp 中的 dlt_delegate 实现:

  • read_block_by_num() — 检查 dlt_block_log,然后检查 fork_db。
  • accept_block() — 调用 push_block();捕获 unlinkable_block_exception → 存储到 fork_db。
  • get_fork_branch_tips() — 从当前 head 周围的 fork_db 获取。
  • is_tapos_block_known() — 委托给 db.is_known_block()

参见:P2P 概述同步场景Snapshot 插件区块日志