Skip to content

About

DIG Network L2 protocol types extending Chia's wire protocol (opcodes 200+)

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

dig-peer-protocol

DIG Network L2 protocol types — superset of chia-protocol with extension opcodes 200–226 — plus DigLink, the websocket peer link that can actually carry them.

One dependency replaces five: chia-protocol, chia-sdk-client, chia-ssl, chia-traits, chia_streamable_macro — all re-exported verbatim.


Install

[dependencies]
dig-peer-protocol = { version = "0.3", features = ["rustls"] }

Features

Flag Forwards to Adds re-exports
native-tls chia-sdk-client/native-tls Client, ClientState, Connector, create_native_tls_connector
rustls chia-sdk-client/rustls Client, ClientState, Connector, create_rustls_connector

Neither default. Without a TLS feature: DIG types and Peer still compile; Client does not.


Why this crate exists

chia_protocol::Message stores msg_type as ProtocolMessageTypes — a closed #[repr(u8)] enum covering opcodes 0–107. Message::from_bytes rejects any unknown opcode. DIG extension opcodes (200+) cannot decode through stock Message.

DigMessage has identical wire layout but stores msg_type as raw u8. Encodes/decodes any opcode (Chia or DIG) without touching upstream types. Conversions to/from Message are lossless for opcodes the Chia enum recognizes. The byte-identity is asserted exhaustively — over every opcode chia-protocol accepts — in tests/wire_compatibility.rs.

The same closed enum breaks the transport, not just the type: chia_sdk_client::Peer decodes inbound frames with Message::from_bytes, so one DIG frame ends its receive loop and drops the connection. DigLink is the replacement — a websocket link written against the wire format that frames DigMessage.

use dig_peer_protocol::{DigLink, LinkOptions, DIG_MESSAGE};

let (link, mut inbound) = DigLink::connect(addr, connector, LinkOptions::default()).await?;
link.send_dig(DIG_MESSAGE, envelope_bytes.into()).await?;

while let Some(message) = inbound.recv().await {
    // message.msg_type is a raw u8 — Chia opcode or DIG opcode, both arrive.
}

DIG previously carried vendored forks of chia-protocol and chia-sdk-client to get this. DigLink replaces them, so chia-protocol is an ordinary dependency again with no [patch.crates-io].


Wire format

DigMessage binary layout (same as chia_protocol::Message):

[u8 msg_type] [u8 has_id (0|1)] [u16 id (big-endian, if has_id==1)] [u32 data_len (BE)] [u8; data_len payload]
  • msg_type < 200 → Chia standard opcode
  • msg_type ≥ 200 → DIG extension opcode (see DigMessageType)

Opcode assignments

DIG band: 200..=219. Chia band: 0..=107 (+ reserves). Gap 108..=199 reserved for future Chia.

Opcode Variant Payload Gossip strategy
200 NewAttestation — Plumtree eager push
201 NewCheckpointProposal — Plumtree eager push
202 NewCheckpointSignature — Plumtree eager push
203 RequestCheckpointSignatures — Unicast request
204 RespondCheckpointSignatures — Unicast response
205 RequestStatus — Unicast request
206 RespondStatus — Unicast response
207 NewCheckpointSubmission — Plumtree eager push
208 ValidatorAnnounce — Broadcast flood
209 RequestBlockTransactions — Unicast request
210 RespondBlockTransactions — Unicast response
211 ReconciliationSketch — ERLAY reconciliation
212 ReconciliationResponse — ERLAY reconciliation
213 StemTransaction — Dandelion++ stem
214 PlumtreeLazyAnnounce — Plumtree lazy
215 PlumtreePrune — Plumtree control
216 PlumtreeGraft — Plumtree control
217 PlumtreeRequestByHash — Plumtree pull
218 RegisterPeer RegisterPeer struct Unicast → introducer
219 RegisterAck RegisterAck struct Unicast ← introducer

Payload types for 200–217 are TBD (defined by consumer crates); the protocol crate only defines discriminants + framing.


Public interface — DIG types

DigMessage — raw-opcode wire message

pub struct DigMessage {
    pub msg_type: u8,         // 0..=255, any opcode
    pub id: Option<u16>,      // correlation id for req/resp
    pub data: Bytes,          // serialized payload
}

impl DigMessage {
    pub const MAX_MESSAGE_SIZE: usize; // 16 MiB — data_len prefixes above this decode to None

    pub fn new(msg_type: u8, id: Option<u16>, data: Bytes) -> Self;

    // Wire codec (same layout as chia_protocol::Message)
    pub fn to_bytes(&self) -> Vec<u8>;
    pub fn from_bytes(bytes: &[u8]) -> Option<Self>;         // None on short/malformed/oversized buffer
    pub fn from_bytes_owned(buf: Vec<u8>) -> Option<Self>;   // same rules; moves payload instead of copying

