Skip to content

Cishoon/mini-solana

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

78 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Mini-Solana

基于历史证明(PoH)共识的区块链系统实现

项目背景

Mini-Solana 是一个学习性质的 Solana 区块链简化实现,旨在帮助开发者深入理解 Solana 的核心架构和设计理念。

为什么创建这个项目?

  • 学习 Solana 独特的 PoH (Proof of History) 和 Tower BFT 共识机制
  • 理解高性能区块链的流水线架构设计
  • 掌握 Rust 在分布式系统中的应用实践

实现范围: 本项目实现了 Solana 的核心子系统,包括 PoH 时钟、交易处理流水线、多节点共识、Gossip 网络发现等,但简化了分片(Shred)、Turbine 传播树等生产级优化

快速开始

编译项目

# 克隆仓库
git clone https://github.com/Cishoon/mini-solana
cd mini-solana

# 编译所有 crate
cargo build

启动验证器

cd mini-solana/validator
./init.sh
./create-genesis.sh

./run.sh 1
# 支持多验证节点,可开启另一个窗口运行 ./run.sh 2  ./run.sh 3

客户端使用

打开新终端,使用客户端与验证器交互:

# 生成新钱包
cargo run -p mini-solana-client -- keygen -a alice

# 查看公钥
cargo run -p mini-solana-client -- pubkey -a alice

# 查询余额
cargo run -p mini-solana-client -- balance -a alice

# 从 faucet 转账给 alice(需要先获取 alice 的公钥)
cargo run -p mini-solana-client -- transfer -a validator <alice_pubkey> 100000

核心特性

共识与时钟

  • PoH (Proof of History) - 可验证延迟函数实现全局时钟,通过连续 SHA-256 哈希生成不可伪造的时间链
  • Tower BFT 投票机制 - 基于 lockout 的分叉选择,防止验证者快速切换分叉
  • ForkChoice 分叉权重计算 - 基于 stake 权重选择最佳分叉

交易处理

  • TPU (Transaction Processing Unit) - 交易处理流水线,支持 Leader/Forwarding 双模式
  • Bank 账户状态管理 - 原子执行交易并维护账户余额
  • BankForks 分叉管理 - 多分叉状态隔离,支持并行处理

区块生产与传播

  • PohRecorder - 将交易批量混入 PoH 哈希链,生成 Entry
  • BroadcastStage - Entry 签名与网络广播
  • Leader 调度 - 基于 stake 的轮换机制,每 4 个 slot 轮换一次

区块验证与重放

  • TVU (Transaction Validation Unit) - 区块验证流水线
  • VerifyStage - 验证 Entry 的 Leader 签名是否与 Leader Schedule 匹配
  • RetransmitStage - Entry 转发与 Blockstore 存储
  • ReplayStage - PoH 验证与交易重放,更新 BankForks

网络与发现

  • Gossip 协议 - 节点发现与状态同步
  • CRDS (Cluster Replicated Data Store) - 集群数据存储,维护 ContactInfo、EpochSlots、Vote
  • EpochSlots - 节点 slot 进度广播
  • Repair Service - 缺失 slot 修复,保证数据完整性

存储

  • Blockstore - 按 slot 的 Entry 持久化存储
  • SignedEntry - 带 Leader 签名的 Entry 存储格式
  • 账本重放 - 启动时从创世块重建状态

接口

  • JSON-RPC 服务 - 提供 getBalance、sendTransaction、getRecentBlockhash 等接口
  • CLI 客户端 - 支持 keygen、balance、transfer 命令

系统架构

架构概述

Mini-Solana 采用 Cargo workspace 组织,包含三个 crate:

Crate 职责
mini-solana-sdk 核心类型定义:Pubkey、Keypair、Transaction、Entry、Hash 等基础数据结构
validator 验证器节点:PoH 生成、交易处理、区块验证、Gossip 网络、账本存储
client CLI 客户端:密钥管理、余额查询、转账操作

Validator 内部采用模块化设计,主要分为以下几个子系统:

  • Core - 核心逻辑:Bank 状态管理、PoH 时钟、TPU/TVU 流水线、Tower 共识
  • Gossip - 网络层:节点发现、元数据同步、投票传播
  • Ledger - 存储层:Blockstore 持久化、Slot 元数据管理
  • RPC - 接口层:JSON-RPC 服务,供客户端调用

架构图

graph TB
    subgraph Client["Client (CLI)"]
        CLI[CLI Commands]
        RpcClient[RPC Client]
    end

    subgraph SDK["mini-solana-sdk"]
        Types[Core Types]
        Crypto[Crypto Primitives]
    end

    subgraph Validator["Validator"]
        subgraph TPU["TPU (Leader Mode)"]
            TpuReceiver[TPU Receiver]
            PohRecorder[PoH Recorder]
            PohService[PoH Service]
            BroadcastStage[Broadcast Stage]
        end

        subgraph TVU["TVU (Follower Mode)"]
            TvuReceiver[TVU Receiver]
            VerifyStage[Verify Stage]
            RetransmitStage[Retransmit Stage]
            ReplayStage[Replay Stage]
        end

        subgraph Core["Core"]
            Bank[Bank]
            BankForks[BankForks]
            Tower[Tower BFT]
            ForkChoice[Fork Choice]
            LeaderSchedule[Leader Schedule]
        end

        subgraph Network["Network"]
            Gossip[Gossip Service]
            CRDS[CRDS]
            Repair[Repair Service]
        end

        subgraph Storage["Storage"]
            Blockstore[Blockstore]
        end

        RPC[JSON-RPC Server]
    end

    CLI --> RpcClient
    RpcClient --> RPC
    RPC --> TpuReceiver
    TpuReceiver --> PohRecorder
    PohRecorder --> PohService
    PohService --> BroadcastStage
    BroadcastStage --> Blockstore
    BroadcastStage --> TvuReceiver

    TvuReceiver --> VerifyStage
    VerifyStage --> RetransmitStage
    RetransmitStage --> Blockstore
    RetransmitStage --> ReplayStage
    ReplayStage --> BankForks

    PohRecorder --> Bank
    ReplayStage --> Tower
    Tower --> ForkChoice
    ForkChoice --> BankForks

    Gossip --> CRDS
    Repair --> Blockstore
    Repair --> CRDS

    SDK --> Validator
    SDK --> Client
