diff --git a/Cargo.lock b/Cargo.lock index fa8c8c8..bed9868 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1087,6 +1087,7 @@ dependencies = [ "hex", "hkdf", "libc", + "libm", "mdns-sd", "nostr", "nostr-sdk", diff --git a/Cargo.toml b/Cargo.toml index 6c38f75..29a3ed0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ secp256k1 = { version = "0.30", features = ["rand", "global-context"] } sha2 = "0.10" hkdf = "0.12" ring = "0.17" +libm = "0.2" rand = "0.10.1" crossbeam-channel = "0.5" thiserror = "2.0" diff --git a/src/proto/bloom/core.rs b/src/proto/bloom/core.rs index 6ef9600..e7be1d8 100644 --- a/src/proto/bloom/core.rs +++ b/src/proto/bloom/core.rs @@ -141,7 +141,7 @@ impl BloomFilter { /// Current false-positive rate: fill ratio raised to the hash count. pub fn fpr(&self) -> f64 { - self.fill_ratio().powi(self.hash_count as i32) + crate::proto::math::powi(self.fill_ratio(), self.hash_count as u32) } /// Estimate the number of elements in the filter. @@ -170,7 +170,7 @@ impl BloomFilter { return None; } - Some(-(m / k) * (1.0 - fill).ln()) + Some(-(m / k) * libm::log(1.0 - fill)) } /// Check if the filter is empty. diff --git a/src/proto/bloom/tests/state.rs b/src/proto/bloom/tests/state.rs index 6c74746..159b276 100644 --- a/src/proto/bloom/tests/state.rs +++ b/src/proto/bloom/tests/state.rs @@ -1,6 +1,6 @@ //! Tests for `BloomState` (announcement state management). -use std::collections::BTreeMap; +use alloc::collections::BTreeMap; use crate::proto::bloom::{BloomFilter, BloomState}; use crate::testutil::make_node_addr; diff --git a/src/proto/fmp/core.rs b/src/proto/fmp/core.rs index 0ae419c..dd7af64 100644 --- a/src/proto/fmp/core.rs +++ b/src/proto/fmp/core.rs @@ -640,5 +640,5 @@ impl Fmp { /// arithmetic (the exponent is the resend count *after* this attempt). fn next_resend_at_ms(now_ms: u64, interval_ms: u64, backoff: f64, prior_count: u32) -> u64 { let count = prior_count + 1; - now_ms + (interval_ms as f64 * backoff.powi(count as i32)) as u64 + now_ms + (interval_ms as f64 * crate::proto::math::powi(backoff, count)) as u64 } diff --git a/src/proto/fmp/wire.rs b/src/proto/fmp/wire.rs index b9d8b6c..715b1f4 100644 --- a/src/proto/fmp/wire.rs +++ b/src/proto/fmp/wire.rs @@ -9,7 +9,7 @@ use crate::proto::Error; use crate::proto::codec::Reader; use crate::proto::link::LinkMessageType; -use std::fmt; +use ::core::fmt; /// Handshake message type identifiers. /// diff --git a/src/proto/fsp/tests/core.rs b/src/proto/fsp/tests/core.rs index 5c41b62..efe1c68 100644 --- a/src/proto/fsp/tests/core.rs +++ b/src/proto/fsp/tests/core.rs @@ -9,7 +9,7 @@ use crate::proto::fsp::core::{ use crate::proto::fsp::limits::FSP_CUTOVER_DELAY_MS; use crate::proto::stp::TreeCoordinate; use crate::testutil::make_node_addr; -use std::collections::VecDeque; +use alloc::collections::VecDeque; fn coords(byte: u8) -> TreeCoordinate { TreeCoordinate::from_addrs(vec![make_node_addr(byte)]).unwrap() diff --git a/src/proto/fsp/wire.rs b/src/proto/fsp/wire.rs index ce0e933..6c1db97 100644 --- a/src/proto/fsp/wire.rs +++ b/src/proto/fsp/wire.rs @@ -35,7 +35,7 @@ use crate::proto::Error; use crate::proto::codec::{Reader, Writer}; use crate::proto::stp::{TreeCoordinate, decode_coords, decode_optional_coords, encode_coords}; -use std::fmt; +use ::core::fmt; // ============================================================================ // Constants diff --git a/src/proto/link.rs b/src/proto/link.rs index b04d02a..01dca20 100644 --- a/src/proto/link.rs +++ b/src/proto/link.rs @@ -2,7 +2,7 @@ use crate::NodeAddr; use crate::proto::Error; -use std::fmt; +use core::fmt; // ============================================================================ // Link-Layer Message Types diff --git a/src/proto/math.rs b/src/proto/math.rs new file mode 100644 index 0000000..3b7c1dd --- /dev/null +++ b/src/proto/math.rs @@ -0,0 +1,55 @@ +//! no_std-shaped numeric helpers for the protocol cores. +//! +//! `f64::powi` and the transcendental methods live in `std`, not `core`. The +//! helpers here keep the small amount of protocol math the codecs need in a +//! `core`-only shape so the cores stay portable, without changing any result. + +/// Raise `base` to a non-negative integer power via square-and-multiply. +/// +/// Bit-identical to `f64::powi` for the exponents the codecs use: it mirrors the +/// compiler-rt `__powidf2` multiply order (multiply-then-square, skipping the +/// final square), so — floating-point multiplication being non-associative — it +/// reproduces `powi` exactly rather than a naive left-to-right product. The +/// `powi_bit_identical_to_std` test pins this. Keeping it bit-identical matters: +/// the bloom false-positive-rate feeds a reject comparison and the FMP backoff +/// feeds a `u64` timer, so any drift could change a decision. +pub(crate) fn powi(base: f64, exp: u32) -> f64 { + let mut result = 1.0_f64; + let mut b = base; + let mut e = exp; + loop { + if e & 1 == 1 { + result *= b; + } + e >>= 1; + if e == 0 { + break; + } + b *= b; + } + result +} + +#[cfg(test)] +mod tests { + use super::powi; + + #[test] + fn powi_bit_identical_to_std() { + // The core square-and-multiply must match f64::powi bit-for-bit across + // representative bases and every exponent the protocol math reaches, so + // the FPR reject decision and the backoff timer are unchanged. + let bases = [ + 0.0, 1.0, 0.5, 0.5469, 0.5493, 0.5508, 0.9999, 1.5, 2.0, 3.7, 0.1, 1e-3, + ]; + for &base in &bases { + for exp in 0u32..=64 { + assert_eq!( + powi(base, exp).to_bits(), + base.powi(exp as i32).to_bits(), + "powi mismatch at base={base}, exp={exp}" + ); + } + } + } +} diff --git a/src/proto/mod.rs b/src/proto/mod.rs index b48e7b7..ac08f03 100644 --- a/src/proto/mod.rs +++ b/src/proto/mod.rs @@ -13,6 +13,7 @@ pub(crate) mod discovery; pub(crate) mod fmp; pub(crate) mod fsp; pub(crate) mod link; +pub(crate) mod math; pub(crate) mod mmp; pub(crate) mod rate_limit; pub(crate) mod routing; diff --git a/src/proto/stp/tests/limits.rs b/src/proto/stp/tests/limits.rs index 078fd18..387e5dc 100644 --- a/src/proto/stp/tests/limits.rs +++ b/src/proto/stp/tests/limits.rs @@ -1,6 +1,6 @@ //! Flap dampening / hold-down unit tests. -use std::collections::{BTreeMap, BTreeSet}; +use alloc::collections::{BTreeMap, BTreeSet}; use super::util::{make_coords, make_costs, make_node_addr}; use crate::proto::stp::{ParentDeclaration, ParentEval, TreeState}; diff --git a/src/proto/stp/tests/state.rs b/src/proto/stp/tests/state.rs index 70bcaa9..60e0777 100644 --- a/src/proto/stp/tests/state.rs +++ b/src/proto/stp/tests/state.rs @@ -1,6 +1,6 @@ //! ParentDeclaration, TreeState, parent-selection, and find_next_hop unit tests. -use std::collections::{BTreeMap, BTreeSet}; +use alloc::collections::{BTreeMap, BTreeSet}; use super::util::{add_peer, make_coords, make_costs, make_node_addr, make_tree_state}; use crate::proto::stp::{ParentDeclaration, ParentEval, TreeCoordinate, TreeState}; diff --git a/src/proto/stp/tests/util.rs b/src/proto/stp/tests/util.rs index 58ee826..b6b5571 100644 --- a/src/proto/stp/tests/util.rs +++ b/src/proto/stp/tests/util.rs @@ -1,6 +1,6 @@ //! Shared test helpers for the STP primitive unit tests. -use std::collections::BTreeMap; +use alloc::collections::BTreeMap; use crate::NodeAddr; use crate::proto::stp::{ParentDeclaration, TreeCoordinate, TreeState};