    // Interop with stock Chia Message

    // Opcode-range predicates
    pub fn is_dig_extension(&self) -> bool;   // msg_type >= 200
    pub fn is_chia_standard(&self) -> bool;   // msg_type < 200
}

Inputs/outputs

Method Input Output Failure mode
new u8, Option<u16>, Bytes DigMessage infallible
to_bytes &self Vec<u8> infallible
from_bytes &[u8] Option<DigMessage> None if truncated or data_len > MAX_MESSAGE_SIZE
from_bytes_owned Vec<u8> Option<DigMessage> same as from_bytes; moves payload, no copy

from_bytes/from_bytes_owned reject any data_len prefix above MAX_MESSAGE_SIZE (16 MiB) before slicing/allocating the payload. This bounds only the allocation these functions perform — callers MUST still enforce their own per-frame size cap at the transport layer before buffering an incoming frame.


DigMessageType — typed discriminants (200–219)

#[repr(u8)]
pub enum DigMessageType {
    NewAttestation = 200,
    NewCheckpointProposal = 201,
    NewCheckpointSignature = 202,
    RequestCheckpointSignatures = 203,
    RespondCheckpointSignatures = 204,
    RequestStatus = 205,
    RespondStatus = 206,
    NewCheckpointSubmission = 207,
    ValidatorAnnounce = 208,
    RequestBlockTransactions = 209,
    RespondBlockTransactions = 210,
    ReconciliationSketch = 211,
    ReconciliationResponse = 212,
    StemTransaction = 213,
    PlumtreeLazyAnnounce = 214,
    PlumtreePrune = 215,
    PlumtreeGraft = 216,
    PlumtreeRequestByHash = 217,
    RegisterPeer = 218,
    RegisterAck = 219,
}

impl DigMessageType {
    pub const MAX_ASSIGNED: u8 = 219;
    pub const ALL: [Self; 20];    // declaration order

    // TryFrom<u8> — Err(UnknownDigMessageType) when value ∉ 200..=219
}

// Serde: serializes/deserializes as raw u8 (not variant name)
impl Serialize for DigMessageType { ... }
impl<'de> Deserialize<'de> for DigMessageType { ... }
impl Display for DigMessageType { ... }  // "RegisterPeer(218)"

pub struct UnknownDigMessageType(pub u8);  // Error; Display + std::error::Error

Inputs/outputs

Conversion Input Output
as u8 variant u8 in 200..=219
DigMessageType::try_from(u8) u8 Result<Self, UnknownDigMessageType>
serde_json::to_string(&variant) variant "218" (stringified u8)
serde_json::from_str::<DigMessageType> "218" Ok(RegisterPeer) / Err if not 200–219

RegisterPeer — introducer registration request (opcode 218)

#[streamable]
pub struct RegisterPeer {
    pub ip: String,        // externally reachable IP or hostname
    pub port: u16,         // P2P listening port
    pub node_type: NodeType, // e.g. NodeType::FullNode
}

impl RegisterPeer {
    pub fn new(ip: String, port: u16, node_type: NodeType) -> Self;

    // Streamable trait — binary codec for payload body (not full wire message)
    fn to_bytes(&self) -> Result<Vec<u8>, chia_traits::Error>;
    fn from_bytes(bytes: &[u8]) -> Result<Self, chia_traits::Error>;

    // DigMessage wrappers (prepend opcode 218 + framing)
    pub fn to_dig_message(&self, id: Option<u16>) -> Result<DigMessage, chia_traits::Error>;
    pub fn from_dig_message(msg: &DigMessage) -> Option<Result<Self, chia_traits::Error>>;
    // from_dig_message returns None when msg.msg_type != 218
}

RegisterAck — introducer registration response (opcode 219)

#[streamable]
pub struct RegisterAck {
    pub success: bool,   // false == policy rejection (valid wire outcome)
}

impl RegisterAck {
    pub fn new(success: bool) -> Self;
    // Streamable: to_bytes / from_bytes
    pub fn to_dig_message(&self, id: Option<u16>) -> Result<DigMessage, chia_traits::Error>;
    pub fn from_dig_message(msg: &DigMessage) -> Option<Result<Self, chia_traits::Error>>;
    // from_dig_message returns None when msg.msg_type != 219
}

RequestPeersIntroducer / RespondPeersIntroducer — Chia-standard (opcodes 63/64)

#[streamable(message)]
pub struct RequestPeersIntroducer {}

#[streamable(message)]
pub struct RespondPeersIntroducer {
    pub peer_list: Vec<TimestampedPeerInfo>,
}

These use #[streamable(message)] because opcodes 63/64 exist in stock ProtocolMessageTypes. Compatible with Peer::request_infallible directly (no DigMessage wrapper needed).