Loading

技术实现要点

PoH (Proof of History) 原理

传统区块链的时间问题

在传统区块链(如比特币、以太坊)中,节点之间没有统一的时钟。当两笔交易几乎同时发生时,不同节点可能看到不同的顺序。为了解决这个问题,传统方案需要:

  • 比特币:等待 10 分钟出一个块,用"最长链"来确定顺序
  • 以太坊:等待 12 秒出一个块,通过多轮通信达成共识

这些方案的共同问题是:需要等待网络通信来确认时间顺序,速度慢

Solana 的解决方案:PoH

PoH (Proof of History) 的核心思想是:用计算来证明时间的流逝

想象一下,如果我让你连续做 100 万道数学题,每道题的答案都依赖上一道题的结果:

题目1: 计算 SHA256("起始值")           → 答案1
题目2: 计算 SHA256(答案1)              → 答案2
题目3: 计算 SHA256(答案2)              → 答案3
...
题目100万: 计算 SHA256(答案999999)     → 答案100万

由于每道题必须等上一道题算完才能开始(SHA-256 无法并行),所以:

  • 如果你给我看答案100万,我就知道你确实花了时间算完了前面所有题
  • 我可以快速验证:验证阶段可以并行
  • 任何人都无法伪造:你不可能跳过中间步骤直接得到答案100万

这就是 PoH 的本质:一条用 CPU 算力"烧"出来的时间链

三种操作模式

PoH 引擎维护一个不断增长的哈希链,支持三种操作:

┌─────────────────────────────────────────────────────────────────────────────┐
│                              PoH 哈希链示意图                                 │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   [Hash]     [Hash]     [Hash]     [Record]    [Hash]     [Tick]           │
│     │          │          │           │          │          │              │
│     ▼          ▼          ▼           ▼          ▼          ▼              │
│   ┌───┐      ┌───┐      ┌───┐      ┌───┐      ┌───┐      ┌───┐            │
│   │H1 │ ───► │H2 │ ───► │H3 │ ───► │H4 │ ───► │H5 │ ───► │H6 │            │
│   └───┘      └───┘      └───┘      └─┬─┘      └───┘      └───┘            │
│                                      │                      │              │
│                                      ▼                      ▼              │
│                                   交易混入               时间检查点          │
│                                  (Entry)                (Tick Entry)       │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
模式 公式 说明 产出
Hash H_next = SHA256(H_current) 纯粹的时间流逝,像时钟滴答
Record H_next = SHA256(H_current + 交易哈希) 把交易"盖章"到时间链上 Entry(含交易)
Tick H_next = SHA256(H_current) 每隔固定次数产生一个时间检查点 Tick Entry(空)

Record 的时间戳效应

当交易被 Record 进哈希链时,会产生"雪崩效应":

假设原本的哈希链是:
  H1 → H2 → H3 → H4 → H5

在 H2 之后插入交易 Tx:
  H1 → H2 → H2' → H3' → H4' → H5'
            ↑
         Tx 混入

H2' = SHA256(H2 + Tx),之后所有哈希都变了!

这意味着:

  • 交易 Tx 被永久锚定在 H2 和 H3' 之间
  • 任何人都可以验证 Tx 确实发生在这个时间点
  • 无法篡改:如果有人想把 Tx 移到别的位置,整条链都会对不上

为什么不用 sleep?

你可能会问:为什么要浪费 CPU 算哈希?直接 sleep(1秒) 不行吗?

答案是:sleep 可以作弊

如果用 sleep,恶意节点可以:

  1. 关掉 sleep,全速运算
  2. 瞬间生成未来 10 年的所有区块
  3. 网络无法分辨哪个是"真历史"

而 PoH 的哈希链无法加速——你必须一个一个算,这就是"Proof of History"名字的由来:哈希链本身就是时间流逝的证明

代码实现

// 简化的 PoH 工作流程
loop {
    // 1. 推进时间(批量哈希提高效率)
    let need_tick = poh.hash(100);  // 连续算 100 次哈希
    
    if need_tick {
        // 到达 Tick 边界,必须先产生时间检查点
        let tick_entry = poh.tick();
        broadcast(tick_entry);
    }
    
    // 2. 有交易时,把交易"盖章"到时间链上
    if let Some(tx) = recv_transaction() {
        let entry = poh.record(tx.hash());
        broadcast(entry);
    }
}

与传统区块链的对比

