From b1ba3718014b5cbf3d9988adb9243bff7fb8c6d7 Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Tue, 8 Jul 2025 13:09:27 -0700 Subject: [PATCH 1/3] feat!: refactor async I/O interface --- justfile | 4 +- protocol/Cargo.toml | 1 + protocol/src/futures.rs | 433 ++++++++++++++++++++++++++++++++++ protocol/src/io.rs | 297 +---------------------- protocol/src/lib.rs | 2 + protocol/tests/round_trips.rs | 73 ++++++ proxy/src/bin/proxy.rs | 15 +- 7 files changed, 520 insertions(+), 305 deletions(-) create mode 100644 protocol/src/futures.rs diff --git a/justfile b/justfile index fe2c160..67e5c3e 100644 --- a/justfile +++ b/justfile @@ -42,8 +42,8 @@ _default: # Test feature flag matrix compatability. @_test-features: # Build and test with all features, no features, and some combinations if required. - cargo +{{STABLE_TOOLCHAIN}} test --package bip324 --lib --all-features - cargo +{{STABLE_TOOLCHAIN}} test --package bip324 --lib --no-default-features + cargo +{{STABLE_TOOLCHAIN}} test --package bip324 --lib --tests --all-features + cargo +{{STABLE_TOOLCHAIN}} test --package bip324 --lib --tests --no-default-features # Check code with MSRV compiler. @_test-msrv: diff --git a/protocol/Cargo.toml b/protocol/Cargo.toml index bd80276..42064bc 100644 --- a/protocol/Cargo.toml +++ b/protocol/Cargo.toml @@ -30,3 +30,4 @@ chacha20-poly1305 = { version = "0.1.1", default-features = false } # bitcoind version 26.0 includes support for BIP-324's V2 protocol, but it is disabled by default. bitcoind = { package = "corepc-node", version = "0.7.1", default-features = false, features = ["26_0","download"] } hex = { package = "hex-conservative", version = "0.2.0" } +tokio = { version = "1", features = ["io-util", "net", "rt", "rt-multi-thread", "macros"] } diff --git a/protocol/src/futures.rs b/protocol/src/futures.rs new file mode 100644 index 0000000..d1a5c6b --- /dev/null +++ b/protocol/src/futures.rs @@ -0,0 +1,433 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! Future-based asynchronous interfaces for establishing and using BIP-324 +//! encrypted connections over AsyncRead/AsyncWrite transports. +//! +//! This module provides async implementations using the `tokio` runtime. +//! It is only available when the `tokio` feature is enabled. +//! +//! # Example +//! +//! ```no_run +//! use bip324::futures::Protocol; +//! use bip324::{Network, Role}; +//! use tokio::net::TcpStream; +//! +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! let stream = TcpStream::connect("127.0.0.1:8333").await?; +//! let (reader, writer) = stream.into_split(); +//! +//! let protocol = Protocol::new( +//! Network::Bitcoin, +//! Role::Initiator, +//! None, +//! None, +//! reader, +//! writer, +//! ).await?; +//! # Ok(()) +//! # } +//! ``` + +use std::io::Cursor; +use std::vec; +use std::vec::Vec; + +use bitcoin::Network; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; + +use crate::io::{Payload, ProtocolError}; +use crate::{ + handshake::{self, GarbageResult, VersionResult}, + Handshake, InboundCipher, OutboundCipher, PacketType, Role, NUM_ELLIGATOR_SWIFT_BYTES, + NUM_GARBAGE_TERMINTOR_BYTES, NUM_LENGTH_BYTES, +}; + +/// Perform an async BIP-324 handshake and return ready-to-use session components. +/// +/// This function is *not* cancellation safe. +/// +/// # Arguments +/// +/// * `network` - Network which both parties are operating on. +/// * `role` - Role in handshake, initiator or responder. +/// * `garbage` - Optional garbage bytes to send in handshake. +/// * `decoys` - Optional decoy packet contents bytes to send in handshake. +/// * `reader` - Async buffer to read packets sent by peer (takes ownership). +/// * `writer` - Async buffer to write packets to peer (takes mutable reference). +/// +/// # Reader Transformation +/// +/// The I/O reader is transformed in order to handle possible over-read +/// scenarios while attempting to detect the remote's garbage terminator. +/// +/// # Returns +/// +/// A `Result` containing: +/// * `Ok((InboundCipher, OutboundCipher, SessionReader))`: Ready-to-use session components. +/// * `Err(ProtocolError)`: An error that occurred during the handshake. +/// +/// # Errors +/// +/// * `Io` - Includes a flag for if the remote probably only understands the V1 protocol. +pub async fn handshake( + network: Network, + role: Role, + garbage: Option<&[u8]>, + decoys: Option<&[&[u8]]>, + mut reader: R, + writer: &mut W, +) -> Result<(InboundCipher, OutboundCipher, impl AsyncRead + Unpin + Send), ProtocolError> +where + R: AsyncRead + Send + Unpin, + W: AsyncWrite + Unpin, +{ + let handshake = Handshake::::new(network, role)?; + + // Send local public key and optional garbage. + let key_buffer_len = Handshake::::send_key_len(garbage); + let mut key_buffer = vec![0u8; key_buffer_len]; + let handshake = handshake.send_key(garbage, &mut key_buffer)?; + writer.write_all(&key_buffer).await?; + writer.flush().await?; + + // Read remote's public key. + let mut remote_ellswift_buffer = [0u8; NUM_ELLIGATOR_SWIFT_BYTES]; + reader.read_exact(&mut remote_ellswift_buffer).await?; + let handshake = handshake.receive_key(remote_ellswift_buffer)?; + + // Send garbage terminator, decoys, and version. + let version_buffer_len = Handshake::::send_version_len(decoys); + let mut version_buffer = vec![0u8; version_buffer_len]; + let handshake = handshake.send_version(&mut version_buffer, decoys)?; + writer.write_all(&version_buffer).await?; + writer.flush().await?; + + // Receive and process garbage terminator + let mut garbage_buffer = vec![0u8; NUM_GARBAGE_TERMINTOR_BYTES]; + reader.read_exact(&mut garbage_buffer).await?; + + let mut handshake = handshake; + let (mut handshake, garbage_bytes) = loop { + match handshake.receive_garbage(&garbage_buffer) { + Ok(GarbageResult::FoundGarbage { + handshake, + consumed_bytes, + }) => { + break (handshake, consumed_bytes); + } + Ok(GarbageResult::NeedMoreData(h)) => { + handshake = h; + // The 256 bytes is a bit arbitrary. There is a max of 4095, but not sure + // all of that should be allocated right away. + let mut temp = vec![0u8; 256]; + match reader.read(&mut temp).await { + Ok(0) => return Err(ProtocolError::eof()), + Ok(n) => { + garbage_buffer.extend_from_slice(&temp[..n]); + } + Err(e) => return Err(ProtocolError::from(e)), + } + } + Err(e) => return Err(ProtocolError::Internal(e)), + } + }; + + // Process remaining bytes for decoy packets and version. + let mut session_reader = Cursor::new(garbage_buffer[garbage_bytes..].to_vec()).chain(reader); + loop { + // Decrypt packet length. + let mut length_bytes = [0u8; NUM_LENGTH_BYTES]; + session_reader.read_exact(&mut length_bytes).await?; + let packet_len = handshake.decrypt_packet_len(length_bytes)?; + + // Process packet. + let mut packet_bytes = vec![0u8; packet_len]; + session_reader.read_exact(&mut packet_bytes).await?; + match handshake.receive_version(&mut packet_bytes) { + Ok(VersionResult::Complete { cipher }) => { + let (inbound_cipher, outbound_cipher) = cipher.into_split(); + return Ok((inbound_cipher, outbound_cipher, session_reader)); + } + Ok(VersionResult::Decoy(h)) => { + handshake = h; + } + Err(e) => return Err(ProtocolError::Internal(e)), + } + } +} + +/// A protocol session with handshake and send/receive packet management. +pub struct Protocol { + reader: ProtocolReader, + writer: ProtocolWriter, +} + +impl Protocol +where + R: AsyncRead + Unpin + Send, + W: AsyncWrite + Unpin + Send, +{ + /// New protocol session which completes the initial handshake and returns a handler. + /// + /// This function is *not* cancellation safe. + /// + /// # Arguments + /// + /// * `network` - Network which both parties are operating on. + /// * `role` - Role in handshake, initiator or responder. + /// * `garbage` - Optional garbage bytes to send in handshake. + /// * `decoys` - Optional decoy packet contents bytes to send in handshake. + /// * `reader` - Asynchronous buffer to read packets sent by peer (takes ownership). + /// * `writer` - Asynchronous buffer to write packets to peer (takes ownership). + /// + /// # Returns + /// + /// A `Result` containing: + /// * `Ok(Protocol)`: An initialized protocol handler. + /// * `Err(ProtocolError)`: An error that occurred during the handshake. + /// + /// # Errors + /// + /// * `Io` - Includes a flag for if the remote probably only understands the V1 protocol. + pub async fn new<'a>( + network: Network, + role: Role, + garbage: Option<&'a [u8]>, + decoys: Option<&'a [&'a [u8]]>, + reader: R, + mut writer: W, + ) -> Result, ProtocolError> { + let (inbound_cipher, outbound_cipher, session_reader) = + handshake(network, role, garbage, decoys, reader, &mut writer).await?; + + Ok(Protocol { + reader: ProtocolReader { + inbound_cipher, + reader: session_reader, + state: DecryptState::init_reading_length(), + }, + writer: ProtocolWriter { + outbound_cipher, + writer, + }, + }) + } + + /// Split the protocol into a separate reader and writer. + pub fn into_split( + self, + ) -> ( + ProtocolReader, + ProtocolWriter, + ) { + (self.reader, self.writer) + } + + /// Read and decrypt a packet from the underlying reader. + /// + /// This is a convenience method that calls read on the internal reader. + /// + /// # Returns + /// + /// A `Result` containing: + /// * `Ok(Payload)`: A decrypted payload with packet type. + /// * `Err(ProtocolError)`: An error that occurred during the read or decryption. + pub async fn read(&mut self) -> Result { + self.reader.read().await + } + + /// Encrypt and write a packet to the underlying writer. + /// + /// This is a convenience method that calls write on the internal writer. + /// + /// # Arguments + /// + /// * `plaintext` - The data to encrypt and send. + /// + /// # Returns + /// + /// A `Result` containing: + /// * `Ok()`: On successful contents encryption and packet send. + /// * `Err(ProtocolError)`: An error that occurred during the encryption or write. + pub async fn write(&mut self, plaintext: &[u8]) -> Result<(), ProtocolError> { + self.writer.write(plaintext).await + } +} + +/// State machine of an asynchronous packet read. +/// +/// This maintains state between await points to ensure cancellation safety. +#[derive(Debug)] +enum DecryptState { + ReadingLength { + length_bytes: [u8; NUM_LENGTH_BYTES], + bytes_read: usize, + }, + ReadingPayload { + packet_bytes: Vec, + bytes_read: usize, + }, +} + +impl DecryptState { + /// Transition state to reading the length bytes. + fn init_reading_length() -> Self { + DecryptState::ReadingLength { + length_bytes: [0u8; NUM_LENGTH_BYTES], + bytes_read: 0, + } + } + + /// Transition state to reading payload bytes. + fn init_reading_payload(packet_bytes_len: usize) -> Self { + DecryptState::ReadingPayload { + packet_bytes: vec![0u8; packet_bytes_len], + bytes_read: 0, + } + } +} + +/// Manages an async buffer to automatically decrypt contents of received packets. +pub struct ProtocolReader { + inbound_cipher: InboundCipher, + reader: R, + state: DecryptState, +} + +impl ProtocolReader +where + R: AsyncRead + Unpin + Send, +{ + /// Decrypt contents of received packet from buffer. + /// + /// This function is cancellation safe. + /// + /// # Returns + /// + /// A `Result` containing: + /// * `Ok(Payload)`: A decrypted payload with packet type. + /// * `Err(ProtocolError)`: An error that occurred during the read or decryption. + pub async fn read(&mut self) -> Result { + // Storing state between async reads to make function cancellation safe. + loop { + match &mut self.state { + DecryptState::ReadingLength { + length_bytes, + bytes_read, + } => { + while *bytes_read < NUM_LENGTH_BYTES { + *bytes_read += self.reader.read(&mut length_bytes[*bytes_read..]).await?; + } + + let packet_bytes_len = self.inbound_cipher.decrypt_packet_len(*length_bytes); + self.state = DecryptState::init_reading_payload(packet_bytes_len); + } + DecryptState::ReadingPayload { + packet_bytes, + bytes_read, + } => { + while *bytes_read < packet_bytes.len() { + *bytes_read += self.reader.read(&mut packet_bytes[*bytes_read..]).await?; + } + + let plaintext_len = InboundCipher::decryption_buffer_len(packet_bytes.len()); + let mut plaintext_buffer = vec![0u8; plaintext_len]; + self.inbound_cipher + .decrypt(packet_bytes, &mut plaintext_buffer, None)?; + self.state = DecryptState::init_reading_length(); + return Ok(Payload::new(plaintext_buffer)); + } + } + } + } + + /// Consume the protocol reader in exchange for the underlying inbound cipher and reader. + pub fn into_inner(self) -> (InboundCipher, R) { + (self.inbound_cipher, self.reader) + } +} + +/// Manages an async buffer to automatically encrypt and send contents in packets. +pub struct ProtocolWriter { + outbound_cipher: OutboundCipher, + writer: W, +} + +impl ProtocolWriter +where + W: AsyncWrite + Unpin + Send, +{ + /// Encrypt contents and write packet buffer. + /// + /// # Arguments + /// + /// * `plaintext` - The data to encrypt and send. + /// + /// # Returns + /// + /// A `Result` containing: + /// * `Ok()`: On successful contents encryption and packet send. + /// * `Err(ProtocolError)`: An error that occurred during the encryption or write. + pub async fn write(&mut self, plaintext: &[u8]) -> Result<(), ProtocolError> { + let packet_len = OutboundCipher::encryption_buffer_len(plaintext.len()); + let mut packet_buffer = vec![0u8; packet_len]; + + self.outbound_cipher + .encrypt(plaintext, &mut packet_buffer, PacketType::Genuine, None)?; + + self.writer.write_all(&packet_buffer).await?; + self.writer.flush().await?; + Ok(()) + } + + /// Consume the protocol writer in exchange for the underlying outbound cipher and writer. + pub fn into_inner(self) -> (OutboundCipher, W) { + (self.outbound_cipher, self.writer) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use bitcoin::Network; + + #[tokio::test] + async fn test_async_handshake_functions() { + use tokio::io::duplex; + + // Create two duplex channels to simulate network connection. + let (client_stream, server_stream) = duplex(1024); + let (client_read, mut client_write) = tokio::io::split(client_stream); + let (server_read, mut server_write) = tokio::io::split(server_stream); + + let client_handshake = tokio::spawn(async move { + handshake( + Network::Bitcoin, + Role::Initiator, + Some(b"client garbage"), + Some(&[b"client decoy"]), + client_read, + &mut client_write, + ) + .await + }); + + let server_handshake = tokio::spawn(async move { + handshake( + Network::Bitcoin, + Role::Responder, + Some(b"server garbage"), + Some(&[b"server decoy 1", b"server decoy 2"]), + server_read, + &mut server_write, + ) + .await + }); + + let (client_result, server_result) = tokio::join!(client_handshake, server_handshake); + let (_client_inbound, _client_outbound, _client_session) = client_result.unwrap().unwrap(); + let (_server_inbound, _server_outbound, _server_session) = server_result.unwrap().unwrap(); + } +} diff --git a/protocol/src/io.rs b/protocol/src/io.rs index 0e172b1..b76a410 100644 --- a/protocol/src/io.rs +++ b/protocol/src/io.rs @@ -16,9 +16,6 @@ use crate::{ NUM_GARBAGE_TERMINTOR_BYTES, NUM_LENGTH_BYTES, }; -#[cfg(feature = "tokio")] -use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; - /// A decrypted BIP-324 payload. /// /// # Invariants @@ -127,7 +124,7 @@ impl ProtocolError { /// /// This is used when the remote peer closes the connection during handshake, /// which often indicates they don't support the V2 protocol. - fn eof() -> Self { + pub fn eof() -> Self { ProtocolError::Io( std::io::Error::new( std::io::ErrorKind::UnexpectedEof, @@ -166,298 +163,6 @@ impl fmt::Display for ProtocolError { } } -/// A protocol session with handshake and send/receive packet management. -#[cfg(feature = "tokio")] -pub struct AsyncProtocol { - reader: AsyncProtocolReader, - writer: AsyncProtocolWriter, -} - -#[cfg(feature = "tokio")] -impl AsyncProtocol { - /// New protocol session which completes the initial handshake and returns a handler. - /// - /// This function is *not* cancellation safe. - /// - /// # Arguments - /// - /// * `network` - Network which both parties are operating on. - /// * `role` - Role in handshake, initiator or responder. - /// * `garbage` - Optional garbage bytes to send in handshake. - /// * `decoys` - Optional decoy packet contents bytes to send in handshake. - /// * `reader` - Asynchronous buffer to read packets sent by peer. - /// * `writer` - Asynchronous buffer to write packets to peer. - /// - /// # Returns - /// - /// A `Result` containing: - /// * `Ok(AsyncProtocol)`: An initialized protocol handler. - /// * `Err(ProtocolError)`: An error that occurred during the handshake. - /// - /// # Errors - /// - /// * `Io` - Includes a flag for if the remote probably only understands the V1 protocol. - pub async fn new<'a, R, W>( - network: Network, - role: Role, - garbage: Option<&'a [u8]>, - decoys: Option<&'a [&'a [u8]]>, - reader: &mut R, - writer: &mut W, - ) -> Result - where - R: AsyncRead + Unpin + Send, - W: AsyncWrite + Unpin + Send, - { - let handshake = Handshake::::new(network, role)?; - - // Send local public key and optional garbage. - let key_buffer_len = Handshake::::send_key_len(garbage); - let mut key_buffer = vec![0u8; key_buffer_len]; - let handshake = handshake.send_key(garbage, &mut key_buffer)?; - writer.write_all(&key_buffer).await?; - writer.flush().await?; - - // Read remote's public key. - let mut remote_ellswift_buffer = [0u8; NUM_ELLIGATOR_SWIFT_BYTES]; - reader.read_exact(&mut remote_ellswift_buffer).await?; - let handshake = handshake.receive_key(remote_ellswift_buffer)?; - - // Send garbage terminator, decoys, and version. - let version_buffer_len = Handshake::::send_version_len(decoys); - let mut version_buffer = vec![0u8; version_buffer_len]; - let handshake = handshake.send_version(&mut version_buffer, decoys)?; - writer.write_all(&version_buffer).await?; - writer.flush().await?; - - // Receive and process garbage terminator - let mut garbage_buffer = vec![0u8; NUM_GARBAGE_TERMINTOR_BYTES]; - reader.read_exact(&mut garbage_buffer).await?; - - let mut handshake = handshake; - let (mut handshake, garbage_bytes) = loop { - match handshake.receive_garbage(&garbage_buffer) { - Ok(GarbageResult::FoundGarbage { - handshake, - consumed_bytes, - }) => { - break (handshake, consumed_bytes); - } - Ok(GarbageResult::NeedMoreData(h)) => { - handshake = h; - // Use small chunks to avoid reading past garbage, decoys, and version. - let mut temp = vec![0u8; NUM_GARBAGE_TERMINTOR_BYTES]; - match reader.read(&mut temp).await { - Ok(0) => return Err(ProtocolError::eof()), - Ok(n) => { - garbage_buffer.extend_from_slice(&temp[..n]); - } - Err(e) => return Err(ProtocolError::from(e)), - } - } - Err(e) => return Err(ProtocolError::Internal(e)), - } - }; - - // Process remaining bytes and read version packets. - let mut version_buffer = garbage_buffer[garbage_bytes..].to_vec(); - loop { - // Decrypt packet length. - if version_buffer.len() < NUM_LENGTH_BYTES { - let old_len = version_buffer.len(); - version_buffer.resize(NUM_LENGTH_BYTES, 0); - reader.read_exact(&mut version_buffer[old_len..]).await?; - } - let packet_len = handshake - .decrypt_packet_len(version_buffer[..NUM_LENGTH_BYTES].try_into().unwrap())?; - version_buffer.drain(..NUM_LENGTH_BYTES); - - // Process packet. - if version_buffer.len() < packet_len { - let old_len = version_buffer.len(); - version_buffer.resize(packet_len, 0); - reader.read_exact(&mut version_buffer[old_len..]).await?; - } - match handshake.receive_version(&mut version_buffer) { - Ok(VersionResult::Complete { cipher }) => { - let (inbound_cipher, outbound_cipher) = cipher.into_split(); - return Ok(Self { - reader: AsyncProtocolReader { - inbound_cipher, - state: DecryptState::init_reading_length(), - }, - writer: AsyncProtocolWriter { outbound_cipher }, - }); - } - Ok(VersionResult::Decoy(h)) => { - handshake = h; - version_buffer.drain(..packet_len); - } - Err(e) => return Err(ProtocolError::Internal(e)), - } - } - } - - /// Read reference for packet reading operations. - pub fn reader(&mut self) -> &mut AsyncProtocolReader { - &mut self.reader - } - - /// Write reference for packet writing operations. - pub fn writer(&mut self) -> &mut AsyncProtocolWriter { - &mut self.writer - } - - /// Split the protocol into a separate reader and writer. - pub fn into_split(self) -> (AsyncProtocolReader, AsyncProtocolWriter) { - (self.reader, self.writer) - } -} - -/// State machine of an asynchronous packet read. -/// -/// This maintains state between await points to ensure cancellation safety. -#[cfg(feature = "tokio")] -#[derive(Debug)] -enum DecryptState { - ReadingLength { - length_bytes: [u8; NUM_LENGTH_BYTES], - bytes_read: usize, - }, - ReadingPayload { - packet_bytes: Vec, - bytes_read: usize, - }, -} - -#[cfg(feature = "tokio")] -impl DecryptState { - /// Transition state to reading the length bytes. - fn init_reading_length() -> Self { - DecryptState::ReadingLength { - length_bytes: [0u8; NUM_LENGTH_BYTES], - bytes_read: 0, - } - } - - /// Transition state to reading payload bytes. - fn init_reading_payload(packet_bytes_len: usize) -> Self { - DecryptState::ReadingPayload { - packet_bytes: vec![0u8; packet_bytes_len], - bytes_read: 0, - } - } -} - -/// Manages an async buffer to automatically decrypt contents of received packets. -#[cfg(feature = "tokio")] -pub struct AsyncProtocolReader { - inbound_cipher: InboundCipher, - state: DecryptState, -} - -#[cfg(feature = "tokio")] -impl AsyncProtocolReader { - /// Decrypt contents of received packet from buffer. - /// - /// This function is cancellation safe. - /// - /// # Arguments - /// - /// * `buffer` - Asynchronous I/O buffer to pull bytes from. - /// - /// # Returns - /// - /// A `Result` containing: - /// * `Ok(Payload)`: A decrypted payload with packet type. - /// * `Err(ProtocolError)`: An error that occurred during the read or decryption. - pub async fn read_and_decrypt(&mut self, buffer: &mut R) -> Result - where - R: AsyncRead + Unpin + Send, - { - // Storing state between async reads to make function cancellation safe. - loop { - match &mut self.state { - DecryptState::ReadingLength { - length_bytes, - bytes_read, - } => { - while *bytes_read < NUM_LENGTH_BYTES { - *bytes_read += buffer.read(&mut length_bytes[*bytes_read..]).await?; - } - - let packet_bytes_len = self.inbound_cipher.decrypt_packet_len(*length_bytes); - self.state = DecryptState::init_reading_payload(packet_bytes_len); - } - DecryptState::ReadingPayload { - packet_bytes, - bytes_read, - } => { - while *bytes_read < packet_bytes.len() { - *bytes_read += buffer.read(&mut packet_bytes[*bytes_read..]).await?; - } - - let plaintext_len = InboundCipher::decryption_buffer_len(packet_bytes.len()); - let mut plaintext_buffer = vec![0u8; plaintext_len]; - self.inbound_cipher - .decrypt(packet_bytes, &mut plaintext_buffer, None)?; - self.state = DecryptState::init_reading_length(); - return Ok(Payload::new(plaintext_buffer)); - } - } - } - } - - /// Consume the protocol reader in exchange for the underlying inbound cipher. - pub fn into_cipher(self) -> InboundCipher { - self.inbound_cipher - } -} - -/// Manages an async buffer to automatically encrypt and send contents in packets. -#[cfg(feature = "tokio")] -pub struct AsyncProtocolWriter { - outbound_cipher: OutboundCipher, -} - -#[cfg(feature = "tokio")] -impl AsyncProtocolWriter { - /// Encrypt contents and write packet buffer. - /// - /// # Arguments - /// - /// * `buffer` - Asynchronous I/O buffer to write bytes to. - /// - /// # Returns - /// - /// A `Result` containing: - /// * `Ok()`: On successful contents encryption and packet send. - /// * `Err(ProtocolError)`: An error that occurred during the encryption or write. - pub async fn encrypt_and_write( - &mut self, - plaintext: &[u8], - buffer: &mut W, - ) -> Result<(), ProtocolError> - where - W: AsyncWrite + Unpin + Send, - { - let packet_len = OutboundCipher::encryption_buffer_len(plaintext.len()); - let mut packet_buffer = vec![0u8; packet_len]; - - self.outbound_cipher - .encrypt(plaintext, &mut packet_buffer, PacketType::Genuine, None)?; - - buffer.write_all(&packet_buffer).await?; - buffer.flush().await?; - Ok(()) - } - - /// Consume the protocol writer in exchange for the underlying outbound cipher. - pub fn into_cipher(self) -> OutboundCipher { - self.outbound_cipher - } -} - /// Perform a BIP-324 handshake and return ready-to-use session components. /// /// This function handles the complete handshake process and returns the diff --git a/protocol/src/lib.rs b/protocol/src/lib.rs index 9e7b171..2229733 100644 --- a/protocol/src/lib.rs +++ b/protocol/src/lib.rs @@ -22,6 +22,8 @@ extern crate std; mod fschacha20poly1305; +#[cfg(feature = "tokio")] +pub mod futures; mod handshake; #[cfg(feature = "std")] pub mod io; diff --git a/protocol/tests/round_trips.rs b/protocol/tests/round_trips.rs index 9f9ba95..6933ef2 100644 --- a/protocol/tests/round_trips.rs +++ b/protocol/tests/round_trips.rs @@ -1,5 +1,6 @@ // SPDX-License-Identifier: CC0-1.0 +#[cfg(feature = "std")] const PORT: u16 = 18444; #[test] @@ -357,6 +358,76 @@ fn regtest_handshake_std() { println!("Successfully exchanged version messages using into_bip324 API!"); } +#[tokio::test] +#[cfg(feature = "tokio")] +async fn regtest_handshake_async() { + use std::{ + net::{IpAddr, Ipv4Addr, SocketAddr}, + time::{SystemTime, UNIX_EPOCH}, + }; + + use bip324::{ + futures::Protocol, + serde::{deserialize, serialize, NetworkMessage}, + }; + use bitcoin::p2p::{message_network::VersionMessage, Address, ServiceFlags}; + use tokio::net::TcpStream; + + let bitcoind = regtest_process(TransportVersion::V2); + + let stream = TcpStream::connect(bitcoind.params.p2p_socket.unwrap()) + .await + .unwrap(); + + let (reader, writer) = stream.into_split(); + + // Initialize high-level async protocol with handshake + println!("Starting async BIP-324 handshake"); + let mut protocol = Protocol::new( + bip324::Network::Regtest, + bip324::Role::Initiator, + None, // no garbage + None, // no decoys + reader, + writer, + ) + .await + .unwrap(); + + println!("Async handshake completed successfully!"); + + // Create version message. + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("time went backwards") + .as_secs(); + let ip = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), PORT); + let from_and_recv = Address::new(&ip, ServiceFlags::NONE); + let msg = VersionMessage { + version: 70015, + services: ServiceFlags::NONE, + timestamp: now as i64, + receiver: from_and_recv.clone(), + sender: from_and_recv, + nonce: 1, + user_agent: "BIP-324 Async Client".to_string(), + start_height: 0, + relay: false, + }; + + let message = serialize(NetworkMessage::Version(msg)); + println!("Sending version message using async Protocol::write()"); + protocol.write(&message).await.unwrap(); + + println!("Reading version response using async Protocol::read()"); + let payload = protocol.read().await.unwrap(); + + let response_message = deserialize(payload.contents()).unwrap(); + assert_eq!(response_message.cmd(), "version"); + + println!("Successfully exchanged version messages using async Protocol API!"); +} + #[test] #[should_panic] #[cfg(feature = "std")] @@ -384,12 +455,14 @@ fn regtest_handshake_v1_only() { } /// Bitcoind transport versions. +#[cfg(feature = "std")] enum TransportVersion { V1, V2, } /// Fire up a managed regtest bitcoind process. +#[cfg(feature = "std")] fn regtest_process(transport: TransportVersion) -> bitcoind::Node { // Pull executable from auto-downloaded location, unless // environment variable override is present. Some operating diff --git a/proxy/src/bin/proxy.rs b/proxy/src/bin/proxy.rs index 62817b9..a2c9882 100644 --- a/proxy/src/bin/proxy.rs +++ b/proxy/src/bin/proxy.rs @@ -6,7 +6,8 @@ use std::str::FromStr; use bip324::{ - io::{AsyncProtocol, ProtocolFailureSuggestion}, + futures::Protocol, + io::ProtocolFailureSuggestion, serde::{deserialize, serialize}, PacketType, Role, }; @@ -75,15 +76,15 @@ async fn v2_proxy( .expect("connect to remote"); info!("Initiating handshake."); - let (mut remote_reader, mut remote_writer) = remote.into_split(); + let (remote_reader, remote_writer) = remote.into_split(); - let protocol = match AsyncProtocol::new( + let protocol = match Protocol::new( network, Role::Initiator, None, None, - &mut remote_reader, - &mut remote_writer, + remote_reader, + remote_writer, ) .await { @@ -116,11 +117,11 @@ async fn v2_proxy( let contents = serialize(msg); v2_remote_writer - .encrypt_and_write(&contents, &mut remote_writer) + .write(&contents) .await .expect("write to remote"); }, - result = v2_remote_reader.read_and_decrypt(&mut remote_reader) => { + result = v2_remote_reader.read() => { let payload = result.expect("read packet"); // Ignore decoy packets. if payload.packet_type() == PacketType::Genuine { From 4b57a78dc32abe06c13dae481d1964b5898df107 Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Tue, 8 Jul 2025 13:29:12 -0700 Subject: [PATCH 2/3] feat!: prefer RPIT over SessionReader type --- protocol/src/io.rs | 109 ++++------------------------------ protocol/tests/round_trips.rs | 27 +++++---- 2 files changed, 28 insertions(+), 108 deletions(-) diff --git a/protocol/src/io.rs b/protocol/src/io.rs index b76a410..457e2f6 100644 --- a/protocol/src/io.rs +++ b/protocol/src/io.rs @@ -4,7 +4,7 @@ //! connections over Read/Write transports. use core::fmt; -use std::io::{Chain, Cursor, Read, Write}; +use std::io::{Cursor, Read, Write}; use std::vec; use std::vec::Vec; @@ -52,29 +52,6 @@ impl Payload { } } -/// A reader for BIP-324 session data that handles any buffered handshake overflow. -/// -/// This reader ensures seamless transition from handshake to session data by -/// first consuming any data that was read during handshake but belongs to the -/// session stream. -pub struct SessionReader { - inner: Chain>, R>, -} - -impl SessionReader { - fn new(buffer: Vec, reader: R) -> Self { - Self { - inner: std::io::Read::chain(Cursor::new(buffer), reader), - } - } -} - -impl Read for SessionReader { - fn read(&mut self, buf: &mut [u8]) -> std::io::Result { - self.inner.read(buf) - } -} - /// High level error type for the protocol interface. #[derive(Debug)] pub enum ProtocolError { @@ -185,7 +162,7 @@ impl fmt::Display for ProtocolError { /// # Returns /// /// A `Result` containing: -/// * `Ok((InboundCipher, OutboundCipher, SessionReader))`: Ready-to-use session components. +/// * `Ok((InboundCipher, OutboundCipher, impl Read))`: Ready-to-use session components. /// * `Err(ProtocolError)`: An error that occurred during the handshake. /// /// # Errors @@ -198,7 +175,7 @@ pub fn handshake( decoys: Option<&[&[u8]]>, reader: R, writer: &mut W, -) -> Result<(InboundCipher, OutboundCipher, SessionReader), ProtocolError> +) -> Result<(InboundCipher, OutboundCipher, impl Read), ProtocolError> where R: Read, W: Write, @@ -216,7 +193,7 @@ fn handshake_with_initialized( decoys: Option<&[&[u8]]>, mut reader: R, writer: &mut W, -) -> Result<(InboundCipher, OutboundCipher, SessionReader), ProtocolError> +) -> Result<(InboundCipher, OutboundCipher, impl Read), ProtocolError> where R: Read, W: Write, @@ -271,7 +248,10 @@ where }; // Process remaining bytes for decoy packets and version. - let mut session_reader = SessionReader::new(garbage_buffer[garbage_bytes..].to_vec(), reader); + let mut session_reader = std::io::Read::chain( + Cursor::new(garbage_buffer[garbage_bytes..].to_vec()), + reader, + ); loop { // Decrypt packet length. let mut length_bytes = [0u8; NUM_LENGTH_BYTES]; @@ -296,7 +276,7 @@ where /// A synchronous protocol session with handshake and send/receive packet management. pub struct Protocol { - reader: ProtocolReader>, + reader: ProtocolReader, writer: ProtocolWriter, } @@ -332,11 +312,11 @@ where decoys: Option<&'a [&'a [u8]]>, reader: R, mut writer: W, - ) -> Result { + ) -> Result, ProtocolError> { let (inbound_cipher, outbound_cipher, session_reader) = handshake(network, role, garbage, decoys, reader, &mut writer)?; - Ok(Self { + Ok(Protocol { reader: ProtocolReader { inbound_cipher, reader: session_reader, @@ -349,7 +329,7 @@ where } /// Split the protocol into a separate reader and writer. - pub fn into_split(self) -> (ProtocolReader>, ProtocolWriter) { + pub fn into_split(self) -> (ProtocolReader, ProtocolWriter) { (self.reader, self.writer) } @@ -457,69 +437,6 @@ where } } -/// Extension trait to convert duplex streams into BIP-324 Protocol instances. -/// -/// # Example -/// -/// ```rust -/// use std::net::TcpStream; -/// use bip324::io::IntoBip324; -/// use bip324::{Network, Role}; -/// -/// # fn example() -> Result<(), Box> { -/// let stream = TcpStream::connect("127.0.0.1:8333")?; -/// let protocol = stream.into_bip324( -/// Network::Bitcoin, -/// Role::Initiator, -/// None, // no garbage -/// None, // no decoys -/// )?; -/// # Ok(()) -/// # } -/// ``` -pub trait IntoBip324 { - /// Convert this stream into a BIP-324 Protocol after performing the handshake. - /// - /// # Arguments - /// - /// * `network` - Network which both parties are operating on. - /// * `role` - Role in handshake, initiator or responder. - /// * `garbage` - Optional garbage bytes to send in handshake. - /// * `decoys` - Optional decoy packet contents bytes to send in handshake. - /// - /// # Returns - /// - /// A `Result` containing: - /// * `Ok(Protocol)`: An initialized protocol handler. - /// * `Err(ProtocolError)`: An error that occurred during the handshake or stream cloning. - /// - /// # Errors - /// - /// * `Io` - Includes errors from stream cloning and handshake I/O operations. - /// * `Internal` - Protocol-specific errors during handshake. - fn into_bip324( - self, - network: Network, - role: Role, - garbage: Option<&[u8]>, - decoys: Option<&[&[u8]]>, - ) -> Result, ProtocolError>; -} - -impl IntoBip324 for std::net::TcpStream { - fn into_bip324( - self, - network: Network, - role: Role, - garbage: Option<&[u8]>, - decoys: Option<&[&[u8]]>, - ) -> Result, ProtocolError> { - let reader = self.try_clone()?; - let writer = self; - Protocol::new(network, role, garbage, decoys, reader, writer) - } -} - #[cfg(test)] mod tests { use super::*; @@ -639,7 +556,7 @@ mod tests { ); } Ok(n) => panic!("Expected to read 1 byte but read {}", n), - Err(e) => panic!("Unexpected error reading from SessionReader: {}", e), + Err(e) => panic!("Unexpected error reading from session reader: {}", e), } } } diff --git a/protocol/tests/round_trips.rs b/protocol/tests/round_trips.rs index 6933ef2..05a6901 100644 --- a/protocol/tests/round_trips.rs +++ b/protocol/tests/round_trips.rs @@ -304,7 +304,7 @@ fn regtest_handshake_std() { }; use bip324::{ - io::IntoBip324, + io::Protocol, serde::{deserialize, serialize, NetworkMessage}, }; use bitcoin::p2p::{message_network::VersionMessage, Address, ServiceFlags}; @@ -312,17 +312,20 @@ fn regtest_handshake_std() { let bitcoind = regtest_process(TransportVersion::V2); let stream = TcpStream::connect(bitcoind.params.p2p_socket.unwrap()).unwrap(); + let reader = stream.try_clone().unwrap(); + let writer = stream; - // Initialize high-level protocol with handshake using the new into_bip324 method - println!("Starting BIP-324 handshake using into_bip324"); - let mut protocol = stream - .into_bip324( - bip324::Network::Regtest, - bip324::Role::Initiator, - None, // no garbage - None, // no decoys - ) - .unwrap(); + // Initialize high-level protocol with handshake + println!("Starting BIP-324 handshake"); + let mut protocol = Protocol::new( + bip324::Network::Regtest, + bip324::Role::Initiator, + None, // no garbage + None, // no decoys + reader, + writer, + ) + .unwrap(); println!("Handshake completed successfully!"); @@ -355,7 +358,7 @@ fn regtest_handshake_std() { let response_message = deserialize(payload.contents()).unwrap(); assert_eq!(response_message.cmd(), "version"); - println!("Successfully exchanged version messages using into_bip324 API!"); + println!("Successfully exchanged version messages using Protocol API!"); } #[tokio::test] From 645382569fefe8468fffc6a988fabefd5a25c4cb Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Tue, 8 Jul 2025 14:29:38 -0700 Subject: [PATCH 3/3] fix: add more docs on IO usage --- protocol/Cargo.toml | 2 +- protocol/README.md | 13 ++-- protocol/src/futures.rs | 29 ++++++-- protocol/src/io.rs | 49 ++++++++++++- protocol/src/lib.rs | 148 ++++++++++++++++++++++++++++++++++++---- 5 files changed, 214 insertions(+), 27 deletions(-) diff --git a/protocol/Cargo.toml b/protocol/Cargo.toml index 42064bc..367af15 100644 --- a/protocol/Cargo.toml +++ b/protocol/Cargo.toml @@ -30,4 +30,4 @@ chacha20-poly1305 = { version = "0.1.1", default-features = false } # bitcoind version 26.0 includes support for BIP-324's V2 protocol, but it is disabled by default. bitcoind = { package = "corepc-node", version = "0.7.1", default-features = false, features = ["26_0","download"] } hex = { package = "hex-conservative", version = "0.2.0" } -tokio = { version = "1", features = ["io-util", "net", "rt", "rt-multi-thread", "macros"] } +tokio = { version = "1", features = ["io-util", "net", "rt-multi-thread", "macros"] } diff --git a/protocol/README.md b/protocol/README.md index 32404c2..9bca35a 100644 --- a/protocol/README.md +++ b/protocol/README.md @@ -1,17 +1,16 @@ -# Protocol +# BIP-324 Protocol A BIP-324 library to establish and communicate over an encrypted channel. -The library is designed with a bare `no_std` and "Sans I/O" interface to keep it as agnostic as possible to application runtimes, but higher level interfaces are exposed for ease of use. +The library is designed with a bare `no_std` and *Sans I/O* interface to keep it as agnostic as possible to application runtimes, but higher level interfaces are exposed for ease of use. -The `tokio` feature includes the high-level `AsyncProcotol` type which helps create and manage an encrypted channel. - -The lower-level `CipherSession` and `Handshake` types can be directly used by applications which require more control. The handshake performs the one-and-a-half round trip dance between the peers in order to generate secret materials and verify a channel. A successful handshake results in a cipher session which performs the encrypt and decrypt operations for the lifetime of the channel. +* **High-level I/O** - `io::Protocol` (sync) and `futures::Protocol` (async) handle the complete encrypted connection lifecycle including handshake, writes, and reads. +* **Low-level components** - For applications requiring more control, `Handshake` is a type-safe state machine for the handshake protocol and `CipherSession` manages encryption/decryption after the handshake. ## Feature Flags -* `std` -- Standard library dependencies for I/O, memory allocation, and random number generators. -* `tokio` -- High level I/O wrappers for asynchronous tokio runtime. +* `std` - Standard library dependencies for I/O, memory allocation, and random number generators. +* `tokio` - High level I/O wrappers for the asynchronous tokio runtime. ## Minimum Supported Rust Version (MSRV) diff --git a/protocol/src/futures.rs b/protocol/src/futures.rs index d1a5c6b..9511028 100644 --- a/protocol/src/futures.rs +++ b/protocol/src/futures.rs @@ -2,30 +2,44 @@ //! Future-based asynchronous interfaces for establishing and using BIP-324 //! encrypted connections over AsyncRead/AsyncWrite transports. -//! -//! This module provides async implementations using the `tokio` runtime. //! It is only available when the `tokio` feature is enabled. //! +//! # Performance Note +//! +//! The BIP-324 protocol performs many small reads (3-byte length prefixes, +//! 16-byte terminators, etc.). For optimal performance, wrap your reader +//! in a [`tokio::io::BufReader`]. +//! //! # Example //! //! ```no_run //! use bip324::futures::Protocol; //! use bip324::{Network, Role}; //! use tokio::net::TcpStream; +//! use tokio::io::BufReader; //! //! # #[tokio::main] //! # async fn main() -> Result<(), Box> { +//! // Connect to a Bitcoin node //! let stream = TcpStream::connect("127.0.0.1:8333").await?; +//! +//! // Split the stream for reading and writing //! let (reader, writer) = stream.into_split(); +//! let reader = BufReader::new(reader); //! -//! let protocol = Protocol::new( +//! // Establish BIP-324 encrypted connection +//! let mut protocol = Protocol::new( //! Network::Bitcoin, //! Role::Initiator, -//! None, -//! None, +//! None, // no garbage bytes +//! None, // no decoy packets //! reader, //! writer, //! ).await?; +//! +//! // Send and receive encrypted messages +//! let response = protocol.read().await?; +//! println!("Received {} bytes", response.contents().len()); //! # Ok(()) //! # } //! ``` @@ -173,6 +187,11 @@ where /// /// This function is *not* cancellation safe. /// + /// # Performance Note + /// + /// For optimal performance, wrap your `reader` in a [`tokio::io::BufReader`]. + /// The protocol makes many small reads during handshake and operation. + /// /// # Arguments /// /// * `network` - Network which both parties are operating on. diff --git a/protocol/src/io.rs b/protocol/src/io.rs index 457e2f6..9c424a8 100644 --- a/protocol/src/io.rs +++ b/protocol/src/io.rs @@ -1,7 +1,47 @@ // SPDX-License-Identifier: CC0-1.0 -//! High-level interfaces for establishing and using BIP-324 encrypted -//! connections over Read/Write transports. +//! High-level synchronous interfaces for establishing +//! BIP-324 encrypted connections over Read/Write IO transports. +//! For asynchronous support, see the `futures` module. +//! +//! # Performance Note +//! +//! The BIP-324 protocol performs many small reads (3-byte length prefixes, +//! 16-byte terminators, etc.). For optimal performance, wrap your reader +//! in a [`std::io::BufReader`]. +//! +//! # Example +//! +//! ```no_run +//! use std::net::TcpStream; +//! use std::io::BufReader; +//! use bip324::io::Protocol; +//! use bip324::{Network, Role}; +//! +//! # fn main() -> Result<(), Box> { +//! // Connect to a Bitcoin node +//! let stream = TcpStream::connect("127.0.0.1:8333")?; +//! +//! // Split the stream for reading and writing +//! let reader = BufReader::new(stream.try_clone()?); +//! let writer = stream; +//! +//! // Establish BIP-324 encrypted connection +//! let mut protocol = Protocol::new( +//! Network::Bitcoin, +//! Role::Initiator, +//! None, // no garbage bytes +//! None, // no decoy packets +//! reader, +//! writer, +//! )?; +//! +//! // Send and receive encrypted messages +//! let response = protocol.read()?; +//! println!("Received {} bytes", response.contents().len()); +//! # Ok(()) +//! # } +//! ``` use core::fmt; use std::io::{Cursor, Read, Write}; @@ -287,6 +327,11 @@ where { /// New protocol session which completes the initial handshake and returns a handler. /// + /// # Performance Note + /// + /// For optimal performance, wrap your `reader` in a [`std::io::BufReader`]. + /// The protocol makes many small reads during handshake and operation. + /// /// # Arguments /// /// * `network` - Network which both parties are operating on. diff --git a/protocol/src/lib.rs b/protocol/src/lib.rs index 2229733..02b3e58 100644 --- a/protocol/src/lib.rs +++ b/protocol/src/lib.rs @@ -1,21 +1,145 @@ // SPDX-License-Identifier: CC0-1.0 -//! BIP-324 encrypted transport for exchanging bitcoin P2P *messages*. Much like TLS, a connection begins by exchanging ephemeral -//! elliptic curve public keys and performing a Diffie-Hellman handshake. Thereafter, each participant derives shared session secrets, and may -//! freely exchange encrypted packets. +//! BIP-324 encrypted transport protocol for Bitcoin P2P communication. //! -//! ## Packets +//! This crate implements the [BIP-324](https://github.com/bitcoin/bips/blob/master/bip-0324.mediawiki) +//! version 2 encrypted transport protocol, which provides encryption and authentication +//! for bitcoin p2p connections. Like TLS, it begins with a handshake establishing shared +//! secrets, then encrypts all subsequent communication. //! -//! A *packet* has the following layout: -//! * *Length* - 3-byte encrypted length of the *contents* (note this does not include the header byte). -//! * *Header* - 1-byte for transport layer protocol flags, currently only used to flag decoy packets. -//! * *Contents* - Variable length payload. -//! * *Tag* - 16-byte authentication tag. +//! # Quick Start //! -//! ## Application Messages +//! For a complete encrypted connection, use the high-level APIs in the [`io`] or [`futures`] modules: //! -//! Under the new V2 specification, P2P messages are encrypted differently than V1. -//! Read more about the [specification](https://github.com/bitcoin/bips/blob/master/bip-0324.mediawiki). +//! ## Synchronous API (requires `std` feature) +//! +//! ```no_run +//! use bip324::io::Protocol; +//! use bip324::serde::{serialize, deserialize, NetworkMessage}; +//! use std::net::TcpStream; +//! use std::io::BufReader; +//! +//! # fn main() -> Result<(), Box> { +//! let stream = TcpStream::connect("127.0.0.1:8333")?; +//! +//! // Wrap reader in BufReader for efficiency (protocol makes many small reads) +//! let reader = BufReader::new(stream.try_clone()?); +//! let writer = stream; +//! +//! let mut protocol = Protocol::new( +//! bip324::Network::Bitcoin, +//! bip324::Role::Initiator, +//! None, None, // no garbage or decoys +//! reader, +//! writer, +//! )?; +//! +//! let ping_msg = NetworkMessage::Ping(0xdeadbeef); +//! let serialized = serialize(ping_msg); +//! protocol.write(&serialized)?; +//! +//! let response = protocol.read()?; +//! let response_msg: NetworkMessage = deserialize(&response.contents())?; +//! # Ok(()) +//! # } +//! ``` +//! +//! ## Asynchronous API (requires `tokio` feature) +//! +//! ```no_run +//! # #[cfg(feature = "tokio")] +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! use bip324::futures::Protocol; +//! use bip324::serde::{serialize, deserialize, NetworkMessage}; +//! use tokio::net::TcpStream; +//! use tokio::io::BufReader; +//! +//! let stream = TcpStream::connect("127.0.0.1:8333").await?; +//! let (reader, writer) = stream.into_split(); +//! +//! // Wrap reader in BufReader for efficiency (protocol makes many small reads) +//! let buffered_reader = BufReader::new(reader); +//! +//! let mut protocol = Protocol::new( +//! bip324::Network::Bitcoin, +//! bip324::Role::Initiator, +//! None, None, // no garbage or decoys +//! buffered_reader, +//! writer, +//! ).await?; +//! +//! let ping_msg = NetworkMessage::Ping(12345); // nonce +//! let serialized = serialize(ping_msg); +//! protocol.write(&serialized).await?; +//! +//! let response = protocol.read().await?; +//! let response_msg: NetworkMessage = deserialize(&response.contents())?; +//! # Ok(()) +//! # } +//! # #[cfg(not(feature = "tokio"))] +//! # fn main() {} +//! ``` +//! +//! # Message Serialization +//! +//! BIP-324 introduces specific changes to how bitcoin P2P messages are serialized for V2 transport. +//! The [`serde`] module provides these serialization functions. +//! +//! ```no_run +//! # #[cfg(feature = "std")] +//! # fn main() -> Result<(), Box> { +//! use bip324::serde::{serialize, deserialize, NetworkMessage}; +//! +//! let ping_msg = NetworkMessage::Ping(0xdeadbeef); +//! let serialized = serialize(ping_msg); +//! +//! let received_bytes = vec![0x12, 0xef, 0xbe, 0xad, 0xde, 0, 0, 0, 0]; +//! let message: NetworkMessage = deserialize(&received_bytes)?; +//! # Ok(()) +//! # } +//! # #[cfg(not(feature = "std"))] +//! # fn main() {} +//! ``` +//! +//! # Performance Considerations +//! +//! The BIP-324 protocol makes multiple small reads, particularly during the handshake +//! (reading 64-byte keys, 16-byte terminators) and for each message (3-byte length prefix). +//! For optimal performance, wrap your reader in a `BufReader`: +//! +//! ```no_run +//! # #[cfg(feature = "std")] +//! # fn sync_example() -> Result<(), Box> { +//! use std::io::BufReader; +//! use std::net::TcpStream; +//! +//! let stream = TcpStream::connect("127.0.0.1:8333")?; +//! let buffered_reader = BufReader::new(stream.try_clone()?); +//! # Ok(()) +//! # } +//! ``` +//! +//! # Advanced Usage +//! +//! For more control, such as no-std environments, you can use the lower level components. +//! +//! - [`Handshake`] - Type-safe handshake state machine. +//! - [`CipherSession`] - Managed encryption/decryption after handshake. +//! +//! # Protocol Details +//! +//! After the initial handshake, all data is encrypted in packets. +//! +//! | Field | Size | Description | +//! |-------|------|-------------| +//! | Length | 3 bytes | Encrypted length of contents | +//! | Header | 1 byte | Protocol flags including decoy indicator | +//! | Contents | Variable | The serialized message | +//! | Tag | 16 bytes | Authentication tag | +//! +//! The protocol supports both genuine packets containing application data and decoy +//! packets with random data for traffic analysis resistance. #![no_std] #[cfg(feature = "std")]