Re-exports from Chia crates

From chia_protocol::* (full glob)

~100 wire types. Key items:

Category Types
Framing DIG-owned DigMessage, Bytes, NodeType; ProtocolMessageTypes + ChiaProtocolMessage + TimestampedPeerInfo re-exported by name for chia paths
Block FullBlock, HeaderBlock, BlockRecord, Foliage, FoliageBlockData
Peer discovery RequestPeers, RespondPeers, TimestampedPeerInfo
Mempool/tx NewTransaction, RequestTransaction, RespondTransaction, MempoolItemsAdded
State/coins Coin, CoinState, CoinSpend, RequestCoinState, CoinStateUpdate
Consensus/PoS ProofOfSpace, NewSignagePointOrEndOfSubSlot, ChallengeChainSubSlot
Wallet protocol NewPeakWallet, RegisterForCoinUpdates, RegisterForPhUpdates, SendTransaction
Fees FeeEstimate, FeeEstimateGroup, FeeRate, RequestFeeEstimates

See chia-protocol docs for the full list.

From chia_sdk_client

// Always available
pub use chia_sdk_client::{
    ClientError,           // enum — TLS, handshake, io errors
    Network,               // mainnet/testnet selection
    Peer,                  // single-peer connection handle (send/request/receive)
    PeerOptions,           // builder config
    RateLimit,             // per-opcode rate limit entry
    RateLimiter,
    RateLimits,
    V2_RATE_LIMITS,        // static — V2 protocol defaults
    load_ssl_cert,         // load ChiaCertificate from disk
};

// Requires feature = "native-tls" or "rustls"
pub use chia_sdk_client::{Client, ClientState, Connector};

#[cfg(feature = "native-tls")]
pub use chia_sdk_client::create_native_tls_connector;

#[cfg(feature = "rustls")]
pub use chia_sdk_client::create_rustls_connector;

From chia_ssl

pub use chia_ssl::ChiaCertificate;   // { cert_pem: String, key_pem: String }

From chia_traits

pub use chia_traits::Streamable;     // trait — binary codec (to_bytes / from_bytes / hash)

From chia_streamable_macro

pub use chia_streamable_macro::streamable;   // #[streamable] / #[streamable(message)] proc macro

Encode / decode example

use dig_peer_protocol::{DigMessage, DigMessageType, NodeType, RegisterPeer, RegisterAck};

// --- Encode outbound RegisterPeer ---
let rp = RegisterPeer::new("1.2.3.4".into(), 9444, NodeType::FullNode);
let wire: DigMessage = rp.to_dig_message(Some(1)).unwrap();
assert_eq!(wire.msg_type, DigMessageType::RegisterPeer as u8);  // 218
let bytes: Vec<u8> = wire.to_bytes();                            // send over socket

// --- Decode inbound frame ---
let msg: DigMessage = DigMessage::from_bytes(&bytes).expect("valid frame");

match DigMessageType::try_from(msg.msg_type) {
    Ok(DigMessageType::RegisterPeer) => {
        let decoded = RegisterPeer::from_dig_message(&msg).unwrap().unwrap();
        println!("peer {}:{}", decoded.ip, decoded.port);
    }
    Ok(DigMessageType::RegisterAck) => {
        let ack = RegisterAck::from_dig_message(&msg).unwrap().unwrap();
        println!("ack success={}", ack.success);
    }
    Ok(other)   => { /* dispatch other DIG opcode */ }
    Err(_)      => {
        // Not a DIG opcode. `msg` still holds the raw opcode and payload; dispatch on
        // `msg.msg_type` directly. There is deliberately no conversion to
        // `chia_protocol::Message` — see SPEC.md §2.4.
    }
}

Error types

Error Source Cause
UnknownDigMessageType(u8) DigMessageType::try_from Wire value not in 200..=219
chia_traits::Error Streamable::to_bytes/from_bytes Malformed payload
ClientError chia_sdk_client TLS, handshake, IO, protocol violations

DigMessage::from_bytes/from_bytes_owned return Option<DigMessage> (no error type) — None means truncated/malformed frame OR a data_len prefix above DigMessage::MAX_MESSAGE_SIZE (16 MiB).


Invariants

  • DigMessage::to_bytes output is binary-identical to Message::to_bytes for opcodes present in ProtocolMessageTypes.
  • from_bytes(to_bytes(m)) == Some(m) for all valid DigMessage.
  • DigMessageType as u8 ∈ 200..=219 for all variants; TryFrom<u8> is the inverse.
  • Payload types 200–217 are defined by downstream crates; this crate only defines discriminants + framing.

License

Apache-2.0

About

DIG Network L2 protocol types extending Chia's wire protocol (opcodes 200+)

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages