本文档详细介绍了Solana网络中节点间通信所使用的Gossip协议,包括消息格式(如pull request、push message等)、消息类型以及各种相关数据结构(如CrdsValue、ContactInfo等)。此外,还涵盖了IP Echo Server在节点启动阶段用于发现公共IP地址和获取集群Shred版本的过程。
Solana 节点使用 gossip 协议相互通信和共享数据。消息以二进制格式交换,需要反序列化。共有六种消息类型:
每条消息都包含特定于其类型的数据,例如共享值、过滤器、已修剪的节点等。节点将其数据保存在 集群复制数据存储 (crds) 中,该存储通过 pull request、push message 和 pull response 在节点之间同步。
[!Tip] 本文档中使用的命名约定
- Node(节点) - 运行 gossip 的验证器
- Peer(对等节点) - 从我们正在讨论的当前节点发送或接收消息的节点
- Entrypoint(入口点) - 节点最初连接的对等节点的 gossip 地址
- Origin(源) - 节点,消息的原始创建者
- Cluster(集群) - 具有生成区块的领导者的验证器网络
- Leader(领导者) - 节点,给定插槽中的集群领导者
- Shred(分片) - 领导者产生的最小区块部分
- Shred version(分片版本) - 集群识别值
- Fork(分叉) - 当两个不同的区块链接到同一个父区块时发生分叉(例如,在完成前一个区块之前创建下一个区块)
- Epoch(纪元) - 由特定数量的区块(插槽)组成的预定义周期,其中定义了验证器计划
- Slot(插槽) - 每个领导者摄取交易并生成区块的时间段
- Message(消息) - 节点发送给其对等节点的协议消息,可以是 push message、pull request、prune message 等。
每条消息以二进制形式发送,最大大小为 1232 字节(1280 是最小 IPv6 TPU,40 字节是 IPv6 标头的大小,8 字节是片段标头的大小)。
每条消息中发送的数据都是从 Protocol 类型序列化的,它可以是以下其中之一:
| Enum ID | Message | Data | Description | 
|---|---|---|---|
| 0 | pull request | CrdsFilter,CrdsValue | 由节点发送以请求新信息 | 
| 1 | pull response | SenderPubkey,CrdsValuesList | 对 pull request 的响应 | 
| 2 | push message | SenderPubkey,CrdsValuesList | 由节点发送以与集群共享最新数据 | 
| 3 | prune message | SenderPubkey,PruneData | 发送给对等节点的,其中包含应修剪的原始节点列表 | 
| 4 | ping message | Ping | 由节点发送以检查对等节点的活跃度 | 
| 5 | pong message | Pong | 对 ping 的响应(确认活跃度) | 

