Compare commits

...

4 Commits

Author SHA1 Message Date
Tommy Verrall 35b7f655c6 Revert: keep IPR connect request v8-tagged (non-stream transport)
The IPR enforces 'non-stream message must be v8 or lower' in
mixnet_listener.rs (the 'Non-stream message claims v{n}; dropping' path).
The connect handshake goes via plain mixnet, not LP/SphinxStream, so it
must stay v8 even for v9 clients - only data-plane packets use v9.

The earlier commit 70e472b incorrectly tagged the connect request as v9,
which any v9-aware IPR would silently drop, breaking iOS connectivity
against the current gateway fleet.

The v9-accepts-v8 read compat in response_helpers (172171f) remains the
correct fix for the iOS hotfix; that delivers the unblock without
touching the request side.
2026-04-22 17:02:38 +01:00
Tommy Verrall bf12ce2477 Tag connect requests as v9 + clippy unblocker
- nym-ip-packet-client/connect.rs: current::request::IpPacketRequest::new_connect_request
  leaves protocol.version=8 (v9 re-exports v8 types). Use the v9 wrapper so
  the client honestly advertises v9 to the IPR; pairs with the v9-accepts-v8
  reply compat in nym-ip-packet-requests for older gateways.
- nym_offline_compact_ecash/proofs: pre-existing manual_saturating_arithmetic
  lint surfaced after toolchain bump; replace checked_sub().unwrap_or_default()
  with saturating_sub. Behaviorally identical, unblocks workspace clippy
  -D warnings (caught by `cargo clippy --workspace --all-targets`).
2026-04-22 17:02:37 +01:00
Tommy Verrall 9ed80ebf52 Ip-packet-requests - cover v8/v9 IPR version compat
- Replace literal 8/9 with crate::v8::VERSION / crate::v9::VERSION so the
  compat branch tracks future protocol bumps automatically.
- Add tracing::debug! when a v9 client falls back to a v8 reply so the
  fleet rollout is observable.
- Document removal trigger (TODO IPR-v9-rollout) tied to
  crate::v9::MIN_RELEASE_VERSION.
- Unit tests cover: matching versions, v9-accepts-v8 compat, v8-rejects-v9,
  unrelated mismatch, empty payload.
2026-04-22 16:59:23 +01:00
Tommy Verrall 172171f1f2 fix(ip-packet-requests): accept v8 IPR replies from v9 clients
v9 reuses v8 bincode types; strict version check dropped connect responses
from older exit gateways and caused TimeoutWaitingForConnectResponse.
2026-04-22 16:45:41 +01:00
3 changed files with 77 additions and 11 deletions
@@ -3,7 +3,7 @@
use bytes::{Bytes, BytesMut};
use tokio_util::codec::Decoder;
use tracing::{error, info, warn};
use tracing::{debug, error, info, warn};
use crate::{
IpPair,
@@ -37,15 +37,31 @@ pub enum MixnetMessageOutcome {
// nym-ip-packet-client/src/helpers.rs — check_ipr_message_version()
// sdk/rust/nym-sdk/src/ip_packet_client/listener.rs — check_ipr_message_version()
/// Check that the first byte of an IPR message matches the expected protocol version.
///
/// v9 currently reuses the v8 bincode layout (`nym_ip_packet_requests::v9` re-exports v8 types);
/// the version byte signals LP/SphinxStream framing, not a wire-format change. Until exit gateways
/// have rolled past `crate::v9::MIN_RELEASE_VERSION`, a v9 client may still receive v8 replies and
/// must accept them. Revisit this compat branch if a future bump diverges the wire layout.
///
/// TODO(IPR-v9-rollout): remove the v9-accepts-v8 branch once the exit gateway fleet is on
/// `crate::v9::MIN_RELEASE_VERSION` or newer.
pub fn check_ipr_message_version(data: &[u8], expected: u8) -> Result<(), IprResponseError> {
let version = data.first().ok_or(IprResponseError::NoVersionByte)?;
if *version != expected {
return Err(IprResponseError::VersionMismatch {
expected,
received: *version,
});
let version = *data.first().ok_or(IprResponseError::NoVersionByte)?;
if version == expected {
return Ok(());
}
Ok(())
if expected == crate::v9::VERSION && version == crate::v8::VERSION {
debug!(
"accepting v{} IPR reply under v{} client compat",
crate::v8::VERSION,
crate::v9::VERSION
);
return Ok(());
}
Err(IprResponseError::VersionMismatch {
expected,
received: version,
})
}
// Extracted from:
@@ -132,3 +148,52 @@ pub fn handle_ipr_response(data: &[u8]) -> Option<MixnetMessageOutcome> {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn check_version_matches() {
assert!(check_ipr_message_version(&[crate::v9::VERSION], crate::v9::VERSION).is_ok());
assert!(check_ipr_message_version(&[crate::v8::VERSION], crate::v8::VERSION).is_ok());
}
#[test]
fn v9_client_accepts_v8_reply_compat() {
assert!(check_ipr_message_version(&[crate::v8::VERSION], crate::v9::VERSION).is_ok());
}
#[test]
fn v8_client_rejects_v9_reply() {
let err = check_ipr_message_version(&[crate::v9::VERSION], crate::v8::VERSION)
.expect_err("v8 client must not silently accept v9");
assert!(matches!(
err,
IprResponseError::VersionMismatch {
expected: 8,
received: 9
}
));
}
#[test]
fn rejects_unrelated_version_mismatch() {
let err = check_ipr_message_version(&[7], crate::v9::VERSION)
.expect_err("v9 client must reject v7");
assert!(matches!(
err,
IprResponseError::VersionMismatch {
expected: 9,
received: 7
}
));
}
#[test]
fn empty_payload_returns_no_version_byte() {
let err = check_ipr_message_version(&[], crate::v9::VERSION)
.expect_err("empty payload must error");
assert!(matches!(err, IprResponseError::NoVersionByte));
}
}
@@ -31,9 +31,7 @@ where
// instead we could maybe use the `from_bytes` variant and adding some suffix
// when computing the digest until we produce a valid scalar.
let mut bytes = [0u8; 64];
let pad_size = 64usize
.checked_sub(D::OutputSize::to_usize())
.unwrap_or_default();
let pad_size = 64usize.saturating_sub(D::OutputSize::to_usize());
bytes[pad_size..].copy_from_slice(&digest);
+3
View File
@@ -80,6 +80,9 @@ impl IprClientConnect {
}
async fn send_connect_request(&self, ip_packet_router_address: Recipient) -> Result<u64> {
// Connect goes via plain mixnet (non-stream); the IPR enforces "non-stream => v8 or lower"
// (see service-providers/ip-packet-router/src/mixnet_listener.rs ~ "Non-stream message
// claims v9"). Keep this v8-tagged. Only LP-Stream-framed data packets use v9.
let (request, request_id) = IpPacketRequest::new_connect_request(None);
// We use 20 surbs for the connect request because typically the IPR is configured to have