From bcae62d07b77e0c09eb1100e19fab77486eed91d Mon Sep 17 00:00:00 2001 From: bomanaps Date: Mon, 29 Jun 2026 07:54:59 +0100 Subject: [PATCH 1/2] add shadow-compatible quinn-udp fallback --- lean_client/Cargo.lock | 6 - lean_client/Cargo.toml | 3 + lean_client/rust/patch/quinn-udp/Cargo.toml | 21 +++ .../rust/patch/quinn-udp/src/fallback.rs | 126 ++++++++++++++ lean_client/rust/patch/quinn-udp/src/lib.rs | 159 ++++++++++++++++++ lean_client/src/main.rs | 2 +- 6 files changed, 310 insertions(+), 7 deletions(-) create mode 100644 lean_client/rust/patch/quinn-udp/Cargo.toml create mode 100644 lean_client/rust/patch/quinn-udp/src/fallback.rs create mode 100644 lean_client/rust/patch/quinn-udp/src/lib.rs diff --git a/lean_client/Cargo.lock b/lean_client/Cargo.lock index 8f507b9..0f5bd4c 100644 --- a/lean_client/Cargo.lock +++ b/lean_client/Cargo.lock @@ -4683,15 +4683,9 @@ dependencies = [ [[package]] name = "quinn-udp" version = "0.5.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" dependencies = [ - "cfg_aliases", - "libc", - "once_cell", "socket2 0.6.3", "tracing", - "windows-sys 0.52.0", ] [[package]] diff --git a/lean_client/Cargo.toml b/lean_client/Cargo.toml index 5a394ea..9e0f236 100644 --- a/lean_client/Cargo.toml +++ b/lean_client/Cargo.toml @@ -341,3 +341,6 @@ tikv-jemallocator = { workspace = true } [profile.release] lto = true codegen-units = 1 + +[patch.crates-io] +quinn-udp = { path = "rust/patch/quinn-udp" } diff --git a/lean_client/rust/patch/quinn-udp/Cargo.toml b/lean_client/rust/patch/quinn-udp/Cargo.toml new file mode 100644 index 0000000..8b3e1bf --- /dev/null +++ b/lean_client/rust/patch/quinn-udp/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "quinn-udp" +version = "0.5.14" +edition = "2021" +license = "MIT OR Apache-2.0" +description = "UDP sockets for QUIC — Shadow-compatible fallback-only build" + +[features] +default = ["tracing", "log"] +log = ["tracing/log"] +direct-log = ["dep:log"] + +[dependencies] +log = { version = "0.4", optional = true } +tracing = { version = "0.1", optional = true } + +[target.'cfg(not(all(target_family = "wasm", target_os = "unknown")))'.dependencies] +socket2 = { version = "0.6" } + +[lib] +bench = false diff --git a/lean_client/rust/patch/quinn-udp/src/fallback.rs b/lean_client/rust/patch/quinn-udp/src/fallback.rs new file mode 100644 index 0000000..2a8eaf7 --- /dev/null +++ b/lean_client/rust/patch/quinn-udp/src/fallback.rs @@ -0,0 +1,126 @@ +use std::{ + io::{self, IoSliceMut}, + sync::Mutex, + time::Instant, +}; + +use super::{IO_ERROR_LOG_INTERVAL, RecvMeta, Transmit, UdpSockRef, log_sendmsg_error}; + +/// Fallback UDP socket interface that stubs out all special functionality +/// +/// Used when a better implementation is not available for a particular target, at the cost of +/// reduced performance compared to that enabled by some target-specific interfaces. +#[derive(Debug)] +pub struct UdpSocketState { + last_send_error: Mutex, +} + +impl UdpSocketState { + pub fn new(socket: UdpSockRef<'_>) -> io::Result { + socket.0.set_nonblocking(true)?; + let now = Instant::now(); + Ok(Self { + last_send_error: Mutex::new(now.checked_sub(2 * IO_ERROR_LOG_INTERVAL).unwrap_or(now)), + }) + } + + /// Sends a [`Transmit`] on the given socket. + /// + /// This function will only ever return errors of kind [`io::ErrorKind::WouldBlock`]. + /// All other errors will be logged and converted to `Ok`. + /// + /// UDP transmission errors are considered non-fatal because higher-level protocols must + /// employ retransmits and timeouts anyway in order to deal with UDP's unreliable nature. + /// Thus, logging is most likely the only thing you can do with these errors. + /// + /// If you would like to handle these errors yourself, use [`UdpSocketState::try_send`] + /// instead. + pub fn send(&self, socket: UdpSockRef<'_>, transmit: &Transmit<'_>) -> io::Result<()> { + match send(socket, transmit) { + Ok(()) => Ok(()), + Err(e) if e.kind() == io::ErrorKind::WouldBlock => Err(e), + Err(e) => { + log_sendmsg_error(&self.last_send_error, e, transmit); + Ok(()) + } + } + } + + /// Sends a [`Transmit`] on the given socket without any additional error handling. + pub fn try_send(&self, socket: UdpSockRef<'_>, transmit: &Transmit<'_>) -> io::Result<()> { + send(socket, transmit) + } + + pub fn recv( + &self, + socket: UdpSockRef<'_>, + bufs: &mut [IoSliceMut<'_>], + meta: &mut [RecvMeta], + ) -> io::Result { + // Safety: both `IoSliceMut` and `MaybeUninitSlice` promise to have the + // same layout, that of `iovec`/`WSABUF`. Furthermore `recv_vectored` + // promises to not write unitialised bytes to the `bufs` and pass it + // directly to the `recvmsg` system call, so this is safe. + let bufs = unsafe { + &mut *(bufs as *mut [IoSliceMut<'_>] as *mut [socket2::MaybeUninitSlice<'_>]) + }; + let (len, _flags, addr) = socket.0.recv_from_vectored(bufs)?; + meta[0] = RecvMeta { + len, + stride: len, + addr: addr.as_socket().unwrap(), + ecn: None, + dst_ip: None, + }; + Ok(1) + } + + #[inline] + pub fn max_gso_segments(&self) -> usize { + 1 + } + + #[inline] + pub fn gro_segments(&self) -> usize { + 1 + } + + /// Resize the send buffer of `socket` to `bytes` + #[inline] + pub fn set_send_buffer_size(&self, socket: UdpSockRef<'_>, bytes: usize) -> io::Result<()> { + socket.0.set_send_buffer_size(bytes) + } + + /// Resize the receive buffer of `socket` to `bytes` + #[inline] + pub fn set_recv_buffer_size(&self, socket: UdpSockRef<'_>, bytes: usize) -> io::Result<()> { + socket.0.set_recv_buffer_size(bytes) + } + + /// Get the size of the `socket` send buffer + #[inline] + pub fn send_buffer_size(&self, socket: UdpSockRef<'_>) -> io::Result { + socket.0.send_buffer_size() + } + + /// Get the size of the `socket` receive buffer + #[inline] + pub fn recv_buffer_size(&self, socket: UdpSockRef<'_>) -> io::Result { + socket.0.recv_buffer_size() + } + + #[inline] + pub fn may_fragment(&self) -> bool { + true + } +} + +fn send(socket: UdpSockRef<'_>, transmit: &Transmit<'_>) -> io::Result<()> { + socket.0.send_to( + transmit.contents, + &socket2::SockAddr::from(transmit.destination), + )?; + Ok(()) +} + +pub(crate) const BATCH_SIZE: usize = 1; diff --git a/lean_client/rust/patch/quinn-udp/src/lib.rs b/lean_client/rust/patch/quinn-udp/src/lib.rs new file mode 100644 index 0000000..c166aef --- /dev/null +++ b/lean_client/rust/patch/quinn-udp/src/lib.rs @@ -0,0 +1,159 @@ +//! quinn-udp — Shadow-compatible fallback-only build +//! +//! This is a vendored replacement for quinn-udp 0.5.14 that always uses the +//! simple fallback UDP path (send_to/recv_from_vectored) instead of the +//! Linux-optimized unix.rs path. This is required for Shadow network simulator +//! compatibility, since Shadow does not fully emulate sendmsg/recvmsg cmsg, +//! GRO/GSO, or ECN socket options. +//! +//! See: https://shadow.github.io/ +#![warn(unreachable_pub)] +#![warn(clippy::use_self)] + +use std::net::{IpAddr, Ipv6Addr, SocketAddr}; +#[cfg(unix)] +use std::os::unix::io::AsFd; +#[cfg(windows)] +use std::os::windows::io::AsSocket; +use std::{ + sync::Mutex, + time::{Duration, Instant}, +}; + +// Always use fallback — no unix.rs, no windows.rs, no cmsg +#[path = "fallback.rs"] +mod imp; + +#[allow(unused_imports, unused_macros)] +mod log { + #[cfg(all(feature = "direct-log", not(feature = "tracing")))] + pub(crate) use log::{debug, error, info, trace, warn}; + + #[cfg(feature = "tracing")] + pub(crate) use tracing::{debug, error, info, trace, warn}; + + #[cfg(not(any(feature = "direct-log", feature = "tracing")))] + mod no_op { + macro_rules! trace ( ($($tt:tt)*) => {{}} ); + macro_rules! debug ( ($($tt:tt)*) => {{}} ); + macro_rules! info ( ($($tt:tt)*) => {{}} ); + macro_rules! log_warn ( ($($tt:tt)*) => {{}} ); + macro_rules! error ( ($($tt:tt)*) => {{}} ); + + pub(crate) use {debug, error, info, log_warn as warn, trace}; + } + + #[cfg(not(any(feature = "direct-log", feature = "tracing")))] + pub(crate) use no_op::*; +} + +pub use imp::UdpSocketState; + +/// Number of UDP packets to send/receive at a time +pub const BATCH_SIZE: usize = imp::BATCH_SIZE; + +/// Metadata for a single buffer filled with bytes received from the network +#[derive(Debug, Copy, Clone)] +pub struct RecvMeta { + pub addr: SocketAddr, + pub len: usize, + pub stride: usize, + pub ecn: Option, + pub dst_ip: Option, +} + +impl Default for RecvMeta { + fn default() -> Self { + Self { + addr: SocketAddr::new(Ipv6Addr::UNSPECIFIED.into(), 0), + len: 0, + stride: 0, + ecn: None, + dst_ip: None, + } + } +} + +/// An outgoing packet +#[derive(Debug, Clone)] +pub struct Transmit<'a> { + pub destination: SocketAddr, + pub ecn: Option, + pub contents: &'a [u8], + pub segment_size: Option, + pub src_ip: Option, +} + +/// Log at most 1 IO error per minute +const IO_ERROR_LOG_INTERVAL: Duration = std::time::Duration::from_secs(60); + +#[cfg(any(feature = "tracing", feature = "direct-log"))] +fn log_sendmsg_error( + last_send_error: &Mutex, + err: impl core::fmt::Debug, + transmit: &Transmit, +) { + let now = Instant::now(); + let last_send_error = &mut *last_send_error.lock().expect("poisoned lock"); + if now.saturating_duration_since(*last_send_error) > IO_ERROR_LOG_INTERVAL { + *last_send_error = now; + log::warn!( + "sendmsg error: {:?}, Transmit: {{ destination: {:?}, src_ip: {:?}, ecn: {:?}, len: {:?}, segment_size: {:?} }}", + err, + transmit.destination, + transmit.src_ip, + transmit.ecn, + transmit.contents.len(), + transmit.segment_size + ); + } +} + +#[cfg(not(any(feature = "tracing", feature = "direct-log")))] +fn log_sendmsg_error(_: &Mutex, _: impl core::fmt::Debug, _: &Transmit) {} + +/// A borrowed UDP socket +pub struct UdpSockRef<'a>(socket2::SockRef<'a>); + +#[cfg(unix)] +impl<'s, S> From<&'s S> for UdpSockRef<'s> +where + S: AsFd, +{ + fn from(socket: &'s S) -> Self { + Self(socket.into()) + } +} + +#[cfg(windows)] +impl<'s, S> From<&'s S> for UdpSockRef<'s> +where + S: AsSocket, +{ + fn from(socket: &'s S) -> Self { + Self(socket.into()) + } +} + +/// Explicit congestion notification codepoint +#[repr(u8)] +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum EcnCodepoint { + Ect0 = 0b10, + Ect1 = 0b01, + Ce = 0b11, +} + +impl EcnCodepoint { + pub fn from_bits(x: u8) -> Option { + use EcnCodepoint::*; + Some(match x & 0b11 { + 0b10 => Ect0, + 0b01 => Ect1, + 0b11 => Ce, + _ => { + return None; + } + }) + } +} diff --git a/lean_client/src/main.rs b/lean_client/src/main.rs index 882c2be..b8e9443 100644 --- a/lean_client/src/main.rs +++ b/lean_client/src/main.rs @@ -438,7 +438,7 @@ async fn main() -> Result<()> { info!( "Starting grandine v{} ({})", env!("CARGO_PKG_VERSION"), - git_version::git_version!(args = ["--always", "--abbrev=8"]), + git_version::git_version!(args = ["--always", "--abbrev=8"], fallback = "unknown"), ); let args = Args::parse(); From 1cdbb385cfb4bb7e8aa02b5d06fd6591148058be Mon Sep 17 00:00:00 2001 From: bomanaps Date: Fri, 3 Jul 2026 18:12:41 +0100 Subject: [PATCH 2/2] add shadow-integration feature with fake-XMSS sim-cost --- lean_client/Cargo.lock | 8 +- lean_client/Cargo.toml | 7 +- lean_client/Makefile | 33 ++++++ lean_client/networking/src/network/service.rs | 3 +- lean_client/src/main.rs | 53 +++++++++- lean_client/xmss/Cargo.toml | 3 + lean_client/xmss/src/aggregated_signature.rs | 32 ++++++ lean_client/xmss/src/lib.rs | 3 + lean_client/xmss/src/multi_message.rs | 32 ++++++ lean_client/xmss/src/shadow_cost.rs | 100 ++++++++++++++++++ 10 files changed, 267 insertions(+), 7 deletions(-) create mode 100644 lean_client/xmss/src/shadow_cost.rs diff --git a/lean_client/Cargo.lock b/lean_client/Cargo.lock index 0f5bd4c..276680b 100644 --- a/lean_client/Cargo.lock +++ b/lean_client/Cargo.lock @@ -4683,9 +4683,15 @@ dependencies = [ [[package]] name = "quinn-udp" version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" dependencies = [ - "socket2 0.6.3", + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.5.10", "tracing", + "windows-sys 0.52.0", ] [[package]] diff --git a/lean_client/Cargo.toml b/lean_client/Cargo.toml index 9e0f236..a7021c7 100644 --- a/lean_client/Cargo.toml +++ b/lean_client/Cargo.toml @@ -311,6 +311,9 @@ name = "lean_client" version = "0.1.0" edition = { workspace = true } +[features] +shadow-integration = ["xmss/shadow-integration"] + [dependencies] anyhow = { workspace = true } bls = { workspace = true } @@ -342,5 +345,5 @@ tikv-jemallocator = { workspace = true } lto = true codegen-units = 1 -[patch.crates-io] -quinn-udp = { path = "rust/patch/quinn-udp" } +[profile.shadow] +inherits = "release" diff --git a/lean_client/Makefile b/lean_client/Makefile index e1dcb34..29a02c7 100644 --- a/lean_client/Makefile +++ b/lean_client/Makefile @@ -126,6 +126,39 @@ docker-local: ./target/x86_64-unknown-linux-gnu/release/lean_client $(DOCKER_TAGS) \ . +### Shadow-simulator support +# `rust/patch/quinn-udp` is a vendored fallback-only build of quinn-udp so QUIC +# runs under the Shadow simulator (which does not emulate sendmsg cmsg / GRO / +# GSO / ECN). Applied only for the shadow-* targets via cargo's --config +# override, so prod / devnet builds see the crates.io version. +SHADOW_CONFIG := --config 'patch.crates-io."quinn-udp".path="rust/patch/quinn-udp"' +SHADOW_FEATURES := --features shadow-integration + +.PHONY: shadow-build +shadow-build: + cargo build --profile shadow $(SHADOW_FEATURES) $(SHADOW_CONFIG) + +.PHONY: shadow-x86_64-unknown-linux-gnu +shadow-x86_64-unknown-linux-gnu: + CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS="-C target-cpu=x86-64-v3" \ + LEAN_REPO_ROOT=$(LEAN_REPO_ROOT) cross build --bin lean_client --target x86_64-unknown-linux-gnu --profile shadow $(SHADOW_FEATURES) $(SHADOW_CONFIG) + +.PHONY: shadow-aarch64-unknown-linux-gnu +shadow-aarch64-unknown-linux-gnu: + LEAN_REPO_ROOT=$(LEAN_REPO_ROOT) cross build --bin lean_client --target aarch64-unknown-linux-gnu --profile shadow $(SHADOW_FEATURES) $(SHADOW_CONFIG) + +.PHONY: shadow-docker-local +shadow-docker-local: shadow-x86_64-unknown-linux-gnu + @mkdir -p ./bin/amd64 + @cp ./target/x86_64-unknown-linux-gnu/shadow/lean_client ./bin/amd64/lean_client + docker build \ + --file Dockerfile \ + --build-arg COMMIT_SHA=$(COMMIT_SHA) \ + --build-arg BUILD_DATE=$(BUILD_DATE) \ + --build-arg GIT_BRANCH=$(GIT_BRANCH) \ + $(DOCKER_TAGS) \ + . + .PHONY: help help: @awk '/^## / { \ diff --git a/lean_client/networking/src/network/service.rs b/lean_client/networking/src/network/service.rs index a0a188d..90e69ed 100644 --- a/lean_client/networking/src/network/service.rs +++ b/lean_client/networking/src/network/service.rs @@ -636,9 +636,8 @@ where METRICS .get() .map(|m| m.lean_gossip_block_size_bytes.observe(data_len as f64)); - info!(block_root = %signed_block.block.hash_tree_root(), "received block via gossip"); - let slot = signed_block.block.slot.0; + info!(slot, block_root = %signed_block.block.hash_tree_root(), "received block via gossip"); if let Err(err) = self .chain_message_sink diff --git a/lean_client/src/main.rs b/lean_client/src/main.rs index b8e9443..456de74 100644 --- a/lean_client/src/main.rs +++ b/lean_client/src/main.rs @@ -1,4 +1,5 @@ #[cfg(not(target_env = "msvc"))] +#[cfg(not(feature = "shadow-integration"))] #[global_allocator] static ALLOC: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; @@ -418,9 +419,37 @@ struct Args { #[arg(long)] checkpoint_sync_url: Option, + + #[cfg(feature = "shadow-integration")] + #[command(flatten)] + shadow: ShadowOptions, } -#[tokio::main] +#[cfg(feature = "shadow-integration")] +#[derive(clap::Args, Debug)] +struct ShadowOptions { + #[arg(long, default_value_t = false)] + shadow_xmss_fake: bool, + + #[arg(long)] + shadow_xmss_aggregate_signatures_rate: Option, + + #[arg(long)] + shadow_xmss_verify_aggregated_signatures_rate: Option, + + #[arg(long)] + shadow_xmss_merge_rate: Option, + + #[arg( + long, + default_value_t = xmss::shadow_cost::DEFAULT_FAKE_PROOF_SIZE as u64, + value_parser = clap::value_parser!(u64).range(1..=524_288) + )] + shadow_xmss_fake_proof_size: u64, +} + +#[cfg_attr(feature = "shadow-integration", tokio::main(flavor = "current_thread"))] +#[cfg_attr(not(feature = "shadow-integration"), tokio::main)] async fn main() -> Result<()> { let rayon_threads = num_cpus::get().saturating_sub(3).max(1); xmss::configure_rayon_pool(rayon_threads); @@ -438,11 +467,31 @@ async fn main() -> Result<()> { info!( "Starting grandine v{} ({})", env!("CARGO_PKG_VERSION"), - git_version::git_version!(args = ["--always", "--abbrev=8"], fallback = "unknown"), + git_version::git_version!(args = ["--always", "--abbrev=8"]), ); let args = Args::parse(); + #[cfg(feature = "shadow-integration")] + { + let s = &args.shadow; + info!( + fake = s.shadow_xmss_fake, + aggregate_rate = ?s.shadow_xmss_aggregate_signatures_rate, + verify_rate = ?s.shadow_xmss_verify_aggregated_signatures_rate, + merge_rate = ?s.shadow_xmss_merge_rate, + fake_proof_size = s.shadow_xmss_fake_proof_size, + "Applying Shadow XMSS sim-cost / fake-XMSS config" + ); + xmss::shadow_cost::init( + s.shadow_xmss_fake, + s.shadow_xmss_aggregate_signatures_rate, + s.shadow_xmss_verify_aggregated_signatures_rate, + s.shadow_xmss_merge_rate, + s.shadow_xmss_fake_proof_size as usize, + ); + } + for feature in args.features { feature.enable(); } diff --git a/lean_client/xmss/Cargo.toml b/lean_client/xmss/Cargo.toml index 4c941a4..e3527db 100644 --- a/lean_client/xmss/Cargo.toml +++ b/lean_client/xmss/Cargo.toml @@ -24,5 +24,8 @@ typenum = { workspace = true } serde = { workspace = true } zeroize = { workspace = true, features = ["derive"] } +[features] +shadow-integration = [] + [dev-dependencies] rand_chacha = { workspace = true } \ No newline at end of file diff --git a/lean_client/xmss/src/aggregated_signature.rs b/lean_client/xmss/src/aggregated_signature.rs index 4c9fc35..d0bd6ef 100644 --- a/lean_client/xmss/src/aggregated_signature.rs +++ b/lean_client/xmss/src/aggregated_signature.rs @@ -130,6 +130,24 @@ impl AggregatedSignature { let sig_count = public_keys.len(); + #[cfg(feature = "shadow-integration")] + if crate::shadow_cost::fake_xmss() { + stop_and_discard(timer); + let slot_bytes = slot.to_le_bytes(); + let count_bytes = sig_count.to_le_bytes(); + let mut parts: Vec<&[u8]> = Vec::with_capacity(2 + children.len() + 1); + parts.push(message.as_bytes()); + parts.push(&slot_bytes); + for (_, child) in children { + parts.push(child.0.as_bytes()); + } + parts.push(&count_bytes); + let bytes = + crate::shadow_cost::fill_fake_proof(crate::shadow_cost::fake_proof_size(), &parts); + crate::shadow_cost::sleep(crate::shadow_cost::aggregate_delay(sig_count)); + return Self::new(&bytes); + } + let raw_xmss = public_keys .into_iter() .zip(signatures) @@ -169,6 +187,15 @@ impl AggregatedSignature { ) -> Result<()> { setup_aggregation(); + #[cfg(feature = "shadow-integration")] + if crate::shadow_cost::fake_xmss() { + let n = public_keys.into_iter().count(); + crate::shadow_cost::sleep(crate::shadow_cost::verify_delay(n)); + let _ = message; + let _ = slot; + return Ok(()); + } + let _timer = METRICS.get().map(|metrics| { metrics .lean_pq_sig_aggregated_signatures_verification_time_seconds @@ -224,6 +251,11 @@ impl AggregatedSignature { pub fn is_empty(&self) -> bool { self.0.as_bytes().is_empty() } + + #[cfg(feature = "shadow-integration")] + pub(crate) fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } } fn sorted_dedup_lean_pubkeys(pks: &[PublicKey]) -> Vec { diff --git a/lean_client/xmss/src/lib.rs b/lean_client/xmss/src/lib.rs index 4aab323..e2afd82 100644 --- a/lean_client/xmss/src/lib.rs +++ b/lean_client/xmss/src/lib.rs @@ -4,6 +4,9 @@ mod public_key; mod secret_key; mod signature; +#[cfg(feature = "shadow-integration")] +pub mod shadow_cost; + pub use aggregated_signature::{AggregatedSignature, configure_rayon_pool, setup_aggregation}; pub use multi_message::MultiMessageAggregate; pub use public_key::PublicKey; diff --git a/lean_client/xmss/src/multi_message.rs b/lean_client/xmss/src/multi_message.rs index c67940e..359ccdb 100644 --- a/lean_client/xmss/src/multi_message.rs +++ b/lean_client/xmss/src/multi_message.rs @@ -38,6 +38,22 @@ impl MultiMessageAggregate { bail!("multi-message aggregate requires at least one Type-1 component"); } + #[cfg(feature = "shadow-integration")] + if crate::shadow_cost::fake_xmss() { + let merge_n = parts.len(); + let count_bytes = merge_n.to_le_bytes(); + let mut seed: Vec<&[u8]> = Vec::with_capacity(parts.len() + 1); + for (sig, _) in parts { + seed.push(sig.as_bytes()); + } + seed.push(&count_bytes); + let bytes = + crate::shadow_cost::fill_fake_proof(crate::shadow_cost::fake_proof_size(), &seed); + crate::shadow_cost::sleep(crate::shadow_cost::merge_delay(merge_n)); + let _ = log_inv_rate; + return Self::new(&bytes); + } + let parts_lean = parts .iter() .map(|(sig, pks)| { @@ -68,6 +84,11 @@ impl MultiMessageAggregate { ); } + #[cfg(feature = "shadow-integration")] + if crate::shadow_cost::fake_xmss() { + return Ok(()); + } + let pubkeys_per_info = sorted_dedup_lean_pubkeys(pubkeys_per_message); let sig = MultiMessageAggregateSignature::decompress_without_pubkeys( @@ -105,6 +126,17 @@ impl MultiMessageAggregate { ) -> Result { setup_aggregation(); + #[cfg(feature = "shadow-integration")] + if crate::shadow_cost::fake_xmss() { + let bytes = crate::shadow_cost::fill_fake_proof( + crate::shadow_cost::fake_proof_size(), + &[self.proof.as_bytes(), message.as_bytes()], + ); + let _ = pubkeys_per_message; + let _ = log_inv_rate; + return AggregatedSignature::new(&bytes); + } + let pubkeys_per_info = sorted_dedup_lean_pubkeys(pubkeys_per_message); let sig = MultiMessageAggregateSignature::decompress_without_pubkeys( self.proof.as_bytes(), diff --git a/lean_client/xmss/src/shadow_cost.rs b/lean_client/xmss/src/shadow_cost.rs new file mode 100644 index 0000000..7d36daf --- /dev/null +++ b/lean_client/xmss/src/shadow_cost.rs @@ -0,0 +1,100 @@ +//! Shadow-simulator sim-cost + fake-proof backend. Compiled only under the +//! `shadow-integration` feature. + +use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering}; +use std::time::Duration; + +pub const DEFAULT_FAKE_PROOF_SIZE: usize = 32 * 1024; + +static FAKE_ENABLED: AtomicBool = AtomicBool::new(false); +static AGG_RATE: AtomicU64 = AtomicU64::new(0); +static VERIFY_RATE: AtomicU64 = AtomicU64::new(0); +static MERGE_RATE: AtomicU64 = AtomicU64::new(0); +static FAKE_PROOF_SIZE: AtomicUsize = AtomicUsize::new(DEFAULT_FAKE_PROOF_SIZE); + +fn rate_bits(v: Option) -> u64 { + match v { + Some(v) if v.is_finite() && v > 0.0 => v.to_bits(), + _ => 0, + } +} + +pub fn init( + fake: bool, + agg: Option, + verify: Option, + merge: Option, + proof_size: usize, +) { + FAKE_ENABLED.store(fake, Ordering::Relaxed); + AGG_RATE.store(rate_bits(agg), Ordering::Relaxed); + VERIFY_RATE.store(rate_bits(verify), Ordering::Relaxed); + MERGE_RATE.store(rate_bits(merge), Ordering::Relaxed); + FAKE_PROOF_SIZE.store(proof_size, Ordering::Relaxed); +} + +pub fn fake_xmss() -> bool { + FAKE_ENABLED.load(Ordering::Relaxed) +} + +pub fn fake_proof_size() -> usize { + FAKE_PROOF_SIZE.load(Ordering::Relaxed) +} + +fn compute_delay(rate: &AtomicU64, n: usize) -> Duration { + let r = f64::from_bits(rate.load(Ordering::Relaxed)); + if r <= 0.0 || n == 0 { + return Duration::ZERO; + } + let ns = (n as f64 / r) * 1e9; + if !ns.is_finite() || ns <= 0.0 { + return Duration::ZERO; + } + Duration::from_nanos(ns.min(u64::MAX as f64) as u64) +} + +pub fn aggregate_delay(n: usize) -> Duration { + compute_delay(&AGG_RATE, n) +} + +pub fn verify_delay(n: usize) -> Duration { + compute_delay(&VERIFY_RATE, n) +} + +pub fn merge_delay(n: usize) -> Duration { + compute_delay(&MERGE_RATE, n) +} + +pub fn sleep(delay: Duration) { + if !delay.is_zero() { + std::thread::sleep(delay); + } +} + +pub fn fill_fake_proof(len: usize, seed_parts: &[&[u8]]) -> Vec { + const FNV_OFFSET_BASIS: u64 = 0xcbf29ce484222325; + const FNV_PRIME: u64 = 0x100000001b3; + + let mut state = FNV_OFFSET_BASIS; + for part in seed_parts { + for &byte in *part { + state ^= u64::from(byte); + state = state.wrapping_mul(FNV_PRIME); + } + } + + let mut bytes = Vec::with_capacity(len); + while bytes.len() < len { + let z = state.wrapping_add(0x9E3779B97F4A7C15); + state = z; + let z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9); + let z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB); + let z = z ^ (z >> 31); + + let chunk = z.to_le_bytes(); + let remaining = len - bytes.len(); + bytes.extend_from_slice(&chunk[..remaining.min(chunk.len())]); + } + + bytes +}