diff --git a/protocols/v2/codec-sv2/Cargo.toml b/protocols/v2/codec-sv2/Cargo.toml index 4617eef10..361c60e97 100644 --- a/protocols/v2/codec-sv2/Cargo.toml +++ b/protocols/v2/codec-sv2/Cargo.toml @@ -20,10 +20,13 @@ const_sv2 = { version = "2.0.0", path = "../../../protocols/v2/const-sv2"} buffer_sv2 = { version = "1.0.0", path = "../../../utils/buffer"} tracing = { version = "0.1"} +[dev-dependencies] +key-utils = { version = "^1.0.0", path = "../../../utils/key-utils" } + [features] with_serde = ["binary_sv2/with_serde", "serde", "framing_sv2/with_serde", "buffer_sv2/with_serde"] with_buffer_pool = ["framing_sv2/with_buffer_pool"] no_std = [] [package.metadata.docs.rs] -all-features = true \ No newline at end of file +all-features = true diff --git a/protocols/v2/codec-sv2/README.md b/protocols/v2/codec-sv2/README.md new file mode 100644 index 000000000..a57a4a2fd --- /dev/null +++ b/protocols/v2/codec-sv2/README.md @@ -0,0 +1,49 @@ +# `codec_sv2` + +[![crates.io](https://img.shields.io/crates/v/codec_sv2.svg)](https://crates.io/crates/codec_sv2) +[![docs.rs](https://docs.rs/codec_sv2/badge.svg)](https://docs.rs/codec_sv2) +[![rustc+](https://img.shields.io/badge/rustc-1.75.0%2B-lightgrey.svg)](https://blog.rust-lang.org/2023/12/28/Rust-1.75.0.html) +[![license](https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg)](https://github.com/stratum-mining/stratum/blob/main/LICENSE.md) +[![codecov](https://codecov.io/gh/stratum-mining/stratum/branch/main/graph/badge.svg?flag=codec_sv2-coverage)](https://codecov.io/gh/stratum-mining/stratum) + +`codec_sv2` provides the message encoding and decoding functionality for the Stratum V2 (Sv2) +protocol, handling secure communication between Sv2 roles. This crate abstracts the complexity of +message encoding/decoding with optional Noise protocol support, ensuring both regular and encrypted +messages can be serialized, transmitted, and decoded consistently and reliably. + +## Main Components + +- **Encoder**: Encodes Sv2 messages with or without Noise protocol support. +- **Decoder**: Decodes Sv2 messages with or without Noise protocol support. +- **Handshake State**: Manages the current Noise protocol handshake state of the codec. + + +## Usage + +To include this crate in your project, run: + +```bash +cargo add codec_sv2 +``` + +This crate can be built with the following feature flags: + +- `noise_sv2`: Enables support for Noise protocol encryption and decryption. +- `with_buffer_pool`: Enables buffer pooling for more efficient memory management. +- `with_serde`: builds [`binary_sv2`](https://crates.io/crates/binary_sv2) and + [`buffer_sv2`](https://crates.io/crates/buffer_sv2) crates with `serde`-based encoding and + decoding. Note that this feature flag is only used for the Message Generator, and deprecated + for any other kind of usage. It will likely be fully deprecated in the future. + +### Examples + +This crate provides two examples demonstrating how to encode and decode Sv2 frames: + +1. **[Unencrypted Example](https://github.com/stratum-mining/stratum/blob/main/protocols/v2/codec-sv2/examples/unencrypted.rs)**: + Encode and decode standard Sv2 frames, detailing the message serialization and transmission + process for unencrypted communications. + +2. **[Encrypted Example](https://github.com/stratum-mining/stratum/blob/main/protocols/v2/codec-sv2/examples/encrypted.rs)**: + Encode and decode Sv2 frames with Noise protocol encryption, detailing the entire encryption + handshake and transport phase and serialization and transmission process for encrypted + communications. diff --git a/protocols/v2/codec-sv2/examples/encrypted.rs b/protocols/v2/codec-sv2/examples/encrypted.rs new file mode 100644 index 000000000..6713c4679 --- /dev/null +++ b/protocols/v2/codec-sv2/examples/encrypted.rs @@ -0,0 +1,208 @@ +// # Using Sv2 Codec with Noise Encryption +// +// This example demonstrates how to use the `codec-sv2` crate to encode and decode Sv2 frames +// with Noise protocol encryption over a TCP connection. It showcases how to: +// +// * Perform a Noise handshake between the sender and receiver. +// * Create an arbitrary custom message type (`CustomMessage`) for encryption/encoding and +// decryption/decoding. +// * Encode the message into an encrypted Sv2 frame using Noise. +// * Send the encrypted frame over a TCP connection. +// * Decode the encrypted Sv2 frame on the receiving side after completing the Noise handshake. +// +// ## Run +// +// ``` +// cargo run --example encrypted --features noise_sv2 +// ``` + +use binary_sv2::{binary_codec_sv2, Deserialize, Serialize}; +#[cfg(feature = "noise_sv2")] +use codec_sv2::{ + Error, HandshakeRole, Initiator, NoiseEncoder, Responder, StandardEitherFrame, + StandardNoiseDecoder, StandardSv2Frame, State, Sv2Frame, +}; +#[cfg(feature = "noise_sv2")] +use const_sv2::{ + INITIATOR_EXPECTED_HANDSHAKE_MESSAGE_SIZE, RESPONDER_EXPECTED_HANDSHAKE_MESSAGE_SIZE, +}; +#[cfg(feature = "noise_sv2")] +use key_utils::{Secp256k1PublicKey, Secp256k1SecretKey}; +use std::convert::TryInto; +#[cfg(feature = "noise_sv2")] +use std::{ + io::{Read, Write}, + net::{TcpListener, TcpStream}, +}; + +// Arbitrary message type. +// Supported Sv2 message types are listed in the [Sv2 Spec Message +// Types](https://github.com/stratum-mining/sv2-spec/blob/main/08-Message-Types.md). +#[cfg(feature = "noise_sv2")] +const CUSTOM_MSG_TYPE: u8 = 0xff; + +// Emulate a TCP connection +#[cfg(feature = "noise_sv2")] +const TCP_ADDR: &str = "127.0.0.1:3333"; + +#[cfg(feature = "noise_sv2")] +const AUTHORITY_PUBLIC_K: &str = "9auqWEzQDVyd2oe1JVGFLMLHZtCo2FFqZwtKA5gd9xbuEu7PH72"; +#[cfg(feature = "noise_sv2")] +const AUTHORITY_PRIVATE_K: &str = "mkDLTBBRxdBv998612qipDYoTK3YUrqLe8uWw7gu3iXbSrn2n"; +#[cfg(feature = "noise_sv2")] +const CERT_VALIDITY: std::time::Duration = std::time::Duration::from_secs(3600); + +// Example message type. +// In practice, all Sv2 messages are defined in the following crates: +// * `common_messages_sv2` +// * `mining_sv2` +// * `job_declaration_sv2` +// * `template_distribution_sv2` +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CustomMessage { + nonce: u16, +} + +#[cfg(feature = "noise_sv2")] +fn main() { + // Start a receiving listener + let listener_receiver = TcpListener::bind(TCP_ADDR).expect("Failed to bind TCP listener"); + + // Start a sender stream + let mut stream_sender: TcpStream = + TcpStream::connect(TCP_ADDR).expect("Failed to connect to TCP stream"); + + // Start a receiver stream + let mut stream_receiver: TcpStream = listener_receiver + .incoming() + .next() + .expect("Failed to accept incoming TCP stream") + .expect("Failed to connect to incoming TCP stream"); + + // Handshake + let authority_public_k: Secp256k1PublicKey = AUTHORITY_PUBLIC_K + .to_string() + .try_into() + .expect("Failed to convert receiver public key to Secp256k1PublicKey"); + + let authority_private_k: Secp256k1SecretKey = AUTHORITY_PRIVATE_K + .to_string() + .try_into() + .expect("Failed to convert receiver private key to Secp256k1PublicKey"); + + let initiator = Initiator::from_raw_k(authority_public_k.into_bytes()) + .expect("Failed to create initiator role from raw pub key"); + + let responder = Responder::from_authority_kp( + &authority_public_k.into_bytes(), + &authority_private_k.into_bytes(), + CERT_VALIDITY, + ) + .expect("Failed to initialize responder from pub/key pair and/or cert"); + + let mut sender_state = State::initialized(HandshakeRole::Initiator(initiator)); + let mut receiver_state = State::initialized(HandshakeRole::Responder(responder)); + + let first_message = sender_state + .step_0() + .expect("Initiator failed first step of handshake"); + let first_message: [u8; RESPONDER_EXPECTED_HANDSHAKE_MESSAGE_SIZE] = first_message + .get_payload_when_handshaking() + .try_into() + .expect("Handshake remote invlaid message"); + + let (second_message, receiver_state) = receiver_state + .step_1(first_message) + .expect("Responder failed second step of handshake"); + let second_message: [u8; INITIATOR_EXPECTED_HANDSHAKE_MESSAGE_SIZE] = second_message + .get_payload_when_handshaking() + .try_into() + .expect("Handshake remote invlaid message"); + + let sender_state = sender_state + .step_2(second_message) + .expect("Initiator failed third step of handshake"); + + let mut sender_state = match sender_state { + State::Transport(c) => State::with_transport_mode(c), + _ => panic!("todo"), + }; + + let mut receiver_state = match receiver_state { + State::Transport(c) => State::with_transport_mode(c), + _ => panic!("todo"), + }; + + // Create a message + let nonce = 1337; + let msg = CustomMessage { nonce }; + let msg_type = CUSTOM_MSG_TYPE; + // Unique identifier of the extension describing the protocol message, as defined by Sv2 Framing + let extension_type = 0; + // This message is intended for the receiver, so set to false + let channel_msg = false; + + let frame = StandardEitherFrame::::Sv2( + Sv2Frame::from_message(msg, msg_type, extension_type, channel_msg) + .expect("Failed to create the frame"), + ); + + let mut encoder = NoiseEncoder::::new(); + let encoded_frame = encoder + .encode(frame, &mut sender_state) + .expect("Failed to encode the frame"); + + // Send the encoded frame + stream_sender + .write_all(encoded_frame.as_slice()) + .expect("Failed to send the encoded frame"); + + // Initialize the decoder + let mut decoder = StandardNoiseDecoder::::new(); + + let mut decoded_frame; + + // Continuously read the frame from the TCP stream into the decoder buffer until the full + // message is received. + // + // Note: The length of the payload is defined in a header field. Every call to `next_frame` + // will return a `MissingBytes` error, until the full payload is received. + loop { + let decoder_buf = decoder.writable(); + + // Read the frame header into the decoder buffer + stream_receiver + .read_exact(decoder_buf) + .expect("Failed to read the encoded frame header"); + + let result = decoder.next_frame(&mut receiver_state); + match result { + Ok(frame) => { + let frame: StandardSv2Frame = frame + .try_into() + .expect("Failed to decode frame into Sv2Frame"); + decoded_frame = frame; + break; + } + Err(Error::MissingBytes(_)) => {} + Err(_) => panic!("Failed to decode the frame"), + } + } + + // Parse the decoded frame header and payload + let decoded_frame_header = decoded_frame + .get_header() + .expect("Failed to get the frame header"); + + let decoded_msg: CustomMessage = binary_sv2::from_bytes(decoded_frame.payload()) + .expect("Failed to extract the message from the payload"); + + // Assert that the decoded message is as expected + assert_eq!(decoded_frame_header.msg_type(), CUSTOM_MSG_TYPE); + assert_eq!(decoded_msg.nonce, nonce); +} + +#[cfg(not(feature = "noise_sv2"))] +fn main() { + eprintln!("Noise feature not enabled. Skipping example."); +} diff --git a/protocols/v2/codec-sv2/examples/unencrypted.rs b/protocols/v2/codec-sv2/examples/unencrypted.rs new file mode 100644 index 000000000..4b5d53c56 --- /dev/null +++ b/protocols/v2/codec-sv2/examples/unencrypted.rs @@ -0,0 +1,135 @@ +// # Using Sv2 Codec Without Encryption +// +// This example demonstrates how to use the `codec-sv2` crate to encode and decode Sv2 frames +// without encryption over a TCP connection. It showcases how to: +// +// * Create an arbitrary custom message type (`CustomMessage`) for encoding and decoding. +// * Encode the message into a Sv2 frame. +// * Send the encoded frame over a TCP connection. +// * Decode the Sv2 frame on the receiving side and extract the original message. +// +// ## Run +// +// ``` +// cargo run --example unencrypted +// ``` + +use binary_sv2::{binary_codec_sv2, Deserialize, Serialize}; +use codec_sv2::{Encoder, Error, StandardDecoder, StandardSv2Frame, Sv2Frame}; +use std::{ + convert::TryInto, + io::{Read, Write}, + net::{TcpListener, TcpStream}, +}; + +// Arbitrary message type. +// Supported Sv2 message types are listed in the [Sv2 Spec Message +// Types](https://github.com/stratum-mining/sv2-spec/blob/main/08-Message-Types.md). +const CUSTOM_MSG_TYPE: u8 = 0xff; + +// Emulate a TCP connection +const TCP_ADDR: &str = "127.0.0.1:3333"; + +// Example message type. +// In practice, all Sv2 messages are defined in the following crates: +// * `common_messages_sv2` +// * `mining_sv2` +// * `job_declaration_sv2` +// * `template_distribution_sv2` +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CustomMessage { + nonce: u16, +} + +fn main() { + // Start a receiving listener + let listener_receiver = TcpListener::bind(TCP_ADDR).expect("Failed to bind TCP listener"); + + // Start a sender stream + let stream_sender: TcpStream = + TcpStream::connect(TCP_ADDR).expect("Failed to connect to TCP stream"); + + // Start a receiver stream + let stream_receiver: TcpStream = listener_receiver + .incoming() + .next() + .expect("Failed to accept incoming TCP stream") + .expect("Failed to connect to incoming TCP stream"); + + // Server Side + + // Create a message + let nonce = 1337; + let msg = CustomMessage { nonce }; + let msg_type = CUSTOM_MSG_TYPE; + // Unique identifier of the extension describing the protocol message, as defined by Sv2 Framing + let extension_type = 0; + // This message is intended for the receiver, so set to false + let channel_msg = false; + sender_side(stream_sender, msg, msg_type, extension_type, channel_msg); + + // Receiver Side + let mut decoded_frame = receiver_side(stream_receiver); + + // Parse the decoded frame header and payload + let decoded_frame_header = decoded_frame + .get_header() + .expect("Failed to get the frame header"); + let decoded_msg: CustomMessage = binary_sv2::from_bytes(decoded_frame.payload()) + .expect("Failed to extract the message from the payload"); + + // Assert that the decoded message is as expected + assert_eq!(decoded_frame_header.msg_type(), CUSTOM_MSG_TYPE); + assert_eq!(decoded_msg.nonce, nonce); +} + +fn sender_side( + mut stream_sender: TcpStream, + msg: CustomMessage, + msg_type: u8, + extension_type: u16, + channel_msg: bool, +) { + // Create the frame + let frame = + StandardSv2Frame::::from_message(msg, msg_type, extension_type, channel_msg) + .expect("Failed to create the frame"); + + // Encode the frame + let mut encoder = Encoder::::new(); + let encoded_frame = encoder + .encode(frame.clone()) + .expect("Failed to encode the frame"); + + // Send the encoded frame + stream_sender + .write_all(encoded_frame) + .expect("Failed to send the encoded frame"); +} + +fn receiver_side(mut stream_receiver: TcpStream) -> Sv2Frame> { + // Initialize the decoder + let mut decoder = StandardDecoder::::new(); + + // Continuously read the frame from the TCP stream into the decoder buffer until the full + // message is received. + // + // Note: The length of the payload is defined in a header field. Every call to `next_frame` + // will return a `MissingBytes` error, until the full payload is received. + loop { + let decoder_buf = decoder.writable(); + + // Read the frame header into the decoder buffer + stream_receiver + .read_exact(decoder_buf) + .expect("Failed to read the encoded frame header"); + + match decoder.next_frame() { + Ok(decoded_frame) => { + return decoded_frame; + } + Err(Error::MissingBytes(_)) => {} + Err(_) => panic!("Failed to decode the frame"), + } + } +} diff --git a/protocols/v2/codec-sv2/src/decoder.rs b/protocols/v2/codec-sv2/src/decoder.rs index 4d1440018..751ae17d5 100644 --- a/protocols/v2/codec-sv2/src/decoder.rs +++ b/protocols/v2/codec-sv2/src/decoder.rs @@ -1,3 +1,28 @@ +// # Decoder +// +// Provides utilities for decoding messages held by Sv2 frames, with or without Noise protocol +// support. +// +// It includes primitives to both decode encoded standard Sv2 frames and to decrypt and decode +// Noise-encrypted encoded Sv2 frames, ensuring secure communication when required. +// +// ## Usage +// All messages passed between Sv2 roles are encoded as Sv2 frames. These frames are decoded using +// primitives in this module. There are two types of decoders for reading these frames: one for +// regular Sv2 frames [`StandardDecoder`], and another for Noise-encrypted frames +// [`StandardNoiseDecoder`]. Both decoders manage the deserialization of incoming data and, when +// applicable, the decryption of the data upon receiving the transmitted message. +// +// ### Buffer Management +// +// The decoders rely on buffers to hold intermediate data during the decoding process. +// +// - When the `with_buffer_pool` feature is enabled, the internal `Buffer` type is backed by a +// pool-allocated buffer [`binary_sv2::BufferPool`], providing more efficient memory usage, +// particularly in high-throughput scenarios. +// - If this feature is not enabled, a system memory buffer [`binary_sv2::BufferFromSystemMemory`] +// is used for simpler applications where memory efficiency is less critical. + #[cfg(feature = "noise_sv2")] use binary_sv2::Deserialize; #[cfg(feature = "noise_sv2")] @@ -18,38 +43,101 @@ use framing_sv2::{ #[cfg(feature = "noise_sv2")] use noise_sv2::NoiseCodec; +#[cfg(feature = "noise_sv2")] +use crate::error::Error; +use crate::error::Result; + +use crate::Error::MissingBytes; +#[cfg(feature = "noise_sv2")] +use crate::State; + #[cfg(not(feature = "with_buffer_pool"))] use buffer_sv2::{Buffer as IsBuffer, BufferFromSystemMemory as Buffer}; #[cfg(feature = "with_buffer_pool")] use buffer_sv2::{Buffer as IsBuffer, BufferFromSystemMemory, BufferPool}; + +// The buffer type for holding intermediate data during decoding. +// +// When the `with_buffer_pool` feature is enabled, `Buffer` is a pool-allocated buffer type +// [`BufferPool`], which allows for more efficient memory management. Otherwise, it defaults to +// [`BufferFromSystemMemory`]. +// +// `Buffer` is used for storing both serialized Sv2 frames and encrypted Noise data. #[cfg(feature = "with_buffer_pool")] type Buffer = BufferPool; -#[cfg(feature = "noise_sv2")] -use crate::error::Error; -use crate::error::Result; +/// An encoded or decoded Sv2 frame containing either a regular or Noise-protected message. +/// +/// A wrapper around the [`Frame`] enum that represents either a regular or Noise-protected Sv2 +/// frame containing the generic message type (`T`). +pub type StandardEitherFrame = Frame::Slice>; -use crate::Error::MissingBytes; -#[cfg(feature = "noise_sv2")] -use crate::State; +/// An encoded or decoded Sv2 frame. +/// +/// A wrapper around the [`Sv2Frame`] that represents a regular Sv2 frame containing the generic +/// message type (`T`). +pub type StandardSv2Frame = Sv2Frame::Slice>; +/// Standard Sv2 decoder with Noise protocol support. +/// +/// Used for decoding and decrypting generic message types (`T`) encoded in Sv2 frames and +/// encrypted via the Noise protocol. #[cfg(feature = "noise_sv2")] pub type StandardNoiseDecoder = WithNoise; -pub type StandardEitherFrame = Frame::Slice>; -pub type StandardSv2Frame = Sv2Frame::Slice>; + +/// Standard Sv2 decoder without Noise protocol support. +/// +/// Used for decoding generic message types (`T`) encoded in Sv2 frames. pub type StandardDecoder = WithoutNoise; +/// Decoder for Sv2 frames with Noise protocol support. +/// +/// Accumulates the encrypted data into a dedicated buffer until the entire encrypted frame is +/// received. The Noise protocol is then used to decrypt the accumulated data into another +/// dedicated buffer, converting it back into its original serialized form. This decrypted data is +/// then deserialized into the original Sv2 frame and message format. #[cfg(feature = "noise_sv2")] +#[derive(Debug)] pub struct WithNoise { + // Marker for the type of frame being decoded. + // + // Used to maintain the generic type (`T`) information of the message payload held by the + // frame. `T` refers to a type that implements the necessary traits for serialization + // [`binary_sv2::Serialize`] and size calculation [`binary_sv2::GetSize`]. frame: PhantomData, + + // Tracks the number of bytes remaining until the full frame is received. + // + // Ensures that the full encrypted Noise frame has been received by keeping track of the + // remaining bytes. Once the complete frame is received, decoding can proceed. missing_noise_b: usize, + + // Buffer for holding incoming encrypted Noise data to be decrypted. + // + // Stores the incoming encrypted data, allowing the decoder to accumulate the necessary bytes + // for full decryption. Once the entire encrypted frame is received, the decoder processes the + // buffer to extract the underlying frame. noise_buffer: B, + + // Buffer for holding decrypted data to be decoded. + // + // Stores the decrypted data until it is ready to be processed and converted into a Sv2 frame. sv2_buffer: B, } #[cfg(feature = "noise_sv2")] impl<'a, T: Serialize + GetSize + Deserialize<'a>, B: IsBuffer + AeadBuffer> WithNoise { + /// Attempts to decode the next Noise encrypted frame. + /// + /// On success, the decoded and decrypted frame is returned. Otherwise, an error indicating the + /// number of missing bytes required to complete the encoded frame, an error on a badly + /// formatted message header, or an error on decryption failure is returned. + /// + /// In this case of the `Error::MissingBytes`, the user should resize the decoder buffer using + /// `writable`, read another chunk from the incoming message stream, and then call `next_frame` + /// again. This process should be repeated until `next_frame` returns `Ok`, indicating that the + /// full message has been received, and the decoding and decryption of the frame can proceed. #[inline] pub fn next_frame(&mut self, state: &mut State) -> Result> { match state { @@ -96,6 +184,57 @@ impl<'a, T: Serialize + GetSize + Deserialize<'a>, B: IsBuffer + AeadBuffer> Wit } } + /// Provides a writable buffer for receiving incoming Noise-encrypted Sv2 data. + /// + /// This buffer is used to store incoming data, and its size is adjusted based on the number + /// of missing bytes. As new data is read, it is written into this buffer until enough data has + /// been received to fully decode a frame. The buffer must have the correct number of bytes + /// available to progress to the decoding process. + #[inline] + pub fn writable(&mut self) -> &mut [u8] { + self.noise_buffer.get_writable(self.missing_noise_b) + } + + /// Determines whether the decoder's internal buffers can be safely dropped. + /// + /// For more information, refer to the [`buffer_sv2` + /// crate](https://docs.rs/buffer_sv2/latest/buffer_sv2/). + pub fn droppable(&self) -> bool { + self.noise_buffer.is_droppable() && self.sv2_buffer.is_droppable() + } + + // Processes and decodes a Sv2 frame during the Noise protocol handshake phase. + // + // Handles the decoding of a handshake frame from the `noise_buffer`. It converts the received + // data into a `HandShakeFrame` and encapsulates it into a `Frame` for further processing by + // the codec. + // + // This is used exclusively during the initial handshake phase of the Noise protocol, before + // transitioning to regular frame encryption and decryption. + fn while_handshaking(&mut self) -> Frame { + let src = self.noise_buffer.get_data_owned().as_mut().to_vec(); + + // Since the frame length is already validated during the handshake process, this + // operation is infallible + let frame = HandShakeFrame::from_bytes_unchecked(src.into()); + + frame.into() + } + + // Decodes a Noise-encrypted Sv2 frame, handling both the message header and payload + // decryption. + // + // Processes Noise-encrypted Sv2 frames by first decrypting the header, followed by the + // payload. If the frame's data is received in chunks, it ensures that decryption occurs + // incrementally as more encrypted data becomes available. The decrypted data is then stored in + // the `sv2_buffer`, from which the resulting Sv2 frame is extracted and returned. + // + // On success, the decoded frame is returned. Otherwise, an error indicating the number of + // missing bytes required to complete the encoded frame, an error on a badly formatted message + // header, or a decryption failure error is returned. If there are still bytes missing to + // complete the frame, the function will return an `Error::MissingBytes` with the number of + // additional bytes required to fully decrypt the frame. Once all bytes are available, the + // decryption process completes and the frame can be successfully decoded. #[inline] fn decode_noise_frame(&mut self, noise_codec: &mut NoiseCodec) -> Result> { match ( @@ -144,27 +283,14 @@ impl<'a, T: Serialize + GetSize + Deserialize<'a>, B: IsBuffer + AeadBuffer> Wit } } } - - fn while_handshaking(&mut self) -> Frame { - let src = self.noise_buffer.get_data_owned().as_mut().to_vec(); - - // below is inffalible as noise frame length has been already checked - let frame = HandShakeFrame::from_bytes_unchecked(src.into()); - - frame.into() - } - - #[inline] - pub fn writable(&mut self) -> &mut [u8] { - self.noise_buffer.get_writable(self.missing_noise_b) - } - pub fn droppable(&self) -> bool { - self.noise_buffer.is_droppable() && self.sv2_buffer.is_droppable() - } } #[cfg(feature = "noise_sv2")] impl WithNoise { + /// Crates a new [`WithNoise`] decoder with default buffer sizes. + /// + /// Initializes the decoder with default buffer sizes and sets the number of missing bytes to + /// 0. pub fn new() -> Self { Self { frame: PhantomData, @@ -182,14 +308,46 @@ impl Default for WithNoise { } } +/// Decoder for standard Sv2 frames. +/// +/// Accumulates the data into a dedicated buffer until the entire Sv2 frame is received. This data +/// is then deserialized into the original Sv2 frame and message format. #[derive(Debug)] pub struct WithoutNoise { + // Marker for the type of frame being decoded. + // + // Used to maintain the generic type (`T`) information of the message payload held by the + // frame. `T` refers to a type that implements the necessary traits for serialization + // [`binary_sv2::Serialize`] and size calculation [`binary_sv2::GetSize`]. frame: PhantomData, + + // Tracks the number of bytes remaining until the full frame is received. + // + // Ensures that the full Sv2 frame has been received by keeping track of the remaining bytes. + // Once the complete frame is received, decoding can proceed. missing_b: usize, + + // Buffer for holding incoming data to be decoded into a Sv2 frame. + // + // This buffer stores incoming data as it is received, allowing the decoder to accumulate the + // necessary bytes until a full frame is available. Once the full encoded frame has been + // received, the buffer's contents are processed and decoded into an Sv2 frame. buffer: B, } impl WithoutNoise { + /// Attempts to decode the next frame, returning either a frame or an error indicating how many + /// bytes are missing. + /// + /// Attempts to decode the next Sv2 frame. + /// + /// On success, the decoded frame is returned. Otherwise, an error indicating the number of + /// missing bytes required to complete the frame is returned. + /// + /// In the case of `Error::MissingBytes`, the user should resize the decoder buffer using + /// `writable`, read another chunk from the incoming message stream, and then call `next_frame` + /// again. This process should be repeated until `next_frame` returns `Ok`, indicating that the + /// full message has been received, and the frame can be fully decoded. #[inline] pub fn next_frame(&mut self) -> Result> { let len = self.buffer.len(); @@ -210,12 +368,22 @@ impl WithoutNoise { } } + /// Provides a writable buffer for receiving incoming Sv2 data. + /// + /// This buffer is used to store incoming data, and its size is adjusted based on the number of + /// missing bytes. As new data is read, it is written into this buffer until enough data has + /// been received to fully decode a frame. The buffer must have the correct number of bytes + /// available to progress to the decoding process. pub fn writable(&mut self) -> &mut [u8] { self.buffer.get_writable(self.missing_b) } } impl WithoutNoise { + /// Creates a new [`WithoutNoise`] with a buffer of default size. + /// + /// Initializes the decoder with a default buffer size and sets the number of missing bytes to + /// the size of the header. pub fn new() -> Self { Self { frame: PhantomData, @@ -230,3 +398,20 @@ impl Default for WithoutNoise { Self::new() } } + +#[cfg(test)] +mod tests { + use super::*; + use binary_sv2::{binary_codec_sv2, Serialize}; + + #[derive(Serialize)] + pub struct TestMessage {} + + #[test] + fn unencrypted_writable_with_missing_b_initialized_as_header_size() { + let mut decoder = StandardDecoder::::new(); + let actual = decoder.writable(); + let expect = [0u8; Header::SIZE]; + assert_eq!(actual, expect); + } +} diff --git a/protocols/v2/codec-sv2/src/encoder.rs b/protocols/v2/codec-sv2/src/encoder.rs index 21618fda5..62d41cae9 100644 --- a/protocols/v2/codec-sv2/src/encoder.rs +++ b/protocols/v2/codec-sv2/src/encoder.rs @@ -1,3 +1,26 @@ +// # Encoder +// +// Provides utilities for encoding messages into Sv2 frames, with or without Noise protocol +// support. +// +// ## Usage +// +// All messages passed between Sv2 roles are encoded as Sv2 frames using primitives in this module. +// There are two types of encoders for creating these frames: one for regular Sv2 frames +// [`Encoder`], and another for Noise-encrypted frames [`NoiseEncoder`]. Both encoders manage the +// serialization of outgoing data and, when applicable, the encryption of the data before +// transmission. +// +// ### Buffer Management +// +// The encoders rely on buffers to hold intermediate data during the encoding process. +// +// - When the `with_buffer_pool` feature is enabled, the internal `Buffer` type is backed by a +// pool-allocated buffer [`binary_sv2::BufferPool`], providing more efficient memory usage, +// particularly in high-throughput scenarios. +// - If the feature is not enabled, a system memory buffer [`binary_sv2::BufferFromSystemMemory`] +// is used for simpler applications where memory efficiency is less critical. + use alloc::vec::Vec; use binary_sv2::{GetSize, Serialize}; #[allow(unused_imports)] @@ -25,28 +48,93 @@ use buffer_sv2::{Buffer as IsBuffer, BufferFromSystemMemory as Buffer}; #[cfg(feature = "with_buffer_pool")] use buffer_sv2::{Buffer as IsBuffer, BufferFromSystemMemory, BufferPool}; +// The buffer type for holding intermediate data during encoding. +// +// When the `with_buffer_pool` feature is enabled, `Buffer` uses a pool-allocated buffer +// [`BufferPool`], providing more efficient memory management, particularly in high-throughput +// environments. If the feature is not enabled, it defaults to [`BufferFromSystemMemory`], a +// simpler system memory buffer. +// +// `Buffer` is utilized for storing both serialized Sv2 frames and encrypted Noise data during the +// encoding process, ensuring that all frames are correctly handled before transmission. #[cfg(feature = "noise_sv2")] #[cfg(feature = "with_buffer_pool")] type Buffer = BufferPool; +// A simple buffer slice for holding serialized Sv2 frame data before transmission. +// +// When the `with_buffer_pool` feature is disabled, [`Slice`] defaults to a `Vec`, which serves +// as a dynamically allocated array to hold the serialized bytes of Sv2 frames. This provides +// flexibility in managing the encoded data during transmission or further processing, though it +// may not offer the same memory efficiency as the pool-allocated version [`BufferPool`]. #[cfg(not(feature = "with_buffer_pool"))] type Slice = Vec; +// A buffer slice used for holding serialized Sv2 frame data before transmission. +// +// When the `with_buffer_pool` feature is enabled, [`Slice`] defaults to a `buffer_sv2::Slice`, +// which serves as a slice of the `Buffer` that stores the encoded data. It holds the frame's +// serialized bytes temporarily, ensuring the data is ready for transmission or encryption, +// depending on whether Noise protocol support is enabled. #[cfg(feature = "with_buffer_pool")] type Slice = buffer_sv2::Slice; +/// Encoder for Sv2 frames with Noise protocol encryption. +/// +/// Serializes the Sv2 frame into a dedicated buffer. Encrypts this serialized data using the Noise +/// protocol, storing it into another dedicated buffer. Encodes the serialized and encrypted data, +/// such that it is ready for transmission. #[cfg(feature = "noise_sv2")] pub struct NoiseEncoder { + // Buffer for holding encrypted Noise data to be transmitted. + // + // Stores the encrypted data after the Sv2 frame has been processed by the Noise protocol + // and is ready for transmission. This buffer holds the outgoing encrypted data, ensuring + // that the full frame is correctly prepared before being sent. noise_buffer: Buffer, + + // Buffer for holding serialized Sv2 data before encryption. + // + // Stores the data after it has been serialized into an Sv2 frame but before it is encrypted + // by the Noise protocol. The buffer accumulates the frame's serialized bytes before they are + // encrypted and then encoded for transmission. sv2_buffer: Buffer, + + // Marker for the type of frame being encoded. + // + // Used to maintain the generic type information for `T`, which represents the message payload + // contained within the Sv2 frame. `T` refers to a type that implements the necessary traits + // for serialization [`binary_sv2::Serialize`] and size calculation [`binary_sv2::GetSize`], + // ensuring that the encoder can handle different message types correctly during the encoding + // process. frame: PhantomData, } +// A Sv2 frame that will be encoded and optionally encrypted using the Noise protocol. +// +// Represent a Sv2 frame during the encoding process. It encapsulates the frame's generic payload +// message type (`T`) and is stored in a [`Slice`] buffer. The `Item` is passed to the encoder, +// which either processes it for normal transmission or applies Noise encryption, depending on the +// codec's state. #[cfg(feature = "noise_sv2")] type Item = Frame; #[cfg(feature = "noise_sv2")] impl NoiseEncoder { + /// Encodes an Sv2 frame and encrypts it using the Noise protocol. + /// + /// Takes an `item`, which is an Sv2 frame containing a payload of type `T`, and encodes it for + /// transmission. The frame is encrypted after being serialized. The `state` parameter + /// determines whether the encoder is in the handshake or transport phase, guiding the + /// appropriate encoding and encryption action. + /// + /// - In the handshake phase, the initial handshake messages are processed to establish secure + /// communication. + /// - In the transport phase, the full frame is serialized, encrypted, and stored in a buffer + /// for transmission. + /// + /// On success, the method returns an encrypted (`Slice`) (buffer) ready for transmission. + /// Otherwise, errors on an encryption or serialization failure. #[inline] pub fn encode(&mut self, item: Item, state: &mut State) -> Result { match state { @@ -99,6 +187,13 @@ impl NoiseEncoder { Ok(self.noise_buffer.get_data_owned()) } + // Encodes Sv2 frames during the handshake phase of the Noise protocol. + // + // Used when the encoder is in the handshake phase, before secure communication is fully + // established. It encodes the provided `item` into a handshake frame, storing the resulting + // data in the `noise_buffer`. The handshake phase is necessary to exchange initial messages + // and set up the Noise encryption state before transitioning to the transport phase, where + // full frames are encrypted and transmitted. #[inline(never)] fn while_handshaking(&mut self, item: Item) -> Result<()> { // ENCODE THE SV2 FRAME @@ -114,6 +209,7 @@ impl NoiseEncoder { Ok(()) } + /// Determines whether the encoder's internal buffers can be safely dropped. pub fn droppable(&self) -> bool { self.noise_buffer.is_droppable() && self.sv2_buffer.is_droppable() } @@ -121,6 +217,7 @@ impl NoiseEncoder { #[cfg(feature = "noise_sv2")] impl NoiseEncoder { + /// Creates a new `NoiseEncoder` with default buffer sizes. pub fn new() -> Self { #[cfg(not(feature = "with_buffer_pool"))] let size = 512; @@ -141,13 +238,36 @@ impl Default for NoiseEncoder { } } +/// Encoder for standard Sv2 frames. +/// +/// Serializes the Sv2 frame into a dedicated buffer then encodes it, such that it is ready for +/// transmission. #[derive(Debug)] pub struct Encoder { + // Buffer for holding serialized Sv2 data. + // + // Stores the serialized bytes of the Sv2 frame after it has been encoded. Once the frame is + // serialized, the resulting bytes are stored in this buffer to be transmitted. The buffer is + // dynamically resized to accommodate the size of the encoded frame. buffer: Vec, + + // Marker for the type of frame being encoded. + // + // Used to maintain the generic type information for `T`, which represents the message payload + // contained within the Sv2 frame. `T` refers to a type that implements the necessary traits + // for serialization [`binary_sv2::Serialize`] and size calculation [`binary_sv2::GetSize`], + // ensuring that the encoder can handle different message types correctly during the encoding + // process. frame: PhantomData, } impl Encoder { + /// Encodes a standard Sv2 frame for transmission. + /// + /// Takes a standard Sv2 frame containing a payload of type `T` and serializes it into a byte + /// stream. The resulting serialized bytes are stored in the internal `buffer`, preparing the + /// frame for transmission. On success, the method returns a reference to the serialized bytes + /// stored in the internal buffer. Otherwise, errors on a serialization failure. pub fn encode( &mut self, item: Sv2Frame, @@ -161,6 +281,7 @@ impl Encoder { Ok(&self.buffer[..]) } + /// Creates a new `Encoder` with a buffer of default size. pub fn new() -> Self { Self { buffer: Vec::with_capacity(512), diff --git a/protocols/v2/codec-sv2/src/error.rs b/protocols/v2/codec-sv2/src/error.rs index 3201a6f2a..b5810cb85 100644 --- a/protocols/v2/codec-sv2/src/error.rs +++ b/protocols/v2/codec-sv2/src/error.rs @@ -1,3 +1,9 @@ +//! # Error Handling and Result Types +//! +//! This module defines error types and utilities for handling errors in the `codec_sv2` module. +//! It includes the [`Error`] enum for representing various errors, a C-compatible [`CError`] enum +//! for FFI, and a `Result` type alias for convenience. + #[cfg(test)] use core::cmp; use core::fmt; @@ -5,65 +11,95 @@ use framing_sv2::Error as FramingError; #[cfg(feature = "noise_sv2")] use noise_sv2::{AeadError, Error as NoiseError}; +/// A type alias for results returned by the `codec_sv2` modules. +/// +/// `Result` is a convenient wrapper around the [`core::result::Result`] type, using the [`Error`] +/// enum defined in this crate as the error type. pub type Result = core::result::Result; +/// Enumeration of possible errors in the `codec_sv2` module. +/// +/// This enum represents various errors that can occur within the `codec_sv2` module, including +/// errors from related crates like [`binary_sv2`], [`framing_sv2`], and [`noise_sv2`]. #[derive(Debug, PartialEq, Eq)] pub enum Error { - /// Errors from the `binary_sv2` crate + /// AEAD (`snow`) error in the Noise protocol. + #[cfg(feature = "noise_sv2")] + AeadError(AeadError), + + /// Binary Sv2 data format error. BinarySv2Error(binary_sv2::Error), + + /// Framing Sv2 error. + FramingError(FramingError), + + /// Framing Sv2 error. FramingSv2Error(framing_sv2::Error), - /// Errors if there are missing bytes in the Noise protocol - MissingBytes(usize), - /// Errors from the `noise_sv2` crate - #[cfg(feature = "noise_sv2")] - NoiseSv2Error(NoiseError), + + /// Invalid step for initiator in the Noise protocol. #[cfg(feature = "noise_sv2")] - AeadError(AeadError), - /// Error if Noise protocol state is not as expected - UnexpectedNoiseState, + InvalidStepForInitiator, + + /// Invalid step for responder in the Noise protocol. #[cfg(feature = "noise_sv2")] InvalidStepForResponder, + + /// Incomplete frame with the number of missing bytes remaining to completion. + MissingBytes(usize), + + /// Sv2 Noise protocol error. #[cfg(feature = "noise_sv2")] - InvalidStepForInitiator, + NoiseSv2Error(NoiseError), + + /// Noise protocol is not in the expected handshake state. #[cfg(feature = "noise_sv2")] NotInHandShakeState, - FramingError(FramingError), + + /// Unexpected state in the Noise protocol. + UnexpectedNoiseState, } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { use Error::*; match self { + #[cfg(feature = "noise_sv2")] + AeadError(e) => write!(f, "Aead Error: `{:?}`", e), BinarySv2Error(e) => write!(f, "Binary Sv2 Error: `{:?}`", e), + FramingError(e) => write!(f, "Framing error in codec: `{:?}`", e), FramingSv2Error(e) => write!(f, "Framing Sv2 Error: `{:?}`", e), - MissingBytes(u) => write!(f, "Missing `{}` Noise bytes", u), - #[cfg(feature = "noise_sv2")] - NoiseSv2Error(e) => write!(f, "Noise SV2 Error: `{:?}`", e), #[cfg(feature = "noise_sv2")] - AeadError(e) => write!(f, "Aead Error: `{:?}`", e), - UnexpectedNoiseState => { - write!(f, "Noise state is incorrect") - } + InvalidStepForInitiator => write!( + f, + "This noise handshake step can not be executed by an initiato" + ), #[cfg(feature = "noise_sv2")] InvalidStepForResponder => write!( f, "This noise handshake step can not be executed by a responder" ), + MissingBytes(u) => write!(f, "Missing `{}` Noise bytes", u), #[cfg(feature = "noise_sv2")] - InvalidStepForInitiator => write!( - f, - "This noise handshake step can not be executed by an initiato" - ), + NoiseSv2Error(e) => write!(f, "Noise SV2 Error: `{:?}`", e), #[cfg(feature = "noise_sv2")] NotInHandShakeState => write!( f, "This operation can be executed only during the noise handshake" ), - FramingError(e) => write!(f, "Framing error in codec: `{:?}`", e), + UnexpectedNoiseState => { + write!(f, "Noise state is incorrect") + } } } } +#[cfg(feature = "noise_sv2")] +impl From for Error { + fn from(e: AeadError) -> Self { + Error::AeadError(e) + } +} + impl From for Error { fn from(e: binary_sv2::Error) -> Self { Error::BinarySv2Error(e) @@ -76,13 +112,6 @@ impl From for Error { } } -#[cfg(feature = "noise_sv2")] -impl From for Error { - fn from(e: AeadError) -> Self { - Error::AeadError(e) - } -} - #[cfg(feature = "noise_sv2")] impl From for Error { fn from(e: NoiseError) -> Self { @@ -90,28 +119,49 @@ impl From for Error { } } +/// C-compatible enumeration of possible errors in the `codec_sv2` module. +/// +/// This enum mirrors the [`Error`] enum but is designed to be used in C code through FFI. It +/// represents the same set of errors as [`Error`], making them accessible to C programs. #[repr(C)] #[derive(Debug)] pub enum CError { - /// Errors from the `binary_sv2` crate + /// AEAD (`snow`) error in the Noise protocol. + AeadError, + + /// Binary Sv2 data format error. BinarySv2Error, - /// Errors from the `framing_sv2` crate + + /// Framing Sv2 error. + FramingError, + + /// Framing Sv2 error. FramingSv2Error, - /// Errors if there are missing bytes in the Noise protocol + + /// Invalid step for initiator in the Noise protocol. + InvalidStepForInitiator, + + /// Invalid step for responder in the Noise protocol. + InvalidStepForResponder, + + /// Missing bytes in the Noise protocol. MissingBytes(usize), - /// Errors from the `noise_sv2` crate + + /// Sv2 Noise protocol error. NoiseSv2Error, - /// `snow` errors - AeadError, - /// Error if Noise protocol state is not as expected - UnexpectedNoiseState, - InvalidStepForResponder, - InvalidStepForInitiator, + + /// Noise protocol is not in the expected handshake state. NotInHandShakeState, - FramingError, + + /// Unexpected state in the Noise protocol. + UnexpectedNoiseState, } -/// Here only to force cbindgen to create header for CError +/// Force `cbindgen` to create a header for [`CError`]. +/// +/// It ensures that [`CError`] is included in the generated C header file. This function is not meant +/// to be called and will panic if called. Its only purpose is to make [`CError`] visible to +/// `cbindgen`. #[no_mangle] pub extern "C" fn export_cerror() -> CError { unimplemented!() @@ -120,21 +170,21 @@ pub extern "C" fn export_cerror() -> CError { impl From for CError { fn from(e: Error) -> CError { match e { + #[cfg(feature = "noise_sv2")] + Error::AeadError(_) => CError::AeadError, Error::BinarySv2Error(_) => CError::BinarySv2Error, Error::FramingSv2Error(_) => CError::FramingSv2Error, - Error::MissingBytes(u) => CError::MissingBytes(u), - #[cfg(feature = "noise_sv2")] - Error::NoiseSv2Error(_) => CError::NoiseSv2Error, + Error::FramingError(_) => CError::FramingError, #[cfg(feature = "noise_sv2")] - Error::AeadError(_) => CError::AeadError, - Error::UnexpectedNoiseState => CError::UnexpectedNoiseState, + Error::InvalidStepForInitiator => CError::InvalidStepForInitiator, #[cfg(feature = "noise_sv2")] Error::InvalidStepForResponder => CError::InvalidStepForResponder, + Error::MissingBytes(u) => CError::MissingBytes(u), #[cfg(feature = "noise_sv2")] - Error::InvalidStepForInitiator => CError::InvalidStepForInitiator, + Error::NoiseSv2Error(_) => CError::NoiseSv2Error, #[cfg(feature = "noise_sv2")] Error::NotInHandShakeState => CError::NotInHandShakeState, - Error::FramingError(_) => CError::FramingError, + Error::UnexpectedNoiseState => CError::UnexpectedNoiseState, } } } @@ -142,16 +192,16 @@ impl From for CError { impl Drop for CError { fn drop(&mut self) { match self { + CError::AeadError => (), CError::BinarySv2Error => (), + CError::FramingError => (), CError::FramingSv2Error => (), + CError::InvalidStepForInitiator => (), + CError::InvalidStepForResponder => (), CError::MissingBytes(_) => (), CError::NoiseSv2Error => (), - CError::AeadError => (), - CError::UnexpectedNoiseState => (), - CError::InvalidStepForResponder => (), - CError::InvalidStepForInitiator => (), CError::NotInHandShakeState => (), - CError::FramingError => (), + CError::UnexpectedNoiseState => (), }; } } diff --git a/protocols/v2/codec-sv2/src/lib.rs b/protocols/v2/codec-sv2/src/lib.rs index a4eec1efd..8b2c05ce9 100644 --- a/protocols/v2/codec-sv2/src/lib.rs +++ b/protocols/v2/codec-sv2/src/lib.rs @@ -1,3 +1,37 @@ +//! # Stratum V2 Codec Library +//! +//! `codec_sv2` provides the message encoding and decoding functionality for the Stratum V2 (Sv2) +//! protocol, handling secure communication between Sv2 roles. +//! +//! This crate abstracts the complexity of message encoding/decoding with optional Noise protocol +//! support, ensuring both regular and encrypted messages can be serialized, transmitted, and +//! decoded consistently and reliably. +//! +//! +//! ## Usage +//! `codec-sv2` supports both standard Sv2 frames (unencrypted) and Noise-encrypted Sv2 frames to +//! ensure secure communication. To encode messages for transmission, choose between the +//! [`Encoder`] for standard Sv2 frames or the [`NoiseEncoder`] for encrypted frames. To decode +//! received messages, choose between the [`StandardDecoder`] for standard Sv2 frames or +//! [`StandardNoiseDecoder`] to decrypt Noise frames. +//! +//! ## Build Options +//! +//! This crate can be built with the following features: +//! +//! - `noise_sv2`: Enables support for Noise protocol encryption and decryption. +//! - `with_buffer_pool`: Enables buffer pooling for more efficient memory management. +//! - `with_serde`: builds [`binary_sv2`] and [`buffer_sv2`] crates with `serde`-based encoding and +//! decoding. Note that this feature flag is only used for the Message Generator, and deprecated +//! for any other kind of usage. It will likely be fully deprecated in the future. +//! +//! ## Examples +//! +//! See the examples for more information: +//! +//! - [Unencrypted Example](https://github.com/stratum-mining/stratum/blob/main/protocols/v2/codec-sv2/examples/unencrypted.rs) +//! - [Encrypted Example](https://github.com/stratum-mining/stratum/blob/main/protocols/v2/codec-sv2/examples/encrypted.rs) + #![cfg_attr(feature = "no_std", no_std)] pub use framing_sv2::framing::Frame; @@ -34,20 +68,75 @@ pub use buffer_sv2; pub use framing_sv2::{self, framing::handshake_message_to_frame as h2f}; +/// Represents the role in the Noise handshake process, either as an initiator or a responder. +/// +/// The Noise protocol requires two roles during the handshake process: +/// - **Initiator**: The party that starts the handshake by sending the initial message. +/// - **Responder**: The party that waits for the initiator's message and responds to it. +/// +/// This enum distinguishes between these two roles, allowing the codec to handle the handshake +/// process accordingly. +#[allow(clippy::large_enum_variant)] +#[cfg(feature = "noise_sv2")] +#[derive(Debug)] +pub enum HandshakeRole { + /// The initiator role in the Noise handshake process. + /// + /// The initiator starts the handshake by sending the initial message. This variant stores an + /// `Initiator` object, which contains the necessary state and cryptographic materials for the + /// initiator's part in the Noise handshake. + Initiator(Box), + + /// The responder role in the Noise handshake process. + /// + /// The responder waits for the initiator's handshake message and then responds. This variant + /// stores a `Responder` object, which contains the necessary state and cryptographic materials + /// for the responder's part in the Noise handshake. + Responder(Box), +} + +/// Represents the state of the Noise protocol codec during different phases: initialization, +/// handshake, or transport mode, where encryption and decryption are fully operational. +/// +/// The state transitions from initialization [`State::NotInitialized`] to handshake +/// [`State::HandShake`] and finally to transport mode [`State::Transport`] as the encryption +/// handshake is completed. #[cfg(feature = "noise_sv2")] #[derive(Debug)] #[allow(clippy::large_enum_variant)] pub enum State { - /// Not yet initialized + /// The codec has not been initialized yet. + /// + /// This state is used when the codec is first created or reset, before the handshake process + /// begins. The variant carries the expected size of the handshake message, which can vary + /// depending on whether the codec is acting as an initiator or a responder. NotInitialized(usize), - /// Handshake mode where codec is negotiating keys + + /// The codec is in the handshake phase, where cryptographic keys are being negotiated. + /// + /// In this state, the codec is in the process of establishing secure communication by + /// exchanging handshake messages. Once the handshake is complete, the codec transitions to + /// [`State::Transport`] mode. HandShake(HandshakeRole), - /// Transport mode where AEAD is fully operational. The `TransportMode` object in this variant - /// as able to perform encryption and decryption resp. + + /// The codec is in transport mode, where AEAD encryption and decryption are fully operational. + /// + /// In this state, the codec is performing full encryption and decryption using the Noise + /// protocol in transport mode. The [`NoiseCodec`] object is responsible for handling the + /// encryption and decryption of data. Transport(NoiseCodec), } + #[cfg(feature = "noise_sv2")] impl State { + /// Initiates the first step of the handshake process for the initiator. + /// + /// Creates and sends the initial handshake message for the initiator. It is the first step in + /// establishing a secure communication channel. Responders cannot perform this step. + /// + /// nb: This method returns a [`HandShakeFrame`] but does not change the current state + /// (`self`). The state remains `State::HandShake(HandshakeRole::Initiator)` until `step_1` is + /// called to advance the handshake process. pub fn step_0(&mut self) -> core::result::Result { match self { Self::HandShake(h) => match h { @@ -58,6 +147,15 @@ impl State { } } + /// Processes the second step of the handshake process for the responder. + /// + /// The responder receives the public key from the initiator, generates a response message + /// containing the handshake frame, and prepares the [`NoiseCodec`] for transitioning the + /// initiator state to transport mode in `step_2`. + /// + /// nb: Returns a new state [`State::Transport`] but does not update the current state + /// (`self`). The caller is responsible for updating the state, allowing for more flexible + /// control over the handshake process as the caller decides what to do with this state. pub fn step_1( &mut self, re_pub: [u8; const_sv2::RESPONDER_EXPECTED_HANDSHAKE_MESSAGE_SIZE], @@ -74,6 +172,14 @@ impl State { } } + /// Processes the final step of the handshake process for the initiator. + /// + /// Receives the response message from the responder containing the handshake frame, and + /// transitions the state to transport mode. This finalizes the secure communication setup and + /// enables full encryption and decryption in [`State::Transport`] mode. + /// + /// nb: Directly updates the current state (`self`) to [`State::Transport`], completing the + /// handshake process. pub fn step_2( &mut self, message: [u8; const_sv2::INITIATOR_EXPECTED_HANDSHAKE_MESSAGE_SIZE], @@ -89,16 +195,16 @@ impl State { } } } -#[allow(clippy::large_enum_variant)] -#[cfg(feature = "noise_sv2")] -#[derive(Debug)] -pub enum HandshakeRole { - Initiator(Box), - Responder(Box), -} #[cfg(feature = "noise_sv2")] impl State { + /// Creates a new uninitialized handshake [`State`]. + /// + /// Sets the codec to the initial state, [`State::NotInitialized`], based on the provided + /// handshake role. This state is used before the handshake process begins, and the handshake + /// message size guides the codec on how much data to expect before advancing to the next step. + /// The expected size of the handshake message is determined by whether the codec is acting as + /// an initiator or responder. pub fn not_initialized(role: &HandshakeRole) -> Self { match role { HandshakeRole::Initiator(_) => { @@ -110,10 +216,28 @@ impl State { } } + /// Initializes the codec state to [`State::HandShake`] mode with the given handshake role. + /// + /// Transitions the codec into the handshake phase by accepting a [`HandshakeRole`], which + /// determines whether the codec is the initiator or responder in the handshake process. Once + /// in [`State::HandShake`] mode, the codec begins negotiating cryptographic keys with the + /// peer, eventually transitioning to the secure [`State::Transport`] phase. + /// + /// The role passed to this method defines how the handshake proceeds: + /// - [`HandshakeRole::Initiator`]: The codec will start the handshake process. + /// - [`HandshakeRole::Responder`]: The codec will wait for the initiator's handshake message. pub fn initialized(inner: HandshakeRole) -> Self { Self::HandShake(inner) } + /// Transitions the codec state to [`State::Transport`] mode with the given [`NoiseCodec`]. + /// + /// Finalizes the handshake process and transitions the codec into [`State::Transport`] mode, + /// where full encryption and decryption are active. The codec uses the provided [`NoiseCodec`] + /// to perform encryption and decryption for all communication in this mode, ensuring secure + /// data transmission. + /// + /// Once in [`State::Transport`] mode, the codec is fully operational for secure communication. pub fn with_transport_mode(tm: NoiseCodec) -> Self { Self::Transport(tm) } diff --git a/protocols/v2/sv2-ffi/sv2.h b/protocols/v2/sv2-ffi/sv2.h index 23fbc9807..534c12e6e 100644 --- a/protocols/v2/sv2-ffi/sv2.h +++ b/protocols/v2/sv2-ffi/sv2.h @@ -451,24 +451,32 @@ void free_submit_solution(CSubmitSolution s); #include #include +/// C-compatible enumeration of possible errors in the `codec_sv2` module. +/// +/// This enum mirrors the [`Error`] enum but is designed to be used in C code through FFI. It +/// represents the same set of errors as [`Error`], making them accessible to C programs. struct CError { enum class Tag { - /// Errors from the `binary_sv2` crate + /// AEAD (`snow`) error in the Noise protocol. + AeadError, + /// Binary Sv2 data format error. BinarySv2Error, - /// Errors from the `framing_sv2` crate + /// Framing Sv2 error. + FramingError, + /// Framing Sv2 error. FramingSv2Error, - /// Errors if there are missing bytes in the Noise protocol + /// Invalid step for initiator in the Noise protocol. + InvalidStepForInitiator, + /// Invalid step for responder in the Noise protocol. + InvalidStepForResponder, + /// Missing bytes in the Noise protocol. MissingBytes, - /// Errors from the `noise_sv2` crate + /// Sv2 Noise protocol error. NoiseSv2Error, - /// `snow` errors - AeadError, - /// Error if Noise protocol state is not as expected - UnexpectedNoiseState, - InvalidStepForResponder, - InvalidStepForInitiator, + /// Noise protocol is not in the expected handshake state. NotInHandShakeState, - FramingError, + /// Unexpected state in the Noise protocol. + UnexpectedNoiseState, }; struct MissingBytes_Body { @@ -483,7 +491,11 @@ struct CError { extern "C" { -/// Here only to force cbindgen to create header for CError +/// Force `cbindgen` to create a header for [`CError`]. +/// +/// It ensures that [`CError`] is included in the generated C header file. This function is not meant +/// to be called and will panic if called. Its only purpose is to make [`CError`] visible to +/// `cbindgen`. CError export_cerror(); } // extern "C"