From af2c4f50b6f89dfda80f4f2cf7aed0e8a4934c9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 10 Apr 2025 13:43:29 +0100 Subject: [PATCH] Feature/updated sphinx payload keys (#5698) * removed support for legacy packet types from NymCodec I think nodes had plenty of time to upgrade given versioned variant was introduced in 2022 * temp: use local sphinx packet for development * introduce new messages that use more efficient reply surbs encoding * checks for incorrect encoding * generate correct message depending on config value * fixed current packet version * made packet type selection configurable * updated sphinx packet crate to the published version * fixed wasm build * fixes in outfox due to sphinx api changes * additional tests * clippy * fixed log/tracing import --- Cargo.lock | 5 +- Cargo.toml | 7 +- common/client-core/config-types/src/lib.rs | 14 +- .../src/client/cover_traffic_stream.rs | 6 + .../real_messages_control/message_handler.rs | 24 +- .../src/client/real_messages_control/mod.rs | 1 + .../real_traffic_stream.rs | 1 + .../client-core/src/client/received_buffer.rs | 46 +- common/node-tester-utils/src/tester.rs | 10 + .../acknowledgements/src/surb_ack.rs | 3 + common/nymsphinx/addressing/src/nodes.rs | 2 +- common/nymsphinx/anonymous-replies/Cargo.toml | 1 + .../anonymous-replies/src/reply_surb.rs | 47 +- .../src/{requests.rs => requests/mod.rs} | 472 +++++++++++------- .../anonymous-replies/src/requests/v1.rs | 176 +++++++ .../anonymous-replies/src/requests/v2.rs | 187 +++++++ common/nymsphinx/cover/src/lib.rs | 5 + common/nymsphinx/framing/src/codec.rs | 126 ++--- common/nymsphinx/framing/src/packet.rs | 113 ++--- common/nymsphinx/params/src/lib.rs | 11 - common/nymsphinx/params/src/packet_version.rs | 78 +-- common/nymsphinx/src/message.rs | 4 +- common/nymsphinx/src/preparer/mod.rs | 34 +- common/nymsphinx/types/src/lib.rs | 25 +- common/wasm/client-core/src/config/mod.rs | 10 + .../wasm/client-core/src/config/override.rs | 12 + .../src/network_monitor/monitor/preparer.rs | 1 + nym-node/src/throughput_tester/client.rs | 23 +- nym-outfox/src/packet.rs | 2 +- nym-outfox/tests/unittests.rs | 12 +- wasm/node-tester/src/tester.rs | 1 + 31 files changed, 1016 insertions(+), 443 deletions(-) rename common/nymsphinx/anonymous-replies/src/{requests.rs => requests/mod.rs} (59%) create mode 100644 common/nymsphinx/anonymous-replies/src/requests/v1.rs create mode 100644 common/nymsphinx/anonymous-replies/src/requests/v2.rs diff --git a/Cargo.lock b/Cargo.lock index 0cf9e08193..97f1c0779a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6645,6 +6645,7 @@ dependencies = [ "rand_chacha 0.3.1", "serde", "thiserror 2.0.12", + "tracing", "wasm-bindgen", ] @@ -9230,9 +9231,9 @@ dependencies = [ [[package]] name = "sphinx-packet" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b63a72efe7dce8a546d5cb855e60699ae69203d0d7e4335a654eb87e93d7d141" +checksum = "c26f0c20d909fdda1c5d0ece3973127ca421984d55b000215df365e93722fc6e" dependencies = [ "aes", "arrayref", diff --git a/Cargo.toml b/Cargo.toml index ebc2be5e4f..2c57ccc28b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -312,7 +312,7 @@ serde_with = "3.9.0" serde_yaml = "0.9.25" sha2 = "0.10.8" si-scale = "0.2.3" -sphinx-packet = "=0.5.0" +sphinx-packet = "=0.6.0" sqlx = "0.7.4" strum = "0.26" strum_macros = "0.26" @@ -402,6 +402,11 @@ wasm-bindgen-futures = "0.4.49" wasmtimer = "0.4.1" web-sys = "0.3.76" + +# for local development: +#[patch.crates-io] +#sphinx-packet = { path = "../sphinx" } + # Profile settings for individual crates # Compile-time verified queries do quite a bit of work at compile time. Incremental diff --git a/common/client-core/config-types/src/lib.rs b/common/client-core/config-types/src/lib.rs index b84626b28d..1d8b20d003 100644 --- a/common/client-core/config-types/src/lib.rs +++ b/common/client-core/config-types/src/lib.rs @@ -65,7 +65,7 @@ const DEFAULT_MAXIMUM_REPLY_KEY_AGE: Duration = Duration::from_secs(24 * 60 * 60 // stats reporting related -/// Time interval between reporting statistics to the given provider if it exist +/// Time interval between reporting statistics to the given provider if it exists const STATS_REPORT_INTERVAL_SECS: Duration = Duration::from_secs(300); use crate::error::InvalidTrafficModeFailure; @@ -405,6 +405,14 @@ pub struct Traffic { /// Do not set it unless you understand the consequences of that change. pub secondary_packet_size: Option, + /// Specify whether any constructed sphinx packets should use the legacy format, + /// where the payload keys are explicitly attached rather than using the seeds + /// this affects any forward packets, acks and reply surbs + /// this flag should remain disabled until sufficient number of nodes on the network has upgraded + /// and support updated format. + /// in the case of reply surbs, the recipient must also understand the new encoding + pub use_legacy_sphinx_format: bool, + pub packet_type: PacketType, } @@ -432,6 +440,10 @@ impl Default for Traffic { primary_packet_size: PacketSize::RegularPacket, secondary_packet_size: None, packet_type: PacketType::Mix, + + // we should use the legacy format until sufficient number of nodes understand the + // improved encoding + use_legacy_sphinx_format: true, } } } diff --git a/common/client-core/src/client/cover_traffic_stream.rs b/common/client-core/src/client/cover_traffic_stream.rs index 74154d08b4..99b2894d85 100644 --- a/common/client-core/src/client/cover_traffic_stream.rs +++ b/common/client-core/src/client/cover_traffic_stream.rs @@ -62,6 +62,10 @@ where /// Optional secondary predefined packet size used for the loop cover messages. secondary_packet_size: Option, + /// Specify whether any constructed packets should use the legacy format, + /// where the payload keys are explicitly attached rather than using the seeds + use_legacy_sphinx_format: bool, + packet_type: PacketType, stats_tx: ClientStatsSender, @@ -130,6 +134,7 @@ impl LoopCoverTrafficStream { topology_access, primary_packet_size: traffic_config.primary_packet_size, secondary_packet_size: traffic_config.secondary_packet_size, + use_legacy_sphinx_format: traffic_config.use_legacy_sphinx_format, packet_type: traffic_config.packet_type, stats_tx, task_client, @@ -182,6 +187,7 @@ impl LoopCoverTrafficStream { let cover_message = match generate_loop_cover_packet( &mut self.rng, + self.use_legacy_sphinx_format, topology_ref, &self.ack_key, &self.our_full_destination, diff --git a/common/client-core/src/client/real_messages_control/message_handler.rs b/common/client-core/src/client/real_messages_control/message_handler.rs index fc74610b60..13cf1c0717 100644 --- a/common/client-core/src/client/real_messages_control/message_handler.rs +++ b/common/client-core/src/client/real_messages_control/message_handler.rs @@ -109,6 +109,10 @@ pub(crate) struct Config { /// Optional secondary predefined packet size used for the encapsulated messages. secondary_packet_size: Option, + + /// Specify whether any constructed reply surbs should use the legacy format, + /// where the payload keys are explicitly attached rather than using the seeds + use_legacy_sphinx_format: bool, } impl Config { @@ -118,6 +122,7 @@ impl Config { average_packet_delay: Duration, average_ack_delay: Duration, deterministic_route_selection: bool, + use_legacy_reply_surb_format: bool, ) -> Self { Config { ack_key, @@ -127,6 +132,7 @@ impl Config { average_ack_delay, primary_packet_size: PacketSize::default(), secondary_packet_size: None, + use_legacy_sphinx_format: use_legacy_reply_surb_format, } } @@ -186,6 +192,7 @@ where config.sender_address, config.average_packet_delay, config.average_ack_delay, + config.use_legacy_sphinx_format, ); MessageHandler { config, @@ -254,9 +261,11 @@ where let topology_permit = self.topology_access.get_read_permit().await; let topology = self.get_topology(&topology_permit)?; - let reply_surbs = self - .message_preparer - .generate_reply_surbs(amount, topology)?; + let reply_surbs = self.message_preparer.generate_reply_surbs( + self.config.use_legacy_sphinx_format, + amount, + topology, + )?; let reply_keys = reply_surbs .iter() @@ -522,6 +531,7 @@ where self.generate_reply_surbs_with_keys(amount as usize).await?; let message = NymMessage::new_repliable(RepliableMessage::new_additional_surbs( + self.config.use_legacy_sphinx_format, sender_tag, reply_surbs, )); @@ -559,8 +569,12 @@ where .generate_reply_surbs_with_keys(num_reply_surbs as usize) .await?; - let message = - NymMessage::new_repliable(RepliableMessage::new_data(message, sender_tag, reply_surbs)); + let message = NymMessage::new_repliable(RepliableMessage::new_data( + self.config.use_legacy_sphinx_format, + message, + sender_tag, + reply_surbs, + )); self.try_split_and_send_non_reply_message( message, diff --git a/common/client-core/src/client/real_messages_control/mod.rs b/common/client-core/src/client/real_messages_control/mod.rs index 8de385e5ee..cd9515062a 100644 --- a/common/client-core/src/client/real_messages_control/mod.rs +++ b/common/client-core/src/client/real_messages_control/mod.rs @@ -99,6 +99,7 @@ impl<'a> From<&'a Config> for message_handler::Config { cfg.traffic.average_packet_delay, cfg.acks.average_ack_delay, cfg.traffic.deterministic_route_selection, + cfg.traffic.use_legacy_sphinx_format, ) .with_custom_primary_packet_size(cfg.traffic.primary_packet_size) .with_custom_secondary_packet_size(cfg.traffic.secondary_packet_size) diff --git a/common/client-core/src/client/real_messages_control/real_traffic_stream.rs b/common/client-core/src/client/real_messages_control/real_traffic_stream.rs index d20b8040aa..edc313990f 100644 --- a/common/client-core/src/client/real_messages_control/real_traffic_stream.rs +++ b/common/client-core/src/client/real_messages_control/real_traffic_stream.rs @@ -252,6 +252,7 @@ where ( generate_loop_cover_packet( &mut self.rng, + self.config.traffic.use_legacy_sphinx_format, topology_ref, &self.config.ack_key, &self.config.our_full_destination, diff --git a/common/client-core/src/client/received_buffer.rs b/common/client-core/src/client/received_buffer.rs index be27383bd7..468ded249c 100644 --- a/common/client-core/src/client/received_buffer.rs +++ b/common/client-core/src/client/received_buffer.rs @@ -250,10 +250,10 @@ impl ReceivedMessagesBuffer { let mut reconstructed = Vec::new(); for msg in msgs { let (reply_surbs, from_surb_request) = match msg.content { - RepliableMessageContent::Data { - message, - reply_surbs, - } => { + RepliableMessageContent::Data(content) => { + let reply_surbs = content.reply_surbs; + let message = content.message; + trace!( "received message that also contained additional {} reply surbs from {:?}!", reply_surbs.len(), @@ -264,7 +264,9 @@ impl ReceivedMessagesBuffer { (reply_surbs, false) } - RepliableMessageContent::AdditionalSurbs { reply_surbs } => { + RepliableMessageContent::AdditionalSurbs(content) => { + let reply_surbs = content.reply_surbs; + trace!( "received additional {} reply surbs from {:?}!", reply_surbs.len(), @@ -272,9 +274,37 @@ impl ReceivedMessagesBuffer { ); (reply_surbs, true) } - RepliableMessageContent::Heartbeat { - additional_reply_surbs, - } => { + RepliableMessageContent::Heartbeat(content) => { + let additional_reply_surbs = content.additional_reply_surbs; + error!("received a repliable heartbeat message - we don't know how to handle it yet (and we won't know until future PRs)"); + (additional_reply_surbs, false) + } + RepliableMessageContent::DataV2(content) => { + let reply_surbs = content.reply_surbs; + let message = content.message; + + trace!( + "received message that also contained additional {} reply surbs from {:?}!", + reply_surbs.len(), + msg.sender_tag + ); + + reconstructed.push(ReconstructedMessage::new(message, msg.sender_tag)); + + (reply_surbs, false) + } + RepliableMessageContent::AdditionalSurbsV2(content) => { + let reply_surbs = content.reply_surbs; + + trace!( + "received additional {} reply surbs from {:?}!", + reply_surbs.len(), + msg.sender_tag + ); + (reply_surbs, true) + } + RepliableMessageContent::HeartbeatV2(content) => { + let additional_reply_surbs = content.additional_reply_surbs; error!("received a repliable heartbeat message - we don't know how to handle it yet (and we won't know until future PRs)"); (additional_reply_surbs, false) } diff --git a/common/node-tester-utils/src/tester.rs b/common/node-tester-utils/src/tester.rs index 211eb988db..ae02a44299 100644 --- a/common/node-tester-utils/src/tester.rs +++ b/common/node-tester-utils/src/tester.rs @@ -39,6 +39,10 @@ pub struct NodeTester { /// Average delay an acknowledgement packet is going to get delay at a single mixnode. average_ack_delay: Duration, + /// Specify whether any constructed packets should use the legacy format, + /// where the payload keys are explicitly attached rather than using the seeds + use_legacy_sphinx_format: bool, + // while acks are going to be ignored they still need to be constructed // so that the gateway would be able to correctly process and forward the message ack_key: Arc, @@ -57,6 +61,7 @@ where deterministic_route_selection: bool, average_packet_delay: Duration, average_ack_delay: Duration, + use_legacy_sphinx_format: bool, ack_key: Arc, ) -> Self { Self { @@ -67,6 +72,7 @@ where deterministic_route_selection, average_packet_delay, average_ack_delay, + use_legacy_sphinx_format, ack_key, } } @@ -245,6 +251,10 @@ where impl FragmentPreparer for NodeTester { type Rng = R; + fn use_legacy_sphinx_format(&self) -> bool { + self.use_legacy_sphinx_format + } + fn deterministic_route_selection(&self) -> bool { self.deterministic_route_selection } diff --git a/common/nymsphinx/acknowledgements/src/surb_ack.rs b/common/nymsphinx/acknowledgements/src/surb_ack.rs index 43d2ba84e2..5837dd54d5 100644 --- a/common/nymsphinx/acknowledgements/src/surb_ack.rs +++ b/common/nymsphinx/acknowledgements/src/surb_ack.rs @@ -37,8 +37,10 @@ pub enum SurbAckRecoveryError { } impl SurbAck { + #[allow(clippy::too_many_arguments)] pub fn construct( rng: &mut R, + use_legacy_sphinx_format: bool, recipient: &Recipient, ack_key: &AckKey, marshaled_fragment_id: [u8; 5], @@ -67,6 +69,7 @@ impl SurbAck { Some(packet_size), )?, PacketType::Mix => NymPacket::sphinx_build( + use_legacy_sphinx_format, packet_size, surb_ack_payload, &route, diff --git a/common/nymsphinx/addressing/src/nodes.rs b/common/nymsphinx/addressing/src/nodes.rs index 4171410b6c..cbf05e7b41 100644 --- a/common/nymsphinx/addressing/src/nodes.rs +++ b/common/nymsphinx/addressing/src/nodes.rs @@ -199,7 +199,7 @@ impl TryFrom for NymNodeRoutingAddress { type Error = NymNodeRoutingAddressError; fn try_from(value: NodeAddressBytes) -> Result { - Self::try_from_bytes(value.as_bytes_ref()) + Self::try_from_bytes(value.as_bytes()) } } diff --git a/common/nymsphinx/anonymous-replies/Cargo.toml b/common/nymsphinx/anonymous-replies/Cargo.toml index ade1cc7f59..3d4c8aadc7 100644 --- a/common/nymsphinx/anonymous-replies/Cargo.toml +++ b/common/nymsphinx/anonymous-replies/Cargo.toml @@ -12,6 +12,7 @@ rand = { workspace = true } bs58 = { workspace = true } serde = { workspace = true } thiserror = { workspace = true } +tracing = { workspace = true } nym-crypto = { path = "../../crypto", features = ["stream_cipher", "rand"] } nym-sphinx-addressing = { path = "../addressing" } diff --git a/common/nymsphinx/anonymous-replies/src/reply_surb.rs b/common/nymsphinx/anonymous-replies/src/reply_surb.rs index 90bd892d78..7f7da53805 100644 --- a/common/nymsphinx/anonymous-replies/src/reply_surb.rs +++ b/common/nymsphinx/anonymous-replies/src/reply_surb.rs @@ -1,20 +1,25 @@ -// Copyright 2021-2023 - Nym Technologies SA +// Copyright 2021-2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use crate::encryption_key::{SurbEncryptionKey, SurbEncryptionKeyError, SurbEncryptionKeySize}; use nym_crypto::{generic_array::typenum::Unsigned, Digest}; use nym_sphinx_addressing::clients::Recipient; -use nym_sphinx_addressing::nodes::{NymNodeRoutingAddress, MAX_NODE_ADDRESS_UNPADDED_LEN}; +use nym_sphinx_addressing::nodes::{ + NymNodeRoutingAddress, NymNodeRoutingAddressError, MAX_NODE_ADDRESS_UNPADDED_LEN, +}; use nym_sphinx_params::packet_sizes::PacketSize; use nym_sphinx_params::{PacketType, ReplySurbKeyDigestAlgorithm}; -use nym_sphinx_types::{NymPacket, SURBMaterial, SphinxError, SURB}; +use nym_sphinx_types::constants::PAYLOAD_KEY_SEED_SIZE; +use nym_sphinx_types::{ + NymPacket, SURBMaterial, SphinxError, HEADER_SIZE, NODE_ADDRESS_LENGTH, SURB, + X25519_WITH_EXPLICIT_PAYLOAD_KEYS_VERSION, +}; use nym_topology::{NymRouteProvider, NymTopologyError}; use rand::{CryptoRng, RngCore}; use serde::de::{Error as SerdeError, Visitor}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; - use std::fmt::{self, Formatter}; -use std::time; +use std::time::Duration; use thiserror::Error; #[derive(Debug, Error)] @@ -31,6 +36,9 @@ pub enum ReplySurbError { #[error("failed to recover reply SURB from bytes: {0}")] RecoveryError(#[from] SphinxError), + #[error("failed to validate the first hop address of the recovered reply SURB: {0}")] + MalformedSurbFirstHop(#[from] NymNodeRoutingAddressError), + #[error("failed to recover reply SURB encryption key from bytes: {0}")] InvalidEncryptionKeyData(#[from] SurbEncryptionKeyError), } @@ -80,6 +88,10 @@ impl<'de> Deserialize<'de> for ReplySurb { } impl ReplySurb { + /// base overhead of a reply surb that exists regardless of type or number of key materials. + pub(crate) const BASE_OVERHEAD: usize = + SurbEncryptionKeySize::USIZE + HEADER_SIZE + NODE_ADDRESS_LENGTH; + pub fn max_msg_len(packet_size: PacketSize) -> usize { // For detailed explanation (of ack overhead) refer to common\nymsphinx\src\preparer.rs::available_plaintext_per_packet() let ack_overhead = MAX_NODE_ADDRESS_UNPADDED_LEN + PacketSize::AckPacket.size(); @@ -91,7 +103,8 @@ impl ReplySurb { pub fn construct( rng: &mut R, recipient: &Recipient, - average_delay: time::Duration, + average_delay: Duration, + use_legacy_surb_format: bool, topology: &NymRouteProvider, ) -> Result where @@ -101,7 +114,10 @@ impl ReplySurb { let delays = nym_sphinx_routing::generate_hop_delays(average_delay, route.len()); let destination = recipient.as_sphinx_destination(); - let surb_material = SURBMaterial::new(route, delays, destination); + let mut surb_material = SURBMaterial::new(route, delays, destination); + if use_legacy_surb_format { + surb_material = surb_material.with_version(X25519_WITH_EXPLICIT_PAYLOAD_KEYS_VERSION) + } // this can't fail as we know we have a valid route to gateway and have correct number of delays Ok(ReplySurb { @@ -110,14 +126,10 @@ impl ReplySurb { }) } - /// Returns the expected number of bytes the [`ReplySURB`] will take after serialization. + /// Returns the expected number of bytes the [`ReplySURB`] will take after serialization using the new encoding format. /// Useful for deserialization from a bytes stream. - pub fn serialized_len() -> usize { - use nym_sphinx_types::{HEADER_SIZE, NODE_ADDRESS_LENGTH, PAYLOAD_KEY_SIZE}; - - // the SURB itself consists of SURB_header, first hop address and set of payload keys - // for each hop (3x mix + egress) - SurbEncryptionKeySize::USIZE + HEADER_SIZE + NODE_ADDRESS_LENGTH + 4 * PAYLOAD_KEY_SIZE + pub fn v2_serialised_len(num_hops: u8) -> usize { + Self::BASE_OVERHEAD + num_hops as usize * PAYLOAD_KEY_SEED_SIZE } pub fn encryption_key(&self) -> &SurbEncryptionKey { @@ -143,7 +155,12 @@ impl ReplySurb { let surb = match SURB::from_bytes(&bytes[SurbEncryptionKeySize::USIZE..]) { Err(err) => return Err(ReplySurbError::RecoveryError(err)), - Ok(surb) => surb, + Ok(surb) => { + // we can't really check fully validity of the header, but at the very least we could make a sanity check + // to make sure the first hop address is a valid socket address + let _ = NymNodeRoutingAddress::try_from(surb.first_hop())?; + surb + } }; Ok(ReplySurb { diff --git a/common/nymsphinx/anonymous-replies/src/requests.rs b/common/nymsphinx/anonymous-replies/src/requests/mod.rs similarity index 59% rename from common/nymsphinx/anonymous-replies/src/requests.rs rename to common/nymsphinx/anonymous-replies/src/requests/mod.rs index 7503f141e2..6bb382579c 100644 --- a/common/nymsphinx/anonymous-replies/src/requests.rs +++ b/common/nymsphinx/anonymous-replies/src/requests/mod.rs @@ -8,9 +8,15 @@ use std::fmt::{Display, Formatter}; use std::mem; use thiserror::Error; +use crate::requests::v1::{AdditionalSurbsV1, DataV1, HeartbeatV1}; +use crate::requests::v2::{AdditionalSurbsV2, DataV2, HeartbeatV2}; + #[cfg(target_arch = "wasm32")] use wasm_bindgen::prelude::*; +pub(crate) mod v1; +pub(crate) mod v2; + pub const SENDER_TAG_SIZE: usize = 16; #[derive(Debug, Error)] @@ -103,31 +109,23 @@ pub struct RepliableMessage { impl Display for RepliableMessage { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match &self.content { - RepliableMessageContent::Data { - message, - reply_surbs, - } => write!( - f, - "repliable {:.2} kiB data message with {} reply surbs attached from {}", - message.len() as f64 / 1024.0, - reply_surbs.len(), - self.sender_tag, - ), - RepliableMessageContent::AdditionalSurbs { reply_surbs } => write!( - f, - "repliable additional surbs message ({} reply surbs attached) from {}", - reply_surbs.len(), - self.sender_tag, - ), - RepliableMessageContent::Heartbeat { - additional_reply_surbs, - } => { - write!( - f, - "repliable heartbeat message ({} reply surbs attached) from {}", - additional_reply_surbs.len(), - self.sender_tag, - ) + RepliableMessageContent::Data(content) => { + write!(f, "{content} from {}", self.sender_tag) + } + RepliableMessageContent::AdditionalSurbs(content) => { + write!(f, "{content} from {}", self.sender_tag) + } + RepliableMessageContent::Heartbeat(content) => { + write!(f, "{content} from {}", self.sender_tag) + } + RepliableMessageContent::DataV2(content) => { + write!(f, "{content} from {}", self.sender_tag) + } + RepliableMessageContent::AdditionalSurbsV2(content) => { + write!(f, "{content} from {}", self.sender_tag) + } + RepliableMessageContent::HeartbeatV2(content) => { + write!(f, "{content} from {}", self.sender_tag) } } } @@ -135,26 +133,43 @@ impl Display for RepliableMessage { impl RepliableMessage { pub fn new_data( + use_legacy_surb_format: bool, data: Vec, sender_tag: AnonymousSenderTag, reply_surbs: Vec, ) -> Self { - RepliableMessage { - sender_tag, - content: RepliableMessageContent::Data { + let content = if use_legacy_surb_format { + RepliableMessageContent::Data(DataV1 { message: data, reply_surbs, - }, + }) + } else { + RepliableMessageContent::DataV2(DataV2 { + message: data, + reply_surbs, + }) + }; + + RepliableMessage { + sender_tag, + content, } } pub fn new_additional_surbs( + use_legacy_surb_format: bool, sender_tag: AnonymousSenderTag, reply_surbs: Vec, ) -> Self { + let content = if use_legacy_surb_format { + RepliableMessageContent::AdditionalSurbs(AdditionalSurbsV1 { reply_surbs }) + } else { + RepliableMessageContent::AdditionalSurbsV2(AdditionalSurbsV2 { reply_surbs }) + }; + RepliableMessage { sender_tag, - content: RepliableMessageContent::AdditionalSurbs { reply_surbs }, + content, } } @@ -192,35 +207,18 @@ impl RepliableMessage { } } -// this recovery code is shared between all variants containing reply surbs -fn recover_reply_surbs(bytes: &[u8]) -> Result<(Vec, usize), InvalidReplyRequestError> { - let mut consumed = mem::size_of::(); - if bytes.len() < consumed { - return Err(InvalidReplyRequestError::RequestTooShortToDeserialize); - } - let num_surbs = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]); - let surb_size = ReplySurb::serialized_len(); - if bytes[consumed..].len() < num_surbs as usize * surb_size { - return Err(InvalidReplyRequestError::RequestTooShortToDeserialize); - } - - let mut reply_surbs = Vec::with_capacity(num_surbs as usize); - for _ in 0..num_surbs as usize { - let surb_bytes = &bytes[consumed..consumed + surb_size]; - let reply_surb = ReplySurb::from_bytes(surb_bytes)?; - reply_surbs.push(reply_surb); - - consumed += surb_size; - } - - Ok((reply_surbs, consumed)) -} - +#[derive(Debug)] #[repr(u8)] enum RepliableMessageContentTag { Data = 0, AdditionalSurbs = 1, Heartbeat = 2, + + // updated variants that slightly change SURB encoding + // to allow for variable number of hops as well as using payload key seeds + DataV2 = 3, + AdditionalSurbsV2 = 4, + HeartbeatV2 = 5, } impl TryFrom for RepliableMessageContentTag { @@ -233,6 +231,11 @@ impl TryFrom for RepliableMessageContentTag { Ok(Self::AdditionalSurbs) } _ if value == (RepliableMessageContentTag::Heartbeat as u8) => Ok(Self::Heartbeat), + _ if value == (RepliableMessageContentTag::DataV2 as u8) => Ok(Self::DataV2), + _ if value == (RepliableMessageContentTag::AdditionalSurbsV2 as u8) => { + Ok(Self::AdditionalSurbsV2) + } + _ if value == (RepliableMessageContentTag::HeartbeatV2 as u8) => Ok(Self::HeartbeatV2), val => Err(InvalidReplyRequestError::InvalidRepliableContentTag { received: val }), } } @@ -241,58 +244,24 @@ impl TryFrom for RepliableMessageContentTag { // sent by original sender that initialised the communication that knows address of the remote #[derive(Debug)] pub enum RepliableMessageContent { - Data { - message: Vec, - reply_surbs: Vec, - }, - AdditionalSurbs { - reply_surbs: Vec, - }, - Heartbeat { - additional_reply_surbs: Vec, - }, + Data(DataV1), + AdditionalSurbs(AdditionalSurbsV1), + Heartbeat(HeartbeatV1), + + DataV2(DataV2), + AdditionalSurbsV2(AdditionalSurbsV2), + HeartbeatV2(HeartbeatV2), } impl RepliableMessageContent { pub fn into_bytes(self) -> Vec { match self { - RepliableMessageContent::Data { - message, - reply_surbs, - } => { - let num_surbs = reply_surbs.len() as u32; - - num_surbs - .to_be_bytes() - .into_iter() - .chain(reply_surbs.into_iter().flat_map(|s| s.to_bytes())) - .chain(message) - .collect() - } - RepliableMessageContent::AdditionalSurbs { reply_surbs } => { - let num_surbs = reply_surbs.len() as u32; - - num_surbs - .to_be_bytes() - .into_iter() - .chain(reply_surbs.into_iter().flat_map(|s| s.to_bytes())) - .collect() - } - RepliableMessageContent::Heartbeat { - additional_reply_surbs, - } => { - let num_surbs = additional_reply_surbs.len() as u32; - - num_surbs - .to_be_bytes() - .into_iter() - .chain( - additional_reply_surbs - .into_iter() - .flat_map(|s| s.to_bytes()), - ) - .collect() - } + RepliableMessageContent::Data(content) => content.into_bytes(), + RepliableMessageContent::AdditionalSurbs(content) => content.into_bytes(), + RepliableMessageContent::Heartbeat(content) => content.into_bytes(), + RepliableMessageContent::DataV2(content) => content.into_bytes(), + RepliableMessageContent::AdditionalSurbsV2(content) => content.into_bytes(), + RepliableMessageContent::HeartbeatV2(content) => content.into_bytes(), } } @@ -304,19 +273,25 @@ impl RepliableMessageContent { return Err(InvalidReplyRequestError::RequestTooShortToDeserialize); } - let (reply_surbs, n) = recover_reply_surbs(bytes)?; - match tag { - RepliableMessageContentTag::Data => Ok(RepliableMessageContent::Data { - message: bytes[n..].to_vec(), - reply_surbs, - }), - RepliableMessageContentTag::AdditionalSurbs => { - Ok(RepliableMessageContent::AdditionalSurbs { reply_surbs }) + RepliableMessageContentTag::Data => { + Ok(RepliableMessageContent::Data(DataV1::from_bytes(bytes)?)) } - RepliableMessageContentTag::Heartbeat => Ok(RepliableMessageContent::Heartbeat { - additional_reply_surbs: reply_surbs, - }), + RepliableMessageContentTag::AdditionalSurbs => Ok( + RepliableMessageContent::AdditionalSurbs(AdditionalSurbsV1::from_bytes(bytes)?), + ), + RepliableMessageContentTag::Heartbeat => Ok(RepliableMessageContent::Heartbeat( + HeartbeatV1::from_bytes(bytes)?, + )), + RepliableMessageContentTag::DataV2 => { + Ok(RepliableMessageContent::DataV2(DataV2::from_bytes(bytes)?)) + } + RepliableMessageContentTag::AdditionalSurbsV2 => Ok( + RepliableMessageContent::AdditionalSurbsV2(AdditionalSurbsV2::from_bytes(bytes)?), + ), + RepliableMessageContentTag::HeartbeatV2 => Ok(RepliableMessageContent::HeartbeatV2( + HeartbeatV2::from_bytes(bytes)?, + )), } } @@ -327,30 +302,22 @@ impl RepliableMessageContent { RepliableMessageContentTag::AdditionalSurbs } RepliableMessageContent::Heartbeat { .. } => RepliableMessageContentTag::Heartbeat, + RepliableMessageContent::DataV2(_) => RepliableMessageContentTag::DataV2, + RepliableMessageContent::AdditionalSurbsV2(_) => { + RepliableMessageContentTag::AdditionalSurbsV2 + } + RepliableMessageContent::HeartbeatV2(_) => RepliableMessageContentTag::HeartbeatV2, } } fn serialized_size(&self) -> usize { match self { - RepliableMessageContent::Data { - message, - reply_surbs, - } => { - let num_reply_surbs_tag = mem::size_of::(); - num_reply_surbs_tag - + reply_surbs.len() * ReplySurb::serialized_len() - + message.len() - } - RepliableMessageContent::AdditionalSurbs { reply_surbs } => { - let num_reply_surbs_tag = mem::size_of::(); - num_reply_surbs_tag + reply_surbs.len() * ReplySurb::serialized_len() - } - RepliableMessageContent::Heartbeat { - additional_reply_surbs, - } => { - let num_reply_surbs_tag = mem::size_of::(); - num_reply_surbs_tag + additional_reply_surbs.len() * ReplySurb::serialized_len() - } + RepliableMessageContent::Data(content) => content.serialized_len(), + RepliableMessageContent::AdditionalSurbs(content) => content.serialized_len(), + RepliableMessageContent::Heartbeat(content) => content.serialized_len(), + RepliableMessageContent::DataV2(content) => content.serialized_len(), + RepliableMessageContent::AdditionalSurbsV2(content) => content.serialized_len(), + RepliableMessageContent::HeartbeatV2(content) => content.serialized_len(), } } } @@ -514,18 +481,22 @@ mod tests { use super::*; mod fixtures { + use crate::requests::v1::{AdditionalSurbsV1, DataV1, HeartbeatV1}; + use crate::requests::v2::{AdditionalSurbsV2, DataV2, HeartbeatV2}; use crate::requests::{AnonymousSenderTag, RepliableMessageContent, ReplyMessageContent}; use crate::{ReplySurb, SurbEncryptionKey}; use nym_crypto::asymmetric::{encryption, identity}; use nym_sphinx_addressing::clients::Recipient; use nym_sphinx_types::{ Delay, Destination, DestinationAddressBytes, Node, NodeAddressBytes, PrivateKey, - SURBMaterial, NODE_ADDRESS_LENGTH, + SURBMaterial, NODE_ADDRESS_LENGTH, X25519_WITH_EXPLICIT_PAYLOAD_KEYS_VERSION, }; use rand::{Rng, RngCore}; use rand_chacha::rand_core::SeedableRng; use rand_chacha::ChaCha20Rng; + pub(crate) const LEGACY_HOPS: u8 = 4; + pub(super) fn test_rng() -> ChaCha20Rng { let dummy_seed = [42u8; 32]; ChaCha20Rng::from_seed(dummy_seed) @@ -567,11 +538,9 @@ mod tests { } } - pub(super) fn reply_surb(rng: &mut ChaCha20Rng) -> ReplySurb { - // due to gateway - const HOPS: u8 = 4; - let route = (0..HOPS).map(|_| node(rng)).collect(); - let delays = (0..HOPS) + pub(super) fn reply_surb(rng: &mut ChaCha20Rng, legacy: bool, hops: u8) -> ReplySurb { + let route = (0..hops).map(|_| node(rng)).collect(); + let delays = (0..hops) .map(|_| Delay::new_from_nanos(rng.next_u64())) .collect(); let mut destination_bytes = [0u8; 32]; @@ -585,50 +554,58 @@ mod tests { identifier_bytes, ); - let surb = SURBMaterial::new(route, delays, destination) - .construct_SURB() - .unwrap(); + let mut surb_material = SURBMaterial::new(route, delays, destination); + if legacy { + surb_material = + surb_material.with_version(X25519_WITH_EXPLICIT_PAYLOAD_KEYS_VERSION); + } + ReplySurb { - surb, + surb: surb_material.construct_SURB().unwrap(), encryption_key: SurbEncryptionKey::new(rng), } } - pub(super) fn reply_surbs(rng: &mut ChaCha20Rng, n: usize) -> Vec { + pub(super) fn reply_surbs( + rng: &mut ChaCha20Rng, + n: usize, + legacy: bool, + hops: u8, + ) -> Vec { let mut surbs = Vec::with_capacity(n); for _ in 0..n { - surbs.push(reply_surb(rng)) + surbs.push(reply_surb(rng, legacy, hops)) } surbs } - pub(super) fn repliable_content_data( + pub(super) fn repliable_content_data_v1( rng: &mut ChaCha20Rng, msg_len: usize, surbs: usize, ) -> RepliableMessageContent { - RepliableMessageContent::Data { + RepliableMessageContent::Data(DataV1 { message: random_vec_u8(rng, msg_len), - reply_surbs: reply_surbs(rng, surbs), - } + reply_surbs: reply_surbs(rng, surbs, true, LEGACY_HOPS), + }) } - pub(super) fn repliable_content_surbs( + pub(super) fn repliable_content_surbs_v1( rng: &mut ChaCha20Rng, surbs: usize, ) -> RepliableMessageContent { - RepliableMessageContent::AdditionalSurbs { - reply_surbs: reply_surbs(rng, surbs), - } + RepliableMessageContent::AdditionalSurbs(AdditionalSurbsV1 { + reply_surbs: reply_surbs(rng, surbs, true, LEGACY_HOPS), + }) } - pub(super) fn repliable_content_heartbeat( + pub(super) fn repliable_content_heartbeat_v1( rng: &mut ChaCha20Rng, surbs: usize, ) -> RepliableMessageContent { - RepliableMessageContent::Heartbeat { - additional_reply_surbs: reply_surbs(rng, surbs), - } + RepliableMessageContent::Heartbeat(HeartbeatV1 { + additional_reply_surbs: reply_surbs(rng, surbs, true, LEGACY_HOPS), + }) } pub(super) fn reply_content_data( @@ -649,37 +626,70 @@ mod tests { amount: surbs, } } + + pub(super) fn repliable_content_data_v2( + rng: &mut ChaCha20Rng, + msg_len: usize, + surbs: usize, + surb_hops: u8, + ) -> RepliableMessageContent { + RepliableMessageContent::DataV2(DataV2 { + message: random_vec_u8(rng, msg_len), + reply_surbs: reply_surbs(rng, surbs, false, surb_hops), + }) + } + + pub(super) fn repliable_content_surbs_v2( + rng: &mut ChaCha20Rng, + surbs: usize, + surb_hops: u8, + ) -> RepliableMessageContent { + RepliableMessageContent::AdditionalSurbsV2(AdditionalSurbsV2 { + reply_surbs: reply_surbs(rng, surbs, false, surb_hops), + }) + } + + pub(super) fn repliable_content_heartbeat_v2( + rng: &mut ChaCha20Rng, + surbs: usize, + surb_hops: u8, + ) -> RepliableMessageContent { + RepliableMessageContent::HeartbeatV2(HeartbeatV2 { + additional_reply_surbs: reply_surbs(rng, surbs, false, surb_hops), + }) + } } #[cfg(test)] mod repliable_message { use super::*; + use crate::requests::tests::fixtures::LEGACY_HOPS; #[test] - fn serialized_size_matches_actual_serialization() { + fn serialized_size_matches_actual_serialization_for_v1_messages() { let mut rng = fixtures::test_rng(); let data1 = RepliableMessage { sender_tag: fixtures::sender_tag(&mut rng), - content: fixtures::repliable_content_data(&mut rng, 10000, 0), + content: fixtures::repliable_content_data_v1(&mut rng, 10000, 0), }; assert_eq!(data1.serialized_size(), data1.into_bytes().len()); let data2 = RepliableMessage { sender_tag: fixtures::sender_tag(&mut rng), - content: fixtures::repliable_content_data(&mut rng, 10, 100), + content: fixtures::repliable_content_data_v1(&mut rng, 10, 100), }; assert_eq!(data2.serialized_size(), data2.into_bytes().len()); let data3 = RepliableMessage { sender_tag: fixtures::sender_tag(&mut rng), - content: fixtures::repliable_content_data(&mut rng, 100000, 1000), + content: fixtures::repliable_content_data_v1(&mut rng, 100000, 1000), }; assert_eq!(data3.serialized_size(), data3.into_bytes().len()); let additional_surbs1 = RepliableMessage { sender_tag: fixtures::sender_tag(&mut rng), - content: fixtures::repliable_content_surbs(&mut rng, 1), + content: fixtures::repliable_content_surbs_v1(&mut rng, 1), }; assert_eq!( additional_surbs1.serialized_size(), @@ -688,7 +698,7 @@ mod tests { let additional_surbs2 = RepliableMessage { sender_tag: fixtures::sender_tag(&mut rng), - content: fixtures::repliable_content_surbs(&mut rng, 1000), + content: fixtures::repliable_content_surbs_v1(&mut rng, 1000), }; assert_eq!( additional_surbs2.serialized_size(), @@ -697,53 +707,173 @@ mod tests { let heartbeat1 = RepliableMessage { sender_tag: fixtures::sender_tag(&mut rng), - content: fixtures::repliable_content_heartbeat(&mut rng, 1), + content: fixtures::repliable_content_heartbeat_v1(&mut rng, 1), }; assert_eq!(heartbeat1.serialized_size(), heartbeat1.into_bytes().len()); let heartbeat2 = RepliableMessage { sender_tag: fixtures::sender_tag(&mut rng), - content: fixtures::repliable_content_heartbeat(&mut rng, 1000), + content: fixtures::repliable_content_heartbeat_v1(&mut rng, 1000), }; assert_eq!(heartbeat2.serialized_size(), heartbeat2.into_bytes().len()); } + + #[test] + fn serialized_size_matches_actual_serialization_for_v2_messages() { + let mut rng = fixtures::test_rng(); + + let data1 = RepliableMessage { + sender_tag: fixtures::sender_tag(&mut rng), + content: fixtures::repliable_content_data_v2(&mut rng, 10000, 0, LEGACY_HOPS), + }; + assert_eq!(data1.serialized_size(), data1.into_bytes().len()); + + let data2 = RepliableMessage { + sender_tag: fixtures::sender_tag(&mut rng), + content: fixtures::repliable_content_data_v2(&mut rng, 10, 100, LEGACY_HOPS), + }; + assert_eq!(data2.serialized_size(), data2.into_bytes().len()); + + let data3 = RepliableMessage { + sender_tag: fixtures::sender_tag(&mut rng), + content: fixtures::repliable_content_data_v2(&mut rng, 100000, 1000, LEGACY_HOPS), + }; + assert_eq!(data3.serialized_size(), data3.into_bytes().len()); + + let data4 = RepliableMessage { + sender_tag: fixtures::sender_tag(&mut rng), + content: fixtures::repliable_content_data_v2(&mut rng, 100000, 1000, 1), + }; + assert_eq!(data4.serialized_size(), data4.into_bytes().len()); + + let additional_surbs1 = RepliableMessage { + sender_tag: fixtures::sender_tag(&mut rng), + content: fixtures::repliable_content_surbs_v2(&mut rng, 1, LEGACY_HOPS), + }; + assert_eq!( + additional_surbs1.serialized_size(), + additional_surbs1.into_bytes().len() + ); + + let additional_surbs2 = RepliableMessage { + sender_tag: fixtures::sender_tag(&mut rng), + content: fixtures::repliable_content_surbs_v2(&mut rng, 1000, LEGACY_HOPS), + }; + assert_eq!( + additional_surbs2.serialized_size(), + additional_surbs2.into_bytes().len() + ); + + let additional_surbs3 = RepliableMessage { + sender_tag: fixtures::sender_tag(&mut rng), + content: fixtures::repliable_content_surbs_v2(&mut rng, 1000, 1), + }; + assert_eq!( + additional_surbs3.serialized_size(), + additional_surbs3.into_bytes().len() + ); + + let heartbeat1 = RepliableMessage { + sender_tag: fixtures::sender_tag(&mut rng), + content: fixtures::repliable_content_heartbeat_v2(&mut rng, 1, LEGACY_HOPS), + }; + assert_eq!(heartbeat1.serialized_size(), heartbeat1.into_bytes().len()); + + let heartbeat2 = RepliableMessage { + sender_tag: fixtures::sender_tag(&mut rng), + content: fixtures::repliable_content_heartbeat_v2(&mut rng, 1000, LEGACY_HOPS), + }; + assert_eq!(heartbeat2.serialized_size(), heartbeat2.into_bytes().len()); + + let heartbeat3 = RepliableMessage { + sender_tag: fixtures::sender_tag(&mut rng), + content: fixtures::repliable_content_heartbeat_v2(&mut rng, 1000, 1), + }; + assert_eq!(heartbeat3.serialized_size(), heartbeat3.into_bytes().len()); + } } #[cfg(test)] mod repliable_message_content { use super::*; + use crate::requests::tests::fixtures::LEGACY_HOPS; #[test] - fn serialized_size_matches_actual_serialization() { + fn serialized_size_matches_actual_serialization_for_v1_messages() { let mut rng = fixtures::test_rng(); - let data1 = fixtures::repliable_content_data(&mut rng, 10000, 0); + let data1 = fixtures::repliable_content_data_v1(&mut rng, 10000, 0); assert_eq!(data1.serialized_size(), data1.into_bytes().len()); - let data2 = fixtures::repliable_content_data(&mut rng, 10, 100); + let data2 = fixtures::repliable_content_data_v1(&mut rng, 10, 100); assert_eq!(data2.serialized_size(), data2.into_bytes().len()); - let data3 = fixtures::repliable_content_data(&mut rng, 100000, 1000); + let data3 = fixtures::repliable_content_data_v1(&mut rng, 100000, 1000); assert_eq!(data3.serialized_size(), data3.into_bytes().len()); - let additional_surbs1 = fixtures::repliable_content_surbs(&mut rng, 1); + let additional_surbs1 = fixtures::repliable_content_surbs_v1(&mut rng, 1); assert_eq!( additional_surbs1.serialized_size(), additional_surbs1.into_bytes().len() ); - let additional_surbs2 = fixtures::repliable_content_surbs(&mut rng, 1000); + let additional_surbs2 = fixtures::repliable_content_surbs_v1(&mut rng, 1000); assert_eq!( additional_surbs2.serialized_size(), additional_surbs2.into_bytes().len() ); - let heartbeat1 = fixtures::repliable_content_heartbeat(&mut rng, 1); + let heartbeat1 = fixtures::repliable_content_heartbeat_v1(&mut rng, 1); assert_eq!(heartbeat1.serialized_size(), heartbeat1.into_bytes().len()); - let heartbeat2 = fixtures::repliable_content_heartbeat(&mut rng, 1000); + let heartbeat2 = fixtures::repliable_content_heartbeat_v1(&mut rng, 1000); assert_eq!(heartbeat2.serialized_size(), heartbeat2.into_bytes().len()); } + + #[test] + fn serialized_size_matches_actual_serialization_for_v2_messages() { + let mut rng = fixtures::test_rng(); + + let data1 = fixtures::repliable_content_data_v2(&mut rng, 10000, 0, LEGACY_HOPS); + assert_eq!(data1.serialized_size(), data1.into_bytes().len()); + + let data2 = fixtures::repliable_content_data_v2(&mut rng, 10, 100, LEGACY_HOPS); + assert_eq!(data2.serialized_size(), data2.into_bytes().len()); + + let data3 = fixtures::repliable_content_data_v2(&mut rng, 100000, 1000, LEGACY_HOPS); + assert_eq!(data3.serialized_size(), data3.into_bytes().len()); + + let data4 = fixtures::repliable_content_data_v2(&mut rng, 100000, 1000, 1); + assert_eq!(data4.serialized_size(), data4.into_bytes().len()); + + let additional_surbs1 = fixtures::repliable_content_surbs_v2(&mut rng, 1, LEGACY_HOPS); + assert_eq!( + additional_surbs1.serialized_size(), + additional_surbs1.into_bytes().len() + ); + + let additional_surbs2 = + fixtures::repliable_content_surbs_v2(&mut rng, 1000, LEGACY_HOPS); + assert_eq!( + additional_surbs2.serialized_size(), + additional_surbs2.into_bytes().len() + ); + + let additional_surbs3 = fixtures::repliable_content_surbs_v2(&mut rng, 1000, 1); + assert_eq!( + additional_surbs3.serialized_size(), + additional_surbs3.into_bytes().len() + ); + + let heartbeat1 = fixtures::repliable_content_heartbeat_v2(&mut rng, 1, LEGACY_HOPS); + assert_eq!(heartbeat1.serialized_size(), heartbeat1.into_bytes().len()); + + let heartbeat2 = fixtures::repliable_content_heartbeat_v2(&mut rng, 1000, LEGACY_HOPS); + assert_eq!(heartbeat2.serialized_size(), heartbeat2.into_bytes().len()); + + let heartbeat3 = fixtures::repliable_content_heartbeat_v2(&mut rng, 1000, 1); + assert_eq!(heartbeat3.serialized_size(), heartbeat3.into_bytes().len()); + } } #[cfg(test)] diff --git a/common/nymsphinx/anonymous-replies/src/requests/v1.rs b/common/nymsphinx/anonymous-replies/src/requests/v1.rs new file mode 100644 index 0000000000..fec25025ac --- /dev/null +++ b/common/nymsphinx/anonymous-replies/src/requests/v1.rs @@ -0,0 +1,176 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::requests::InvalidReplyRequestError; +use crate::ReplySurb; +use nym_sphinx_types::PAYLOAD_KEY_SIZE; +use std::fmt::Display; +use std::mem; +use tracing::{error, warn}; + +const fn v1_reply_surb_serialised_len() -> usize { + // the SURB itself consists of SURB_header, first hop address and set of payload keys + // for each hop (3x mix + egress) + ReplySurb::BASE_OVERHEAD + 4 * PAYLOAD_KEY_SIZE +} + +fn v1_reply_surbs_serialised_len(surbs: &[ReplySurb]) -> usize { + // sanity checks; this should probably be removed later on + if let Some(reply_surb) = surbs.first() { + if reply_surb.surb.uses_key_seeds() { + error!("using v1 surbs encoding with updated structure - the surbs will be unusable") + } + } + + // when serialising surbs are always prepended with u32-encoded count + 4 + surbs.len() * v1_reply_surb_serialised_len() +} + +// this recovery code is shared between all legacy variants containing reply surbs +// NUM_SURBS (u32) || SURB_DATA +fn recover_reply_surbs_v1( + bytes: &[u8], +) -> Result<(Vec, usize), InvalidReplyRequestError> { + let mut consumed = mem::size_of::(); + if bytes.len() < consumed { + return Err(InvalidReplyRequestError::RequestTooShortToDeserialize); + } + let num_surbs = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]); + + let surb_size = v1_reply_surb_serialised_len(); + if bytes[consumed..].len() < num_surbs as usize * surb_size { + return Err(InvalidReplyRequestError::RequestTooShortToDeserialize); + } + + let mut reply_surbs = Vec::with_capacity(num_surbs as usize); + for _ in 0..num_surbs as usize { + let surb_bytes = &bytes[consumed..consumed + surb_size]; + let reply_surb = ReplySurb::from_bytes(surb_bytes)?; + reply_surbs.push(reply_surb); + + consumed += surb_size; + } + + Ok((reply_surbs, consumed)) +} + +// length (u32) prefixed reply surbs with legacy serialisation of 4 hops and full payload keys attached +fn reply_surbs_bytes_v1(reply_surbs: &[ReplySurb]) -> impl Iterator + use<'_> { + let num_surbs = reply_surbs.len() as u32; + + num_surbs + .to_be_bytes() + .into_iter() + .chain(reply_surbs.iter().flat_map(|s| s.to_bytes())) +} + +#[derive(Debug)] +pub struct DataV1 { + pub message: Vec, + pub reply_surbs: Vec, +} + +impl Display for DataV1 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + write!( + f, + "V1 repliable {:.2} kiB data message with {} reply surbs attached", + self.message.len() as f64 / 1024.0, + self.reply_surbs.len(), + ) + } +} + +#[derive(Debug)] +pub struct AdditionalSurbsV1 { + pub reply_surbs: Vec, +} + +impl Display for AdditionalSurbsV1 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + write!( + f, + "V1 repliable additional surbs message ({} reply surbs attached)", + self.reply_surbs.len(), + ) + } +} + +#[derive(Debug)] +pub struct HeartbeatV1 { + pub additional_reply_surbs: Vec, +} + +impl Display for HeartbeatV1 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + write!( + f, + "V1 repliable heartbeat message ({} reply surbs attached)", + self.additional_reply_surbs.len(), + ) + } +} + +impl DataV1 { + pub fn into_bytes(self) -> Vec { + reply_surbs_bytes_v1(&self.reply_surbs) + .chain(self.message) + .collect() + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + let (reply_surbs, n) = recover_reply_surbs_v1(bytes)?; + Ok(DataV1 { + message: bytes[n..].to_vec(), + reply_surbs, + }) + } + + pub fn serialized_len(&self) -> usize { + v1_reply_surbs_serialised_len(&self.reply_surbs) + self.message.len() + } +} + +impl AdditionalSurbsV1 { + pub fn into_bytes(self) -> Vec { + reply_surbs_bytes_v1(&self.reply_surbs).collect() + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + let (reply_surbs, n) = recover_reply_surbs_v1(bytes)?; + if n != bytes.len() { + let trailing = bytes.len() - n; + + warn!("trailing {trailing} bytes after v1 additional surbs message"); + } + + Ok(AdditionalSurbsV1 { reply_surbs }) + } + + pub fn serialized_len(&self) -> usize { + v1_reply_surbs_serialised_len(&self.reply_surbs) + } +} + +impl HeartbeatV1 { + pub fn into_bytes(self) -> Vec { + reply_surbs_bytes_v1(&self.additional_reply_surbs).collect() + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + let (additional_reply_surbs, n) = recover_reply_surbs_v1(bytes)?; + if n != bytes.len() { + let trailing = bytes.len() - n; + + warn!("trailing {trailing} bytes after v1 heartbeat message"); + } + + Ok(HeartbeatV1 { + additional_reply_surbs, + }) + } + + pub fn serialized_len(&self) -> usize { + v1_reply_surbs_serialised_len(&self.additional_reply_surbs) + } +} diff --git a/common/nymsphinx/anonymous-replies/src/requests/v2.rs b/common/nymsphinx/anonymous-replies/src/requests/v2.rs new file mode 100644 index 0000000000..813fabc276 --- /dev/null +++ b/common/nymsphinx/anonymous-replies/src/requests/v2.rs @@ -0,0 +1,187 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::requests::InvalidReplyRequestError; +use crate::ReplySurb; +use nym_sphinx_types::constants::PAYLOAD_KEY_SEED_SIZE; +use std::fmt::Display; +use std::iter::once; +use tracing::{error, warn}; + +const fn v2_reply_surb_serialised_len(num_hops: u8) -> usize { + ReplySurb::BASE_OVERHEAD + num_hops as usize * PAYLOAD_KEY_SEED_SIZE +} + +// sphinx doesn't support more than 5 hops (so cast to u8 is safe) +// ASSUMPTION: all surbs are generated with the same parameters (if they're not, then the client is hurting itself) +fn reply_surbs_hops(reply_surbs: &[ReplySurb]) -> u8 { + reply_surbs + .first() + .map(|reply_surb| reply_surb.surb.materials_count() as u8) + .unwrap_or_default() +} + +fn v2_reply_surbs_serialised_len(surbs: &[ReplySurb]) -> usize { + let num_surbs = surbs.len(); + let num_hops = reply_surbs_hops(surbs); + + // sanity checks; this should probably be removed later on + if let Some(reply_surb) = surbs.first() { + if !reply_surb.surb.uses_key_seeds() { + error!("using v2 surbs encoding with legacy structure - the surbs will be unusable") + } + } + + // when serialising surbs are always prepended with u16-encoded count an u8-encoded number of hops + 3 + num_surbs * v2_reply_surb_serialised_len(num_hops) +} + +// NUM_SURBS (u16) || HOPS (u8) || SURB_DATA +fn recover_reply_surbs_v2( + bytes: &[u8], +) -> Result<(Vec, usize), InvalidReplyRequestError> { + if bytes.len() < 2 { + return Err(InvalidReplyRequestError::RequestTooShortToDeserialize); + } + + // we're not attaching more than 65k surbs... + let num_surbs = u16::from_be_bytes([bytes[0], bytes[1]]); + let num_hops = bytes[2]; + let mut consumed = 3; + + let surb_size = ReplySurb::v2_serialised_len(num_hops); + if bytes[consumed..].len() < num_surbs as usize * surb_size { + return Err(InvalidReplyRequestError::RequestTooShortToDeserialize); + } + + let mut reply_surbs = Vec::with_capacity(num_surbs as usize); + for _ in 0..num_surbs as usize { + let surb_bytes = &bytes[consumed..consumed + surb_size]; + let reply_surb = ReplySurb::from_bytes(surb_bytes)?; + reply_surbs.push(reply_surb); + + consumed += surb_size; + } + + Ok((reply_surbs, consumed)) +} + +fn reply_surbs_bytes_v2(reply_surbs: &[ReplySurb]) -> impl Iterator + use<'_> { + let num_surbs = reply_surbs.len() as u16; + let num_hops = reply_surbs_hops(reply_surbs); + + num_surbs + .to_be_bytes() + .into_iter() + .chain(once(num_hops)) + .chain(reply_surbs.iter().flat_map(|surb| surb.to_bytes())) +} + +#[derive(Debug)] +pub struct DataV2 { + pub message: Vec, + pub reply_surbs: Vec, +} + +impl Display for DataV2 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + write!( + f, + "V2 repliable {:.2} kiB data message with {} reply surbs attached", + self.message.len() as f64 / 1024.0, + self.reply_surbs.len(), + ) + } +} + +#[derive(Debug)] +pub struct AdditionalSurbsV2 { + pub reply_surbs: Vec, +} + +impl Display for AdditionalSurbsV2 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + write!( + f, + "V2 repliable additional surbs message ({} reply surbs attached)", + self.reply_surbs.len(), + ) + } +} + +#[derive(Debug)] +pub struct HeartbeatV2 { + pub additional_reply_surbs: Vec, +} + +impl Display for HeartbeatV2 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + write!( + f, + "V2 repliable heartbeat message ({} reply surbs attached)", + self.additional_reply_surbs.len(), + ) + } +} + +impl DataV2 { + pub fn into_bytes(self) -> Vec { + reply_surbs_bytes_v2(&self.reply_surbs) + .chain(self.message) + .collect() + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + let (reply_surbs, n) = recover_reply_surbs_v2(bytes)?; + Ok(DataV2 { + message: bytes[n..].to_vec(), + reply_surbs, + }) + } + + pub fn serialized_len(&self) -> usize { + v2_reply_surbs_serialised_len(&self.reply_surbs) + self.message.len() + } +} + +impl AdditionalSurbsV2 { + pub fn into_bytes(self) -> Vec { + reply_surbs_bytes_v2(&self.reply_surbs).collect() + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + let (reply_surbs, n) = recover_reply_surbs_v2(bytes)?; + if n != bytes.len() { + let trailing = bytes.len() - n; + warn!("trailing {trailing} bytes after v2 additional surbs message"); + } + + Ok(AdditionalSurbsV2 { reply_surbs }) + } + + pub fn serialized_len(&self) -> usize { + v2_reply_surbs_serialised_len(&self.reply_surbs) + } +} + +impl HeartbeatV2 { + pub fn into_bytes(self) -> Vec { + reply_surbs_bytes_v2(&self.additional_reply_surbs).collect() + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + let (additional_reply_surbs, n) = recover_reply_surbs_v2(bytes)?; + if n != bytes.len() { + let trailing = bytes.len() - n; + warn!("trailing {trailing} bytes after v2 heartbeat message"); + } + + Ok(HeartbeatV2 { + additional_reply_surbs, + }) + } + + pub fn serialized_len(&self) -> usize { + v2_reply_surbs_serialised_len(&self.additional_reply_surbs) + } +} diff --git a/common/nymsphinx/cover/src/lib.rs b/common/nymsphinx/cover/src/lib.rs index 1c6c5430fb..337fa87bf6 100644 --- a/common/nymsphinx/cover/src/lib.rs +++ b/common/nymsphinx/cover/src/lib.rs @@ -34,6 +34,7 @@ pub enum CoverMessageError { pub fn generate_loop_cover_surb_ack( rng: &mut R, + use_legacy_sphinx_format: bool, topology: &NymRouteProvider, ack_key: &AckKey, full_address: &Recipient, @@ -45,6 +46,7 @@ where { Ok(SurbAck::construct( rng, + use_legacy_sphinx_format, full_address, ack_key, COVER_FRAG_ID.to_bytes(), @@ -57,6 +59,7 @@ where #[allow(clippy::too_many_arguments)] pub fn generate_loop_cover_packet( rng: &mut R, + use_legacy_sphinx_format: bool, topology: &NymRouteProvider, ack_key: &AckKey, full_address: &Recipient, @@ -71,6 +74,7 @@ where // we don't care about total ack delay - we will not be retransmitting it anyway let (_, ack_bytes) = generate_loop_cover_surb_ack( rng, + use_legacy_sphinx_format, topology, ack_key, full_address, @@ -126,6 +130,7 @@ where // once merged, that's an easy rng injection point for sphinx packets : ) let packet = match packet_type { PacketType::Mix => NymPacket::sphinx_build( + use_legacy_sphinx_format, packet_size.payload_size(), packet_payload, &route, diff --git a/common/nymsphinx/framing/src/codec.rs b/common/nymsphinx/framing/src/codec.rs index 9ecceaadaf..85685581aa 100644 --- a/common/nymsphinx/framing/src/codec.rs +++ b/common/nymsphinx/framing/src/codec.rs @@ -5,6 +5,7 @@ use crate::packet::{FramedNymPacket, Header}; use bytes::{Buf, BufMut, BytesMut}; use nym_sphinx_params::packet_sizes::{InvalidPacketSize, PacketSize}; use nym_sphinx_params::packet_types::InvalidPacketType; +use nym_sphinx_params::packet_version::{InvalidPacketVersion, PacketVersion}; use nym_sphinx_params::PacketType; use nym_sphinx_types::{NymPacket, NymPacketError}; use std::io; @@ -13,16 +14,25 @@ use tokio_util::codec::{Decoder, Encoder}; #[derive(Error, Debug)] pub enum NymCodecError { - #[error("the packet size information was malformed - {0}")] + #[error("the packet size information was malformed: {0}")] InvalidPacketSize(#[from] InvalidPacketSize), - #[error("the packet mode information was malformed - {0}")] + #[error("the packet mode information was malformed: {0}")] InvalidPacketType(#[from] InvalidPacketType), - #[error("encountered an IO error - {0}")] + #[error("the packet version information was malformed: {0}")] + InvalidPacketVersion(#[from] InvalidPacketVersion), + + #[error("received unsupported packet version {received}. max supported is {max_supported}")] + UnsupportedPacketVersion { + received: PacketVersion, + max_supported: PacketVersion, + }, + + #[error("encountered an IO error: {0}")] IoError(#[from] io::Error), - #[error("encountered a packet error - {0}")] + #[error("encountered a packet error: {0}")] NymPacket(#[from] NymPacketError), #[error("could not convert to bytes")] @@ -56,7 +66,7 @@ impl Decoder for NymCodec { if src.is_empty() { // can't do anything if we have no bytes, but let's reserve enough for the most // conservative case, i.e. receiving an ack packet - src.reserve(Header::LEGACY_SIZE + PacketSize::AckPacket.size()); + src.reserve(Header::SIZE + PacketSize::AckPacket.size()); return Ok(None); } @@ -68,7 +78,7 @@ impl Decoder for NymCodec { }; let packet_size = header.packet_size.size(); - let frame_len = header.size() + packet_size; + let frame_len = Header::SIZE + packet_size; if src.len() < frame_len { // we don't have enough bytes to read the rest of frame @@ -77,7 +87,7 @@ impl Decoder for NymCodec { } // advance buffer past the header - at this point we have enough bytes - src.advance(header.size()); + src.advance(Header::SIZE); let packet_bytes = src.split_to(packet_size); let packet = if let Some(slice) = packet_bytes.get(..) { // here it could be debatable whether stream is corrupt or not, @@ -104,11 +114,11 @@ impl Decoder for NymCodec { // we also assume the next packet coming from the same client will use exactly the same versioning // as the current packet - let mut allocate_for_next_packet = header.size() + PacketSize::AckPacket.size(); + let mut allocate_for_next_packet = Header::SIZE + PacketSize::AckPacket.size(); if !src.is_empty() { match Header::decode(src) { Ok(Some(next_header)) => { - allocate_for_next_packet = next_header.size() + next_header.packet_size.size(); + allocate_for_next_packet = Header::SIZE + next_header.packet_size.size(); } Ok(None) => { // we don't have enough information to know how much to reserve, fallback to the ack case @@ -199,8 +209,15 @@ mod packet_encoding { SphinxDelay::new_from_nanos(42), SphinxDelay::new_from_nanos(42), ]; - NymPacket::sphinx_build(size.payload_size(), b"foomp", &route, &destination, &delays) - .unwrap() + NymPacket::sphinx_build( + false, + size.payload_size(), + b"foomp", + &route, + &destination, + &delays, + ) + .unwrap() } #[test] @@ -252,34 +269,10 @@ mod packet_encoding { assert!(NymCodec.decode(&mut empty_bytes).unwrap().is_none()); assert_eq!( empty_bytes.capacity(), - Header::LEGACY_SIZE + PacketSize::AckPacket.size() + Header::SIZE + PacketSize::AckPacket.size() ); } - #[test] - fn for_bytes_with_legacy_header() { - // if header gets decoded there should be enough bytes for the entire frame - let packet_sizes = vec![ - PacketSize::AckPacket, - PacketSize::RegularPacket, - PacketSize::ExtendedPacket8, - PacketSize::ExtendedPacket16, - PacketSize::ExtendedPacket32, - ]; - for packet_size in packet_sizes { - let header = Header { - packet_version: PacketVersion::Legacy, - packet_size, - ..Default::default() - }; - let mut bytes = BytesMut::new(); - header.encode(&mut bytes); - assert!(NymCodec.decode(&mut bytes).unwrap().is_none()); - - assert_eq!(bytes.capacity(), Header::LEGACY_SIZE + packet_size.size()) - } - } - #[test] fn for_bytes_with_versioned_header() { // if header gets decoded there should be enough bytes for the entire frame @@ -292,7 +285,7 @@ mod packet_encoding { ]; for packet_size in packet_sizes { let header = Header { - packet_version: PacketVersion::Versioned(123), + packet_version: PacketVersion::new(), packet_size, ..Default::default() }; @@ -300,33 +293,10 @@ mod packet_encoding { header.encode(&mut bytes); assert!(NymCodec.decode(&mut bytes).unwrap().is_none()); - assert_eq!( - bytes.capacity(), - Header::VERSIONED_SIZE + packet_size.size() - ) + assert_eq!(bytes.capacity(), Header::SIZE + packet_size.size()) } } - #[test] - fn for_full_frame_with_legacy_header() { - // if full frame is used exactly, there should be enough space for header + ack packet - let packet = FramedNymPacket { - header: Header { - packet_version: PacketVersion::Legacy, - ..Default::default() - }, - packet: make_valid_sphinx_packet(Default::default()), - }; - - let mut bytes = BytesMut::new(); - NymCodec.encode(packet, &mut bytes).unwrap(); - assert!(NymCodec.decode(&mut bytes).unwrap().is_some()); - assert_eq!( - bytes.capacity(), - Header::LEGACY_SIZE + PacketSize::AckPacket.size() - ); - } - #[test] fn for_full_frame_with_versioned_header() { // if full frame is used exactly, there should be enough space for header + ack packet @@ -340,40 +310,10 @@ mod packet_encoding { assert!(NymCodec.decode(&mut bytes).unwrap().is_some()); assert_eq!( bytes.capacity(), - Header::VERSIONED_SIZE + PacketSize::AckPacket.size() + Header::SIZE + PacketSize::AckPacket.size() ); } - #[test] - fn for_full_frame_with_extra_bytes_with_legacy_header() { - // if there was at least 2 byte left, there should be enough space for entire next frame - let packet_sizes = vec![ - PacketSize::AckPacket, - PacketSize::RegularPacket, - PacketSize::ExtendedPacket8, - PacketSize::ExtendedPacket16, - PacketSize::ExtendedPacket32, - ]; - - for packet_size in packet_sizes { - let first_packet = FramedNymPacket { - header: Header { - packet_version: PacketVersion::Legacy, - ..Default::default() - }, - packet: make_valid_sphinx_packet(Default::default()), - }; - - let mut bytes = BytesMut::new(); - NymCodec.encode(first_packet, &mut bytes).unwrap(); - bytes.put_u8(packet_size as u8); - bytes.put_u8(PacketType::default() as u8); - assert!(NymCodec.decode(&mut bytes).unwrap().is_some()); - - assert!(bytes.capacity() >= Header::LEGACY_SIZE + packet_size.size()) - } - } - #[test] fn for_full_frame_with_extra_bytes_with_versioned_header() { // if there was at least 3 byte left, there should be enough space for entire next frame @@ -393,7 +333,7 @@ mod packet_encoding { let mut bytes = BytesMut::new(); NymCodec.encode(first_packet, &mut bytes).unwrap(); - bytes.put_u8(PacketVersion::new_versioned(123).as_u8().unwrap()); + bytes.put_u8(PacketVersion::new().as_u8()); bytes.put_u8(packet_size as u8); bytes.put_u8(PacketType::default() as u8); assert!(NymCodec.decode(&mut bytes).unwrap().is_some()); diff --git a/common/nymsphinx/framing/src/packet.rs b/common/nymsphinx/framing/src/packet.rs index 13045d7cff..184444f807 100644 --- a/common/nymsphinx/framing/src/packet.rs +++ b/common/nymsphinx/framing/src/packet.rs @@ -4,7 +4,7 @@ use crate::codec::NymCodecError; use bytes::{BufMut, BytesMut}; use nym_sphinx_params::packet_sizes::PacketSize; -use nym_sphinx_params::packet_version::PacketVersion; +use nym_sphinx_params::packet_version::{PacketVersion, CURRENT_PACKET_VERSION}; use nym_sphinx_params::PacketType; use nym_sphinx_types::NymPacket; @@ -81,8 +81,7 @@ pub struct Header { } impl Header { - pub(crate) const LEGACY_SIZE: usize = 2; - pub(crate) const VERSIONED_SIZE: usize = 3; + pub(crate) const SIZE: usize = 3; pub fn outfox() -> Header { Header { @@ -92,53 +91,39 @@ impl Header { } } - pub(crate) fn size(&self) -> usize { - if self.packet_version.is_legacy() { - Self::LEGACY_SIZE - } else { - Self::VERSIONED_SIZE - } - } - pub(crate) fn encode(&self, dst: &mut BytesMut) { - // we reserve one byte for `packet_size` and the other for `mode` - dst.reserve(Self::LEGACY_SIZE); - if let Some(version) = self.packet_version.as_u8() { - dst.reserve(Self::VERSIONED_SIZE); - dst.put_u8(version) - } + dst.reserve(Self::SIZE); + dst.put_u8(self.packet_version.as_u8()); dst.put_u8(self.packet_size as u8); dst.put_u8(self.packet_type as u8); + // reserve bytes for the actual packet dst.reserve(self.packet_size.size()); } pub(crate) fn decode(src: &mut BytesMut) -> Result, NymCodecError> { - if src.len() < Self::LEGACY_SIZE { + if src.len() < Self::SIZE { // can't do anything if we don't have enough bytes - but reserve enough for the next call - src.reserve(Self::LEGACY_SIZE); + src.reserve(Self::SIZE); return Ok(None); } - let packet_version = PacketVersion::from(src[0]); - if packet_version.is_legacy() { - Ok(Some(Header { - packet_version, - packet_size: PacketSize::try_from(src[0])?, - packet_type: PacketType::try_from(src[1])?, - })) - } else if src.len() < Self::VERSIONED_SIZE { - // we're missing that 1 byte to read the full header... - src.reserve(Self::VERSIONED_SIZE); - Ok(None) - } else { - Ok(Some(Header { - packet_version, - packet_size: PacketSize::try_from(src[1])?, - packet_type: PacketType::try_from(src[2])?, - })) + let packet_version = PacketVersion::try_from(src[0])?; + if packet_version > CURRENT_PACKET_VERSION { + // received an unsupported packet version - we don't know how it's meant to look like! + // (this is in preparation for the dual support of breaking sphinx changes) + return Err(NymCodecError::UnsupportedPacketVersion { + received: packet_version, + max_supported: CURRENT_PACKET_VERSION, + }); } + + Ok(Some(Header { + packet_version, + packet_size: PacketSize::try_from(src[1])?, + packet_type: PacketType::try_from(src[2])?, + })) } } @@ -165,7 +150,7 @@ mod header_encoding { // due to the hack used to get legacy mode compatibility let mut bytes = BytesMut::from( [ - PacketVersion::new_versioned(123).as_u8().unwrap(), + PacketVersion::new().as_u8(), unknown_packet_size, PacketType::default() as u8, ] @@ -180,7 +165,14 @@ mod header_encoding { // make sure this is still 'unknown' for if we make changes in the future assert!(PacketType::try_from(unknown_packet_type).is_err()); - let mut bytes = BytesMut::from([PacketSize::default() as u8, unknown_packet_type].as_ref()); + let mut bytes = BytesMut::from( + [ + PacketVersion::new().as_u8(), + PacketSize::default() as u8, + unknown_packet_type, + ] + .as_ref(), + ); assert!(Header::decode(&mut bytes).is_err()) } @@ -189,16 +181,16 @@ mod header_encoding { let mut empty_bytes = BytesMut::new(); let decode_attempt_1 = Header::decode(&mut empty_bytes).unwrap(); assert!(decode_attempt_1.is_none()); - assert!(empty_bytes.capacity() > Header::LEGACY_SIZE); + assert!(empty_bytes.capacity() > Header::SIZE); let mut empty_bytes = BytesMut::with_capacity(1); let decode_attempt_2 = Header::decode(&mut empty_bytes).unwrap(); assert!(decode_attempt_2.is_none()); - assert!(empty_bytes.capacity() > Header::LEGACY_SIZE); + assert!(empty_bytes.capacity() > Header::SIZE); } #[test] - fn header_encoding_reserves_enough_bytes_for_full_sphinx_packet_in_legacy_mode() { + fn header_encoding_reserves_enough_bytes_for_full_sphinx_packet_() { let packet_sizes = vec![ PacketSize::AckPacket, PacketSize::RegularPacket, @@ -208,7 +200,7 @@ mod header_encoding { ]; for packet_size in packet_sizes { let header = Header { - packet_version: PacketVersion::Legacy, + packet_version: PacketVersion::new(), packet_size, ..Default::default() }; @@ -219,23 +211,26 @@ mod header_encoding { } #[test] - fn header_encoding_reserves_enough_bytes_for_full_sphinx_packet_in_versioned_mode() { - let packet_sizes = vec![ - PacketSize::AckPacket, - PacketSize::RegularPacket, - PacketSize::ExtendedPacket8, - PacketSize::ExtendedPacket16, - PacketSize::ExtendedPacket32, - ]; - for packet_size in packet_sizes { - let header = Header { - packet_version: PacketVersion::Versioned(123), - packet_size, - ..Default::default() - }; - let mut bytes = BytesMut::new(); - header.encode(&mut bytes); - assert_eq!(bytes.capacity(), bytes.len() + packet_size.size()) + fn header_decoding_will_reject_future_versions() { + let future_version = PacketVersion::try_from(123).unwrap(); + + let unchecked_header = Header { + packet_version: future_version, + packet_size: PacketSize::RegularPacket, + packet_type: PacketType::Mix, + }; + let mut bytes = BytesMut::new(); + unchecked_header.encode(&mut bytes); + + match Header::decode(&mut bytes).unwrap_err() { + NymCodecError::UnsupportedPacketVersion { + received, + max_supported, + } => { + assert_eq!(received, future_version); + assert_eq!(max_supported, CURRENT_PACKET_VERSION); + } + _ => panic!("unexpected error variant"), } } } diff --git a/common/nymsphinx/params/src/lib.rs b/common/nymsphinx/params/src/lib.rs index f5d3fd7afb..69c8368e99 100644 --- a/common/nymsphinx/params/src/lib.rs +++ b/common/nymsphinx/params/src/lib.rs @@ -22,17 +22,6 @@ pub mod packet_version; pub const FRAG_ID_LEN: usize = 5; pub type SerializedFragmentIdentifier = [u8; FRAG_ID_LEN]; -// wait, wait, but why are we starting with version 7? -// when packet header gets serialized, the following bytes (in that order) are put onto the wire: -// - packet_version (starting with v1.1.0) -// - packet_size indicator -// - packet_type -// it also just so happens that the only valid values for packet_size indicator include values 1-6 -// therefore if we receive byte `7` (or larger than that) we'll know we received a versioned packet, -// otherwise we should treat it as legacy -/// Increment it whenever we perform any breaking change in the wire format! -const CURRENT_PACKET_VERSION_NUMBER: u8 = 7; - // TODO: ask @AP about the choice of below algorithms /// Hashing algorithm used during hkdf for ephemeral shared key generation per sphinx packet payload. diff --git a/common/nymsphinx/params/src/packet_version.rs b/common/nymsphinx/params/src/packet_version.rs index 8639dea499..d0d3f89fc6 100644 --- a/common/nymsphinx/params/src/packet_version.rs +++ b/common/nymsphinx/params/src/packet_version.rs @@ -2,56 +2,70 @@ // SPDX-License-Identifier: Apache-2.0 use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; +use thiserror::Error; -use crate::{PacketSize, CURRENT_PACKET_VERSION_NUMBER}; +// wait, wait, but why are we starting with version 7? +// when packet header gets serialized, the following bytes (in that order) are put onto the wire: +// - packet_version (starting with v1.1.0) +// - packet_size indicator +// - packet_type +// it also just so happens that the only valid values for packet_size indicator include values 1-6 +// therefore if we receive byte `7` (or larger than that) we'll know we received a versioned packet, +// otherwise we should treat it as legacy +/// Increment it whenever we perform any breaking change in the wire format! +pub const INITIAL_PACKET_VERSION_NUMBER: u8 = 7; -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum PacketVersion { - // this will allow updated mixnodes to still understand packets from before the update - Legacy, - Versioned(u8), +pub const CURRENT_PACKET_VERSION_NUMBER: u8 = INITIAL_PACKET_VERSION_NUMBER; +pub const CURRENT_PACKET_VERSION: PacketVersion = + PacketVersion::unchecked(CURRENT_PACKET_VERSION_NUMBER); + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +pub struct PacketVersion(u8); + +impl Display for PacketVersion { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } } +#[derive(Debug, Error)] +#[error("attempted to use legacy packet version")] +pub struct InvalidPacketVersion; + impl PacketVersion { pub fn new() -> Self { - Self::new_versioned(CURRENT_PACKET_VERSION_NUMBER) + PacketVersion(CURRENT_PACKET_VERSION_NUMBER) } - pub fn new_legacy() -> Self { - PacketVersion::Legacy + const fn unchecked(version: u8) -> PacketVersion { + PacketVersion(version) } - pub fn new_versioned(version: u8) -> Self { - PacketVersion::Versioned(version) - } - - pub fn is_legacy(&self) -> bool { - matches!(self, PacketVersion::Legacy) - } - - pub fn as_u8(&self) -> Option { - match self { - PacketVersion::Legacy => None, - PacketVersion::Versioned(version) => Some(*version), - } + pub fn as_u8(&self) -> u8 { + (*self).into() } } -impl From for PacketVersion { - fn from(v: u8) -> Self { - match v { - n if n == PacketSize::RegularPacket as u8 => PacketVersion::Legacy, - n if n == PacketSize::AckPacket as u8 => PacketVersion::Legacy, - n if n == PacketSize::ExtendedPacket8 as u8 => PacketVersion::Legacy, - n if n == PacketSize::ExtendedPacket16 as u8 => PacketVersion::Legacy, - n if n == PacketSize::ExtendedPacket32 as u8 => PacketVersion::Legacy, - n => PacketVersion::Versioned(n), +impl TryFrom for PacketVersion { + type Error = InvalidPacketVersion; + + fn try_from(value: u8) -> Result { + if value < INITIAL_PACKET_VERSION_NUMBER { + return Err(InvalidPacketVersion); } + Ok(PacketVersion(value)) + } +} + +impl From for u8 { + fn from(packet_version: PacketVersion) -> Self { + packet_version.0 } } impl Default for PacketVersion { fn default() -> Self { - PacketVersion::Versioned(CURRENT_PACKET_VERSION_NUMBER) + PacketVersion::new() } } diff --git a/common/nymsphinx/src/message.rs b/common/nymsphinx/src/message.rs index c8ad5f40c2..c87bd05b51 100644 --- a/common/nymsphinx/src/message.rs +++ b/common/nymsphinx/src/message.rs @@ -113,7 +113,8 @@ impl NymMessage { match self { NymMessage::Plain(data) => data, NymMessage::Repliable(repliable) => match repliable.content { - RepliableMessageContent::Data { message, .. } => message, + RepliableMessageContent::Data(content) => content.message, + RepliableMessageContent::DataV2(content) => content.message, _ => Vec::new(), }, NymMessage::Reply(reply) => match reply.content { @@ -309,6 +310,7 @@ mod tests { // a single variant for each repliable and reply is enough as they are more thoroughly tested // internally let repliable = NymMessage::new_repliable(RepliableMessage::new_data( + true, vec![1, 2, 3, 4, 5], [42u8; 16].into(), vec![], diff --git a/common/nymsphinx/src/preparer/mod.rs b/common/nymsphinx/src/preparer/mod.rs index 5e8beca63a..0d4c75c2ca 100644 --- a/common/nymsphinx/src/preparer/mod.rs +++ b/common/nymsphinx/src/preparer/mod.rs @@ -51,29 +51,14 @@ impl From for MixPacket { pub trait FragmentPreparer { type Rng: CryptoRng + Rng; + fn use_legacy_sphinx_format(&self) -> bool; + fn deterministic_route_selection(&self) -> bool; fn rng(&mut self) -> &mut Self::Rng; fn nonce(&self) -> i32; fn average_packet_delay(&self) -> Duration; fn average_ack_delay(&self) -> Duration; - fn generate_reply_surbs( - &mut self, - amount: usize, - topology: &NymRouteProvider, - reply_recipient: &Recipient, - ) -> Result, NymTopologyError> { - let mut reply_surbs = Vec::with_capacity(amount); - let packet_delay = self.average_packet_delay(); - for _ in 0..amount { - let reply_surb = - ReplySurb::construct(self.rng(), reply_recipient, packet_delay, topology)?; - reply_surbs.push(reply_surb) - } - - Ok(reply_surbs) - } - fn generate_surb_ack( &mut self, recipient: &Recipient, @@ -83,9 +68,11 @@ pub trait FragmentPreparer { packet_type: PacketType, ) -> Result { let ack_delay = self.average_ack_delay(); + let use_legacy_sphinx_format = self.use_legacy_sphinx_format(); SurbAck::construct( self.rng(), + use_legacy_sphinx_format, recipient, ack_key, fragment_id.to_bytes(), @@ -264,6 +251,7 @@ pub trait FragmentPreparer { Some(packet_size.plaintext_size()), )?, PacketType::Mix => NymPacket::sphinx_build( + self.use_legacy_sphinx_format(), packet_size.payload_size(), packet_payload, &route, @@ -323,6 +311,10 @@ pub struct MessagePreparer { /// Average delay an acknowledgement packet is going to get delay at a single mixnode. average_ack_delay: Duration, + /// Specify whether any constructed packets should use the legacy format, + /// where the payload keys are explicitly attached rather than using the seeds + use_legacy_sphinx_format: bool, + nonce: i32, } @@ -336,6 +328,7 @@ where sender_address: Recipient, average_packet_delay: Duration, average_ack_delay: Duration, + use_legacy_sphinx_format: bool, ) -> Self { let mut rng = rng; let nonce = rng.gen(); @@ -345,6 +338,7 @@ where sender_address, average_packet_delay, average_ack_delay, + use_legacy_sphinx_format, nonce, } } @@ -356,6 +350,7 @@ where pub fn generate_reply_surbs( &mut self, + use_legacy_reply_surb_format: bool, amount: usize, topology: &NymRouteProvider, ) -> Result, NymTopologyError> { @@ -365,6 +360,7 @@ where &mut self.rng, &self.sender_address, self.average_packet_delay, + use_legacy_reply_surb_format, topology, )?; reply_surbs.push(reply_surb) @@ -446,6 +442,10 @@ where impl FragmentPreparer for MessagePreparer { type Rng = R; + fn use_legacy_sphinx_format(&self) -> bool { + self.use_legacy_sphinx_format + } + fn deterministic_route_selection(&self) -> bool { self.deterministic_route_selection } diff --git a/common/nymsphinx/types/src/lib.rs b/common/nymsphinx/types/src/lib.rs index aa70d69d8f..cd620e32b3 100644 --- a/common/nymsphinx/types/src/lib.rs +++ b/common/nymsphinx/types/src/lib.rs @@ -26,10 +26,13 @@ pub use sphinx_packet::{ crypto::{self, PrivateKey, PublicKey}, header::{self, delays, delays::Delay, ProcessedHeader, SphinxHeader, HEADER_SIZE}, packet::builder::DEFAULT_PAYLOAD_SIZE, - payload::{Payload, PAYLOAD_OVERHEAD_SIZE}, + payload::{ + key::{PayloadKey, PayloadKeySeed}, + Payload, PAYLOAD_OVERHEAD_SIZE, + }, route::{Destination, DestinationAddressBytes, Node, NodeAddressBytes, SURBIdentifier}, surb::{SURBMaterial, SURB}, - version::Version, + version::*, Error as SphinxError, ProcessedPacket, ProcessedPacketData, }; @@ -84,17 +87,25 @@ impl fmt::Debug for NymPacket { impl NymPacket { #[cfg(feature = "sphinx")] pub fn sphinx_build>( + use_legacy_sphinx_format: bool, size: usize, message: M, route: &[Node], destination: &Destination, delays: &[Delay], ) -> Result { - Ok(NymPacket::Sphinx( - SphinxPacketBuilder::new() - .with_payload_size(size) - .build_packet(message, route, destination, delays)?, - )) + let mut builder = SphinxPacketBuilder::new().with_payload_size(size); + + if use_legacy_sphinx_format { + builder = builder.with_version(X25519_WITH_EXPLICIT_PAYLOAD_KEYS_VERSION) + }; + + Ok(NymPacket::Sphinx(builder.build_packet( + message, + route, + destination, + delays, + )?)) } #[cfg(feature = "sphinx")] pub fn sphinx_from_bytes(bytes: &[u8]) -> Result { diff --git a/common/wasm/client-core/src/config/mod.rs b/common/wasm/client-core/src/config/mod.rs index f8b0500407..a8f4f30662 100644 --- a/common/wasm/client-core/src/config/mod.rs +++ b/common/wasm/client-core/src/config/mod.rs @@ -181,6 +181,14 @@ pub struct TrafficWasm { /// Controls whether the sent sphinx packet use the NON-DEFAULT bigger size. pub use_extended_packet_size: bool, + /// Specify whether any constructed sphinx packets should use the legacy format, + /// where the payload keys are explicitly attached rather than using the seeds + /// this affects any forward packets, acks and reply surbs + /// this flag should remain disabled until sufficient number of nodes on the network has upgraded + /// and support updated format. + /// in the case of reply surbs, the recipient must also understand the new encoding + pub use_legacy_sphinx_format: bool, + /// Controls whether the sent packets should use outfox as opposed to the default sphinx. pub use_outfox: bool, } @@ -214,6 +222,7 @@ impl From for ConfigTraffic { .disable_main_poisson_packet_distribution, primary_packet_size: PacketSize::RegularPacket, secondary_packet_size: use_extended_packet_size, + use_legacy_sphinx_format: traffic.use_legacy_sphinx_format, packet_type, } } @@ -229,6 +238,7 @@ impl From for TrafficWasm { maximum_number_of_retransmissions: traffic.maximum_number_of_retransmissions, disable_main_poisson_packet_distribution: traffic .disable_main_poisson_packet_distribution, + use_legacy_sphinx_format: traffic.use_legacy_sphinx_format, use_extended_packet_size: traffic.secondary_packet_size.is_some(), use_outfox: traffic.packet_type == PacketType::Outfox, } diff --git a/common/wasm/client-core/src/config/override.rs b/common/wasm/client-core/src/config/override.rs index bc9275660a..eaa492a145 100644 --- a/common/wasm/client-core/src/config/override.rs +++ b/common/wasm/client-core/src/config/override.rs @@ -109,6 +109,15 @@ pub struct TrafficWasmOverride { #[tsify(optional)] pub use_extended_packet_size: Option, + /// Specify whether any constructed sphinx packets should use the legacy format, + /// where the payload keys are explicitly attached rather than using the seeds + /// this affects any forward packets, acks and reply surbs + /// this flag should remain disabled until sufficient number of nodes on the network has upgraded + /// and support updated format. + /// in the case of reply surbs, the recipient must also understand the new encoding + #[tsify(optional)] + pub use_legacy_sphinx_format: Option, + /// Controls whether the sent packets should use outfox as opposed to the default sphinx. #[tsify(optional)] pub use_outfox: Option, @@ -132,6 +141,9 @@ impl From for TrafficWasm { disable_main_poisson_packet_distribution: value .disable_main_poisson_packet_distribution .unwrap_or(def.disable_main_poisson_packet_distribution), + use_legacy_sphinx_format: value + .use_legacy_sphinx_format + .unwrap_or(def.use_legacy_sphinx_format), use_extended_packet_size: value .use_extended_packet_size .unwrap_or(def.use_extended_packet_size), diff --git a/nym-api/src/network_monitor/monitor/preparer.rs b/nym-api/src/network_monitor/monitor/preparer.rs index 7ee9888478..433288c4a2 100644 --- a/nym-api/src/network_monitor/monitor/preparer.rs +++ b/nym-api/src/network_monitor/monitor/preparer.rs @@ -129,6 +129,7 @@ impl PacketPreparer { false, DEFAULT_AVERAGE_PACKET_DELAY, DEFAULT_AVERAGE_ACK_DELAY, + true, self.ack_key.clone(), ) } diff --git a/nym-node/src/throughput_tester/client.rs b/nym-node/src/throughput_tester/client.rs index ae1530d9dd..a402511f7b 100644 --- a/nym-node/src/throughput_tester/client.rs +++ b/nym-node/src/throughput_tester/client.rs @@ -16,10 +16,12 @@ use nym_sphinx_framing::codec::{NymCodec, NymCodecError}; use nym_sphinx_framing::packet::FramedNymPacket; use nym_sphinx_params::PacketSize; use nym_sphinx_routing::generate_hop_delays; -use nym_sphinx_types::constants::{EXPANDED_SHARED_SECRET_LENGTH, HKDF_INPUT_SEED}; -use nym_sphinx_types::header::keys::PayloadKey; +use nym_sphinx_types::constants::{ + EXPANDED_SHARED_SECRET_HKDF_INFO, EXPANDED_SHARED_SECRET_HKDF_SALT, + EXPANDED_SHARED_SECRET_LENGTH, +}; use nym_sphinx_types::{ - Destination, DestinationAddressBytes, Node, NymPacket, DESTINATION_ADDRESS_LENGTH, + Destination, DestinationAddressBytes, Node, NymPacket, PayloadKey, DESTINATION_ADDRESS_LENGTH, IDENTIFIER_LENGTH, }; use nym_task::ShutdownToken; @@ -100,13 +102,14 @@ pub(crate) struct ThroughputTestingClient { } fn rederive_lioness_payload_key(shared_secret: &[u8; 32]) -> PayloadKey { - let hkdf = Hkdf::::new(None, shared_secret); + let hkdf = Hkdf::::new(Some(EXPANDED_SHARED_SECRET_HKDF_SALT), shared_secret); // expanded shared secret let mut output = [0u8; EXPANDED_SHARED_SECRET_LENGTH]; // SAFETY: the length of the provided okm is within the allowed range #[allow(clippy::unwrap_used)] - hkdf.expand(HKDF_INPUT_SEED, &mut output).unwrap(); + hkdf.expand(EXPANDED_SHARED_SECRET_HKDF_INFO, &mut output) + .unwrap(); *array_ref!(&output, 32, 192) } @@ -156,7 +159,7 @@ impl ThroughputTestingClient { let payload = PacketSize::RegularPacket.payload_size(); let forward_packet = - NymPacket::sphinx_build(payload, b"foomp", &route, &destination, &delays)?; + NymPacket::sphinx_build(true, payload, b"foomp", &route, &destination, &delays)?; // SAFETY: we constructed a sphinx packet... #[allow(clippy::unwrap_used)] @@ -171,7 +174,7 @@ impl ThroughputTestingClient { .diffie_hellman(&header.shared_secret); let payload_key = rederive_lioness_payload_key(shared_secret.as_bytes()); - let unwrapped_payload = sphinx_packet.payload.unwrap(&payload_key)?; + let unwrapped_payload = sphinx_packet.payload.unwrap(payload_key)?; let unwrapped_forward_payload_bytes = unwrapped_payload.into_bytes(); let start = Instant::now(); @@ -230,11 +233,7 @@ impl ThroughputTestingClient { } fn lioness_encrypt(&self, block: &mut [u8]) -> anyhow::Result<()> { - let lioness_cipher = Lioness::::new_raw(array_ref!( - self.payload_key, - 0, - lioness::RAW_KEY_SIZE - )); + let lioness_cipher = Lioness::::new_raw(&self.payload_key); lioness_cipher.encrypt(block)?; Ok(()) } diff --git a/nym-outfox/src/packet.rs b/nym-outfox/src/packet.rs index 97a1562177..bbe3bb431c 100644 --- a/nym-outfox/src/packet.rs +++ b/nym-outfox/src/packet.rs @@ -131,7 +131,7 @@ impl OutfoxPacket { &mut buffer[range], &secret_key, processing_node.pub_key, - destination_node.address.as_bytes_ref(), + destination_node.address.as_bytes(), )?; } diff --git a/nym-outfox/tests/unittests.rs b/nym-outfox/tests/unittests.rs index 6a9e928d8c..0748173393 100644 --- a/nym-outfox/tests/unittests.rs +++ b/nym-outfox/tests/unittests.rs @@ -138,11 +138,11 @@ mod tests { let mut packet = OutfoxPacket::try_from(packet_bytes.as_slice()).unwrap(); let next_address = packet.decode_next_layer(&node1_pk).unwrap(); - assert_eq!(next_address, node2.address.as_bytes()); + assert_eq!(&next_address, node2.address.as_bytes()); let next_address = packet.decode_next_layer(&node2_pk).unwrap(); - assert_eq!(next_address, node3.address.as_bytes()); + assert_eq!(&next_address, node3.address.as_bytes()); let next_address = packet.decode_next_layer(&node3_pk).unwrap(); - assert_eq!(next_address, gateway.address.as_bytes()); + assert_eq!(&next_address, gateway.address.as_bytes()); let destination_address = packet.decode_next_layer(&gateway_pk).unwrap(); assert_eq!(destination_address, destination.address.as_bytes()); @@ -194,11 +194,11 @@ mod tests { let mut packet = OutfoxPacket::try_from(packet_bytes.as_slice()).unwrap(); let next_address = packet.decode_next_layer(&node1_pk).unwrap(); - assert_eq!(next_address, node2.address.as_bytes()); + assert_eq!(&next_address, node2.address.as_bytes()); let next_address = packet.decode_next_layer(&node2_pk).unwrap(); - assert_eq!(next_address, node3.address.as_bytes()); + assert_eq!(&next_address, node3.address.as_bytes()); let next_address = packet.decode_next_layer(&node3_pk).unwrap(); - assert_eq!(next_address, gateway.address.as_bytes()); + assert_eq!(&next_address, gateway.address.as_bytes()); let destination_address = packet.decode_next_layer(&gateway_pk).unwrap(); assert_eq!(destination_address, destination.address.as_bytes()); diff --git a/wasm/node-tester/src/tester.rs b/wasm/node-tester/src/tester.rs index e0ef0d4350..8ab6b23918 100644 --- a/wasm/node-tester/src/tester.rs +++ b/wasm/node-tester/src/tester.rs @@ -238,6 +238,7 @@ impl NymNodeTesterBuilder { false, Duration::from_millis(5), Duration::from_millis(5), + true, managed_keys.ack_key(), );