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
This commit is contained in:
Jędrzej Stuczyński
2025-04-10 13:43:29 +01:00
committed by GitHub
parent 02ed64557d
commit af2c4f50b6
31 changed files with 1016 additions and 443 deletions
Generated
+3 -2
View File
@@ -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",
+6 -1
View File
@@ -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
+13 -1
View File
@@ -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<PacketSize>,
/// 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,
}
}
}
@@ -62,6 +62,10 @@ where
/// Optional secondary predefined packet size used for the loop cover messages.
secondary_packet_size: Option<PacketSize>,
/// 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<OsRng> {
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<OsRng> {
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,
@@ -109,6 +109,10 @@ pub(crate) struct Config {
/// Optional secondary predefined packet size used for the encapsulated messages.
secondary_packet_size: Option<PacketSize>,
/// 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,
@@ -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)
@@ -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,
@@ -250,10 +250,10 @@ impl<R: MessageReceiver> ReceivedMessagesBuffer<R> {
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<R: MessageReceiver> ReceivedMessagesBuffer<R> {
(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<R: MessageReceiver> ReceivedMessagesBuffer<R> {
);
(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)
}
+10
View File
@@ -39,6 +39,10 @@ pub struct NodeTester<R> {
/// 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<AckKey>,
@@ -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<AckKey>,
) -> 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<R: CryptoRng + Rng> FragmentPreparer for NodeTester<R> {
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
}
@@ -37,8 +37,10 @@ pub enum SurbAckRecoveryError {
}
impl SurbAck {
#[allow(clippy::too_many_arguments)]
pub fn construct<R>(
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,
+1 -1
View File
@@ -199,7 +199,7 @@ impl TryFrom<NodeAddressBytes> for NymNodeRoutingAddress {
type Error = NymNodeRoutingAddressError;
fn try_from(value: NodeAddressBytes) -> Result<Self, Self::Error> {
Self::try_from_bytes(value.as_bytes_ref())
Self::try_from_bytes(value.as_bytes())
}
}
@@ -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" }
@@ -1,20 +1,25 @@
// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2021-2025 - Nym Technologies SA <contact@nymtech.net>
// 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<R>(
rng: &mut R,
recipient: &Recipient,
average_delay: time::Duration,
average_delay: Duration,
use_legacy_surb_format: bool,
topology: &NymRouteProvider,
) -> Result<Self, NymTopologyError>
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 {
@@ -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<u8>,
sender_tag: AnonymousSenderTag,
reply_surbs: Vec<ReplySurb>,
) -> 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<ReplySurb>,
) -> 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<ReplySurb>, usize), InvalidReplyRequestError> {
let mut consumed = mem::size_of::<u32>();
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<u8> for RepliableMessageContentTag {
@@ -233,6 +231,11 @@ impl TryFrom<u8> 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<u8> for RepliableMessageContentTag {
// sent by original sender that initialised the communication that knows address of the remote
#[derive(Debug)]
pub enum RepliableMessageContent {
Data {
message: Vec<u8>,
reply_surbs: Vec<ReplySurb>,
},
AdditionalSurbs {
reply_surbs: Vec<ReplySurb>,
},
Heartbeat {
additional_reply_surbs: Vec<ReplySurb>,
},
Data(DataV1),
AdditionalSurbs(AdditionalSurbsV1),
Heartbeat(HeartbeatV1),
DataV2(DataV2),
AdditionalSurbsV2(AdditionalSurbsV2),
HeartbeatV2(HeartbeatV2),
}
impl RepliableMessageContent {
pub fn into_bytes(self) -> Vec<u8> {
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::<u32>();
num_reply_surbs_tag
+ reply_surbs.len() * ReplySurb::serialized_len()
+ message.len()
}
RepliableMessageContent::AdditionalSurbs { reply_surbs } => {
let num_reply_surbs_tag = mem::size_of::<u32>();
num_reply_surbs_tag + reply_surbs.len() * ReplySurb::serialized_len()
}
RepliableMessageContent::Heartbeat {
additional_reply_surbs,
} => {
let num_reply_surbs_tag = mem::size_of::<u32>();
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<ReplySurb> {
pub(super) fn reply_surbs(
rng: &mut ChaCha20Rng,
n: usize,
legacy: bool,
hops: u8,
) -> Vec<ReplySurb> {
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)]
@@ -0,0 +1,176 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// 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<ReplySurb>, usize), InvalidReplyRequestError> {
let mut consumed = mem::size_of::<u32>();
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<Item = u8> + 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<u8>,
pub reply_surbs: Vec<ReplySurb>,
}
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<ReplySurb>,
}
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<ReplySurb>,
}
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<u8> {
reply_surbs_bytes_v1(&self.reply_surbs)
.chain(self.message)
.collect()
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, InvalidReplyRequestError> {
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<u8> {
reply_surbs_bytes_v1(&self.reply_surbs).collect()
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, InvalidReplyRequestError> {
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<u8> {
reply_surbs_bytes_v1(&self.additional_reply_surbs).collect()
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, InvalidReplyRequestError> {
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)
}
}
@@ -0,0 +1,187 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// 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<ReplySurb>, 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<Item = u8> + 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<u8>,
pub reply_surbs: Vec<ReplySurb>,
}
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<ReplySurb>,
}
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<ReplySurb>,
}
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<u8> {
reply_surbs_bytes_v2(&self.reply_surbs)
.chain(self.message)
.collect()
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, InvalidReplyRequestError> {
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<u8> {
reply_surbs_bytes_v2(&self.reply_surbs).collect()
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, InvalidReplyRequestError> {
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<u8> {
reply_surbs_bytes_v2(&self.additional_reply_surbs).collect()
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, InvalidReplyRequestError> {
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)
}
}
+5
View File
@@ -34,6 +34,7 @@ pub enum CoverMessageError {
pub fn generate_loop_cover_surb_ack<R>(
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<R>(
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,
+33 -93
View File
@@ -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());
+54 -59
View File
@@ -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<Option<Self>, 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"),
}
}
}
-11
View File
@@ -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.
+46 -32
View File
@@ -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<u8> {
match self {
PacketVersion::Legacy => None,
PacketVersion::Versioned(version) => Some(*version),
}
pub fn as_u8(&self) -> u8 {
(*self).into()
}
}
impl From<u8> 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<u8> for PacketVersion {
type Error = InvalidPacketVersion;
fn try_from(value: u8) -> Result<Self, Self::Error> {
if value < INITIAL_PACKET_VERSION_NUMBER {
return Err(InvalidPacketVersion);
}
Ok(PacketVersion(value))
}
}
impl From<PacketVersion> 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()
}
}
+3 -1
View File
@@ -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![],
+17 -17
View File
@@ -51,29 +51,14 @@ impl From<PreparedFragment> 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<Vec<ReplySurb>, 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<SurbAck, NymTopologyError> {
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<R> {
/// 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<Vec<ReplySurb>, 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<R: CryptoRng + Rng> FragmentPreparer for MessagePreparer<R> {
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
}
+18 -7
View File
@@ -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<M: AsRef<[u8]>>(
use_legacy_sphinx_format: bool,
size: usize,
message: M,
route: &[Node],
destination: &Destination,
delays: &[Delay],
) -> Result<NymPacket, NymPacketError> {
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<NymPacket, NymPacketError> {
+10
View File
@@ -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<TrafficWasm> 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<ConfigTraffic> 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,
}
@@ -109,6 +109,15 @@ pub struct TrafficWasmOverride {
#[tsify(optional)]
pub use_extended_packet_size: Option<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
#[tsify(optional)]
pub use_legacy_sphinx_format: Option<bool>,
/// Controls whether the sent packets should use outfox as opposed to the default sphinx.
#[tsify(optional)]
pub use_outfox: Option<bool>,
@@ -132,6 +141,9 @@ impl From<TrafficWasmOverride> 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),
@@ -129,6 +129,7 @@ impl PacketPreparer {
false,
DEFAULT_AVERAGE_PACKET_DELAY,
DEFAULT_AVERAGE_ACK_DELAY,
true,
self.ack_key.clone(),
)
}
+11 -12
View File
@@ -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::<Sha256>::new(None, shared_secret);
let hkdf = Hkdf::<Sha256>::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::<VarBlake2b, ChaCha>::new_raw(array_ref!(
self.payload_key,
0,
lioness::RAW_KEY_SIZE
));
let lioness_cipher = Lioness::<VarBlake2b, ChaCha>::new_raw(&self.payload_key);
lioness_cipher.encrypt(block)?;
Ok(())
}
+1 -1
View File
@@ -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(),
)?;
}
+6 -6
View File
@@ -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());
+1
View File
@@ -238,6 +238,7 @@ impl NymNodeTesterBuilder {
false,
Duration::from_millis(5),
Duration::from_millis(5),
true,
managed_keys.ack_key(),
);