特性 比特币 以太坊 Solana (PoH)
出块时间 ~10 分钟 ~12 秒 ~400 毫秒
时间证明 工作量证明 (PoW) 权益证明 (PoS) 历史证明 (PoH)
确定顺序 等待多个确认 等待最终性 哈希链即顺序
吞吐量 ~7 TPS ~30 TPS ~65,000 TPS

PoH 让 Solana 能够在不等待网络共识的情况下确定交易顺序,这是其高性能的关键。

交易处理流程 (TPU)

整体流水线

TPU (Transaction Processing Unit) 是交易处理的核心流水线,负责接收、执行、打包和广播交易:

TPU Block Diagram

各阶段职责

阶段 职责 输入 输出
RPC Server 接收客户端请求,解码交易 Base58 编码的交易 Transaction 对象
PohRecorder 将交易混入 PoH 哈希链,生成 Entry Transaction Entry
Bank 原子执行交易,更新账户状态 Transaction 状态变更
BroadcastStage 签名 Entry,写入 Blockstore,广播给其他节点 Entry SignedEntry

交易执行流程

当一笔交易到达 RPC Server 后,经历以下步骤:

// 1. RPC 接收并解码交易
let tx: Transaction = bincode::deserialize(&tx_bytes)?;

// 2. 检查是否是 Leader
if poh_recorder.has_bank() {
    // 是 Leader:直接处理
    tx_recorder.record_transactions(vec![tx])?;
} else {
    // 不是 Leader:转发给当前 Leader
    forward_transaction(&tx_bytes)?;
}

// 3. PohRecorder 处理交易
//    a) 计算交易哈希
let tx_hash = hash_transaction(&transactions);
//    b) 混入 PoH 哈希链
let poh_entry = poh.record(tx_hash);
//    c) 构造 Entry
let entry = Entry { num_hashes, hash, transactions };
//    d) Bank 执行交易
bank.process_entry(&entry)?;
//    e) 发送 Entry 给 BroadcastStage
entry_sender.send(entry)?;

// 4. BroadcastStage 处理 Entry
//    a) 签名 Entry
let signed_entry = SignedEntry::new(slot, entry, keypair);
//    b) 写入 Blockstore
blockstore.write_signed_entry(&signed_entry)?;
//    c) 广播给其他验证者
broadcast_to_peers(&signed_entry)?;

Bank 交易执行

Bank 是账户状态管理器,负责原子执行交易:

pub fn process_transaction(&mut self, tx: &Transaction) -> Result<(), String> {
    // 1. 验证 recent_blockhash(防止重放攻击)
    if !self.is_blockhash_valid(&tx.message.recent_blockhash) {
        return Err("Invalid recent blockhash");
    }

    // 2. 验证签名
    if !tx.verify_signatures() {
        return Err("Invalid signature");
    }

    // 3. 克隆账户状态(用于原子性回滚)
    let mut accounts_snapshot = self.accounts.clone();

    // 4. 执行所有指令
    for ix in &tx.message.instructions {
        process_instruction(&mut accounts_snapshot, ix)?;
    }

    // 5. 全部成功,提交状态
    self.accounts = accounts_snapshot;
    Ok(())
}

关键设计:

  • 原子性:先在副本上执行所有指令,全部成功才提交
  • recent_blockhash:每个 slot 结束时注册新的 blockhash,旧的会过期(最多保留 150 个)
  • 签名验证:确保交易由正确的私钥签名

TPU 双模式:Producing vs Forwarding

TPU 根据当前节点是否是 Leader,自动切换两种模式:

Producing 模式(当前节点是 Leader):

  • poh_recorder.has_bank() 返回 true
  • 交易直接由本地 PohRecorder 处理
  • Bank 执行交易并更新状态
  • BroadcastStage 签名并广播 Entry

Forwarding 模式(当前节点不是 Leader):

  • poh_recorder.has_bank() 返回 false
  • 从 LeaderSchedule 查找当前 Leader
  • 从 CRDS 获取 Leader 的 TPU 地址
  • 通过 UDP 转发交易给 Leader
// RPC 中的模式判断
async fn send_transaction(&self, tx_base58: String) -> Result<String> {
    let tx = decode_transaction(&tx_base58)?;
    
    if self.poh_recorder.has_bank() {
        // Producing 模式:本地处理
        self.tx_recorder.record_transactions(vec![tx])?;
    } else {
        // Forwarding 模式:转发给 Leader
        let leader = self.leader_schedule.leader_at(current_slot);
        let leader_tpu = self.crds.get_contact_info(&leader)?.tpu;
        udp_socket.send_to(&tx_bytes, leader_tpu)?;
    }
    
    Ok(signature)
}

TPU Receiver

Leader 节点通过 TPU Receiver 接收其他节点转发的交易:

Non-Leader RPC ──UDP──► Leader TPU Receiver ──► PohRecorder

TPU Receiver 监听 UDP 端口,反序列化收到的交易,提交给 PohRecorder 处理。

区块验证流水线 (TVU)

整体架构

TVU (Transaction Validation Unit) 是非 Leader 节点接收、验证和重放区块的核心流水线。当其他验证者生产区块时,TVU 负责:

tvu

  1. 接收 Leader 广播的 Entry
  2. 验证 Leader 签名
  3. 转发给其他节点
  4. 重放交易更新本地状态

各阶段职责

