DiemNet 消息协议(Messaging Protocol v1)深度解析:网络消息类型、RPC/DirectSend 语义与 8 MiB 帧定界
2026/9/23 6:55:35 网站建设 项目流程
  • 区块链
  • 金融科技

【免费下载链接】diem

Diem’s mission is to build a trusted and innovative financial network that empowers people and businesses around the world.

项目地址:https://gitcode.com/gh_mirrors/di/diem
点击查看免费下载

DiemNet 是 Diem 生态中任意两个节点之间通信的主网络协议,而消息协议(Messaging Protocol v1)定义了连接建立后所有应用数据在网线上的字节形态与交互语义:NetworkMessage消息类型、ProtocolId应用协议标识、RPC 与 DirectSend 两种消息模式、优先级字段、错误码以及 4 字节长度前缀 + 8 MiB 上限的帧定界规则。本文以 messaging-v1.md 规格文档为骨架,对照仓库中 wire/messaging/v1/mod.rs 的真实实现与测试用例,逐层拆解这套协议的版本协商、消息编码、发送接收链路与边界约束,帮助读者既掌握协议本身的实战细节,也能在源码中准确找到对应落点。

DiemNet 与消息协议的整体定位

在 DiemNet README 中,DiemNet 被定义为主要承载 Diem 生态内任意两个节点间通信的网络协议,它只描述网线上消息的结构与顺序,实际投递依赖底层 TCP 传输,并且所有通信必须经过 Noise 协议 的加密与认证。DiemNet 支持在单条连接上多路复用多个应用协议,并为每个应用协议提供两种消息语义:

  1. DirectSend:单向、fire-and-forget(发出即忘)的消息投递;
  2. RPC:一元(unary)请求-响应调用。

这两者正是本文主角NetworkMessage枚举所承载的两类核心消息。一条 DiemNet 连接从建立到可收发消息需要依次经过:TCP 握手 → Noise IK 安全握手 → 版本握手(handshake-v1.md)→ 消息协议。也就是说,消息协议是连接建立与升级完成后、节点间交换共识、mempool、状态同步等业务数据的最终载体。

从仓库源码看,消息协议的完整落点在 network/src/protocols/wire/messaging/v1/mod.rs,其模块注释明确写道:该模块定义了 DiemNet v1 消息类型、序列化/反序列化方式,并为在一个抽象 IO 对象(通常是一个 socket)上发送NetworkMessage提供了SinkStream实现,且直接引用了本文所依据的这份规格文档。

版本化机制:MessagingProtocolVersion 与握手协商

消息协议通过MessagingProtocolVersion进行版本化,该版本在连接建立与升级过程中由 DiemNet 握手协议 协商确定。

/// Enum representing different versions of the Diem network protocol. These /// should be listed from old to new, old having the smallest value. We derive /// [`PartialOrd`] since nodes need to find highest intersecting protocol version. pub enum MessagingProtocolVersion { V1 = 0, }