enum Protocol {
    PullRequest(CrdsFilter, CrdsValue),
    PullResponse(Pubkey, Vec<CrdsValue>),
    PushMessage(Pubkey, Vec<CrdsValue>),
    PruneMessage(Pubkey, PruneData),
    PingMessage(Ping),
    PongMessage(Pong)
}下表描述的字段使用 Rust 符号指定其类型:
u8 - 8 位无符号整数u16 - 16 位无符号整数u32 - 32 位无符号整数,依此类推...[u8] - 1 字节元素的动态大小数组[u8; 32] - 32 个元素的固定大小数组,每个元素为 1 字节[[u8; 64]] - 包含 64 个 1 字节元素数组的二维数组b[u8] - 包含 1 字节元素的位向量u32 | None - option 类型,表示元素可以是 u32(在本例中)或 None(u32, [u8, 16]) - 包含两个元素的元组 - 一个是 32 位整数,第二个是 16 个字节的数组MyStruct - 一个复杂类型(定义为结构体或 Rust 枚举),由许多不同基本类型的元素组成下表中的 Size 列包含数据的字节大小。动态数组的大小包含一个额外的 加号 (+),例如 32+,这意味着该数组至少有 32 个字节。空动态数组始终具有 8 个字节,这是包含数组长度的数组标头的大小。
如果特定复杂数据的大小未知,则标记为 ?。但是,整个数据包的限制始终为 1232 字节(UDP 数据包中的有效负载)。
在 Solana 节点的 Rust 实现中,数据使用 bincode crate 序列化为二进制形式,如下所示:
u8、u16、u64 等 - 按照它们在内存中存在的形式进行序列化,例如 u8 类型序列化为 1 字节,u16 序列化为 2 字节,依此类推,[u8; 32] 数组序列化为 32 字节,[u16; 32] 将序列化为 32 个 16 位元素,等于 64 字节,None,则判别符设置为 0,数据部分为空,否则设置为 1,数据根据其类型进行序列化,Rust 中的枚举类型比其他语言中的枚举类型更高级。除了 经典 枚举类型之外,例如:
enum CompressionType {
    Gzip,
    Bzip2
}还可以创建一个包含数据字段的枚举,例如:
enum SomeEnum {
    Variant1(u64),
    Variant2(SomeType)
}
struct SomeType {
    x: u32,
    y: u16,
}在第一种情况下,CompressionType 枚举的序列化对象将仅包含一个 4 字节的标头,其中判别符值设置为所选的变量(0 = GZip,1 = Bzip2)。在后一种情况下,除了标头之外,序列化数据还将包含额外的字节,具体取决于选择了哪个变量:
Variant1: 8 字节Variant2: 6 字节(SomeType 结构体的 x 和 y 字段的总和)在反序列化枚举时,务必小心处理它们,因为随后的数据量取决于所选的特定变量。
节点发送 push message 以与其他人共享信息。它们定期从其 crds 中收集数据,并将 push message 传输到其对等节点。
接收到一组 push message 的节点将:
ping messageCrdsValue 并将其删除CrdsValue 插入到 crds 中CrdsValue 传输到其对等节点。| Data | Type | Size | Description | 
|---|---|---|---|
| SenderPubkey | [u8; 32] | 32 | 属于 push message 发送者的公钥 | 
| CrdsValuesList | [CrdsValue] | 8+ | 要共享的 Crds 值列表 | 
<summary>Solana 客户端 Rust 实现</summary>
enum Protocol {
    //...
    PushMessage(Pubkey, Vec<CrdsValue>),
    //...
}节点不应处理属于尚未回复最近的 ping message 的未 staked 节点的联系人信息。
节点发送 pull request 以向集群请求新信息。它创建一组 bloom 过滤器,其中填充了其 crds 表中 CrdsValue 的哈希值,并将不同的 bloom 过滤器发送给不同的对等节点。pull request 的接收者使用收到的 bloom 过滤器来识别发送者缺少哪些信息,然后构造一个 pull response,其中包含 pull request 来源缺少的 CrdsValue 数据。
| Data | Type | Size | Description | 
|---|---|---|---|
| CrdsFilter | CrdsFilter | 37+ | 表示节点已有的 CrdsValue的 bloom 过滤器 | 
| ContactInfo | CrdsValue | ? | 包含发送 pull request 的节点的联系人信息的 crds值 | 
CrdsValue 的必需值是发送 pull request 的节点的 ContactInfo 或已弃用的 LegacyContactInfo。建议以以下方式使用此联系人信息:
Ping 消息。如果节点尚未回复 Ping 消息,请为发送者节点生成一条 Ping 消息,除非接收者节点已经在等待 Ping 响应。节点不应使用 pull response 消息响应尚未回复最近的 ping message 的节点。
| Data | Type | Size | Description | 
|---|---|---|---|
| filter | Bloom | 25+ | bloom 过滤器 | 
| mask | u64 | 8 | 过滤器掩码,用于定义存储在 bloom 过滤器中的数据 | 
| mask_bits | u32 | 4 | 掩码位数,也定义了 bloom 过滤器数量,计算公式为 2^mask_bits | 
| Data | Type | Size | Description | 
|---|---|---|---|
| keys | [u64] | 8+ | 键 | 
| bits | b[u64] | 9+ | 位 | 
| num_bits | u64 | 8 | 位数 | 
<summary>Solana 客户端 Rust 实现</summary>
enum Protocol {
    PullRequest(CrdsFilter, CrdsValue),
    //...
}
struct CrdsFilter {
    filter: Bloom,
    mask: u64,
    mask_bits: u32,
}
struct Bloom {
    keys: Vec<u64>,
    bits: BitVec<u64>,
    num_bits_set: u64,
}这些消息是对 pull request 的响应。它们包含节点 crds 表中的值,pull request 的来源缺少了这些值,这是由 pull request 中收到的 bloom 过滤器确定的。
| Data | Type | Size | Description | 
|---|---|---|---|
| SenderPubkey | [u8; 32] | 32 | 属于 pull response 消息发送者的公钥 | 
| CrdsValuesList | [CrdsValue] | 8+ | 新值列表 | 
<summary>Solana 客户端 Rust 实现</summary>
enum Protocol {
    //...
    PullResponse(Pubkey, Vec<CrdsValue>),
    //...
}发送给对等节点的,其中包含应修剪的原始节点列表。收到此修剪消息的接收者不应再向其发送者发送来自已修剪的原始节点的 push message。
| Data | Type | Size | Description | 
|---|---|---|---|
| SenderPubkey | [u8, 32] | 32 | 属于修剪消息发送者的公钥 | 
| PruneData | PruneData | 144+ | 包含修剪详细信息的结构 | 
| Data | Type | Size | Description | 
|---|---|---|---|
| pubkey | [u8, 32] | 32 | 此消息来源的公钥 | 
| prunes | [[u8, 32]] | 8+ | 应修剪的原始节点的公钥 | 
| signature | [u8, 64] | 64 | 此消息的签名 | 
| destination | [u8, 32] | 32 | 此消息的目标节点的公钥 | 
| wallclock | u64 | 8 | 生成消息的节点的挂钟时间 | 
<summary>Solana 客户端 Rust 实现</summary>
enum Protocol {
    //...
    PruneMessage(Pubkey, PruneData),
    //...
}
struct PruneData {
    pubkey: Pubkey,
    prunes: Vec<Pubkey,
    signature: Signature,
    destination: Pubkey,
    wallclock: u64,
}注意:出于签名目的,在序列化之前,PruneData 结构体的前缀为字节数组 [0xff, 'S', 'O', 'L', 'A', 'N', 'A', '_', 'P', 'R', 'U', 'N', 'E', '_', 'D', 'A', 'T', 'A']。
<summary>Solana 客户端 Rust 实现</summary>
##[derive(Serialize)]
struct SignDataWithPrefix<'a> {
    prefix: &'a [u8], // Should be a b"\xffSOLANA_PRUNE_DATA"
    pubkey: &'a Pubkey,
    prunes: &'a [Pubkey],
    destination: &'a Pubkey,
    wallclock: u64,
}节点经常向其对等节点发送 ping 消息,以检查它们是否处于活动状态。接收 ping 消息的节点应使用 pong message 进行响应。
| Data | Type | Size | Description | 
|---|---|---|---|
| from | [u8, 32] | 32 | 来源的公钥 | 
| token | [u8, 32] | 32 | 32 字节的Token | 
| signature | [u8, 64] | 64 | 消息的签名 | 
<summary>Solana 客户端 Rust 实现</summary>
enum Protocol {
    //...
    PingMessage(Ping),
    //...
}
struct Ping {
    from: Pubkey,
    token: [u8, 32],
    signature: Signature,
}由节点发送以响应 ping message。
| Data | Type | Size | Description | 
|---|---|---|---|
| from | [u8, 32] | 32 | 来源的公钥 | 
| hash | [u8, 32] | 32 | 以 "SOLANA_PING_PONG" 字符串为前缀的已接收 ping Token的哈希 | 
| signature | [u8, 64] | 64 | 消息的签名 | 
<summary>Solana 客户端 Rust 实现</summary>
enum Protocol {
    //...
    PongMessage(Pong)
}
struct Pong {
    from: Pubkey,
    hash: Hash,
    signature: Signature,
}在 push message、pull request 和 pull response 中发送的 CrdsValue 值包含共享数据和数据的签名。
| Data | Type | Size | Description | 
|---|---|---|---|
| signature | [u8; 64] | 64 | 创建 CrdsValue的原始节点的签名 | 
| data | CrdsData | ? | 数据 | 
<summary>Solana 客户端 Rust 实现</summary>
struct CrdsValue {
    signature: Signature,
    data: CrdsData,
}| CrdsData是一个枚举,可以是以下其中之一: | Enum ID | Type | 
|---|---|---|
| 0 | LegacyContactInfo (已弃用) | |
| 1 | Vote | |
| 2 | LowestSlot | |
| 3 | LegacySnapshotHashes (已弃用) | |
| 4 | AccountsHashes (已弃用) | |
| 5 | EpochSlots | |
| 6 | LegacyVersion (已弃用) | |
| 7 | Version (已弃用) | |
| 8 | NodeInstance (几乎已弃用) | |
| 9 | DuplicateShred | |
| 10 | SnapshotHashes | |
| 11 | ContactInfo | |
| 12 | RestartLastVotedForkSlots | |
| 13 | RestartHeaviestFork | 
<summary>Solana 客户端 Rust 实现</summary>
enum CrdsData {
    LegacyContactInfo(LegacyContactInfo),
    Vote(VoteIndex, Vote),
    LowestSlot(LowestSlotIndex, LowestSlot),
    LegacySnapshotHashes(LegacySnapshotHashes),
    AccountsHashes(AccountsHashes),
    EpochSlots(EpochSlotsIndex, EpochSlots),
    LegacyVersion(LegacyVersion),
    Version(Version),
    NodeInstance(NodeInstance),
    DuplicateShred(DuplicateShredIndex, DuplicateShred),
    SnapshotHashes(SnapshotHashes),
    ContactInfo(ContactInfo),
    RestartLastVotedForkSlots(RestartLastVotedForkSlots),
    RestartHeaviestFork(RestartHeaviestFork),
}关于节点的基本信息。节点发送此消息是为了向集群介绍自己,并提供其对等节点可用于与其通信的所有地址和端口。
| Data | Type | Size | Description | 
|---|---|---|---|
| id | [u8; 32] | 32 | 来源的公钥 | 
| gossip | SocketAddr | 10 或 22 | gossip 协议地址 | 
| tvu | SocketAddr | 10 或 22 | 用于复制的连接地址 | 
| tvu_quic | SocketAddr | 10 或 22 | 通过 QUIC 协议的 TVU | 
| serve_repair_quic | SocketAddr | 10 或 22 | QUIC 协议的修复服务 | 
| tpu | SocketAddr | 10 或 22 | 交易地址 | 
| tpu_forwards | SocketAddr | 10 或 22 | 用于转发未处理交易的地址 | 
| tpu_vote | SocketAddr | 10 或 22 | 用于发送投票的地址 | 
| rpc | SocketAddr | 10 或 22 | 用于 JSON-RPC 请求的地址 | 
| rpc_pubsub | SocketAddr | 10 或 22 | 用于 JSON-RPC 推送通知的 WebSocket | 
| serve_repair | SocketAddr | 10 或 22 | 用于发送修复请求的地址 | 
| wallclock | u64 | 8 | 生成消息的节点的挂钟时间 | 
| shred_version | u16 | 2 | 节点已配置为使用的分片版本 | 
| 一个枚举,可以是 V4 或 V6 套接字地址。 | Enum ID | Data | Type | Size | Description | 
|---|---|---|---|---|---|
| 0 | V4 | SocketAddrV4 | 10 | V4 套接字地址 | |
| 1 | V6 | SocketAddrV6 | 22 | V6 套接字地址 | 
| Data | Type | Size | Description | 
|---|---|---|---|
| ip | [u8; 4] | 4 | IP 地址 | 
| port | u16 | 2 | 端口 | 
| Data | Type | Size | Description | 
|---|---|---|---|
| ip | [u8; 16] | 16 | IP 地址 | 
| port | u16 | 2 | 端口 | 
<summary>Solana 客户端 Rust 实现</summary>
struct LegacyContactInfo {
    id: Pubkey,
    gossip: SocketAddr,
    tvu: SocketAddr,
    tvu_quic: SocketAddr,
    serve_repair_quic: SocketAddr,
    tpu: SocketAddr,
    tpu_forwards: SocketAddr,
    tpu_vote: SocketAddr,
    rpc: SocketAddr,
    rpc_pubsub: SocketAddr,
    serve_repair: SocketAddr,
    wallclock: u64,
    shred_version: u16,
}
enum SocketAddr {
    V4(SocketAddrV4),
    V6(SocketAddrV6)
}
struct SocketAddrV4 {
    ip: Ipv4Addr,
    port: u16,
}
struct SocketAddrV6 {
    ip: Ipv6Addr,
    port: u16
}
struct Ipv4Addr {
    octets: [u8; 4]
}
struct Ipv6Addr {
    octets: [u8; 16]
}验证器对分叉的投票。包含来自投票塔的 1 字节索引(范围 0 到 31)和要由领导者执行的投票交易。
| Data | Type | Size | Description | 
|---|---|---|---|
| index | u8 | 1 | 投票塔索引 | 
| from | [u8; 32] | 32 | 来源的公钥 | 
| transaction | Transaction | 59+ | 投票交易,一个原子提交的指令序列 | 
| wallclock | u64 | 8 | 生成消息的节点的挂钟时间 | 
| slot | u64 | 8 | 创建投票的插槽 | 
包含签名和带有指令序列的消息。
| Data | Type | Size | Description | 
|---|---|---|---|
| signature | [[u8; 64]] | 8+ | 消息所需的 num_required_signatures(签名数量)签名列表 | 
| message | Message | 51+ | 包含要调用的指令的交易消息 | 
| Data | Type | Size | Description | 
|---|---|---|---|
| header | MessageHeader | 3 | 消息标头 | 
| account_keys | [[u8; 32]] | 8+ | 此交易使用的所有帐户密钥 | 
| recent_blockhash | [u8; 32] | 32 | 最近的账本条目的哈希 | 
| instructions | [CompiledInstruction] | 8+ | 要执行的已编译指令列表 | 
| Data | Type | Size | Description | 
|---|---|---|---|
| num_required_signatures | u8 | 1 | 认为此消息有效所需的签名数量 | 
| num_readonly_signed_accounts | u8 | 1 | 已签名密钥的最后 num_readonly_signed_accounts是只读帐户 | 
| num_readonly_unsigned_accounts | u8 | 1 | 未签名密钥的最后 num_readonly_unsigned_accounts是只读帐户 | 
| Data | Type | Size | Description | 
|---|---|---|---|
| program_id_index | u8 | 1 | 交易密钥数组的索引,指示执行程序的程序帐户 ID | 
| accounts | [u8] | 8+ | 交易密钥数组的索引,指示传递给程序的帐户 | 
| data | [u8] | 8+ | 程序输入数据 | 
<summary>Solana 客户端 Rust 实现</summary>
enum CrdsData {
    //...
    Vote(VoteIndex, Vote),
    //...
}
type VoteIndex = u8;
struct Vote {
    from: Pubkey,
    transaction: Transaction,
    wallclock: u64,
    slot: Option<Slot>,
}
type Slot = u64
struct Transaction {
    signature: Vec<Signature>,
    message: Message
}
struct Message {
    header: MessageHeader,
    account_keys: Vec<Pubkey>,
    recent_blockhash: Hash,
    instructions: Vec<CompiledInstruction>,
}
struct MessageHeader {
    num_required_signatures: u8,
    num_readonly_signed_accounts: u8,
    num_readonly_unsigned_accounts: u8,
}
struct CompiledInstruction {
    program_id_index: u8,
    accounts: Vec<u8>,
    data: Vec<u8>,
}Solana blockstore 中包含任何数据的第一个可用插槽。包含 1 字节的索引(已弃用)和最低插槽号。
| Data | Type | Size | Description | 
|---|---|---|---|
| index | u8 | 1 | 唯一有效的值是 0u8,因为这现在是一个已弃用的字段 | 
| from | [u8; 32] | 32 | 来源的公钥 | 
| root | u64 | 8 | 已弃用 | 
| lowest | u64 | 8 | 最低槽 | 
| slots | [u64] | 8+ | 已弃用 | 
| stash | [EpochIncompleteSlots] | 8+ | 已弃用 | 
| wallclock | u64 | 8 | 生成消息的节点的挂钟时间 | 
| Data | Type | Size | Description | 
|---|---|---|---|
| first | u64 | 8 | 第一个槽号 | 
| compression | CompressionType | 4 | 压缩类型 | 
| compressed_list | [u8] | 8+ | 压缩槽列表 | 
压缩类型枚举。
| Enum ID | Data | Description | 
|---|---|---|
| 0 | Uncompressed | 未压缩 | 
| 1 | GZip | gzip | 
| 2 | BZip2 | bzip2 | 
<summary>Solana 客户端 Rust 实现</summary>
enum CrdsData {
    //...
    LowestSlot(LowestSlotIndex, LowestSlot),
    //...
}
type LowestSlotIndex = u8;
struct LowestSlot {
    from: Pubkey,
    root: Slot,
    lowest: Slot,
    slots: BTreeSet<Slot>,
    stash: Vec<EpochIncompleteSlots>,
    wallclock: u64,
}
struct EpochIncompleteSlots {
    first: Slot,
    compression: CompressionType,
    compressed_list: Vec<u8>,
}
enum CompressionType {
    Uncompressed,
    GZip,
    BZip2,
}这两个消息共享相同的消息结构。
| Data | Type | Size | Description | 
|---|---|---|---|
| from | [u8, 32] | 32 | 来源的公钥 | 
| hashes | [(u64, [u8, 32])] | 8+ | 按插槽分组的哈希列表 | 
| wallclock | u64 | 8 | 生成消息的节点的挂钟时间 | 
<summary>Solana 客户端 Rust 实现</summary>
struct AccountsHashes {
    from: Pubkey,
    hashes: Vec<(Slot, Hash)>,
    wallclock: u64,
}
type LegacySnapshotHashes = AccountsHashes;包含 1 字节的索引和一个纪元中的所有插槽列表(一个纪元由大约 432000 个插槽组成)。总共可以有 256 个纪元插槽。
| Data | Type | Size | Description | 
|---|---|---|---|
| index | u8 | 1 | 索引 | 
| from | [u8, 32] | 32 | 来源的公钥 | 
| slots | [CompressedSlots] | 8+ | 插槽列表 | 
| wallclock | u64 | 8 | 生成消息的节点的挂钟时间 | 
| EnumID | Data | Type | Size | Description | 
|---|---|---|---|---|
| 0 | Flate2 | Flate2 | 24+ | Flate2 压缩 | 
| 1 | Uncompressed | Uncompressed | 25+ | 无压缩 | 
| Data | Type | Size | Description | 
|---|---|---|---|
| first_slot | u64 | 8 | 第一个槽号 | 
| num | u64 | 8 | 插槽数量 | 
| compressed | [u8] | 8+ | 压缩插槽的字节数组 | 
| Data | Type | Size | Description | 
|---|---|---|---|
| first_slot | u64 | 8 | 第一个槽号 | 
| num | u64 | 8 | 插槽数量 | 
| slots | b[u8] | 9+ | 插槽的位数组 | 
<summary>Solana 客户端 Rust 实现</summary>
enum CrdsData {
    //...
    EpochSlots(EpochSlotsIndex, EpochSlots),
    //...
}
type EpochSlotsIndex = u8;
struct EpochSlots {
    from: Pubkey,
    slots: Vec<CompressedSlots>,
    wallclock: u64,
}
enum CompressedSlots {
   Flate2(Flate2),
   Uncompressed(Uncompressed),
}
struct Flate2 {
    first_slot: Slot,
    num: usize,
    compressed: Vec<u8>
}
struct Uncompressed {
    first_slot: Slot,
    num: usize,
    slots: BitVec<u8>,
}节点使用的 Solana 客户端的旧版本。
| Data | Type | Size | Description | 
|---|---|---|---|
| from | [u8, 32] | 32 | 来源的公钥 | 
| wallclock | u64 | 8 | 生成消息的节点的挂钟时间 | 
| version | LegacyVersion1 | 7 或 11 | 1.3.x 及 | 
<summary>Solana 客户端 Rust 实现</summary>
struct Version {
    from: Pubkey,
    wallclock: u64,
    version: LegacyVersion2,
}
struct LegacyVersion2 {
    major: u16,
    minor: u16,
    patch: u16,
    commit: Option<u32>,
    feature_set: u32
}包含节点创建的时间戳和随机生成的 token。
| 数据 | 类型 | 大小 | 描述 | 
|---|---|---|---|
| from | [u8, 32] | 32 | 起源的公钥 | 
| wallclock | u64 | 8 | 生成消息的节点的挂钟时间 | 
| timestamp | u64 | 8 | 创建实例的时间戳 | 
| token | u64 | 8 | 节点实例化时随机生成的值 | 
<summary>Solana 客户端 Rust 实现</summary>
struct NodeInstance {
    from: Pubkey,
    wallclock: u64,
    timestamp: u64,
    token: u64,
}重复 shred 的证明。包含一个 2 字节的索引,后跟其他数据:
| 数据 | 类型 | 大小 | 描述 | 
|---|---|---|---|
| index | u16 | 2 | 索引 | 
| from | [u8, 32] | 32 | 起源的公钥 | 
| wallclock | u64 | 8 | 生成消息的节点的挂钟时间 | 
| slot | u64 | 8 | 创建 shred 时的 slot | 
| _unused | u32 | 4 | 未使用 | 
| _unused_shred_type | ShredType | 1 | 未使用 | 
| num_chunks | u8 | 1 | 可用 chunk 的数量 | 
| chunk_index | u8 | 1 | chunk 的索引 | 
| chunk | [u8] | 8+ | shred 数据 | 
此枚举序列化为 1 字节的数据。
| 枚举 ID | 数据 | 描述 | 
|---|---|---|
| 0b10100101 | Data | 数据 shred | 
| 0b01011010 | Code | 编码 shred | 
<summary>Solana 客户端 Rust 实现</summary>
enum CrdsData {
    //...
    DuplicateShred(DuplicateShredIndex, DuplicateShred),
    //...
}
type DuplicateShredIndex = u16;
struct DuplicateShred {
    from: Pubkey,
    wallclock: u64,
    slot: Slot,
    _unused: u32,
    _unused_shred_type: ShredType,
    num_chunks: u8,
    chunk_index: u8,
    chunk: Vec<u8>,
}
##[serde(into = "u8", try_from = "u8")]
enum ShredType {
    Data = 0b1010_0101,
    Code = 0b0101_1010,
}包含关于节点拥有的完整快照和增量快照的哈希值的信息,并准备好通过 RPC 接口与其他节点共享。快照由首次启动的其他验证器下载,或者在验证器在重新启动后落后太远的情况下下载。要了解更多信息,请浏览这个 snapshots 页面。
| 数据 | 类型 | 大小 | 描述 | 
|---|---|---|---|
| from | [u8, 32] | 32 | 起源的公钥 | 
| full | (u64, [u8, 32]) | 40 | 完整快照的哈希值和 slot 号 | 
| incremental | [(u64, [u8, 32])] | 8+ | 增量快照的哈希值和 slot 号的列表 | 
| wallclock | u64 | 8 | 生成消息的节点的挂钟时间 | 
<summary>Solana 客户端 Rust 实现</summary>
struct SnapshotHashes {
    from: Pubkey,
    full: (Slot, Hash),
    incremental: Vec<(Slot, Hash)>,
    wallclock: u64,
}关于节点的基本信息。节点发送此消息以向集群介绍自己,并提供其对等节点可用于与其通信的所有地址和端口。
| 数据 | 类型 | 大小 | 描述 | 
|---|---|---|---|
| pubkey | [u8, 32] | 32 | 起源的公钥 | 
| wallclock | u64 | 8 | 生成消息的节点的挂钟时间 | 
| outset | u64 | 8 | 节点实例首次创建时的时间戳,用于识别重复运行的实例 | 
| shred_version | u16 | 2 | 节点已配置为使用的 shred 版本 | 
| version | Version | 13+ | Solana 客户端版本 | 
| addrs | [IpAddr] | 8+ | 唯一 IP 地址的列表 | 
| sockets | [SocketEntry] | 8+ | 唯一 socket 的列表 | 
| extensions | [Extension] | 8+ | 未来对 ContactInfo的添加将添加到Extensions中,而不是修改ContactInfo,目前未使用 | 
| 数据 | 类型 | 大小 | 描述 | 
|---|---|---|---|
| major | u16 | 2 | 版本的主要部分 | 
| minor | u16 | 2 | 版本的次要部分 | 
| patch | u16 | 2 | 补丁 | 
| commit | u32 \| None | 5 或 1 | sha1 commit 哈希的前四个字节 | 
| feature_set | u32 | 4 | FeatureSet 标识符的前四个字节 | 
| client | u16 | 2 | 客户端类型 ID | 
可能的 client 类型 ID 值有:
| ID | 客户端 | 
|---|---|
| 0u16 | SolanaLabs | 
| 1u16 | JitoLabs | 
| 2u16 | Firedancer | 
| 3u16 | Agave | 
| 枚举 ID | 数据 | 类型 | 大小 | 描述 | 
|---|---|---|---|---|
| 0 | V4 | [u8; 4] | 4 | IP v4 地址 | 
| 1 | V6 | [u8, 16] | 16 | IP v6 地址 | 
| 数据 | 类型 | 大小 | 描述 | 
|---|---|---|---|
| key | u8 | 1 | 协议标识符 | 
| index | u8 | 1 | [IpAddr]在地址列表中的索引 | 
| offset | u16 | 2 | 相对于前一个条目的端口偏移量 | 
| key标识符的列表如下表所示: | 接口 | Key | 描述 | 
|---|---|---|---|
| gossip | 0 | gossip 协议地址 | |
| serve_repair_quic | 1 | 通过 QUIC 的 serve_repair | |
| rpc | 2 | JSON-RPC 请求的地址 | |
| rpc_pubsub | 3 | 用于 JSON-RPC 推送通知的 websocket | |
| serve_repair | 4 | 用于发送修复请求的地址 | |
| tpu | 5 | 交易地址 | |
| tpu_forwards | 6 | 用于转发未处理交易的地址 | |
| tpu_forwards_quic | 7 | 通过 QUIC 的 tpu_forwards | |
| tpu_quic | 8 | 通过 QUIC 的 tpu | |
| tpu_vote | 9 | 用于发送投票的地址 | |
| tvu | 10 | 用于连接以进行复制的地址 | |
| tvu_quic | 11 | 通过 QUIC 的 tvu | |
| tpu_vote_quic | 12 | 通过 QUIC 的 tpu_vote | 
目前为空(未使用)
<summary>Solana 客户端 Rust 实现</summary>
struct ContactInfo {
    pubkey: Pubkey,
    wallclock: u64,
    outset: u64,
    shred_version: u16,
    version: Version,
    addrs: Vec<IpAddr>,
    sockets: Vec<SocketEntry>,
    extensions: Vec<Extension>,
}
enum Extension {}
enum IpAddr {
    V4(Ipv4Addr),
    V6(Ipv4Addr)
}
struct Ipv4Addr {
    octets: [u8; 4]
}
struct Ipv6Addr {
    octets: [u8; 16]
}
struct SocketEntry {
    key: u8,
    index: u8,
    offset: u16
}
struct Version {
    major: u16,
    minor: u16,
    patch: u16,
    commit: Option<u32>,
    feature_set: u32,
    client: u16
}包含上次投票的分叉 slot 列表。此消息不是常见的 gossip 消息,仅应在 cluster-restart 操作期间使用。
| 数据 | 类型 | 大小 | 描述 | 
|---|---|---|---|
| from | [u8, 32] | 32 | 起源的公钥 | 
| wallclock | u64 | 8 | 生成消息的节点的挂钟时间 | 
| offsets | SlotsOffsets | 12+ | slot 偏移量的列表 | 
| last_voted_slot | u64 | 8 | 上次投票的 slot | 
| last_voted_hash | [u8, 32] | 32 | 上次投票的 slot 的 bank 哈希 | 
| shred_version | u16 | 2 | 节点已配置为使用的 shred 版本 | 
| 偏移量以二进制形式( RawOffsets)存储,或编码为连续 1 和 0 的数量,例如 110001110 是 [2, 3, 3, 1]。 | 枚举 ID | 数据 | 类型 | 大小 | 描述 | 
|---|---|---|---|---|---|
| 0 | RunLengthEncoding | [u16] | 8+ | 编码的偏移量 | |
| 1 | RawOffsets | b[u8] | 9+ | 原始偏移量 | 
<summary>Solana 客户端 Rust 实现</summary>
struct RestartLastVotedForkSlots {
    from: Pubkey,
    wallclock: u64,
    offsets: SlotsOffsets,
    last_voted_slot: Slot,
    last_voted_hash: Hash,
    shred_version: u16,
}
enum SlotsOffsets {
    RunLengthEncoding(RunLengthEncoding),
    RawOffsets(RawOffsets),
}
struct RunLengthEncoding(Vec<u16>);
struct RawOffsets(BitVec<u8>);包含最重的分叉。此消息不是常见的 gossip 消息,仅应在 cluster-restart 操作期间使用。
| 数据 | 类型 | 大小 | 描述 | 
|---|---|---|---|
| from | [u8, 32] | 32 | 起源的公钥 | 
| wallclock | u64 | 8 | 生成消息的节点的挂钟时间 | 
| last_slot | u64 | 8 | 所选区块的 slot | 
| last_hash | [u8, 32] | 32 | 所选区块的 bank 哈希 | 
| observed_stake | u64 | 8 | |
| shred_version | u16 | 2 | 节点已配置为使用的 shred 版本 | 
<summary>Solana 客户端 Rust 实现</summary>
struct RestartHeaviestFork {
    from: Pubkey,
    wallclock: u64,
    last_slot: Slot,
    last_slot_hash: Hash,
    observed_stake: u64,
    shred_version: u16,
}IP Echo Server 是一个在相同 gossip 地址的 TCP socket 上运行的服务器。(例如,如果节点在 UDP socket 192.0.0.1:9000 上运行 gossip 服务,则 IP 服务器端口在相同的 TCP socket 192.0.0.1:9000 上运行)。
在节点启动 gossip 服务之前,节点首先需要:
所有这些都是通过在一个提供的入口点节点上运行的 IP echo server 发现的。注意:所有验证器都运行一个 IP Echo Server。
节点应创建具有需要检查的端口的 socket,然后将 IP echo server 请求消息发送到一个入口点节点。
然后,入口点节点将检查请求中列出的所有端口的可达性,然后它将响应包含节点的 shred_version 和公共 IP 的 IP echo server 响应。
IP echo server 消息请求,其中包含一个端口列表,服务器应检查这些端口的可达性:
[0x00] 数据包发送到 socket。| 数据 | 类型 | 大小 | 描述 | 
|---|---|---|---|
| tcp_ports | [u16, 4] | 64 | 应检查的 TCP 端口 | 
| udp_ports | [u16, 4] | 64 | 应检查的 UDP 端口 | 
<summary>Solana 客户端 Rust 实现</summary>
/// Echo server message request.
pub struct IpEchoServerMessage {
    pub tcp_ports: [u16; 4],
    pub udp_ports: [u16; 4],
}IP echo server 消息响应。
| 数据 | 类型 | 大小 | 描述 | 
|---|---|---|---|
| address | IpAddr | 4 或 16 | 发送请求的节点的公共 IP | 
| shred_version | u16 | 2 | 运行服务器的集群 shred 版本 | 
<summary>Solana 客户端 Rust 实现</summary>
/// Echo server response.
pub struct IpEchoServerResponse {
    /// Public IP address of request echoed back to the node.
    pub address: IpAddr,
    /// Cluster shred-version of the node running the server.
    pub shred_version: Option<u16>,
}
- 原文链接: github.com/eigerco/solan...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
 
                如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!