阶段 职责 关键操作
TVU Receiver 接收网络数据包 监听 UDP 端口,反序列化 TvuPacket
Verify Stage 验证 Leader 签名 检查签名者是否是该 slot 的合法 Leader
Retransmit Stage 转发与存储 转发给其他节点,写入 Blockstore
Replay Stage 重放交易 验证 PoH 哈希链,执行交易,更新 BankForks

PoH 快速验证

项目使用 Rust 的 rayon 库实现并行验证。核心思想是:每个 Entry 的验证只依赖它的前一个 Entry 的哈希,而这些哈希在验证前就已经知道了(存储在 Entry 中)。因此可以预先构建 prev_hashes 数组,然后并行验证所有 Entry:

use rayon::prelude::*;

/// 验证 Entry 列表的哈希链完整性
/// 使用 rayon 并行实现,提高验证效率
pub fn verify_entries(entries: &[Entry], starting_hash: &Hash) -> bool {
    if entries.is_empty() {
        return true;
    }

    // 构建每个 Entry 的前置哈希数组
    // [starting_hash, entry[0].hash, entry[1].hash, ..., entry[n-2].hash]
    let prev_hashes: Vec<Hash> = std::iter::once(*starting_hash)
        .chain(entries.iter().map(|e| e.hash))
        .take(entries.len())  // 只取前 n 个
        .collect();

    // 使用 rayon 并行验证每个 Entry
    // par_iter() 自动将工作分配到多个线程
    prev_hashes
        .par_iter()
        .zip(entries.par_iter())
        .all(|(prev_hash, entry)| entry.verify(prev_hash))
}

为什么可以并行?

Entry[0]: verify(starting_hash) → entry[0].hash ✓
Entry[1]: verify(entry[0].hash) → entry[1].hash ✓   ← 这些验证互不依赖!
Entry[2]: verify(entry[1].hash) → entry[2].hash ✓
Entry[3]: verify(entry[2].hash) → entry[3].hash ✓

每个 entry.verify(prev_hash) 只需要:

  1. 输入:prev_hash(已知,存储在前一个 Entry 中)
  2. 计算:根据 num_hashestransactions 计算期望哈希
  3. 比较:期望哈希 == entry.hash

这三步完全独立,不依赖其他 Entry 的验证结果,因此可以完美并行。

Retransmit 转发机制

retransmit

Retransmit Stage 负责将收到的 Entry 转发给其他节点,形成类似"洪泛"的传播模式:

Leader ──► Node A ──► Node C
              │
              └──► Node D
       │
       └──► Node B ──► Node E

转发规则:

  • 不回传:不发送给数据来源节点
  • 不自发:不发送给自己
  • 过滤旧 slot:超过阈值的旧 slot 不转发(但仍写入 Blockstore)
  • Repair 包不转发:修复请求的响应包只存储,不转发
// 转发过滤逻辑
fn should_retransmit(packet: &TvuPacket, from: &Pubkey, my_pubkey: &Pubkey) -> bool {
    // 不回传给来源
    if packet.header.source == *from { return false; }
    // 不发给自己
    if *from == *my_pubkey { return false; }
    // Repair 包不转发
    if packet.header.packet_type == PacketType::Repair { return false; }
    true
}

Replay 重放流程

Replay Stage 是 TVU 的核心,负责将收到的 Entry 重放到本地状态:

// Replay 一个完整的 slot
pub fn replay_slot(slot: Slot, blockstore: &Blockstore, bank_forks: &BankForks) -> Result<()> {
    // 1. 读取 slot 的所有 Entry
    let entries = blockstore.get_slot_entries(slot)?;
    
    // 2. 获取父 slot 的最后哈希
    let parent_hash = get_parent_last_hash(slot, blockstore)?;
    
    // 3. 验证 PoH 哈希链
    if !verify_entries(&entries, &parent_hash) {
        return Err("PoH verification failed");
    }
    
    // 4. 从父 Bank 创建新 Bank
    let parent_slot = get_parent_slot(slot)?;
    let child_bank = bank_forks.new_from_parent(parent_slot, slot)?;
    
    // 5. 执行所有交易
    for entry in &entries {
        if !entry.is_tick() {
            child_bank.process_entry(entry)?;
        }
    }
    
    // 6. 冻结 Bank,注册 blockhash
    child_bank.freeze();
    
    Ok(())
}

Slot 可重放条件

  • is_full = true:所有 Entry 都已收到
  • 父 slot 已在 BankForks 中
  • 当前 slot 尚未在 BankForks 中

角色转换

Replay Stage 还负责检测角色转换。当重放完 slot N 后,检查自己是否是 slot N+1 的 Leader:

// 重放完成后检查角色转换
fn check_role_transition(completed_slot: Slot) {
    let next_slot = completed_slot + 1;
    
    // 检查是否是下一个 slot 的 Leader
    if leader_schedule.is_leader_at(next_slot, &my_pubkey) {
        // 创建新 Bank
        let new_bank = bank_forks.new_from_parent(completed_slot, next_slot)?;
        
        // 获取父 slot 的最后哈希作为 PoH 起点
        let last_hash = get_last_hash(completed_slot)?;
        
        // 切换到 TPU 模式
        poh_recorder.reset_to_slot(next_slot, new_bank, last_hash);
        
        log::info!("Role transition: becoming leader for slot {}", next_slot);
    }
}

Repair 修复机制

为什么需要 Repair?