从源码注释可以确认两个关键设计点:

  • 版本枚举从旧到新排列,旧版本值最小(V1 = 0),并且派生PartialOrd,因为节点之间需要找出“双方共同支持的最高版本”。
  • 协商逻辑位于HandshakeMsg::perform_handshake(network/src/protocols/wire/handshake/v1/mod.rs#L229-L276):先校验双方chain_idnetwork_id一致,再对supported_protocols映射按版本从大到小迭代,寻找两个节点都能支持的最高MessagingProtocolVersion;找不到交集则返回HandshakeError::NoCommonProtocols

握手消息本身(HandshakeMsg)是“从MessagingProtocolVersionSupportedProtocols的映射”,其中SupportedProtocols是一个 bit-vector,第i位为 1 当且仅当该节点支持第iProtocolId变体。握手完成后,双方必须只使用接收方声明支持的ProtocolId,否则接收方可以返回ErrorCode::NotSupported错误消息(详见下文“错误码”一节)。

NetworkMessage:最原始的网线消息类型

规格文档开篇给出了消息协议的核心数据结构——NetworkMessage枚举,所有 DiemNet 端点都必须能够处理接收到的全部这些消息:

/// Most primitive message type set on the network. Note this can only support up to 127 message /// types. The first byte in any message is the message type itself starting from 0. enum NetworkMessage { Error(ErrorCode), RpcRequest(RpcRequest), RpcResponse(RpcResponse), DirectSendMsg(DirectSendMsg), }

对应的真实实现位于 network/src/protocols/wire/messaging/v1/mod.rs#L38-L46,与规格文档完全一致,并派生Clone, Debug, PartialEq, Eq, Deserialize, Serialize以支持 BCS 序列化。

这里有两个值得注意的底层约束:

  • 最多 127 种消息类型NetworkMessage枚举变体本身编码在消息的第一个字节中(从 0 开始编号),由于 Rust 枚举判别式在 BCS 中通常占用一个字节,理论上限被限制在 127 个变体以内。
  • 线编码为 BCS:规格文档明确所有 DiemNet 消息在网线上使用 [bcs] 编码。仓库测试 test.rs 中的network_message_canonical_serialization用 proptest 断言任意NetworkMessage的规范化编码-解码往返一致,保证编码的确定性。

消息在网线上的字节布局示例

测试文件 libranet_wire_test_vectors 给出了一个非常直观的线上字节示例:发送一个DirectSendMsgprotocol_id = MempoolDirectSendpriority = 0raw_msg = "hello world"),实际产生的完整字节流为:

[0, 0, 0, 15] // 帧长度前缀(4 字节大端 u32)= 15 [3] // NetworkMessage 类型:DirectSendMsg 的判别值 [2] // protocol_id:MempoolDirectSend [0] // priority [11] // raw_msg 长度 [104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100] // "hello world" 的 ASCII 字节

这个测试同时验证了NetworkMessageStream能正确反序列化这些字节、NetworkMessageSink能把这些消息序列化成完全相同的字节,是理解整个帧定界与编码规则的绝佳起点。

ProtocolId:应用协议标识符

