From 98a37d10be9a30a31804bd180d6843207fbd1883 Mon Sep 17 00:00:00 2001 From: eshork <1829176+eshork@users.noreply.github.com> Date: Sat, 25 Apr 2026 20:29:49 -0400 Subject: [PATCH 1/2] Add doc comments to all public API items and enforce deny(missing_docs) Resolves #1. Adds /// doc comments to every undocumented public API item across peeroxide-dht and peeroxide crates (551 items total), then enforces #![deny(missing_docs)] in both crate roots to prevent future regressions. Files changed: blind_relay.rs, compact_encoding.rs, crypto.rs, hyperdht.rs, hyperdht_messages.rs, lib.rs (both crates), messages.rs, noise.rs, noise_wrap.rs, protomux.rs, rpc.rs, secret_stream.rs, error.rs, peer_info.rs, swarm.rs. --- peeroxide-dht/src/blind_relay.rs | 18 +++ peeroxide-dht/src/compact_encoding.rs | 127 ++++++++++++++++++++- peeroxide-dht/src/crypto.rs | 8 ++ peeroxide-dht/src/hyperdht.rs | 95 +++++++++++++++- peeroxide-dht/src/hyperdht_messages.rs | 147 +++++++++++++++++++++++++ peeroxide-dht/src/lib.rs | 15 +++ peeroxide-dht/src/messages.rs | 37 +++++++ peeroxide-dht/src/noise.rs | 9 ++ peeroxide-dht/src/noise_wrap.rs | 6 +- peeroxide-dht/src/protomux.rs | 26 +++++ peeroxide-dht/src/rpc.rs | 57 ++++++++++ peeroxide-dht/src/secret_stream.rs | 23 +++- peeroxide/src/error.rs | 5 + peeroxide/src/lib.rs | 1 + peeroxide/src/peer_info.rs | 21 ++++ peeroxide/src/swarm.rs | 1 + 16 files changed, 585 insertions(+), 11 deletions(-) diff --git a/peeroxide-dht/src/blind_relay.rs b/peeroxide-dht/src/blind_relay.rs index 76f6449..b93b641 100644 --- a/peeroxide-dht/src/blind_relay.rs +++ b/peeroxide-dht/src/blind_relay.rs @@ -29,15 +29,20 @@ use tracing::debug; /// Pair message — requests relay pairing with a 32-byte token. #[derive(Debug, Clone, PartialEq)] pub struct PairMessage { + /// Indicates whether this peer initiated the pair request. pub is_initiator: bool, + /// Relay token used to match the pair and unpair messages. pub token: [u8; 32], + /// Stream identifier assigned by the local peer. pub id: u64, + /// Sequence number for the pair request. pub seq: u64, } /// Unpair message — cancels a relay pairing. #[derive(Debug, Clone, PartialEq)] pub struct UnpairMessage { + /// Relay token used to cancel a pending pair request. pub token: [u8; 32], } @@ -50,6 +55,7 @@ pub const MSG_TYPE_PAIR: u32 = 0; /// Protomux message type index for unpair. pub const MSG_TYPE_UNPAIR: u32 = 1; +/// Pre-encodes a [`PairMessage`], advancing the state cursor. pub fn preencode_pair(state: &mut State, msg: &PairMessage) { state.end += 1; // bitfield(7) = 1 byte state.end += 32; // fixed32 token @@ -57,6 +63,7 @@ pub fn preencode_pair(state: &mut State, msg: &PairMessage) { c::preencode_uint(state, msg.seq); } +/// Encodes a [`PairMessage`] into the state buffer. pub fn encode_pair(state: &mut State, msg: &PairMessage) { let flags: u8 = if msg.is_initiator { 1 } else { 0 }; c::encode_uint8(state, flags); @@ -65,6 +72,7 @@ pub fn encode_pair(state: &mut State, msg: &PairMessage) { c::encode_uint(state, msg.seq); } +/// Decodes a [`PairMessage`] from the state buffer. pub fn decode_pair(state: &mut State) -> c::Result { let flags = c::decode_uint8(state)?; let is_initiator = flags & 1 != 0; @@ -79,16 +87,19 @@ pub fn decode_pair(state: &mut State) -> c::Result { }) } +/// Pre-encodes a [`UnpairMessage`], advancing the state cursor. pub fn preencode_unpair(state: &mut State, _msg: &UnpairMessage) { state.end += 1; // bitfield(7) = 1 byte state.end += 32; // fixed32 token } +/// Encodes a [`UnpairMessage`] into the state buffer. pub fn encode_unpair(state: &mut State, msg: &UnpairMessage) { c::encode_uint8(state, 0); // flags = 0 c::encode_fixed32(state, &msg.token); } +/// Decodes a [`UnpairMessage`] from the state buffer. pub fn decode_unpair(state: &mut State) -> c::Result { let _flags = c::decode_uint8(state)?; let token = c::decode_fixed32(state)?; @@ -127,20 +138,26 @@ pub fn decode_unpair_from_slice(data: &[u8]) -> c::Result { // ── Client ─────────────────────────────────────────────────────────────────── +/// Errors that can occur while using the blind relay client. #[derive(Debug, Error)] pub enum RelayError { + /// A Protomux operation failed while opening, sending, or receiving. #[error("protomux error: {0}")] Protomux(#[from] protomux::ProtomuxError), + /// Encoding or decoding of relay messages failed. #[error("encoding error: {0}")] Encoding(#[from] c::EncodingError), + /// The channel closed before a matching pair response arrived. #[error("channel closed before pair response")] ChannelClosed, + /// The client was destroyed before the operation could complete. #[error("relay client destroyed")] Destroyed, + /// A pair request with this token is already in flight. #[error("already pairing with this token")] AlreadyPairing, } @@ -148,6 +165,7 @@ pub enum RelayError { /// Response from a successful relay pairing. #[derive(Debug, Clone)] pub struct PairResponse { + /// Relay-assigned remote stream identifier. pub remote_id: u64, } diff --git a/peeroxide-dht/src/compact_encoding.rs b/peeroxide-dht/src/compact_encoding.rs index 8cfea6f..4fe78a2 100644 --- a/peeroxide-dht/src/compact_encoding.rs +++ b/peeroxide-dht/src/compact_encoding.rs @@ -1,31 +1,54 @@ use thiserror::Error; +/// Errors returned by compact encoding operations #[derive(Error, Debug)] pub enum EncodingError { #[error("out of bounds: need {need} bytes, have {have}")] - OutOfBounds { need: usize, have: usize }, + /// The buffer did not contain enough bytes + OutOfBounds { + /// The number of bytes needed + need: usize, + /// The number of bytes available + have: usize, + }, #[error("incorrect buffer size: expected {expected}, got {got}")] - IncorrectBufferSize { expected: usize, got: usize }, + /// The input buffer did not match the expected size + IncorrectBufferSize { + /// The expected buffer length + expected: usize, + /// The actual buffer length + got: usize, + }, #[error("array too large: {0} elements (max 1048576)")] - ArrayTooLarge(usize), + /// The array length exceeds the supported maximum + ArrayTooLarge(#[doc = "The number of elements requested"] usize), #[error("invalid IP family: {0}")] - InvalidIpFamily(u8), + /// The encoded IP family tag was not recognized + InvalidIpFamily(#[doc = "The invalid family tag value"] u8), #[error("invalid IPv4 address: {0}")] - InvalidIpv4(String), + /// The provided IPv4 address was invalid + InvalidIpv4(#[doc = "The invalid IPv4 address string"] String), #[error("invalid IPv6 address: {0}")] - InvalidIpv6(String), + /// The provided IPv6 address was invalid + InvalidIpv6(#[doc = "The invalid IPv6 address string"] String), } +/// Result type used by compact encoding operations pub type Result = std::result::Result; +/// Encoding cursor and backing buffer state #[derive(Debug, Clone)] pub struct State { + /// The current read or write position pub start: usize, + /// The allocated end position for pre-encoded data pub end: usize, + /// The backing byte buffer pub buffer: Vec, } impl State { + /// Creates a new empty state pub fn new() -> Self { Self { start: 0, @@ -34,6 +57,7 @@ impl State { } } + /// Creates a state from an existing buffer pub fn from_buffer(buffer: &[u8]) -> Self { Self { start: 0, @@ -42,6 +66,7 @@ impl State { } } + /// Allocates a buffer based on pre-encoded size pub fn alloc(&mut self) { if self.buffer.len() < self.end { self.buffer.resize(self.end, 0); @@ -69,15 +94,18 @@ impl Default for State { } } +/// Pre-encodes a uint8 value, advancing the state cursor pub fn preencode_uint8(state: &mut State, _val: u8) { state.end += 1; } +/// Encodes a uint8 value into the state buffer pub fn encode_uint8(state: &mut State, val: u8) { state.buffer[state.start] = val; state.start += 1; } +/// Decodes a uint8 value from the state buffer pub fn decode_uint8(state: &mut State) -> Result { state.check_remaining(1)?; let val = state.buffer[state.start]; @@ -85,10 +113,12 @@ pub fn decode_uint8(state: &mut State) -> Result { Ok(val) } +/// Pre-encodes a uint16 value, advancing the state cursor pub fn preencode_uint16(state: &mut State, _val: u16) { state.end += 2; } +/// Encodes a uint16 value into the state buffer pub fn encode_uint16(state: &mut State, val: u16) { let bytes = val.to_le_bytes(); state.buffer[state.start] = bytes[0]; @@ -96,6 +126,7 @@ pub fn encode_uint16(state: &mut State, val: u16) { state.start += 2; } +/// Decodes a uint16 value from the state buffer pub fn decode_uint16(state: &mut State) -> Result { state.check_remaining(2)?; let val = u16::from_le_bytes([state.buffer[state.start], state.buffer[state.start + 1]]); @@ -103,10 +134,12 @@ pub fn decode_uint16(state: &mut State) -> Result { Ok(val) } +/// Pre-encodes a uint24 value, advancing the state cursor pub fn preencode_uint24(state: &mut State, _val: u32) { state.end += 3; } +/// Encodes a uint24 value into the state buffer pub fn encode_uint24(state: &mut State, val: u32) { state.buffer[state.start] = val as u8; state.buffer[state.start + 1] = (val >> 8) as u8; @@ -114,6 +147,7 @@ pub fn encode_uint24(state: &mut State, val: u32) { state.start += 3; } +/// Decodes a uint24 value from the state buffer pub fn decode_uint24(state: &mut State) -> Result { state.check_remaining(3)?; let val = state.buffer[state.start] as u32 @@ -123,16 +157,19 @@ pub fn decode_uint24(state: &mut State) -> Result { Ok(val) } +/// Pre-encodes a uint32 value, advancing the state cursor pub fn preencode_uint32(state: &mut State, _val: u32) { state.end += 4; } +/// Encodes a uint32 value into the state buffer pub fn encode_uint32(state: &mut State, val: u32) { let bytes = val.to_le_bytes(); state.buffer[state.start..state.start + 4].copy_from_slice(&bytes); state.start += 4; } +/// Decodes a uint32 value from the state buffer pub fn decode_uint32(state: &mut State) -> Result { state.check_remaining(4)?; let val = u32::from_le_bytes([ @@ -145,15 +182,18 @@ pub fn decode_uint32(state: &mut State) -> Result { Ok(val) } +/// Pre-encodes a uint40 value, advancing the state cursor pub fn preencode_uint40(state: &mut State, _val: u64) { state.end += 5; } +/// Encodes a uint40 value into the state buffer pub fn encode_uint40(state: &mut State, val: u64) { encode_uint8(state, val as u8); encode_uint32(state, (val >> 8) as u32); } +/// Decodes a uint40 value from the state buffer pub fn decode_uint40(state: &mut State) -> Result { state.check_remaining(5)?; let lo = decode_uint8(state)? as u64; @@ -161,15 +201,18 @@ pub fn decode_uint40(state: &mut State) -> Result { Ok(lo | (hi << 8)) } +/// Pre-encodes a uint48 value, advancing the state cursor pub fn preencode_uint48(state: &mut State, _val: u64) { state.end += 6; } +/// Encodes a uint48 value into the state buffer pub fn encode_uint48(state: &mut State, val: u64) { encode_uint16(state, val as u16); encode_uint32(state, (val >> 16) as u32); } +/// Decodes a uint48 value from the state buffer pub fn decode_uint48(state: &mut State) -> Result { state.check_remaining(6)?; let lo = decode_uint16(state)? as u64; @@ -177,15 +220,18 @@ pub fn decode_uint48(state: &mut State) -> Result { Ok(lo | (hi << 16)) } +/// Pre-encodes a uint56 value, advancing the state cursor pub fn preencode_uint56(state: &mut State, _val: u64) { state.end += 7; } +/// Encodes a uint56 value into the state buffer pub fn encode_uint56(state: &mut State, val: u64) { encode_uint24(state, val as u32 & 0xFFFFFF); encode_uint32(state, (val >> 24) as u32); } +/// Decodes a uint56 value from the state buffer pub fn decode_uint56(state: &mut State) -> Result { state.check_remaining(7)?; let lo = decode_uint24(state)? as u64; @@ -193,10 +239,12 @@ pub fn decode_uint56(state: &mut State) -> Result { Ok(lo | (hi << 24)) } +/// Pre-encodes a uint64 value, advancing the state cursor pub fn preencode_uint64(state: &mut State, _val: u64) { state.end += 8; } +/// Encodes a uint64 value into the state buffer pub fn encode_uint64(state: &mut State, val: u64) { let lo = val as u32; let hi = (val >> 32) as u32; @@ -204,6 +252,7 @@ pub fn encode_uint64(state: &mut State, val: u64) { encode_uint32(state, hi); } +/// Decodes a uint64 value from the state buffer pub fn decode_uint64(state: &mut State) -> Result { state.check_remaining(8)?; let lo = decode_uint32(state)? as u64; @@ -211,6 +260,7 @@ pub fn decode_uint64(state: &mut State) -> Result { Ok(lo | (hi << 32)) } +/// Pre-encodes a uint value, advancing the state cursor pub fn preencode_uint(state: &mut State, n: u64) { if n <= 0xfc { state.end += 1; @@ -223,6 +273,7 @@ pub fn preencode_uint(state: &mut State, n: u64) { } } +/// Encodes a uint value into the state buffer pub fn encode_uint(state: &mut State, n: u64) { if n <= 0xfc { encode_uint8(state, n as u8); @@ -241,6 +292,7 @@ pub fn encode_uint(state: &mut State, n: u64) { } } +/// Decodes a uint value from the state buffer pub fn decode_uint(state: &mut State) -> Result { let a = decode_uint8(state)?; if a <= 0xfc { @@ -263,80 +315,98 @@ fn zigzag_decode(n: u64) -> i64 { ((n >> 1) as i64) ^ -((n & 1) as i64) } +/// Pre-encodes an int value, advancing the state cursor pub fn preencode_int(state: &mut State, n: i64) { preencode_uint(state, zigzag_encode(n)); } +/// Encodes an int value into the state buffer pub fn encode_int(state: &mut State, n: i64) { encode_uint(state, zigzag_encode(n)); } +/// Decodes an int value from the state buffer pub fn decode_int(state: &mut State) -> Result { Ok(zigzag_decode(decode_uint(state)?)) } +/// Pre-encodes an int8 value, advancing the state cursor pub fn preencode_int8(state: &mut State, _val: i8) { state.end += 1; } +/// Encodes an int8 value into the state buffer pub fn encode_int8(state: &mut State, val: i8) { let z = zigzag_encode(val as i64); encode_uint8(state, z as u8); } +/// Decodes an int8 value from the state buffer pub fn decode_int8(state: &mut State) -> Result { Ok(zigzag_decode(decode_uint8(state)? as u64) as i8) } +/// Pre-encodes an int16 value, advancing the state cursor pub fn preencode_int16(state: &mut State, _val: i16) { state.end += 2; } +/// Encodes an int16 value into the state buffer pub fn encode_int16(state: &mut State, val: i16) { let z = zigzag_encode(val as i64); encode_uint16(state, z as u16); } +/// Decodes an int16 value from the state buffer pub fn decode_int16(state: &mut State) -> Result { Ok(zigzag_decode(decode_uint16(state)? as u64) as i16) } +/// Pre-encodes an int32 value, advancing the state cursor pub fn preencode_int32(state: &mut State, _val: i32) { state.end += 4; } +/// Encodes an int32 value into the state buffer pub fn encode_int32(state: &mut State, val: i32) { let z = zigzag_encode(val as i64); encode_uint32(state, z as u32); } +/// Decodes an int32 value from the state buffer pub fn decode_int32(state: &mut State) -> Result { Ok(zigzag_decode(decode_uint32(state)? as u64) as i32) } +/// Pre-encodes an int64 value, advancing the state cursor pub fn preencode_int64(state: &mut State, _val: i64) { state.end += 8; } +/// Encodes an int64 value into the state buffer pub fn encode_int64(state: &mut State, val: i64) { let z = zigzag_encode(val); encode_uint64(state, z); } +/// Decodes an int64 value from the state buffer pub fn decode_int64(state: &mut State) -> Result { Ok(zigzag_decode(decode_uint64(state)?)) } +/// Pre-encodes a float32 value, advancing the state cursor pub fn preencode_float32(state: &mut State, _val: f32) { state.end += 4; } +/// Encodes a float32 value into the state buffer pub fn encode_float32(state: &mut State, val: f32) { let bytes = val.to_le_bytes(); state.buffer[state.start..state.start + 4].copy_from_slice(&bytes); state.start += 4; } +/// Decodes a float32 value from the state buffer pub fn decode_float32(state: &mut State) -> Result { state.check_remaining(4)?; let val = f32::from_le_bytes([ @@ -349,16 +419,19 @@ pub fn decode_float32(state: &mut State) -> Result { Ok(val) } +/// Pre-encodes a float64 value, advancing the state cursor pub fn preencode_float64(state: &mut State, _val: f64) { state.end += 8; } +/// Encodes a float64 value into the state buffer pub fn encode_float64(state: &mut State, val: f64) { let bytes = val.to_le_bytes(); state.buffer[state.start..state.start + 8].copy_from_slice(&bytes); state.start += 8; } +/// Decodes a float64 value from the state buffer pub fn decode_float64(state: &mut State) -> Result { state.check_remaining(8)?; let val = f64::from_le_bytes([ @@ -375,18 +448,22 @@ pub fn decode_float64(state: &mut State) -> Result { Ok(val) } +/// Pre-encodes a bool value, advancing the state cursor pub fn preencode_bool(state: &mut State, _val: bool) { state.end += 1; } +/// Encodes a bool value into the state buffer pub fn encode_bool(state: &mut State, val: bool) { encode_uint8(state, if val { 1 } else { 0 }); } +/// Decodes a bool value from the state buffer pub fn decode_bool(state: &mut State) -> Result { Ok(decode_uint8(state)? != 0) } +/// Pre-encodes an optional buffer, advancing the state cursor pub fn preencode_buffer(state: &mut State, buf: Option<&[u8]>) { match buf { Some(b) => { @@ -397,6 +474,7 @@ pub fn preencode_buffer(state: &mut State, buf: Option<&[u8]>) { } } +/// Encodes an optional buffer into the state buffer pub fn encode_buffer(state: &mut State, buf: Option<&[u8]>) { match buf { Some(b) => { @@ -411,6 +489,7 @@ pub fn encode_buffer(state: &mut State, buf: Option<&[u8]>) { } } +/// Decodes an optional buffer from the state buffer pub fn decode_buffer(state: &mut State) -> Result>> { let len = decode_uint(state)? as usize; if len == 0 { @@ -422,12 +501,14 @@ pub fn decode_buffer(state: &mut State) -> Result>> { Ok(Some(val)) } +/// Pre-encodes a string value, advancing the state cursor pub fn preencode_string(state: &mut State, s: &str) { let len = s.len(); preencode_uint(state, len as u64); state.end += len; } +/// Encodes a string value into the state buffer pub fn encode_string(state: &mut State, s: &str) { let bytes = s.as_bytes(); encode_uint(state, bytes.len() as u64); @@ -435,6 +516,7 @@ pub fn encode_string(state: &mut State, s: &str) { state.start += bytes.len(); } +/// Decodes a string value from the state buffer pub fn decode_string(state: &mut State) -> Result { let len = decode_uint(state)? as usize; state.check_remaining(len)?; @@ -443,6 +525,7 @@ pub fn decode_string(state: &mut State) -> Result { Ok(val) } +/// Pre-encodes a fixed-length buffer, advancing the state cursor pub fn preencode_fixed(state: &mut State, n: usize, buf: &[u8]) -> Result<()> { if buf.len() != n { return Err(EncodingError::IncorrectBufferSize { @@ -454,11 +537,13 @@ pub fn preencode_fixed(state: &mut State, n: usize, buf: &[u8]) -> Result<()> { Ok(()) } +/// Encodes a fixed-length buffer into the state buffer pub fn encode_fixed(state: &mut State, buf: &[u8]) { state.buffer[state.start..state.start + buf.len()].copy_from_slice(buf); state.start += buf.len(); } +/// Decodes a fixed-length buffer from the state buffer pub fn decode_fixed(state: &mut State, n: usize) -> Result> { state.check_remaining(n)?; let val = state.buffer[state.start..state.start + n].to_vec(); @@ -466,14 +551,17 @@ pub fn decode_fixed(state: &mut State, n: usize) -> Result> { Ok(val) } +/// Pre-encodes a 32-byte fixed buffer, advancing the state cursor pub fn preencode_fixed32(state: &mut State, buf: &[u8; 32]) -> Result<()> { preencode_fixed(state, 32, buf) } +/// Encodes a 32-byte fixed buffer into the state buffer pub fn encode_fixed32(state: &mut State, buf: &[u8; 32]) { encode_fixed(state, buf); } +/// Decodes a 32-byte fixed buffer from the state buffer pub fn decode_fixed32(state: &mut State) -> Result<[u8; 32]> { state.check_remaining(32)?; let mut val = [0u8; 32]; @@ -482,14 +570,17 @@ pub fn decode_fixed32(state: &mut State) -> Result<[u8; 32]> { Ok(val) } +/// Pre-encodes a 64-byte fixed buffer, advancing the state cursor pub fn preencode_fixed64(state: &mut State, buf: &[u8; 64]) -> Result<()> { preencode_fixed(state, 64, buf) } +/// Encodes a 64-byte fixed buffer into the state buffer pub fn encode_fixed64(state: &mut State, buf: &[u8; 64]) { encode_fixed(state, buf); } +/// Decodes a 64-byte fixed buffer from the state buffer pub fn decode_fixed64(state: &mut State) -> Result<[u8; 64]> { state.check_remaining(64)?; let mut val = [0u8; 64]; @@ -498,10 +589,12 @@ pub fn decode_fixed64(state: &mut State) -> Result<[u8; 64]> { Ok(val) } +/// Pre-encodes an IPv4 address, advancing the state cursor pub fn preencode_ipv4(state: &mut State, _addr: &str) { state.end += 4; } +/// Encodes an IPv4 address into the state buffer pub fn encode_ipv4(state: &mut State, addr: &str) -> Result<()> { let parts: Vec<&str> = addr.split('.').collect(); if parts.len() != 4 { @@ -517,6 +610,7 @@ pub fn encode_ipv4(state: &mut State, addr: &str) -> Result<()> { Ok(()) } +/// Decodes an IPv4 address from the state buffer pub fn decode_ipv4(state: &mut State) -> Result { state.check_remaining(4)?; let addr = format!( @@ -530,10 +624,12 @@ pub fn decode_ipv4(state: &mut State) -> Result { Ok(addr) } +/// Pre-encodes an IPv6 address, advancing the state cursor pub fn preencode_ipv6(state: &mut State, _addr: &str) { state.end += 16; } +/// Encodes an IPv6 address into the state buffer pub fn encode_ipv6(state: &mut State, addr: &str) -> Result<()> { let parsed: std::net::Ipv6Addr = addr .parse() @@ -544,6 +640,7 @@ pub fn encode_ipv6(state: &mut State, addr: &str) -> Result<()> { Ok(()) } +/// Decodes an IPv6 address from the state buffer pub fn decode_ipv6(state: &mut State) -> Result { state.check_remaining(16)?; let mut octets = [0u8; 16]; @@ -556,6 +653,7 @@ pub fn decode_ipv6(state: &mut State) -> Result { const IP_FAMILY_V4: u8 = 4; const IP_FAMILY_V6: u8 = 6; +/// Pre-encodes an IP address, advancing the state cursor pub fn preencode_ip(state: &mut State, addr: &str) { if addr.contains(':') { preencode_uint8(state, IP_FAMILY_V6); @@ -566,6 +664,7 @@ pub fn preencode_ip(state: &mut State, addr: &str) { } } +/// Encodes an IP address into the state buffer pub fn encode_ip(state: &mut State, addr: &str) -> Result<()> { if addr.contains(':') { encode_uint8(state, IP_FAMILY_V6); @@ -576,6 +675,7 @@ pub fn encode_ip(state: &mut State, addr: &str) -> Result<()> { } } +/// Decodes an IP address from the state buffer pub fn decode_ip(state: &mut State) -> Result { let family = decode_uint8(state)?; match family { @@ -585,34 +685,40 @@ pub fn decode_ip(state: &mut State) -> Result { } } +/// Pre-encodes an IPv4 address and port, advancing the state cursor pub fn preencode_ipv4_address(state: &mut State, addr: &str, _port: u16) { preencode_ipv4(state, addr); state.end += 2; } +/// Encodes an IPv4 address and port into the state buffer pub fn encode_ipv4_address(state: &mut State, addr: &str, port: u16) -> Result<()> { encode_ipv4(state, addr)?; encode_uint16(state, port); Ok(()) } +/// Decodes an IPv4 address and port from the state buffer pub fn decode_ipv4_address(state: &mut State) -> Result<(String, u16)> { let addr = decode_ipv4(state)?; let port = decode_uint16(state)?; Ok((addr, port)) } +/// Pre-encodes an IPv6 address and port, advancing the state cursor pub fn preencode_ipv6_address(state: &mut State, addr: &str, _port: u16) { preencode_ipv6(state, addr); state.end += 2; } +/// Encodes an IPv6 address and port into the state buffer pub fn encode_ipv6_address(state: &mut State, addr: &str, port: u16) -> Result<()> { encode_ipv6(state, addr)?; encode_uint16(state, port); Ok(()) } +/// Decodes an IPv6 address and port from the state buffer pub fn decode_ipv6_address(state: &mut State) -> Result<(String, u16)> { let addr = decode_ipv6(state)?; let port = decode_uint16(state)?; @@ -621,6 +727,7 @@ pub fn decode_ipv6_address(state: &mut State) -> Result<(String, u16)> { const MAX_ARRAY_LENGTH: usize = 0x100000; +/// Pre-encodes a uint array, advancing the state cursor pub fn preencode_uint_array(state: &mut State, arr: &[u64]) { preencode_uint(state, arr.len() as u64); for &val in arr { @@ -628,6 +735,7 @@ pub fn preencode_uint_array(state: &mut State, arr: &[u64]) { } } +/// Encodes a uint array into the state buffer pub fn encode_uint_array(state: &mut State, arr: &[u64]) { encode_uint(state, arr.len() as u64); for &val in arr { @@ -635,6 +743,7 @@ pub fn encode_uint_array(state: &mut State, arr: &[u64]) { } } +/// Decodes a uint array from the state buffer pub fn decode_uint_array(state: &mut State) -> Result> { let len = decode_uint(state)? as usize; if len > MAX_ARRAY_LENGTH { @@ -647,6 +756,7 @@ pub fn decode_uint_array(state: &mut State) -> Result> { Ok(arr) } +/// Pre-encodes a buffer array, advancing the state cursor pub fn preencode_buffer_array(state: &mut State, arr: &[Option<&[u8]>]) { preencode_uint(state, arr.len() as u64); for buf in arr { @@ -654,6 +764,7 @@ pub fn preencode_buffer_array(state: &mut State, arr: &[Option<&[u8]>]) { } } +/// Encodes a buffer array into the state buffer pub fn encode_buffer_array(state: &mut State, arr: &[Option<&[u8]>]) { encode_uint(state, arr.len() as u64); for buf in arr { @@ -661,6 +772,7 @@ pub fn encode_buffer_array(state: &mut State, arr: &[Option<&[u8]>]) { } } +/// Decodes a buffer array from the state buffer pub fn decode_buffer_array(state: &mut State) -> Result>>> { let len = decode_uint(state)? as usize; if len > MAX_ARRAY_LENGTH { @@ -673,6 +785,7 @@ pub fn decode_buffer_array(state: &mut State) -> Result>>> { Ok(arr) } +/// Pre-encodes a string array, advancing the state cursor pub fn preencode_string_array(state: &mut State, arr: &[&str]) { preencode_uint(state, arr.len() as u64); for s in arr { @@ -680,6 +793,7 @@ pub fn preencode_string_array(state: &mut State, arr: &[&str]) { } } +/// Encodes a string array into the state buffer pub fn encode_string_array(state: &mut State, arr: &[&str]) { encode_uint(state, arr.len() as u64); for s in arr { @@ -687,6 +801,7 @@ pub fn encode_string_array(state: &mut State, arr: &[&str]) { } } +/// Decodes a string array from the state buffer pub fn decode_string_array(state: &mut State) -> Result> { let len = decode_uint(state)? as usize; if len > MAX_ARRAY_LENGTH { diff --git a/peeroxide-dht/src/crypto.rs b/peeroxide-dht/src/crypto.rs index 6bdc55e..4ce1243 100644 --- a/peeroxide-dht/src/crypto.rs +++ b/peeroxide-dht/src/crypto.rs @@ -9,6 +9,7 @@ type Blake2bMac256 = Blake2bMac; // ── BLAKE2b primitives ────────────────────────────────────────────────────── +/// Computes a BLAKE2b-256 hash of the given data. pub fn hash(data: &[u8]) -> [u8; 32] { let output = Blake2b256::digest(data); let mut result = [0u8; 32]; @@ -16,6 +17,7 @@ pub fn hash(data: &[u8]) -> [u8; 32] { result } +/// Computes a BLAKE2b-256 hash over multiple byte slices concatenated together. pub fn hash_batch(parts: &[&[u8]]) -> [u8; 32] { let mut h = Blake2b256::new(); for part in parts { @@ -60,14 +62,19 @@ pub fn namespace(name: &str, ids: &[u8]) -> Vec<[u8; 32]> { result } +/// Precomputed namespace hash for announce operations. pub static NS_ANNOUNCE: LazyLock<[u8; 32]> = LazyLock::new(|| namespace("hyperswarm/dht", &[4])[0]); +/// Precomputed namespace hash for unannounce operations. pub static NS_UNANNOUNCE: LazyLock<[u8; 32]> = LazyLock::new(|| namespace("hyperswarm/dht", &[5])[0]); +/// Precomputed namespace hash for mutable put operations. pub static NS_MUTABLE_PUT: LazyLock<[u8; 32]> = LazyLock::new(|| namespace("hyperswarm/dht", &[6])[0]); +/// Precomputed namespace hash for peer handshake operations. pub static NS_PEER_HANDSHAKE: LazyLock<[u8; 32]> = LazyLock::new(|| namespace("hyperswarm/dht", &[0])[0]); +/// Precomputed namespace hash for peer holepunch operations. pub static NS_PEER_HOLEPUNCH: LazyLock<[u8; 32]> = LazyLock::new(|| namespace("hyperswarm/dht", &[1])[0]); @@ -81,6 +88,7 @@ pub fn sign_detached(message: &[u8], secret_key: &[u8; 64]) -> [u8; 64] { signing_key.sign(message).to_bytes() } +/// Verifies an Ed25519 detached signature against the given message and public key. pub fn verify_detached(signature: &[u8; 64], message: &[u8], public_key: &[u8; 32]) -> bool { let Ok(verifying_key) = VerifyingKey::from_bytes(public_key) else { return false; diff --git a/peeroxide-dht/src/hyperdht.rs b/peeroxide-dht/src/hyperdht.rs index 644bac9..398bffd 100644 --- a/peeroxide-dht/src/hyperdht.rs +++ b/peeroxide-dht/src/hyperdht.rs @@ -67,45 +67,66 @@ fn is_addr_private(host: &str) -> bool { } #[derive(Debug, Error)] +/// Errors returned by HyperDHT operations. pub enum HyperDhtError { + /// Error propagated from the underlying DHT client. #[error("DHT error: {0}")] Dht(#[from] DhtError), + /// Error while encoding or decoding protocol data. #[error("encoding error: {0}")] Encoding(#[from] crate::compact_encoding::EncodingError), + /// Error from Noise handshake or session setup. #[error("noise error: {0}")] Noise(#[from] crate::noise::NoiseError), + /// Error from the Noise wrapper layer. #[error("noise wrap error: {0}")] NoiseWrap(#[from] crate::noise_wrap::NoiseWrapError), + /// Error from the router state machine. #[error("router error: {0}")] Router(#[from] crate::router::RouterError), + /// Error while wrapping or unwrapping secure payloads. #[error("secure payload error: {0}")] SecurePayload(#[from] crate::secure_payload::SecurePayloadError), + /// This DHT instance has been destroyed. #[error("node destroyed")] Destroyed, + /// A signature did not verify. #[error("invalid signature")] InvalidSignature, + /// A content hash did not match. #[error("invalid hash")] InvalidHash, + /// The internal channel was closed. #[error("channel closed")] ChannelClosed, + /// No peer was found for the requested target. #[error("peer not found")] PeerNotFound, + /// No relay nodes were available for the operation. #[error("no relay nodes available")] NoRelayNodes, + /// The handshake failed with the given message. #[error("handshake failed: {0}")] HandshakeFailed(String), + /// Hole punching did not succeed. #[error("holepunch failed")] HolepunchFailed, + /// Hole punching was aborted by the remote side. #[error("holepunch aborted")] HolepunchAborted, + /// The remote firewall rejected the connection. #[error("firewall rejected")] FirewallRejected, + /// Error from the UDX transport layer. #[error("UDX error: {0}")] Udx(#[from] libudx::UdxError), + /// Error from the secret stream layer. #[error("secret stream error: {0}")] SecretStream(#[from] SecretStreamError), + /// Failed to establish a UDX stream. #[error("stream establishment failed: {0}")] StreamEstablishment(String), + /// Error from the relay subsystem. #[error("relay error: {0}")] Relay(#[from] RelayError), } @@ -113,37 +134,53 @@ pub enum HyperDhtError { // ── Server events (forwarded to listen() subscribers) ──────────────────────── #[derive(Debug)] +/// Events forwarded to server-side listeners. pub enum ServerEvent { + /// A peer handshake request that may need local server handling. PeerHandshake { + /// The decoded handshake message. msg: HandshakeMessage, + /// Address of the peer that sent the request. from: Ipv4Peer, + /// Optional DHT target associated with the request. target: Option, + /// Reply channel for the generated response. reply_tx: oneshot::Sender>>, }, + /// A peer holepunch request that may need local server handling. PeerHolepunch { + /// The decoded holepunch message. msg: HolepunchMessage, + /// Address of the peer that sent the request. from: Ipv4Peer, + /// Address of the peer we should punch toward. peer_address: Ipv4Peer, + /// Optional DHT target associated with the request. target: Option, + /// Reply channel for the generated response. reply_tx: oneshot::Sender>>, }, } // ── KeyPair ─────────────────────────────────────────────────────────────────── -/// An Ed25519 key pair (libsodium layout: seed‖public_key). #[derive(Clone)] +/// An Ed25519 key pair (libsodium layout: seed‖public_key). pub struct KeyPair { + /// The 32-byte public key. pub public_key: [u8; 32], + /// The 64-byte secret key in libsodium layout. pub secret_key: [u8; 64], } impl KeyPair { + /// Generate a new random key pair. pub fn generate() -> Self { let seed: [u8; 32] = random(); Self::from_seed(seed) } + /// Derive a deterministic key pair from a 32-byte seed. pub fn from_seed(seed: [u8; 32]) -> Self { let signing_key = SigningKey::from_bytes(&seed); let pk: [u8; 32] = signing_key.verifying_key().to_bytes(); @@ -177,47 +214,74 @@ impl KeyPair { // ── Result types ───────────────────────────────────────────────────────────── #[derive(Debug, Clone)] +/// Result from a LOOKUP query. pub struct LookupResult { + /// Node that returned the lookup result. pub from: Ipv4Peer, + /// Optional intermediate hop used to reach the node. pub to: Option, + /// Peers advertised by the node. pub peers: Vec, } #[derive(Debug, Clone)] +/// Result from an ANNOUNCE operation. pub struct AnnounceResult { + /// Closest nodes contacted during the announce. pub closest_nodes: Vec, } #[derive(Debug, Clone)] +/// Result from an immutable put operation. pub struct ImmutablePutResult { + /// Content hash used as the target key. pub hash: [u8; 32], + /// Closest nodes contacted during the write. pub closest_nodes: Vec, } #[derive(Debug, Clone)] +/// Result from a mutable put operation. pub struct MutablePutResult { + /// Public key used as the mutable record key. pub public_key: [u8; 32], + /// Closest nodes contacted during the write. pub closest_nodes: Vec, + /// Record sequence number that was written. pub seq: u64, + /// Signature over the stored value. pub signature: [u8; 64], } #[derive(Debug, Clone)] +/// Result from a mutable get operation. pub struct MutableGetResult { + /// Retrieved value bytes. pub value: Vec, + /// Sequence number attached to the value. pub seq: u64, + /// Signature verifying the value. pub signature: [u8; 64], + /// Node that returned the value. pub from: Ipv4Peer, } #[derive(Debug, Clone)] +/// Metadata needed to establish a peer connection. pub struct ConnectResult { + /// Remote peer's public key. pub remote_public_key: [u8; 32], + /// Address used to reach the server during handshake. pub server_address: Ipv4Peer, + /// Address of the client-side peer endpoint. pub client_address: Ipv4Peer, + /// Whether the connection was relayed through a third party. pub is_relayed: bool, + /// Final Noise state and negotiated keys. pub noise: NoiseWrapResult, + /// Local UDX stream id to use for the connection. pub local_stream_id: u32, + /// Remote UDX metadata advertised by the peer. pub remote_udx: Option, } @@ -226,7 +290,9 @@ pub struct ConnectResult { /// Wraps a [`SecretStream`] over a UDX transport, keeping the underlying /// socket alive for the connection's lifetime. pub struct PeerConnection { + /// Encrypted bidirectional stream to the peer. pub stream: SecretStream, + /// Remote peer's public key. pub remote_public_key: [u8; 32], /// Remote peer's network address (used by server-side relay to connect data streams). pub remote_addr: Option, @@ -282,8 +348,11 @@ impl fmt::Debug for PeerConnection { } } +/// Configuration used by the server-side handshake and holepunch handler. pub struct ServerConfig { + /// Server identity key pair. pub key_pair: KeyPair, + /// Firewall mode advertised to connecting peers. pub firewall: u64, } @@ -302,8 +371,11 @@ pub const DEFAULT_BOOTSTRAP: [&str; 3] = [ // ── Config ──────────────────────────────────────────────────────────────────── #[derive(Debug, Clone, Default)] +/// Configuration for a HyperDHT instance. pub struct HyperDhtConfig { + /// DHT transport and bootstrap settings. pub dht: DhtConfig, + /// Persistent storage settings for stored records. pub persistent: PersistentConfig, } @@ -327,6 +399,7 @@ impl HyperDhtConfig { // ── HyperDhtHandle ──────────────────────────────────────────────────────────── #[derive(Clone)] +/// Main public HyperDHT API handle. pub struct HyperDhtHandle { dht: DhtHandle, router: Arc>, @@ -336,6 +409,7 @@ pub struct HyperDhtHandle { impl HyperDhtHandle { // ── LOOKUP ──────────────────────────────────────────────────────────────── + /// Query the DHT for peers advertising the target. pub async fn lookup(&self, target: [u8; 32]) -> Result, HyperDhtError> { let replies = self .dht @@ -367,6 +441,7 @@ impl HyperDhtHandle { // ── ANNOUNCE ───────────────────────────────────────────────────────────── + /// Announce this peer under the given target. pub async fn announce( &self, target: [u8; 32], @@ -440,6 +515,7 @@ impl HyperDhtHandle { // ── FIND_PEER ───────────────────────────────────────────────────────────── + /// Return the first peer record found for the target. pub async fn find_peer( &self, target: [u8; 32], @@ -488,6 +564,7 @@ impl HyperDhtHandle { // ── UNANNOUNCE ──────────────────────────────────────────────────────────── + /// Remove a previously announced peer record. pub async fn unannounce( &self, target: [u8; 32], @@ -557,6 +634,7 @@ impl HyperDhtHandle { // ── IMMUTABLE_PUT ──────────────────────────────────────────────────────── + /// Store immutable content under its content hash. pub async fn immutable_put( &self, value: &[u8], @@ -607,6 +685,7 @@ impl HyperDhtHandle { // ── IMMUTABLE_GET ──────────────────────────────────────────────────────── + /// Fetch immutable content by content hash. pub async fn immutable_get( &self, target: [u8; 32], @@ -634,6 +713,7 @@ impl HyperDhtHandle { // ── MUTABLE_PUT ─────────────────────────────────────────────────────────── + /// Store a signed mutable record for the given key pair. pub async fn mutable_put( &self, key_pair: &KeyPair, @@ -700,6 +780,7 @@ impl HyperDhtHandle { // ── MUTABLE_GET ─────────────────────────────────────────────────────────── + /// Fetch and verify a mutable record for the given public key. pub async fn mutable_get( &self, public_key: &[u8; 32], @@ -740,10 +821,12 @@ impl HyperDhtHandle { Ok(None) } + /// Wait until the DHT is bootstrapped. pub async fn bootstrapped(&self) -> Result<(), HyperDhtError> { self.dht.bootstrapped().await.map_err(HyperDhtError::Dht) } + /// Destroy the underlying DHT instance. pub async fn destroy(&self) -> Result<(), HyperDhtError> { self.dht.destroy().await.map_err(HyperDhtError::Dht) } @@ -758,14 +841,17 @@ impl HyperDhtHandle { self.dht.local_port().await.map_err(HyperDhtError::Dht) } + /// Access the shared router state. pub fn router(&self) -> &Arc> { &self.router } + /// Access the underlying DHT handle. pub fn dht(&self) -> &DhtHandle { &self.dht } + /// Mark a target as having a local server available. pub fn register_server(&self, target: &[u8; 32]) { if let Ok(mut router) = self.router.lock() { router.set( @@ -779,18 +865,21 @@ impl HyperDhtHandle { } } + /// Remove the local-server marker for a target. pub fn unregister_server(&self, target: &[u8; 32]) { if let Ok(mut router) = self.router.lock() { router.delete(target); } } + /// Access the server event sender. pub fn server_sender(&self) -> &mpsc::UnboundedSender { &self.server_tx } // ── CONNECT (client-side holepunch orchestration) ───────────────────── + /// Connect to a remote peer using the DHT and relay fallback. pub async fn connect( &self, key_pair: &KeyPair, @@ -1411,7 +1500,9 @@ pub async fn establish_stream( // ── Server-side event handler ───────────────────────────────────────────────── +/// Per-server state for pending handshake and holepunch exchanges. pub struct ServerSession { + /// Cached holepunch secrets indexed by remote public key. holepunch_secrets: std::collections::HashMap<[u8; 32], ServerPeerState>, } @@ -1424,6 +1515,7 @@ struct ServerPeerState { remote_udx: Option, } +/// Run the server-side request loop for peer handshakes and holepunches. pub async fn run_server( mut event_rx: mpsc::UnboundedReceiver, config: ServerConfig, @@ -1622,6 +1714,7 @@ async fn handle_server_holepunch( // ── Spawn ───────────────────────────────────────────────────────────────────── +/// Create a HyperDHT instance and start its background tasks. pub async fn spawn( runtime: &UdxRuntime, config: HyperDhtConfig, diff --git a/peeroxide-dht/src/hyperdht_messages.rs b/peeroxide-dht/src/hyperdht_messages.rs index 2afa195..4bc7585 100644 --- a/peeroxide-dht/src/hyperdht_messages.rs +++ b/peeroxide-dht/src/hyperdht_messages.rs @@ -4,63 +4,95 @@ use crate::compact_encoding::{ }; use crate::messages::Ipv4Peer; +/// Type alias for results returned by encoding and decoding functions in this module. pub type Result = std::result::Result; // ── HyperDHT command IDs ──────────────────────────────────────────────────── +/// Command ID for peer handshake messages. pub const PEER_HANDSHAKE: u64 = 0; +/// Command ID for peer hole-punch messages. pub const PEER_HOLEPUNCH: u64 = 1; +/// Command ID for find-peer queries. pub const FIND_PEER: u64 = 2; +/// Command ID for DHT lookup queries. pub const LOOKUP: u64 = 3; +/// Command ID for announce messages. pub const ANNOUNCE: u64 = 4; +/// Command ID for unannounce messages. pub const UNANNOUNCE: u64 = 5; +/// Command ID for mutable put requests. pub const MUTABLE_PUT: u64 = 6; +/// Command ID for mutable get requests. pub const MUTABLE_GET: u64 = 7; +/// Command ID for immutable put requests. pub const IMMUTABLE_PUT: u64 = 8; +/// Command ID for immutable get requests. pub const IMMUTABLE_GET: u64 = 9; // ── Handshake routing modes ───────────────────────────────────────────────── +/// Routing mode indicating the message originates from the client. pub const MODE_FROM_CLIENT: u64 = 0; +/// Routing mode indicating the message originates from the server. pub const MODE_FROM_SERVER: u64 = 1; +/// Routing mode indicating the message originates from a relay node. pub const MODE_FROM_RELAY: u64 = 2; +/// Routing mode indicating the message originates from a second relay node. pub const MODE_FROM_SECOND_RELAY: u64 = 3; +/// Routing mode indicating this is a reply message. pub const MODE_REPLY: u64 = 4; // ── Firewall constants ────────────────────────────────────────────────────── +/// Firewall state is unknown. pub const FIREWALL_UNKNOWN: u64 = 0; +/// Firewall is open — NAT maps all connections to the same public address and port. pub const FIREWALL_OPEN: u64 = 1; +/// Firewall uses a consistent port mapping for the same destination IP. pub const FIREWALL_CONSISTENT: u64 = 2; +/// Firewall uses random port mapping for each new destination. pub const FIREWALL_RANDOM: u64 = 3; // ── Error constants ───────────────────────────────────────────────────────── +/// No error. pub const ERROR_NONE: u64 = 0; +/// The operation was aborted. pub const ERROR_ABORTED: u64 = 1; +/// Protocol version mismatch between peers. pub const ERROR_VERSION_MISMATCH: u64 = 2; +/// The remote is busy; try again later. pub const ERROR_TRY_LATER: u64 = 3; +/// The sequence number has already been used. pub const ERROR_SEQ_REUSED: u64 = 16; +/// The sequence number is too low. pub const ERROR_SEQ_TOO_LOW: u64 = 17; // ── HyperPeer ─────────────────────────────────────────────────────────────── #[derive(Debug, Clone)] +/// A HyperDHT peer identified by its public key and optional relay addresses. pub struct HyperPeer { + /// The 32-byte Ed25519 public key identifying this peer. pub public_key: [u8; 32], + /// IPv4 relay addresses through which this peer can be reached. pub relay_addresses: Vec, } +/// Pre-encodes a [`HyperPeer`] to calculate the required buffer size. pub fn preencode_hyper_peer(state: &mut State, peer: &HyperPeer) { state.end += 32; preencode_ipv4_peer_array(state, &peer.relay_addresses); } +/// Encodes a [`HyperPeer`] into the compact-encoding buffer. pub fn encode_hyper_peer(state: &mut State, peer: &HyperPeer) -> Result<()> { encode_fixed32(state, &peer.public_key); encode_ipv4_peer_array(state, &peer.relay_addresses) } +/// Decodes a [`HyperPeer`] from the compact-encoding buffer. pub fn decode_hyper_peer(state: &mut State) -> Result { let public_key = decode_fixed32(state)?; let relay_addresses = decode_ipv4_peer_array(state)?; @@ -70,6 +102,7 @@ pub fn decode_hyper_peer(state: &mut State) -> Result { }) } +/// Serializes a [`HyperPeer`] to a byte vector. pub fn encode_hyper_peer_to_bytes(peer: &HyperPeer) -> Result> { let mut state = State::new(); preencode_hyper_peer(&mut state, peer); @@ -78,6 +111,7 @@ pub fn encode_hyper_peer_to_bytes(peer: &HyperPeer) -> Result> { Ok(state.buffer) } +/// Deserializes a [`HyperPeer`] from bytes. pub fn decode_hyper_peer_from_bytes(buf: &[u8]) -> Result { let mut state = State::from_buffer(buf); decode_hyper_peer(&mut state) @@ -91,13 +125,19 @@ const FLAG_SIGNATURE: u64 = 0x4; const FLAG_BUMP: u64 = 0x8; #[derive(Debug, Clone)] +/// Wire message for announcing a peer on the DHT. pub struct AnnounceMessage { + /// The peer being announced, if present. pub peer: Option, + /// A 32-byte refresh token, if present. pub refresh: Option<[u8; 32]>, + /// A 64-byte signature over the announcement, if present. pub signature: Option<[u8; 64]>, + /// Bump counter used to force re-announcement. pub bump: u64, } +/// Pre-encodes an [`AnnounceMessage`] to calculate the required buffer size. pub fn preencode_announce(state: &mut State, m: &AnnounceMessage) { state.end += 1; // flags byte (always fits in 1 byte, max 15) if let Some(peer) = &m.peer { @@ -114,6 +154,7 @@ pub fn preencode_announce(state: &mut State, m: &AnnounceMessage) { } } +/// Encodes an [`AnnounceMessage`] into the compact-encoding buffer. pub fn encode_announce(state: &mut State, m: &AnnounceMessage) -> Result<()> { let flags = (if m.peer.is_some() { FLAG_PEER } else { 0 }) | (if m.refresh.is_some() { FLAG_REFRESH } else { 0 }) @@ -135,6 +176,7 @@ pub fn encode_announce(state: &mut State, m: &AnnounceMessage) -> Result<()> { Ok(()) } +/// Decodes an [`AnnounceMessage`] from the compact-encoding buffer. pub fn decode_announce(state: &mut State) -> Result { let flags = decode_uint(state)?; let peer = if flags & FLAG_PEER != 0 { @@ -165,6 +207,7 @@ pub fn decode_announce(state: &mut State) -> Result { }) } +/// Serializes an [`AnnounceMessage`] to a byte vector. pub fn encode_announce_to_bytes(m: &AnnounceMessage) -> Result> { let mut state = State::new(); preencode_announce(&mut state, m); @@ -173,6 +216,7 @@ pub fn encode_announce_to_bytes(m: &AnnounceMessage) -> Result> { Ok(state.buffer) } +/// Deserializes an [`AnnounceMessage`] from bytes. pub fn decode_announce_from_bytes(buf: &[u8]) -> Result { let mut state = State::from_buffer(buf); decode_announce(&mut state) @@ -181,11 +225,15 @@ pub fn decode_announce_from_bytes(buf: &[u8]) -> Result { // ── LookupRawReply ────────────────────────────────────────────────────────── #[derive(Debug, Clone)] +/// Raw reply message returned from a DHT lookup containing matching peers. pub struct LookupRawReply { + /// The list of peers returned by the lookup. pub peers: Vec, + /// Bump counter echoed from the corresponding announce. pub bump: u64, } +/// Pre-encodes a [`LookupRawReply`] to calculate the required buffer size. pub fn preencode_lookup_raw_reply(state: &mut State, m: &LookupRawReply) { preencode_uint(state, m.peers.len() as u64); for peer in &m.peers { @@ -194,6 +242,7 @@ pub fn preencode_lookup_raw_reply(state: &mut State, m: &LookupRawReply) { preencode_uint(state, m.bump); } +/// Encodes a [`LookupRawReply`] into the compact-encoding buffer. pub fn encode_lookup_raw_reply(state: &mut State, m: &LookupRawReply) -> Result<()> { encode_uint(state, m.peers.len() as u64); for peer in &m.peers { @@ -203,6 +252,7 @@ pub fn encode_lookup_raw_reply(state: &mut State, m: &LookupRawReply) -> Result< Ok(()) } +/// Decodes a [`LookupRawReply`] from the compact-encoding buffer. pub fn decode_lookup_raw_reply(state: &mut State) -> Result { let count = decode_uint(state)? as usize; let mut peers = Vec::with_capacity(count); @@ -217,6 +267,7 @@ pub fn decode_lookup_raw_reply(state: &mut State) -> Result { Ok(LookupRawReply { peers, bump }) } +/// Serializes a [`LookupRawReply`] to a byte vector. pub fn encode_lookup_raw_reply_to_bytes(m: &LookupRawReply) -> Result> { let mut state = State::new(); preencode_lookup_raw_reply(&mut state, m); @@ -225,6 +276,7 @@ pub fn encode_lookup_raw_reply_to_bytes(m: &LookupRawReply) -> Result> { Ok(state.buffer) } +/// Deserializes a [`LookupRawReply`] from bytes. pub fn decode_lookup_raw_reply_from_bytes(buf: &[u8]) -> Result { let mut state = State::from_buffer(buf); decode_lookup_raw_reply(&mut state) @@ -233,13 +285,19 @@ pub fn decode_lookup_raw_reply_from_bytes(buf: &[u8]) -> Result // ── MutablePutRequest ─────────────────────────────────────────────────────── #[derive(Debug, Clone, PartialEq, Eq)] +/// Request to store a mutable value on the DHT. pub struct MutablePutRequest { + /// The 32-byte public key of the value's owner. pub public_key: [u8; 32], + /// Monotonically increasing sequence number for this value. pub seq: u64, + /// The value payload to store. pub value: Vec, + /// A 64-byte Ed25519 signature over `(seq, value)`. pub signature: [u8; 64], } +/// Pre-encodes a [`MutablePutRequest`] to calculate the required buffer size. pub fn preencode_mutable_put_request(state: &mut State, m: &MutablePutRequest) { state.end += 32; preencode_uint(state, m.seq); @@ -247,6 +305,7 @@ pub fn preencode_mutable_put_request(state: &mut State, m: &MutablePutRequest) { state.end += 64; } +/// Encodes a [`MutablePutRequest`] into the compact-encoding buffer. pub fn encode_mutable_put_request(state: &mut State, m: &MutablePutRequest) -> Result<()> { encode_fixed32(state, &m.public_key); encode_uint(state, m.seq); @@ -255,6 +314,7 @@ pub fn encode_mutable_put_request(state: &mut State, m: &MutablePutRequest) -> R Ok(()) } +/// Decodes a [`MutablePutRequest`] from the compact-encoding buffer. pub fn decode_mutable_put_request(state: &mut State) -> Result { let public_key = decode_fixed32(state)?; let seq = decode_uint(state)?; @@ -268,6 +328,7 @@ pub fn decode_mutable_put_request(state: &mut State) -> Result Result> { let mut state = State::new(); preencode_mutable_put_request(&mut state, m); @@ -276,6 +337,7 @@ pub fn encode_mutable_put_request_to_bytes(m: &MutablePutRequest) -> Result Result { let mut state = State::from_buffer(buf); decode_mutable_put_request(&mut state) @@ -284,18 +346,24 @@ pub fn decode_mutable_put_request_from_bytes(buf: &[u8]) -> Result, + /// A 64-byte Ed25519 signature over `(seq, value)`. pub signature: [u8; 64], } +/// Pre-encodes a [`MutableGetResponse`] to calculate the required buffer size. pub fn preencode_mutable_get_response(state: &mut State, m: &MutableGetResponse) { preencode_uint(state, m.seq); compact_encoding::preencode_buffer(state, Some(&m.value)); state.end += 64; } +/// Encodes a [`MutableGetResponse`] into the compact-encoding buffer. pub fn encode_mutable_get_response(state: &mut State, m: &MutableGetResponse) -> Result<()> { encode_uint(state, m.seq); compact_encoding::encode_buffer(state, Some(&m.value)); @@ -303,6 +371,7 @@ pub fn encode_mutable_get_response(state: &mut State, m: &MutableGetResponse) -> Ok(()) } +/// Decodes a [`MutableGetResponse`] from the compact-encoding buffer. pub fn decode_mutable_get_response(state: &mut State) -> Result { let seq = decode_uint(state)?; let value = compact_encoding::decode_buffer(state)?.unwrap_or_default(); @@ -314,6 +383,7 @@ pub fn decode_mutable_get_response(state: &mut State) -> Result Result> { let mut state = State::new(); preencode_mutable_get_response(&mut state, m); @@ -322,6 +392,7 @@ pub fn encode_mutable_get_response_to_bytes(m: &MutableGetResponse) -> Result Result { let mut state = State::from_buffer(buf); decode_mutable_get_response(&mut state) @@ -330,28 +401,35 @@ pub fn decode_mutable_get_response_from_bytes(buf: &[u8]) -> Result, } +/// Pre-encodes a [`MutableSignable`] to calculate the required buffer size. pub fn preencode_mutable_signable(state: &mut State, m: &MutableSignable) { preencode_uint(state, m.seq); compact_encoding::preencode_buffer(state, Some(&m.value)); } +/// Encodes a [`MutableSignable`] into the compact-encoding buffer. pub fn encode_mutable_signable(state: &mut State, m: &MutableSignable) -> Result<()> { encode_uint(state, m.seq); compact_encoding::encode_buffer(state, Some(&m.value)); Ok(()) } +/// Decodes a [`MutableSignable`] from the compact-encoding buffer. pub fn decode_mutable_signable(state: &mut State) -> Result { let seq = decode_uint(state)?; let value = compact_encoding::decode_buffer(state)?.unwrap_or_default(); Ok(MutableSignable { seq, value }) } +/// Serializes a [`MutableSignable`] to a byte vector. pub fn encode_mutable_signable_to_bytes(m: &MutableSignable) -> Result> { let mut state = State::new(); preencode_mutable_signable(&mut state, m); @@ -360,6 +438,7 @@ pub fn encode_mutable_signable_to_bytes(m: &MutableSignable) -> Result> Ok(state.buffer) } +/// Deserializes a [`MutableSignable`] from bytes. pub fn decode_mutable_signable_from_bytes(buf: &[u8]) -> Result { let mut state = State::from_buffer(buf); decode_mutable_signable(&mut state) @@ -422,13 +501,19 @@ fn decode_ipv6_peer_array(state: &mut State) -> Result> { // ── HandshakeMessage (PEER_HANDSHAKE wire format) ─────────────────────────── #[derive(Debug, Clone, PartialEq, Eq)] +/// Wire message exchanged during the PEER_HANDSHAKE phase. pub struct HandshakeMessage { + /// Routing mode for this handshake (one of the `MODE_*` constants). pub mode: u64, + /// Raw Noise protocol handshake bytes. pub noise: Vec, + /// Optional IPv4 address of the connecting peer. pub peer_address: Option, + /// Optional IPv4 address of the relay through which this message is routed. pub relay_address: Option, } +/// Pre-encodes a [`HandshakeMessage`] to calculate the required buffer size. pub fn preencode_handshake(state: &mut State, m: &HandshakeMessage) { preencode_uint(state, 0); // flags preencode_uint(state, m.mode); @@ -441,6 +526,7 @@ pub fn preencode_handshake(state: &mut State, m: &HandshakeMessage) { } } +/// Encodes a [`HandshakeMessage`] into the compact-encoding buffer. pub fn encode_handshake(state: &mut State, m: &HandshakeMessage) -> Result<()> { let flags = (if m.peer_address.is_some() { 1u64 } else { 0 }) | (if m.relay_address.is_some() { 2 } else { 0 }); @@ -456,6 +542,7 @@ pub fn encode_handshake(state: &mut State, m: &HandshakeMessage) -> Result<()> { Ok(()) } +/// Decodes a [`HandshakeMessage`] from the compact-encoding buffer. pub fn decode_handshake(state: &mut State) -> Result { let flags = decode_uint(state)?; let mode = decode_uint(state)?; @@ -480,6 +567,7 @@ pub fn decode_handshake(state: &mut State) -> Result { }) } +/// Serializes a [`HandshakeMessage`] to a byte vector. pub fn encode_handshake_to_bytes(m: &HandshakeMessage) -> Result> { let mut state = State::new(); preencode_handshake(&mut state, m); @@ -488,6 +576,7 @@ pub fn encode_handshake_to_bytes(m: &HandshakeMessage) -> Result> { Ok(state.buffer) } +/// Deserializes a [`HandshakeMessage`] from bytes. pub fn decode_handshake_from_bytes(buf: &[u8]) -> Result { let mut state = State::from_buffer(buf); decode_handshake(&mut state) @@ -496,13 +585,19 @@ pub fn decode_handshake_from_bytes(buf: &[u8]) -> Result { // ── HolepunchMessage (PEER_HOLEPUNCH wire format) ─────────────────────────── #[derive(Debug, Clone, PartialEq, Eq)] +/// Wire message exchanged during the PEER_HOLEPUNCH phase. pub struct HolepunchMessage { + /// Routing mode for this holepunch message (one of the `MODE_*` constants). pub mode: u64, + /// Session identifier for this holepunch attempt. pub id: u64, + /// Encrypted holepunch payload bytes. pub payload: Vec, + /// Optional IPv4 address of the peer involved in the holepunch. pub peer_address: Option, } +/// Pre-encodes a [`HolepunchMessage`] to calculate the required buffer size. pub fn preencode_holepunch_msg(state: &mut State, m: &HolepunchMessage) { preencode_uint(state, 0); // flags preencode_uint(state, m.mode); @@ -513,6 +608,7 @@ pub fn preencode_holepunch_msg(state: &mut State, m: &HolepunchMessage) { } } +/// Encodes a [`HolepunchMessage`] into the compact-encoding buffer. pub fn encode_holepunch_msg(state: &mut State, m: &HolepunchMessage) -> Result<()> { let flags: u64 = if m.peer_address.is_some() { 1 } else { 0 }; encode_uint(state, flags); @@ -525,6 +621,7 @@ pub fn encode_holepunch_msg(state: &mut State, m: &HolepunchMessage) -> Result<( Ok(()) } +/// Decodes a [`HolepunchMessage`] from the compact-encoding buffer. pub fn decode_holepunch_msg(state: &mut State) -> Result { let flags = decode_uint(state)?; let mode = decode_uint(state)?; @@ -544,6 +641,7 @@ pub fn decode_holepunch_msg(state: &mut State) -> Result { }) } +/// Serializes a [`HolepunchMessage`] to a byte vector. pub fn encode_holepunch_msg_to_bytes(m: &HolepunchMessage) -> Result> { let mut state = State::new(); preencode_holepunch_msg(&mut state, m); @@ -552,6 +650,7 @@ pub fn encode_holepunch_msg_to_bytes(m: &HolepunchMessage) -> Result> { Ok(state.buffer) } +/// Deserializes a [`HolepunchMessage`] from bytes. pub fn decode_holepunch_msg_from_bytes(buf: &[u8]) -> Result { let mut state = State::from_buffer(buf); decode_holepunch_msg(&mut state) @@ -560,8 +659,11 @@ pub fn decode_holepunch_msg_from_bytes(buf: &[u8]) -> Result { // ── RelayInfo ─────────────────────────────────────────────────────────────── #[derive(Debug, Clone, PartialEq, Eq)] +/// A pair of addresses describing a relay-assisted path between two peers. pub struct RelayInfo { + /// The IPv4 address of the relay node. pub relay_address: Ipv4Peer, + /// The IPv4 address of the remote peer as seen by the relay. pub peer_address: Ipv4Peer, } @@ -620,8 +722,11 @@ fn decode_relay_info_array(state: &mut State) -> Result> { // ── HolepunchInfo ─────────────────────────────────────────────────────────── #[derive(Debug, Clone, PartialEq, Eq)] +/// Metadata for an ongoing hole-punch session, including relay candidates. pub struct HolepunchInfo { + /// Unique session identifier for this hole-punch attempt. pub id: u64, + /// List of relay-peer address pairs available for this hole-punch. pub relays: Vec, } @@ -645,10 +750,15 @@ fn decode_holepunch_info(state: &mut State) -> Result { // ── UdxInfo ───────────────────────────────────────────────────────────────── #[derive(Debug, Clone, PartialEq, Eq)] +/// UDX transport parameters exchanged during the Noise handshake. pub struct UdxInfo { + /// UDX protocol version advertised by the sender. pub version: u64, + /// Whether the sender's UDP socket can be reused across connections. pub reusable_socket: bool, + /// Local UDX stream identifier chosen by the sender. pub id: u64, + /// Initial sequence number for the UDX stream. pub seq: u64, } @@ -682,7 +792,9 @@ fn decode_udx_info(state: &mut State) -> Result { // ── SecretStreamInfo ──────────────────────────────────────────────────────── #[derive(Debug, Clone, PartialEq, Eq)] +/// Secret stream parameters exchanged during the Noise handshake. pub struct SecretStreamInfo { + /// Secret stream protocol version advertised by the sender. pub version: u64, } @@ -703,9 +815,13 @@ fn decode_secret_stream_info(state: &mut State) -> Result { // ── RelayThroughInfo ──────────────────────────────────────────────────────── #[derive(Debug, Clone, PartialEq, Eq)] +/// Information about a relay node through which a connection should be routed. pub struct RelayThroughInfo { + /// Protocol version for the relay-through capability. pub version: u64, + /// 32-byte public key of the relay node. pub public_key: [u8; 32], + /// 32-byte authorization token for connecting through the relay. pub token: [u8; 32], } @@ -738,16 +854,27 @@ fn decode_relay_through_info(state: &mut State) -> Result { // ── NoisePayload (exchanged inside Noise handshake) ───────────────────────── #[derive(Debug, Clone, PartialEq, Eq)] +/// Payload carried inside the Noise protocol handshake messages. pub struct NoisePayload { + /// Protocol version of this payload (must be `1` for valid messages). pub version: u64, + /// Error code from the sender (one of the `ERROR_*` constants). pub error: u64, + /// Firewall type reported by the sender (one of the `FIREWALL_*` constants). pub firewall: u64, + /// Optional hole-punch session info, present when hole-punching is in progress. pub holepunch: Option, + /// IPv4 addresses the sender is reachable on. pub addresses4: Vec, + /// IPv6 addresses the sender is reachable on. pub addresses6: Vec, + /// Optional UDX transport parameters. pub udx: Option, + /// Optional secret stream parameters. pub secret_stream: Option, + /// Optional relay-through info for relayed connections. pub relay_through: Option, + /// Optional list of relay IPv4 addresses available for this peer. pub relay_addresses: Option>, } @@ -759,6 +886,7 @@ const NP_FLAG_SECRET_STREAM: u64 = 16; const NP_FLAG_RELAY_THROUGH: u64 = 32; const NP_FLAG_RELAY_ADDRESSES: u64 = 64; +/// Pre-encodes a [`NoisePayload`] to calculate the required buffer size. pub fn preencode_noise_payload(state: &mut State, m: &NoisePayload) { state.end += 4; // version + flags + error + firewall (each 1 byte for small values) if let Some(hp) = &m.holepunch { @@ -784,6 +912,7 @@ pub fn preencode_noise_payload(state: &mut State, m: &NoisePayload) { } } +/// Encodes a [`NoisePayload`] into the compact-encoding buffer. pub fn encode_noise_payload(state: &mut State, m: &NoisePayload) -> Result<()> { let mut flags = 0u64; if m.holepunch.is_some() { @@ -837,6 +966,7 @@ pub fn encode_noise_payload(state: &mut State, m: &NoisePayload) -> Result<()> { Ok(()) } +/// Decodes a [`NoisePayload`] from the compact-encoding buffer. pub fn decode_noise_payload(state: &mut State) -> Result { let version = decode_uint(state)?; if version != 1 { @@ -907,6 +1037,7 @@ pub fn decode_noise_payload(state: &mut State) -> Result { }) } +/// Serializes a [`NoisePayload`] to a byte vector. pub fn encode_noise_payload_to_bytes(m: &NoisePayload) -> Result> { let mut state = State::new(); preencode_noise_payload(&mut state, m); @@ -915,6 +1046,7 @@ pub fn encode_noise_payload_to_bytes(m: &NoisePayload) -> Result> { Ok(state.buffer) } +/// Deserializes a [`NoisePayload`] from bytes. pub fn decode_noise_payload_from_bytes(buf: &[u8]) -> Result { let mut state = State::from_buffer(buf); decode_noise_payload(&mut state) @@ -923,15 +1055,25 @@ pub fn decode_noise_payload_from_bytes(buf: &[u8]) -> Result { // ── HolepunchPayload (encrypted, exchanged during hole-punch rounds) ──────── #[derive(Debug, Clone, PartialEq, Eq)] +/// Encrypted payload exchanged between peers during each hole-punch round. pub struct HolepunchPayload { + /// Error code from the sender (one of the `ERROR_*` constants). pub error: u64, + /// Firewall type reported by the sender (one of the `FIREWALL_*` constants). pub firewall: u64, + /// Current hole-punch round number. pub round: u64, + /// Whether the sender has already established a direct connection. pub connected: bool, + /// Whether the sender is actively sending hole-punch packets. pub punching: bool, + /// List of local IPv4 addresses the sender is reachable on, if present. pub addresses: Option>, + /// The remote address the sender is trying to punch to, if known. pub remote_address: Option, + /// A 32-byte token sent by the initiator to prove liveness. pub token: Option<[u8; 32]>, + /// A 32-byte token sent by the remote peer to prove liveness. pub remote_token: Option<[u8; 32]>, } @@ -942,6 +1084,7 @@ const HP_FLAG_REMOTE_ADDRESS: u64 = 8; const HP_FLAG_TOKEN: u64 = 16; const HP_FLAG_REMOTE_TOKEN: u64 = 32; +/// Pre-encodes a [`HolepunchPayload`] to calculate the required buffer size. pub fn preencode_holepunch_payload(state: &mut State, m: &HolepunchPayload) { state.end += 4; // flags + error + firewall + round if let Some(addrs) = &m.addresses { @@ -958,6 +1101,7 @@ pub fn preencode_holepunch_payload(state: &mut State, m: &HolepunchPayload) { } } +/// Encodes a [`HolepunchPayload`] into the compact-encoding buffer. pub fn encode_holepunch_payload(state: &mut State, m: &HolepunchPayload) -> Result<()> { let mut flags = 0u64; if m.connected { @@ -999,6 +1143,7 @@ pub fn encode_holepunch_payload(state: &mut State, m: &HolepunchPayload) -> Resu Ok(()) } +/// Decodes a [`HolepunchPayload`] from the compact-encoding buffer. pub fn decode_holepunch_payload(state: &mut State) -> Result { let flags = decode_uint(state)?; let error = decode_uint(state)?; @@ -1040,6 +1185,7 @@ pub fn decode_holepunch_payload(state: &mut State) -> Result { }) } +/// Serializes a [`HolepunchPayload`] to a byte vector. pub fn encode_holepunch_payload_to_bytes(m: &HolepunchPayload) -> Result> { let mut state = State::new(); preencode_holepunch_payload(&mut state, m); @@ -1048,6 +1194,7 @@ pub fn encode_holepunch_payload_to_bytes(m: &HolepunchPayload) -> Result Ok(state.buffer) } +/// Deserializes a [`HolepunchPayload`] from bytes. pub fn decode_holepunch_payload_from_bytes(buf: &[u8]) -> Result { let mut state = State::from_buffer(buf); decode_holepunch_payload(&mut state) diff --git a/peeroxide-dht/src/lib.rs b/peeroxide-dht/src/lib.rs index 086e0ca..6bb1f01 100644 --- a/peeroxide-dht/src/lib.rs +++ b/peeroxide-dht/src/lib.rs @@ -1,4 +1,5 @@ #![forbid(unsafe_code)] +#![deny(missing_docs)] //! Rust port of [HyperDHT](https://github.com/holepunchto/hyperdht) — a //! Kademlia distributed hash table with NAT hole-punching, Noise-encrypted @@ -56,16 +57,30 @@ #![deny(clippy::all)] +/// Blind relay for proxying encrypted traffic between peers behind restrictive NATs. pub mod blind_relay; +/// Compact binary encoding primitives compatible with the +/// [compact-encoding](https://github.com/holepunchto/compact-encoding) wire format. pub mod compact_encoding; +/// BLAKE2b hashing, Ed25519 signing, and namespace derivation helpers. pub mod crypto; +/// High-level HyperDHT node: peer discovery, announce/unannounce, mutable/immutable +/// storage, and Noise-encrypted connections. pub mod hyperdht; +/// Wire-format message types for HyperDHT peer handshake, holepunch, and relay +/// operations. pub mod hyperdht_messages; +/// DHT RPC request/response message encoding and decoding. pub mod messages; +/// Noise IK handshake for establishing shared secrets between peers. pub mod noise; +/// Noise handshake wrapper that adds framing and key splitting for stream encryption. pub mod noise_wrap; +/// Lightweight multiplexer for running multiple channels over a single connection. pub mod protomux; +/// DHT RPC transport layer: request dispatch, reply handling, and node communication. pub mod rpc; +/// Noise-encrypted bidirectional byte stream over any `AsyncRead + AsyncWrite` transport. pub mod secret_stream; // Internal protocol modules — public for advanced use but hidden from diff --git a/peeroxide-dht/src/messages.rs b/peeroxide-dht/src/messages.rs index c30750a..e9baeca 100644 --- a/peeroxide-dht/src/messages.rs +++ b/peeroxide-dht/src/messages.rs @@ -2,15 +2,22 @@ use crate::compact_encoding::{self, EncodingError, State}; use crate::peer::NodeId; #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] +/// DHT RPC command kinds. pub enum Command { + /// Pings a peer. Ping = 0, + /// Pings a peer over NAT. PingNat = 1, + /// Finds the closest nodes to a target. FindNode = 2, + /// Reports a down-hint for a peer. DownHint = 3, + /// Sends a delayed ping. DelayedPing = 4, } impl Command { + /// Converts a numeric code into a [`Command`]. pub fn from_u64(v: u64) -> Option { match v { 0 => Some(Self::Ping), @@ -34,37 +41,60 @@ const FLAG_TARGET_OR_ERROR: u8 = 0b0000_1000; const FLAG_VALUE: u8 = 0b0001_0000; #[derive(Debug, Clone, PartialEq, Eq)] +/// An IPv4 peer address. pub struct Ipv4Peer { + /// The peer host. pub host: String, + /// The peer port. pub port: u16, } #[derive(Debug, Clone)] +/// A DHT request message. pub struct Request { + /// The transaction identifier. pub tid: u16, + /// The destination peer. pub to: Ipv4Peer, + /// The optional sender id. pub id: Option, + /// The optional request token. pub token: Option<[u8; 32]>, + /// Whether the request is internal. pub internal: bool, + /// The numeric command code. pub command: u64, + /// The optional target node id. pub target: Option, + /// The optional payload value. pub value: Option>, } #[derive(Debug, Clone)] +/// A DHT response message. pub struct Response { + /// The transaction identifier. pub tid: u16, + /// The destination peer. pub to: Ipv4Peer, + /// The optional sender id. pub id: Option, + /// The optional response token. pub token: Option<[u8; 32]>, + /// The closer peers returned by the lookup. pub closer_nodes: Vec, + /// The response error code. pub error: u64, + /// The optional payload value. pub value: Option>, } #[derive(Debug, Clone)] +/// A decoded DHT wire message. pub enum Message { + /// A request message. Request(Request), + /// A response message. Response(Response), } @@ -112,6 +142,7 @@ fn decode_ipv4_array(state: &mut State) -> Result, EncodingError> Ok(peers) } +/// Pre-encodes a [`Request`] to calculate the required buffer size. pub fn preencode_request(state: &mut State, req: &Request) { state.end += 1; // type_version state.end += 1; // flags @@ -133,6 +164,7 @@ pub fn preencode_request(state: &mut State, req: &Request) { } } +/// Encodes a [`Request`] into the compact-encoding buffer. pub fn encode_request(state: &mut State, req: &Request) -> Result<(), EncodingError> { compact_encoding::encode_uint8(state, REQUEST_ID); @@ -174,6 +206,7 @@ pub fn encode_request(state: &mut State, req: &Request) -> Result<(), EncodingEr Ok(()) } +/// Pre-encodes a [`Response`] to calculate the required buffer size. pub fn preencode_response(state: &mut State, res: &Response) { state.end += 1; // type_version state.end += 1; // flags @@ -197,6 +230,7 @@ pub fn preencode_response(state: &mut State, res: &Response) { } } +/// Encodes a [`Response`] into the compact-encoding buffer. pub fn encode_response(state: &mut State, res: &Response) -> Result<(), EncodingError> { compact_encoding::encode_uint8(state, RESPONSE_ID); @@ -240,6 +274,7 @@ pub fn encode_response(state: &mut State, res: &Response) -> Result<(), Encoding Ok(()) } +/// Decodes a [`Message`] from the compact-encoding buffer. pub fn decode_message(buf: &[u8]) -> Result { if buf.is_empty() { return Err(EncodingError::OutOfBounds { @@ -333,6 +368,7 @@ pub fn decode_message(buf: &[u8]) -> Result { } } +/// Serializes a [`Request`] to a byte vector. pub fn encode_request_to_bytes(req: &Request) -> Result, EncodingError> { let mut state = State::new(); preencode_request(&mut state, req); @@ -341,6 +377,7 @@ pub fn encode_request_to_bytes(req: &Request) -> Result, EncodingError> Ok(state.buffer) } +/// Serializes a [`Response`] to a byte vector. pub fn encode_response_to_bytes(res: &Response) -> Result, EncodingError> { let mut state = State::new(); preencode_response(&mut state, res); diff --git a/peeroxide-dht/src/noise.rs b/peeroxide-dht/src/noise.rs index 7e4f661..3e20669 100644 --- a/peeroxide-dht/src/noise.rs +++ b/peeroxide-dht/src/noise.rs @@ -31,14 +31,19 @@ const PROTOCOL_NAME_IK: &[u8] = b"Noise_IK_Ed25519_ChaChaPoly_BLAKE2b"; // ─── Error type ────────────────────────────────────────────────────────────── +/// Errors that can occur during a Noise protocol handshake. #[derive(Debug, thiserror::Error)] pub enum NoiseError { + /// The provided public key could not be decompressed to a valid curve point. #[error("invalid public key")] InvalidPublicKey, + /// AEAD decryption or authentication failed. #[error("decryption failed")] DecryptionFailed, + /// A send or receive was attempted after the handshake already completed. #[error("handshake already complete")] HandshakeComplete, + /// The handshake state machine received a message out of order. #[error("unexpected handshake state")] UnexpectedState, } @@ -298,6 +303,7 @@ impl SymmetricState { /// Ed25519 keypair in libsodium 64-byte format: `seed[32] || pubkey[32]`. #[derive(Clone)] pub struct Keypair { + /// Ed25519 public key (32-byte compressed Edwards Y point). pub public_key: [u8; 32], /// 64-byte libsodium format: first 32 bytes are the seed, last 32 are the /// compressed Edwards Y public key. @@ -669,14 +675,17 @@ impl HandshakeIK { self.e = Some(keypair); } + /// Returns true after the handshake has completed. pub fn complete(&self) -> bool { self.result.is_some() } + /// Access the handshake result (available once [`complete`](Self::complete) returns true). pub fn result(&self) -> Option<&HandshakeResult> { self.result.as_ref() } + /// Returns the remote peer's static public key, if known. pub fn remote_static_key(&self) -> Option<&[u8; 32]> { self.rs.as_ref() } diff --git a/peeroxide-dht/src/noise_wrap.rs b/peeroxide-dht/src/noise_wrap.rs index 44cf16f..b6c95a7 100644 --- a/peeroxide-dht/src/noise_wrap.rs +++ b/peeroxide-dht/src/noise_wrap.rs @@ -1,6 +1,6 @@ //! NoiseWrap — IK-pattern handshake with typed payload encoding. //! -//! Combines [`HandshakeIK`] with [`NoisePayload`] encoding/decoding, +//! Combines [`HandshakeIK`](crate::noise::HandshakeIK) with [`NoisePayload`](crate::hyperdht_messages::NoisePayload) encoding/decoding, //! and derives the `holepunch_secret` after finalisation. //! //! Reference: `hyperdht/lib/noise-wrap.js`. @@ -20,14 +20,18 @@ type Blake2bMac256 = Blake2bMac; // ─── Error type ────────────────────────────────────────────────────────────── +/// Errors from the [`NoiseWrap`] handshake layer. #[derive(Debug, thiserror::Error)] pub enum NoiseWrapError { + /// The underlying Noise IK handshake failed. #[error("noise handshake failed: {0}")] Noise(#[from] crate::noise::NoiseError), + /// Payload compact-encoding or decoding failed. #[error("payload encoding failed: {0}")] Encoding(#[from] crate::compact_encoding::EncodingError), + /// [`NoiseWrap::finalize`] was called before both messages were exchanged. #[error("handshake not yet complete")] NotComplete, } diff --git a/peeroxide-dht/src/protomux.rs b/peeroxide-dht/src/protomux.rs index e0a6398..8ee57c7 100644 --- a/peeroxide-dht/src/protomux.rs +++ b/peeroxide-dht/src/protomux.rs @@ -68,26 +68,34 @@ const MAX_BATCH: usize = 8 * 1024 * 1024; // ── Errors ─────────────────────────────────────────────────────────────────── #[derive(Debug, Error)] +/// Errors returned by the Protomux encoder, decoder, and channel runtime. pub enum ProtomuxError { + /// Encoding failed while serializing a frame. #[error("encoding error: {0}")] Encoding(#[from] c::EncodingError), + /// The underlying framed stream was closed. #[error("stream closed")] StreamClosed, + /// The incoming frame was malformed. #[error("invalid frame: {0}")] InvalidFrame(String), + /// The channel was already closed. #[error("channel closed")] ChannelClosed, + /// No local channel exists for the requested ID. #[error("channel not found: local_id={0}")] ChannelNotFound(u64), + /// An internal mux invariant was violated. #[error("internal error: {0}")] Internal(String), } +/// Result type used by Protomux operations. pub type Result = std::result::Result; // ── Frame encoding ─────────────────────────────────────────────────────────── @@ -218,16 +226,25 @@ pub fn encode_batch(entries: &[(u64, Vec)]) -> Vec { /// Decoded control frame. #[derive(Debug, Clone, PartialEq)] pub enum ControlFrame { + /// Opened channel metadata from the remote peer. Open { + /// Remote local channel ID. local_id: u64, + /// Protocol name. protocol: String, + /// Optional sub-channel identifier. id: Option>, + /// Optional handshake payload. handshake_state: Option>, }, + /// Remote requested channel closure. Close { + /// Remote local channel ID. local_id: u64, }, + /// Remote rejected the channel. Reject { + /// Remote channel ID. remote_id: u64, }, } @@ -235,7 +252,9 @@ pub enum ControlFrame { /// A single item from a decoded batch. #[derive(Debug, Clone, PartialEq)] pub struct BatchItem { + /// Channel ID that the batched message belongs to. pub channel_id: u64, + /// Raw inner batch payload. pub data: Vec, } @@ -248,8 +267,11 @@ pub enum DecodedFrame { Batch(Vec), /// A single data message on a channel. Message { + /// Channel ID. channel_id: u64, + /// Message type index. message_type: u64, + /// Message payload. payload: Vec, }, } @@ -361,15 +383,19 @@ pub trait FramedStream: Send + 'static { pub enum ChannelEvent { /// Remote opened the channel (with optional handshake data). Opened { + /// Optional handshake payload from the remote peer. handshake: Option>, }, /// Received a message on this channel. Message { + /// Message type index. message_type: u32, + /// Message payload. data: Vec, }, /// Channel was closed. Closed { + /// `true` if the remote peer initiated the close. is_remote: bool, }, } diff --git a/peeroxide-dht/src/rpc.rs b/peeroxide-dht/src/rpc.rs index aacc900..d77dc34 100644 --- a/peeroxide-dht/src/rpc.rs +++ b/peeroxide-dht/src/rpc.rs @@ -40,15 +40,21 @@ const ERR_UNKNOWN_COMMAND: u64 = 1; // ── Error ───────────────────────────────────────────────────────────────────── #[derive(Debug, Error)] +/// Errors returned by [`DhtHandle`] and [`spawn`]. pub enum DhtError { + /// Underlying I/O failed. #[error("IO error: {0}")] Io(#[from] crate::io::IoError), + /// The DHT node has been destroyed. #[error("node destroyed")] Destroyed, + /// The internal command channel is closed. #[error("command channel closed")] ChannelClosed, + /// Bootstrapping did not complete successfully. #[error("bootstrap failed")] BootstrapFailed, + /// A request failed with the given message. #[error("request failed: {0}")] RequestFailed(String), } @@ -58,12 +64,19 @@ pub enum DhtError { /// Configuration for creating a DHT node. #[derive(Debug, Clone)] pub struct DhtConfig { + /// Bootstrap node addresses. pub bootstrap: Vec, + /// Local port to bind. pub port: u16, + /// Local host to bind. pub host: String, + /// Whether to force ephemeral mode. pub ephemeral: Option, + /// Whether to advertise as firewalled. pub firewalled: bool, + /// Query concurrency limit. pub concurrency: usize, + /// Maximum query window size. pub max_window: usize, } @@ -82,57 +95,89 @@ impl Default for DhtConfig { } #[derive(Debug, Clone)] +/// Response to a ping request. pub struct PingResponse { + /// Remote peer that replied. pub from: Ipv4Peer, + /// Optional peer id reported by the remote node. pub id: Option, + /// Round-trip time for the ping. pub rtt: Duration, } #[derive(Debug, Clone)] +/// Data returned from a DHT request. pub struct ResponseData { + /// Remote peer that replied. pub from: Ipv4Peer, + /// Optional peer id reported by the remote node. pub id: Option, + /// Optional response token. pub token: Option<[u8; 32]>, + /// Nodes returned by the remote peer. pub closer_nodes: Vec, + /// Response error code. pub error: u64, + /// Optional response value. pub value: Option>, + /// Round-trip time for the request. pub rtt: Duration, } #[derive(Debug, Clone)] +/// Parameters for a user-driven DHT query. pub struct UserQueryParams { + /// Query target node id. pub target: NodeId, + /// RPC command to send. pub command: u64, + /// Optional query payload. pub value: Option>, + /// Whether the query is a commit. pub commit: bool, + /// Optional per-query concurrency override. pub concurrency: Option, } #[derive(Debug, Clone)] +/// Parameters for a user-driven DHT request. pub struct UserRequestParams { + /// Optional request token. pub token: Option<[u8; 32]>, + /// RPC command to send. pub command: u64, + /// Optional target node id. pub target: Option, + /// Optional request payload. pub value: Option>, } +/// An incoming user-facing request forwarded from the DHT. pub struct UserRequest { + /// Origin peer for the request. pub from: Ipv4Peer, + /// Optional origin peer id. pub id: Option, + /// Optional request token. pub token: Option<[u8; 32]>, + /// RPC command received. pub command: u64, + /// Optional target node id. pub target: Option, + /// Optional request payload. pub value: Option>, reply_tx: Option>)>>, } impl UserRequest { + /// Replies to the request with a value and success code. pub fn reply(&mut self, value: Option>) { if let Some(tx) = self.reply_tx.take() { let _ = tx.send((0, value)); } } + /// Replies to the request with an error code. pub fn error(&mut self, code: u64) { if let Some(tx) = self.reply_tx.take() { let _ = tx.send((code, None)); @@ -212,12 +257,14 @@ struct DeferredReply { // ── DhtHandle (user-facing, Send + Sync + Clone) ────────────────────────────── +/// Handle for interacting with a running DHT node. #[derive(Clone)] pub struct DhtHandle { cmd_tx: mpsc::UnboundedSender, } impl DhtHandle { + /// Waits until the node has finished bootstrapping. pub async fn bootstrapped(&self) -> Result<(), DhtError> { let (tx, rx) = oneshot::channel(); self.cmd_tx @@ -226,6 +273,7 @@ impl DhtHandle { rx.await.map_err(|_| DhtError::ChannelClosed)? } + /// Sends a ping to `host:port`. pub async fn ping(&self, host: &str, port: u16) -> Result { let (tx, rx) = oneshot::channel(); self.cmd_tx @@ -238,6 +286,7 @@ impl DhtHandle { rx.await.map_err(|_| DhtError::ChannelClosed)? } + /// Runs a `find_node` query for `target`. pub async fn find_node(&self, target: NodeId) -> Result, DhtError> { let (tx, rx) = oneshot::channel(); self.cmd_tx @@ -249,6 +298,7 @@ impl DhtHandle { rx.await.map_err(|_| DhtError::ChannelClosed)? } + /// Runs a custom DHT query. pub async fn query(&self, params: UserQueryParams) -> Result, DhtError> { let (tx, rx) = oneshot::channel(); self.cmd_tx @@ -260,6 +310,7 @@ impl DhtHandle { rx.await.map_err(|_| DhtError::ChannelClosed)? } + /// Sends a request to a remote peer. pub async fn request( &self, params: UserRequestParams, @@ -279,6 +330,7 @@ impl DhtHandle { } /// Fire-and-forget relay send (no response tracking). + /// Relays an RPC command to `to` without waiting for a reply. pub fn relay( &self, command: u64, @@ -296,6 +348,7 @@ impl DhtHandle { .map_err(|_| DhtError::ChannelClosed) } + /// Subscribes to forwarded user requests. pub async fn subscribe_requests(&self) -> Option> { let (tx, rx) = oneshot::channel(); self.cmd_tx @@ -304,6 +357,7 @@ impl DhtHandle { rx.await.ok() } + /// Returns the current routing table size. pub async fn table_size(&self) -> Result { let (tx, rx) = oneshot::channel(); self.cmd_tx @@ -312,6 +366,7 @@ impl DhtHandle { rx.await.map_err(|_| DhtError::ChannelClosed) } + /// Destroys the running DHT node. pub async fn destroy(&self) -> Result<(), DhtError> { let (tx, rx) = oneshot::channel(); self.cmd_tx @@ -320,6 +375,7 @@ impl DhtHandle { rx.await.map_err(|_| DhtError::ChannelClosed)? } + /// Returns the local bound port. pub async fn local_port(&self) -> Result { let (tx, rx) = oneshot::channel(); self.cmd_tx @@ -1311,6 +1367,7 @@ impl DhtNode { // ── Spawn ───────────────────────────────────────────────────────────────────── +/// Spawns a DHT node and returns its join handle and public handle. pub async fn spawn( runtime: &UdxRuntime, config: DhtConfig, diff --git a/peeroxide-dht/src/secret_stream.rs b/peeroxide-dht/src/secret_stream.rs index 8091ca6..941451f 100644 --- a/peeroxide-dht/src/secret_stream.rs +++ b/peeroxide-dht/src/secret_stream.rs @@ -57,30 +57,47 @@ fn ns_responder() -> &'static [u8; 32] { // ── Errors ─────────────────────────────────────────────────────────────────── #[derive(Debug, Error)] +/// Errors returned by `SecretStream` and its framing helpers. pub enum SecretStreamError { + /// An underlying I/O operation failed. #[error("I/O error: {0}")] Io(#[from] std::io::Error), + /// Noise handshake negotiation failed. #[error("noise handshake failed: {0}")] Noise(#[from] NoiseError), + /// Secretstream decryption failed. #[error("secretstream decryption failed: {0}")] Decrypt(#[from] secretstream::SecretstreamError), + /// The handshake ended before completion. #[error("handshake did not complete")] HandshakeIncomplete, + /// EOF was encountered while the handshake was still in progress. #[error("unexpected EOF during handshake")] UnexpectedEof, + /// The ID header length was invalid. #[error("invalid ID header: expected {expected} bytes, got {got}")] - InvalidIdHeader { expected: usize, got: usize }, - + InvalidIdHeader { + /// The expected header length in bytes. + expected: usize, + /// The actual header length received. + got: usize, + }, + + /// The remote stream ID did not match the expected identity. #[error("stream ID mismatch: remote peer sent wrong identity")] StreamIdMismatch, + /// The framed message exceeded the maximum allowed length. #[error("message too large: {len} bytes exceeds maximum {MAX_MESSAGE_LEN}")] - MessageTooLarge { len: usize }, + MessageTooLarge { + /// The length of the oversized message in bytes. + len: usize, + }, } // ── SecretStream ───────────────────────────────────────────────────────────── diff --git a/peeroxide/src/error.rs b/peeroxide/src/error.rs index 5263a16..3ae50ee 100644 --- a/peeroxide/src/error.rs +++ b/peeroxide/src/error.rs @@ -3,18 +3,23 @@ use thiserror::Error; /// Errors from the Hyperswarm layer. #[derive(Debug, Error)] pub enum SwarmError { + /// Error from the HyperDHT layer. #[error("DHT error: {0}")] Dht(#[from] peeroxide_dht::hyperdht::HyperDhtError), + /// Error from the DHT RPC transport. #[error("DHT RPC error: {0}")] DhtRpc(#[from] peeroxide_dht::rpc::DhtError), + /// Error from the UDX transport layer. #[error("UDX error: {0}")] Udx(#[from] libudx::UdxError), + /// The swarm has been destroyed and can no longer process commands. #[error("swarm destroyed")] Destroyed, + /// An internal channel was closed unexpectedly. #[error("channel closed")] ChannelClosed, } diff --git a/peeroxide/src/lib.rs b/peeroxide/src/lib.rs index 48f3a89..016c5e4 100644 --- a/peeroxide/src/lib.rs +++ b/peeroxide/src/lib.rs @@ -1,4 +1,5 @@ #![forbid(unsafe_code)] +#![deny(missing_docs)] //! Rust port of [Hyperswarm](https://github.com/holepunchto/hyperswarm) — //! topic-based P2P peer discovery and encrypted connections over the diff --git a/peeroxide/src/peer_info.rs b/peeroxide/src/peer_info.rs index 941cbdd..2262118 100644 --- a/peeroxide/src/peer_info.rs +++ b/peeroxide/src/peer_info.rs @@ -15,10 +15,15 @@ const PROVEN_THRESHOLD_SECS: u64 = 15; #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[repr(u8)] pub enum Priority { + /// Lowest priority (4+ failed attempts). VeryLow = 0, + /// Low priority (3 attempts, or 2 unproven). Low = 1, + /// Default priority (0 attempts, or 1 unproven). Normal = 2, + /// High priority (2 attempts, proven). High = 3, + /// Highest priority (1 attempt, proven). VeryHigh = 4, } @@ -26,22 +31,34 @@ pub enum Priority { /// /// Modelled after `lib/peer-info.js` in the Node.js Hyperswarm. pub struct PeerInfo { + /// The peer's Ed25519 public key. pub public_key: [u8; 32], + /// Relay addresses learned from DHT discovery. pub relay_addresses: Vec, + /// Whether we are reconnecting after a proven connection dropped. pub reconnecting: bool, + /// Whether this peer has had a successful connection lasting ≥ 15 seconds. pub proven: bool, + /// Whether this peer has been banned. pub banned: bool, + /// Whether this peer is queued for an outgoing connection attempt. pub queued: bool, + /// Whether a connection attempt is currently in progress. pub connecting: bool, + /// Whether this peer was explicitly added (not discovered via DHT). pub explicit: bool, + /// Topics this peer is associated with. pub topics: Vec<[u8; 32]>, + /// Number of connection attempts since the last successful connection. pub attempts: u32, + /// Current computed priority level for connection scheduling. pub priority: Priority, connected_at: Option, waiting: bool, } impl PeerInfo { + /// Create a new [`PeerInfo`] for the given public key and relay addresses. pub fn new(public_key: [u8; 32], relay_addresses: Vec) -> Self { Self { public_key, @@ -83,6 +100,7 @@ impl PeerInfo { } } + /// Record that a connection to this peer has been established. pub fn connected(&mut self) { self.reconnecting = false; self.connected_at = Some(Instant::now()); @@ -99,6 +117,7 @@ impl PeerInfo { } } + /// Returns `true` if this peer is eligible for garbage collection. pub fn should_gc(&self) -> bool { if self.banned || self.queued || self.explicit || self.waiting { return false; @@ -106,10 +125,12 @@ impl PeerInfo { self.topics.is_empty() } + /// Returns `true` if this peer is in a retry-backoff waiting period. pub fn is_waiting(&self) -> bool { self.waiting } + /// Set whether this peer is in a retry-backoff waiting period. pub fn set_waiting(&mut self, w: bool) { self.waiting = w; } diff --git a/peeroxide/src/swarm.rs b/peeroxide/src/swarm.rs index 4b4e924..57889d6 100644 --- a/peeroxide/src/swarm.rs +++ b/peeroxide/src/swarm.rs @@ -148,6 +148,7 @@ pub struct SwarmConnection { } impl SwarmConnection { + /// Returns the remote peer's static public key. pub fn remote_public_key(&self) -> &[u8; 32] { &self.peer.remote_public_key } From 10d62e09d026a82b29efa9b73917142792901680 Mon Sep 17 00:00:00 2001 From: eshork <1829176+eshork@users.noreply.github.com> Date: Sat, 25 Apr 2026 21:01:50 -0400 Subject: [PATCH 2/2] docs: clarify Ipv4Peer carries both IPv4 and IPv6 addresses Update doc comment to note the struct may hold either address family, and that the name is inherited from the upstream JS implementation. Addresses PR review feedback from Copilot. --- peeroxide-dht/src/messages.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/peeroxide-dht/src/messages.rs b/peeroxide-dht/src/messages.rs index e9baeca..e41ec80 100644 --- a/peeroxide-dht/src/messages.rs +++ b/peeroxide-dht/src/messages.rs @@ -41,9 +41,14 @@ const FLAG_TARGET_OR_ERROR: u8 = 0b0000_1000; const FLAG_VALUE: u8 = 0b0001_0000; #[derive(Debug, Clone, PartialEq, Eq)] -/// An IPv4 peer address. +/// A peer network address (host and port). +/// +/// Despite the name, this type may carry either an IPv4 or IPv6 address +/// depending on context—for example, `addresses6` fields decode into +/// `Vec`. The name is inherited from the upstream JavaScript +/// implementation. pub struct Ipv4Peer { - /// The peer host. + /// The peer host (IPv4 or IPv6 address string). pub host: String, /// The peer port. pub port: u16,