在分布式网络中,数据包可能丢失或延迟。当节点发现某些 slot 不完整时,需要主动向其他节点请求缺失的数据。

Repair Service 架构

Repair Service 包含两个线程:

┌─────────────────────────────────────────────────────────────────────────────┐
│                           Repair Service                                    │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   ┌─────────────────────────────────────┐                                   │
│   │        Listener Thread              │                                   │
│   │  - 监听 repair_port (UDP)           │                                   │
│   │  - 接收 RepairRequest               │                                   │
│   │  - 检查 slot 完整性                  │                                   │
│   │  - 发送 SignedEntry 给请求者         │                                   │
│   └─────────────────────────────────────┘                                   │
│                                                                             │
│   ┌─────────────────────────────────────┐                                   │
│   │        Requester Thread             │                                   │
│   │  - 定期扫描缺失的 slot              │                                   │
│   │  - 从 CRDS 查找拥有该 slot 的节点    │                                   │
│   │  - 发送 RepairRequest 给邻居         │                                   │
│   └─────────────────────────────────────┘                                   │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

缺失 Slot 发现

Requester 线程定期扫描 Blockstore,找出需要修复的 slot:

// 扫描需要修复的 slot
pub fn get_need_repair_slots(&self, highest_known_slot: Slot) -> Vec<Slot> {
    let mut need_repair = Vec::new();
    
    // 遍历从 root 到 highest_known_slot 的所有 slot
    for slot in self.root_slot..=highest_known_slot {
        // 跳过已完整的 slot
        if self.is_full(slot) { continue; }
        
        // 跳过完全没有数据的 slot(可能还没开始)
        if !self.has_any_data(slot) { continue; }
        
        // 这个 slot 有部分数据但不完整,需要修复
        need_repair.push(slot);
    }
    
    need_repair
}

智能选择修复节点

不是随机选择节点,而是从 CRDS 的 EpochSlots 中找到确实拥有该 slot 的节点:

// 从 CRDS 获取拥有指定 slot 的节点
pub fn get_peers_with_slot(&self, slot: Slot, exclude: &Pubkey) -> Vec<ContactInfo> {
    let mut peers = Vec::new();
    
    for epoch_slots in self.epoch_slots.values() {
        // 检查该节点是否有这个 slot
        if epoch_slots.has_slot(slot) {
            // 排除自己
            if epoch_slots.from != *exclude {
                if let Some(contact) = self.get_contact_info(&epoch_slots.from) {
                    peers.push(contact.clone());
                }
            }
        }
    }
    
    peers
}

保留原始签名

Repair 响应时,直接转发原始的 SignedEntry,不重新签名

// 处理 Repair 请求
fn handle_repair_request(request: &RepairRequest, blockstore: &Blockstore) {
    // 获取原始的 SignedEntry(保留 Leader 签名)
    let signed_entries = blockstore.get_slot_signed_entries(request.slot)?;
    
    for signed_entry in signed_entries {
        // 直接转发,不重新签名!
        // 这样接收方可以验证原始 Leader 的签名
        let packet = TvuPacket::repair(my_pubkey, signed_entry);
        socket.send_to(&packet.to_bytes()?, request.tvu_addr)?;
    }
}

这样设计的好处:

  • 接收方可以用 Leader Schedule 验证签名
  • 防止恶意节点伪造数据
  • 保持数据的可追溯性

Gossip 协议

为什么需要 Gossip?

在分布式区块链网络中,节点需要解决几个关键问题:

  • 节点发现:新节点如何找到其他节点?
  • 状态同步:节点如何知道其他节点的状态(如拥有哪些 slot)?
  • 投票传播:验证者的投票如何快速传播给其他节点?

Gossip 协议通过"八卦"式的点对点通信解决这些问题:每个节点定期向邻居广播自己的信息,邻居再传播给它们的邻居,最终信息扩散到整个网络。

CRDS (Cluster Replicated Data Store)

CRDS 是 Gossip 协议的核心数据结构,存储集群中所有已知节点的元数据:

┌─────────────────────────────────────────────────────────────────────────────┐
│                              CRDS 数据结构                                   │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   ┌─────────────────────────────────────────────────────────────────────┐   │
│   │  CrdsKey = (Pubkey, Label)                                          │   │
│   │                                                                     │   │
│   │  Label 类型:                                                        │   │
│   │    - ContactInfo: 节点网络地址                                       │   │
│   │    - EpochSlots:  节点已处理的 slot                                  │   │
│   │    - Vote:        验证者投票                                         │   │
│   └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
│   ┌─────────────────────────────────────────────────────────────────────┐   │
│   │  Table: HashMap<CrdsKey, CrdsValue>                                 │   │
│   │                                                                     │   │
│   │  (NodeA, ContactInfo) → { gossip: 1.2.3.4:8000, tpu: ..., ... }    │   │
│   │  (NodeA, EpochSlots)  → { slots: [0,1,2,5,6], wallclock: ... }     │   │
│   │  (NodeA, Vote)        → { slots: [5], hash: ..., wallclock: ... }  │   │
│   │  (NodeB, ContactInfo) → { gossip: 5.6.7.8:8000, tpu: ..., ... }    │   │
│   │  ...                                                                │   │
│   └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

关键设计

  • 复合键(Pubkey, Label) 允许每个节点存储多种类型的数据
  • Wallclock 时间戳:只接受比现有数据更新的信息,防止旧数据覆盖新数据
  • 自动过期:定期清理超时未更新的节点信息