每个应用协议都有一个唯一的ProtocolId标识。规格文档给出的枚举包含 8 个变体(ConsensusRpc、ConsensusDirectSend、MempoolDirectSend、StateSyncDirectSend、DiscoveryDirectSend、HealthCheckerRpc、IdentityDirectSend、OnchainDiscoveryRpc)。仓库当前版本的实际实现(network/src/protocols/wire/handshake/v1/mod.rs#L36-L45)略有演进:

#[repr(u8)] pub enum ProtocolId { ConsensusRpc = 0, ConsensusDirectSend = 1, MempoolDirectSend = 2, StateSyncDirectSend = 3, DiscoveryDirectSend = 4, HealthCheckerRpc = 5, // json provides flexibility for backwards compatible upgrade ConsensusDirectSendJSON = 6, }

与规格文档相比,仓库实现将IdentityDirectSendOnchainDiscoveryRpc移除/替换,新增了ConsensusDirectSendJSON = 6,并在注释中说明这是为了向后兼容升级而提供的 JSON 序列化变体。从ProtocolId::to_bytes/from_bytes(network/src/protocols/wire/handshake/v1/mod.rs#L73-L89)可以看到,除ConsensusDirectSendJSON使用serde_json外,其余协议统一使用 BCS 序列化。

由于#[repr(u8)]ProtocolId在 BCS 编码中恰好占用 1 字节,测试protocol_id_serialization(test.rs#L12-L17)专门断言了ProtocolId::ConsensusRpc编码为单个字节0x00ProtocolId的值同时也被用于握手阶段SupportedProtocolsbit-vector 的位索引,因此协议 ID 的数值一旦分配就应保持稳定。

错误码:ParsingError 与 NotSupported

ErrorCode枚举定义了两类协议级错误:

enum ErrorCode { /// Failed to parse NetworkMessage, the entries are the first two bytes of the message: /// NetworkMessage type and possibly the ProtocolId ParsingError(u8, u8), /// A message was received for a message / protocol that is not supported over this connection: /// The NetworkMessage type is encoded as a u8. NotSupported(u8, ProtocolId), }

仓库实现(mod.rs#L48-L79)对这两个变体做了结构化细化,语义保持一致:

  • ParsingError(ParsingErrorType { message: u8, protocol: u8 }):无法解析对方消息的头信息,携带尽可能多的头信息(NetworkMessage 类型字节与可能的 ProtocolId 字节)以便对方诊断;
  • NotSupported(NotSupportedType::RpcRequest(ProtocolId) | NotSupportedType::DirectSendMsg(ProtocolId)):消息可以被解析出头信息,但当前连接上不支持该协议(例如收到了未在握手中向对方通告的ProtocolId)。

规格文档给出了一个典型场景:如果收到了针对某个未向对方通告的ProtocolIdRpcRequest,则发送错误消息ErrorCode::NotSupported(1, ProtocolId),其中1RpcRequestNetworkMessage枚举中的索引。测试error_code(test.rs#L19-L27)验证了ParsingError编码为[0, 9, 5](错误码类型 + message + protocol 两个字节)。

RPC 协议:请求-响应语义

RPC 协议的流程非常直接:

  1. 请求方向响应方发送NetworkMessage::RpcRequest,携带一个request_id
  2. 响应方发送NetworkMessage::RpcResponserequest_id原样复制自对应的请求。
struct RpcRequest { protocol_id: ProtocolId, // 应用协议标识符 request_id: RequestId, // RequestId = u32 priority: Priority, // 0..=255 raw_request: Vec<u8>, // 请求负载,由应用层 handler 解析 } struct RpcResponse { request_id: RequestId, // 与请求中的 request_id 完全一致 priority: Priority, raw_response: Vec<u8>, // 响应负载 }

两个要点:

  • protocol_id只出现在请求中,响应对象不包含该字段——request_id是唯一将响应关联回请求的纽带;
  • 任何应用层处理错误都应包装在RpcResponse消息内部,即“应用错误不占用协议级错误码”,ErrorCode仅用于传输/解析层的错误。

RpcRequest的 BCS 编码顺序由测试 rpc_request 完整展示:protocol_id(1 字节)→request_id(4 字节小端 u32,例如 25 编码为[25, 0, 0, 0])→priority(1 字节)→raw_request长度(4 字节)→raw_request字节。这个字节序与RequestId = u32的类型别名、Priority = u8的类型别名(mod.rs#L81-L85)一一对应。

DirectSend 协议:单向即发即忘

DirectSend 提供单向、fire-and-forget 式的消息投递:

struct DirectSendMsg { protocol_id: ProtocolId, // 应用协议标识符 priority: Priority, // 0..=255 raw_msg: Vec<u8>, // 消息负载 }

发送方将消息负载装入NetworkMessage::DirectSendMsg发出,接收方根据protocol_id将负载交给对应的应用层 handler;协议本身不保证送达、不提供确认与重传。这也是共识(Consensus)、mempool、状态同步等模块中广播类业务所依赖的基础通道。

消息优先级:best-effort 的调度提示

RpcRequestRpcResponseDirectSendMsg都带有priority字段(类型Priority = u8,取值范围0..=255),其语义是:

  • 优先级是一个best-effort信号,数值越高表示越紧急;
  • 发送端与接收端都可以据此进行消息调度;
  • 对于 RPC,接收方可以尊重请求的优先级,并为出站响应附加相同的优先级值;
  • 待处理的入站/出站消息 MAY 根据优先级被重排或丢弃MAY是规格中的可选能力,不是强制要求)。

规格文档特别指出:虽然协议允许按优先级重排或丢弃消息,但DiemNet 参考实现目前并不执行priority("the DiemNet reference implementation does not currently respectpriority")。这一点在源码中同样可以看到——messaging/v1/mod.rs 中的NetworkMessageStream/NetworkMessageSink只是按序读取、序列化并发送消息帧,并没有针对priority做任何排队或抢占逻辑;优先级字段目前仅作为协议预留的、面向未来的调度扩展点。

错误处理与流控

关于协议级错误与背压,规格文档明确了三条规则:

  1. 不强制响应错误:收到错误消息的一方不要求必须回复;
  2. 最小触发长度:一条消息至少要有 2 字节长度才会触发错误响应,否则错误信息本身会因数据不足而失去意义(例如ParsingError至少要能带上消息类型字节);
  3. 无内置流控:DiemNet 不定义任何背压/流控(back-pressure / flow-control)机制或策略,每个端点可以自由实现本地策略来防御“话痨邻居”(chatty neighbors),例如通过不发放 TCP window update 来限制对方的发送速率。

最后一点在实际实现中有一个有趣的对偶:虽然协议层不做流控,但 NetworkMessageStream/Sink 在构造时会包一层AsyncRateLimiter(来自diem_rate_limiter),接收Option<SharedBucket>参数——也就是说速率限制(rate limiting)作为一个可选能力被注入到消息收发路径中,而不是协议本身的强制机制,这与规格“每个端-point 自行决定本地策略”的表述一致。

帧定界:u32 长度前缀 + BCS 消息

每条序列化后的 DiemNet 消息以4 字节大端(big-endian)u32长度前缀进行帧定界,然后这些消息帧被送入 Noise 加密 socket(Noise 层有自己内部的成帧、加密与解密)。因此:

  • 单个消息帧可能横跨多个 Noise 帧;
  • 单个 Noise 帧也可能包含多个消息帧。

忽略底层加密与成帧后,网线上序列化的NetworkMsg序列看起来就是“长度前缀 + 消息字节”的重复:

[u32-length-prefix] || [serialized-message-bytes] || ..

源码中的对应实现是network_message_frame_codec(mod.rs#L148-L154),它基于tokio_util::codec::LengthDelimitedCodec构建:

pub fn network_message_frame_codec(max_frame_size: usize) -> LengthDelimitedCodec { LengthDelimitedCodec::builder() .max_frame_length(max_frame_size) .length_field_length(4) .big_endian() .new_codec() }

length_field_length(4).big_endian()正是规格文档中“big-endian encoded u32 (4-bytes) length prefix”的代码实现。关于长度前缀与噪声层的协作,DiemNet README 还补充了一个重要细节:Noise 层将单帧大小限制在至多65535字节,其中16字节始终保留给 AES-GCM 认证标签,因此发送一条序列化NetworkMsg(含 4 字节长度前缀)时,需要先将其切成不超过65535 - 16 = 65519字节的块,逐块加密后再发送。

最大帧大小:8 MiB 的硬性边界

规格文档对帧大小给出了强制约束:

每条serialized-message-bytes必须小于或等于 8 MiB(8388608 字节)。注意该长度不包含u32长度前缀。DiemNet 服务器必须拒绝超过 8 MiB 上限的入站消息,DiemNet 客户端不得发送超过 8 MiB 上限的出站消息。

源码中该常量定义在 network/src/constants.rs#L20:

pub const MAX_FRAME_SIZE: usize = 8 * 1024 * 1024; /* 8 MiB */

规格文档同时给出读取单条 DiemNet 消息的参考伪代码:

const MAX_DIEMNET_FRAME_LEN: u32 = 8388608; // 8 MiB // read the 4-byte length prefix first let length_prefix: u32 = noise_socket.read(4).to_host_endian(); // reject messages that are too large if length_prefix > MAX_DIEMNET_FRAME_LEN { reject; } // read the actual bcs-serialized message let message_bytes = noise_socket.read(length_prefix); // deserialize the message let message = bcs::from_bytes(message_bytes);

这一约束在测试中有两处直接验证(test.rs):

  • send_fails_when_larger_than_frame_limit(test.rs#L90-L103):用 64 字节的帧上限构造NetworkMessageSink,发送 123 字节负载的消息,send返回Err——即发送端拒绝超大出站消息;
  • recv_fails_when_larger_than_frame_limit(test.rs#L105-L123):发送端帧上限 128 字节、接收端帧上限 64 字节,发送 80 字节负载的消息后,接收端读取返回Err——即接收端拒绝超大入站消息。

这两条测试共同印证了规格文档“客户端不得发送 / 服务器必须拒绝”的双向约束,而LengthDelimitedCodecmax_frame_length正是这一上限在实际读写路径上的强制者。

发送与接收的实现:NetworkMessageSink 与 NetworkMessageStream

在 wire/messaging/v1/mod.rs 中,消息协议收发被封装为两个异步组件:

  • NetworkMessageSink<TWriteSocket>:实现Sink<&NetworkMessage>,负责把NetworkMessagebcs::to_bytes序列化、交给LengthDelimitedCodec成帧后写到底层 socket。序列化失败产生WriteError::SerializeError,IO 失败产生WriteError::IoError(mod.rs#L136-L144);
  • NetworkMessageStream<TReadSocket>:实现Stream<Item = Result<NetworkMessage, ReadError>>,从底层 socket 读出帧后用bcs::from_bytes反序列化;反序列化失败时保留帧长度与前 8 字节便于调试,产生ReadError::DeserializeError(mod.rs#L126-L134)。

两者都接受max_frame_size与可选的速率限制桶(Option<SharedBucket>)作为构造参数。proptest 用例network_message_socket_roundtrip(test.rs#L190-L221)在读写两端同时开启/关闭碎片化(fragmented read/write)的情况下,验证SinkStream能够互相理解并完整保留所有NetworkMessage——这也回应了规格文档中“一个消息帧可能横跨多个 Noise 帧”的描述:即使底层字节被切碎,长度前缀定界依然能正确还原出完整消息。

总结:一份协议、一套实现、一组测试

通过对照 messaging-v1.md 规格与仓库实现可以总结出消息协议 v1 的全貌:

  • 消息模型NetworkMessage四种变体(Error / RpcRequest / RpcResponse / DirectSendMsg),BCS 编码,首字节为消息类型,上限 127 种;
  • 协议标识ProtocolId#[repr(u8)]单字节编码,在握手中通过SupportedProtocolsbit-vector 通告,消息协议版本由HandshakeMsg::perform_handshake协商出最高交集;
  • 两种语义:RPC 靠request_id关联请求与响应;DirectSend 即发即忘,protocol_id决定负载去向;
  • 质量信号priority(0..=255)是 best-effort 调度提示,参考实现暂未执行;
  • 错误处理ParsingError/NotSupported覆盖解析与协议不支持两类场景,应用错误一律封装在RpcResponse内,错误响应非强制,消息长度至少 2 字节才触发错误;
  • 帧定界:4 字节大端u32长度前缀 + BCS 消息,Noise 层分包上限 65519 字节,消息帧上限 8 MiB(MAX_FRAME_SIZE),发送与接收两端分别由NetworkMessageSink/NetworkMessageStream强制。

无论读者是要实现一个兼容的 DiemNet 对端、排查线上消息解析问题,还是为应用接入某个ProtocolId通道,都可以以本文的协议语义为纲、以 wire/messaging/v1/mod.rs 与 test.rs 为实现的参照与验证基准。

  • 区块链
  • 金融科技

【免费下载链接】diem

Diem’s mission is to build a trusted and innovative financial network that empowers people and businesses around the world.

项目地址:https://gitcode.com/gh_mirrors/di/diem
点击查看免费下载

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

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

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

立即咨询