三种数据类型

类型 用途 更新频率
ContactInfo 节点网络地址(gossip/tpu/tvu/repair 端口) 每 15 秒
EpochSlots 节点已处理的 slot 列表,用于 Repair 发现 每 2 秒
Vote 验证者投票,用于 ForkChoice 权重计算 投票时立即广播

Gossip 消息类型

enum GossipMessage {
    // 心跳检测
    Ping { from: Pubkey, timestamp: u64 },
    Pong { from: Pubkey, ping_timestamp: u64 },
    
    // Push 模型:主动推送自己的更新
    PushContactInfo(ContactInfo),  // 广播节点地址
    PushEpochSlots(EpochSlots),    // 广播已处理的 slot
    PushVote(Vote),                // 广播投票
    
    // Pull 模型:请求其他节点的信息
    PullRequest { from: Pubkey },
    PullResponse { contacts: Vec<ContactInfo>, epoch_slots: Vec<EpochSlots> },
}

GossipService 工作流程

┌─────────────────────────────────────────────────────────────────────────────┐
│                           GossipService                                     │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   ┌─────────────────────────────────────────────────────────────────────┐   │
│   │                        主循环 (tokio::select!)                       │   │
│   │                                                                     │   │
│   │   1. 接收 UDP 消息 ──► 更新 CRDS                                     │   │
│   │   2. 每 15s ──► 广播 ContactInfo                                    │   │
│   │   3. 每 2s  ──► 广播 EpochSlots                                     │   │
│   │   4. 每 15s ──► 清理过期节点                                         │   │
│   │   5. 投票时 ──► 立即广播 Vote                                        │   │
│   └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

节点发现流程

新节点加入网络时:

1. 新节点启动,配置 entrypoint(入口节点地址)
2. 向 entrypoint 发送 PullRequest
3. entrypoint 返回 PullResponse(所有已知节点信息)
4. 新节点将收到的信息存入 CRDS
5. 开始定期广播自己的 ContactInfo
6. 其他节点收到后,也将新节点加入 CRDS
// 连接入口节点
pub fn connect_to_entrypoints(&self) {
    let msg = GossipMessage::PullRequest { from: self.my_pubkey };
    
    for entrypoint in &self.config.entrypoints {
        self.send_to(&msg, entrypoint)?;
        log::info!("Sent PullRequest to entrypoint {}", entrypoint);
    }
}

EpochSlots 与 Repair 的配合

EpochSlots 是 Repair 机制的关键:

1. 节点 A 完成 slot 5 的重放
2. 节点 A 更新本地 EpochSlots,添加 slot 5
3. 节点 A 通过 Gossip 广播 EpochSlots
4. 节点 B 收到后更新 CRDS
5. 节点 C 发现缺失 slot 5
6. 节点 C 查询 CRDS,发现节点 A 和 B 有 slot 5
7. 节点 C 向节点 A 发送 RepairRequest

CompressedSlots 压缩存储

EpochSlots 使用位图压缩存储 slot 列表,每个 bit 代表一个 slot:

slots: [0, 1, 2, 5, 6, 10]

位图表示 (first_slot=0):
  byte[0] = 0b01100111  // slots 0,1,2,5,6
  byte[1] = 0b00000100  // slot 10

空间效率:65536 个 slot 只需 8KB

投票传播

验证者投票后,通过 Gossip 快速传播给其他节点:

// Tower 投票后调用
pub fn broadcast_vote(&self, vote: Vote) {
    // 1. 存入本地 CRDS
    self.crds.write().unwrap().insert_vote(vote.clone());
    
    // 2. 广播给所有已知节点
    let msg = GossipMessage::PushVote(vote);
    for peer in self.crds.read().unwrap().get_peers(&self.my_pubkey) {
        self.send_to(&msg, &peer.gossip)?;
    }
}

投票传播的重要性:

  • ForkChoice 需要知道其他验证者的投票来计算分叉权重
  • 快速传播投票可以加速共识收敛
  • 投票信息比 Entry 小得多,传播更快

Leader 调度

在 Solana 官方实现中,Leader 调度是一个复杂的系统:

  • 基于 stake 权重的随机选择
  • 每个 epoch(约 2 天)重新计算调度表
  • 使用 VRF(可验证随机函数)确保公平性

Mini-Solana 采用简化方案:Leader 列表直接写在创世配置中,按固定顺序轮换。

Leader Index = (slot / 4) % leaders.len()
  • 每 4 个 slot 轮换一次 Leader
  • 轮换顺序由创世配置中的 leaders 数组决定
  • 所有节点使用相同的创世配置,因此调度结果一致

Leader 列表在创世配置 (genesis.json) 中定义:

{
  "creation_time": 1234567890,
  "accounts": { ... },
  "leaders": [
    "Validator1Pubkey...",
    "Validator2Pubkey...",
    "Validator3Pubkey..."
  ],
  "stakes": { ... }
}

Tower BFT 共识与分叉选择

为什么需要 Tower BFT?

在分布式区块链网络中,由于网络延迟和分区,不同节点可能看到不同的区块顺序,导致分叉。Tower BFT 是 Solana 的共识机制,解决两个核心问题:

  1. 分叉选择:当存在多个分叉时,选择哪个作为"正确"的链?
  2. 防止快速切换:如何防止验证者在分叉之间频繁切换(可能导致双花攻击)?

核心概念

BankForks:管理多个分叉状态

  • 每个 slot 对应一个 Bank(账户状态快照)
  • 分叉形成树状结构,root 是已确认不会回滚的 slot
  • 支持从任意父 slot 创建子 slot

ForkChoice:基于 stake 权重选择最佳分叉

  • 记录每个验证者的投票
  • 计算每个分叉的累计权重(包含所有子孙的投票)
  • 选择权重最高的分叉作为"最佳分叉"

Tower:本地投票状态管理

  • 维护投票栈(最多 32 个投票)
  • 实现 Lockout 机制,防止快速切换分叉
  • 决定何时可以投票、何时被锁定

Lockout 机制

Lockout 是 Tower BFT 的核心安全机制。每次投票都会产生一个"锁定期",在锁定期内不能切换到其他分叉。

锁定期计算公式

lockout_duration = 2^confirmation_count slots
  • confirmation_count:该投票被后续投票确认的次数
  • 每次在同一分叉上投票,之前所有投票的 confirmation_count 都 +1
  • 锁定期呈指数增长,使得切换分叉的代价越来越高

可视化示例(运行 cargo test test_tower_lockout_visualization -- --nocapture):

╔══════════════════════════════════════════════════════════════════════════════════════════╗
║                                    🔒 Tower BFT Lockout Mechanism Demo                   ║
║                                    Showing how lockout prevents rapid fork switching     ║
╚══════════════════════════════════════════════════════════════════════════════════════════╝

📋 Lockout Formula: duration = 2^confirmation_count slots

┌─────────┬───────────────────┬────────────────────────────────────────────────────────────┐
│  Vote   │ Confirmation Cnt  │ Lockout Duration                                           │
├─────────┼───────────────────┼────────────────────────────────────────────────────────────┤
│ Slot  1 │         0         │     1 slots  █                                             │
│ Slot  2 │         1         │     2 slots  ██                                            │
│ Slot  3 │         2         │     4 slots  ████                                          │
│ Slot  4 │         3         │     8 slots  ████████                                      │
│ Slot  5 │         4         │    16 slots  ████████████████                              │
│ Slot  6 │         5         │    32 slots  ████████████████████████████████              │
│ Slot  7 │         6         │    64 slots  ████████████████████████████████████████████████████████████████  │

└─────────┴───────────────────┴────────────────────────────────────────────────────────────┘

💡 Key insight: Each new vote DOUBLES the lockout of previous votes!
   This makes it exponentially harder to switch forks as you vote deeper.

┌──────────────────────────────────────────────────────────────────────────────┐
│ 🗳️  Can Vote Check Examples                                                  │
├──────────────────────────────────────────────────────────────────────────────┤
│ Vote on Slot 9 (same fork):      ✅ ALLOWED                                  │
│ Vote on Slot 9 (different fork): ❌ BLOCKED                                  │
└──────────────────────────────────────────────────────────────────────────────┘

关键洞察

  • 在同一分叉上连续投票是安全的(锁定期不影响)
  • 切换到不同分叉需要等待所有冲突投票的锁定期过期
  • 投票越深,切换代价越高(指数增长)

分叉选择可视化

以下是一个完整的分叉选择演示(运行 cargo test test_fork_choice_visualization_demo -- --nocapture):

初始分叉结构

              0
              |
              1
            / | \
           2  5  8
          /|     |
         3 4     9
         |
         6
         |
         7

Round 1: 初始状态 - 无投票

╔══════════════════════════════════════════════════════════════╗
║                    🌳 Fork Tree Visualization                 ║
╠══════════════════════════════════════════════════════════════╣
║ [Slot 0] 💰0
║    ╰──[Slot 1] 💰0
║        ├──[Slot 2] 💰0
║        │   ├──[Slot 3] 💰0
║        │   │   ╰──[Slot 6] 💰0
║        │   │       ╰──[Slot 7] 💰0
║        │   ╰──[Slot 4] 💰0
║        ├──[Slot 5] 💰0
║        ╰──[Slot 8] 💰0
║            ╰──[Slot 9] 💰0 🏆
╠══════════════════════════════════════════════════════════════╣
║ Legend: 🏆 = Best Fork  💰 = Stake Weight  🗳️ = Votes        ║
╚══════════════════════════════════════════════════════════════╝

Round 2: Alice (stake=100) 投票给 Slot 7

╔══════════════════════════════════════════════════════════════╗
║                    🌳 Fork Tree Visualization                 ║
╠══════════════════════════════════════════════════════════════╣
║ [Slot 0] 💰100
║    ╰──[Slot 1] 💰100
║        ├──[Slot 2] 💰100
║        │   ├──[Slot 3] 💰100
║        │   │   ╰──[Slot 6] 💰100
║        │   │       ╰──[Slot 7] 💰100 🗳️ [Alice(100)] 🏆
║        │   ╰──[Slot 4] 💰0
║        ├──[Slot 5] 💰0
║        ╰──[Slot 8] 💰0
║            ╰──[Slot 9] 💰0
╠══════════════════════════════════════════════════════════════╣
║ Legend: 🏆 = Best Fork  💰 = Stake Weight  🗳️ = Votes        ║
╚══════════════════════════════════════════════════════════════╝

Round 3: Bob (stake=150) 投票给 Slot 9

╔══════════════════════════════════════════════════════════════╗
║                    🌳 Fork Tree Visualization                 ║
╠══════════════════════════════════════════════════════════════╣
║ [Slot 0] 💰250
║    ╰──[Slot 1] 💰250
║        ├──[Slot 2] 💰100
║        │   ├──[Slot 3] 💰100
║        │   │   ╰──[Slot 6] 💰100
║        │   │       ╰──[Slot 7] 💰100 🗳️ [Alice(100)]
║        │   ╰──[Slot 4] 💰0
║        ├──[Slot 5] 💰0
║        ╰──[Slot 8] 💰150
║            ╰──[Slot 9] 💰150 🗳️ [Bob(150)] 🏆
╠══════════════════════════════════════════════════════════════╣
║ Legend: 🏆 = Best Fork  💰 = Stake Weight  🗳️ = Votes        ║
╚══════════════════════════════════════════════════════════════╝

Round 4: Charlie (stake=200) 投票给 Slot 4

╔══════════════════════════════════════════════════════════════╗
║                    🌳 Fork Tree Visualization                 ║
╠══════════════════════════════════════════════════════════════╣
║ [Slot 0] 💰450
║    ╰──[Slot 1] 💰450
║        ├──[Slot 2] 💰300
║        │   ├──[Slot 3] 💰100
║        │   │   ╰──[Slot 6] 💰100
║        │   │       ╰──[Slot 7] 💰100 🗳️ [Alice(100)]
║        │   ╰──[Slot 4] 💰200 🗳️ [Charlie(200)] 🏆
║        ├──[Slot 5] 💰0
║        ╰──[Slot 8] 💰150
║            ╰──[Slot 9] 💰150 🗳️ [Bob(150)]
╠══════════════════════════════════════════════════════════════╣
║ Legend: 🏆 = Best Fork  💰 = Stake Weight  🗳️ = Votes        ║
╚══════════════════════════════════════════════════════════════╝

Round 6: Bob 改投 Slot 4 - Fork 2 成为最重分叉!

╔══════════════════════════════════════════════════════════════╗
║                    🌳 Fork Tree Visualization                 ║
╠══════════════════════════════════════════════════════════════╣
║ [Slot 0] 💰700
║    ╰──[Slot 1] 💰700
║        ├──[Slot 2] 💰450
║        │   ├──[Slot 3] 💰100
║        │   │   ╰──[Slot 6] 💰100
║        │   │       ╰──[Slot 7] 💰100 🗳️ [Alice(100)]
║        │   ╰──[Slot 4] 💰350 🗳️ [Charlie(200), Bob(150)] 🏆
║        ├──[Slot 5] 💰100 🗳️ [Alice(100)]
║        ╰──[Slot 8] 💰150
║            ╰──[Slot 9] 💰150 🗳️ [Bob(150)]
╠══════════════════════════════════════════════════════════════╣
║ Legend: 🏆 = Best Fork  💰 = Stake Weight  🗳️ = Votes        ║
╚══════════════════════════════════════════════════════════════╝

╔════════════════════════════════════════════════════════════════╗
║                      📊 Final Summary                          ║
╠════════════════════════════════════════════════════════════════╣
║ Fork weights (subtree totals):                                 ║
║   Slot 2 subtree: 450 (includes 3,4,6,7)                       ║
║   Slot 5 subtree: 100                                          ║
║   Slot 8 subtree: 150 (includes 9)                             ║
║                                                                ║
║ 🏆 Best fork tip: Slot 4                                       ║
╚════════════════════════════════════════════════════════════════╝

分叉选择算法

ForkChoice 使用贪心算法选择最佳分叉:

// 从 root 开始,每层选择权重最高的子节点
fn best_slot(&self, bank_forks: &BankForks) -> Option<Slot> {
    let mut current = bank_forks.root();
    
    loop {
        let children = bank_forks.get_children(current);
        if children.is_empty() {
            return Some(current);  // 叶子节点即为最佳 slot
        }
        
        // 选择子树权重最高的子节点
        current = children
            .into_iter()
            .max_by_key(|&child| self.compute_subtree_weight(child, bank_forks))
            .unwrap();
    }
}

子树权重计算

  • 直接权重:直接投票给该 slot 的 stake 总和
  • 子树权重:直接权重 + 所有子孙的直接权重

这确保了投票会"向上传播"——投票给 Slot 7 也会增加 Slot 6、3、2、1、0 的权重。

投票流程

// ReplayStage 重放成功后
fn on_replay_success(slot: Slot, hash: Hash) {
    // 1. 检查是否可以投票(Lockout 检查)
    let ancestors = bank_forks.get_ancestors(slot);
    if !tower.can_vote(slot, hash, &ancestors) {
        log::info!("Locked out, cannot vote for slot {}", slot);
        return;
    }
    
    // 2. 记录投票到本地 Tower
    tower.record_vote(slot, hash);
    
    // 3. 更新 ForkChoice
    fork_choice.add_vote(&my_pubkey, slot);
    
    // 4. 通过 Gossip 广播投票
    gossip.broadcast_vote(Vote { slot, hash });
}

运行可视化测试

你可以运行以下命令查看完整的可视化演示:

# 分叉选择演示
cargo test test_fork_choice_visualization_demo -- --nocapture

# Lockout 机制演示
cargo test test_tower_lockout_visualization -- --nocapture

About

学习版的solana。

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors