From 18e53b9bddf8426c6de746880bde169a278f9945 Mon Sep 17 00:00:00 2001 From: "K. Isom" Date: Wed, 21 Oct 2026 22:39:09 -0700 Subject: [PATCH] refactor(core): make local constants crate-private MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Decision K (spec §5): the local capacities and timings are defaults that will become configuration, so they are `pub(crate)`. The wire constants stay public. - MAX_PEERS, SUMMARY_INTERVAL_MS, REASSEMBLY_TIMEOUT_MS, SEND_QUEUE_CAP, MIN_MTU, EVENT_QUEUE_CAP, DROP_TABLE_CAP, BCAST_SENT_CAP, ACK_BUFFER_CAP and PEER_IDLE_MS are now `pub(crate)`; a compile_fail doctest pins it. - MIN_MTU's doc claimed it was the floor below which a fragment has no room. It is only the frag sweep's lower bound (BLE ATT's 23). The real floors are now stated on `Transport::mtu` and pinned by `consts::tests::mtu_floors`: 85 to carry every valid observation, 66 to fragment at all, 35 for an Ack, 26 for a summary. - tines-bt caps live connections with its own MAX_PEERS (8). - tines-sim's props mirror MAX_PEERS/SEND_QUEUE_CAP, checked against core through the hidden `conformance` module; the fuzz twin reads it there. - node.rs: drain_events' rustdoc no longer links the now-private consts. Co-Authored-By: Claude Opus 5.5 --- tines-bt/src/transport.rs | 8 ++- tines-core/src/consts.rs | 100 +++++++++++++++++++++++++++++----- tines-core/src/node.rs | 4 +- tines-core/src/transport.rs | 11 ++++ tines-core/tests/fuzz_twin.rs | 3 +- tines-sim/tests/props.rs | 26 +++++++-- 6 files changed, 129 insertions(+), 23 deletions(-) diff --git a/tines-bt/src/transport.rs b/tines-bt/src/transport.rs index 718db742..1e0042dd 100644 --- a/tines-bt/src/transport.rs +++ b/tines-bt/src/transport.rs @@ -53,7 +53,7 @@ use bluer::{ Adapter, Address, AddressType, Session, Uuid, UuidExt, }; -use tines_core::consts::{MAX_FRAME, MAX_PEERS}; +use tines_core::consts::MAX_FRAME; use tines_core::frame_sender; use tines_core::observation::NodeId; use tines_core::transport::{Event, FrameBuf, SendError, Transport}; @@ -230,6 +230,12 @@ impl Transport for BtTransport { /// Smallest MTU we are willing to report (ATT's default minimum is 23). const MIN_MTU: usize = 23; +/// Most live connections at once. Kept equal to tines-core's peer-table +/// capacity (8, a crate-private local constant since tines-core 1.0, decision +/// K): a connection beyond it would carry frames core's exchange has no slot +/// for. Both become configuration in a later release. +const MAX_PEERS: usize = 8; + /// L2CAP PSM the tines byte pipe runs on. Shared with the peer (agreed /// out-of-band): both sides connect/listen on this PSM. const TINES_L2CAP_PSM: u16 = 0x008D; diff --git a/tines-core/src/consts.rs b/tines-core/src/consts.rs index 43caf6c2..ba58cb85 100644 --- a/tines-core/src/consts.rs +++ b/tines-core/src/consts.rs @@ -16,7 +16,7 @@ /// WIRE. The first byte of every frame on the wire; changing it is a /// `WIRE_VERSION` bump. `crate::wire::Frame::decode` rejects a leading byte -/// that differs (`WireError::VersionMismatch`), and [`crate::wire::frame_sender`] +/// that differs (`WireError::VersionMismatch`), and [`crate::frame_sender`] /// emits this. `4` is the first value in the `4..=0x7F` range that fits a /// single-byte postcard enum tag and sits above the 0.x tag values (`0..=3`). pub const WIRE_VERSION: u8 = 4; @@ -51,40 +51,112 @@ pub const ACK_IDS_CAP: usize = 90; // --------------------------------------------------------------------------- /// LOCAL. Node-local peer table; never sent on the wire. -pub const MAX_PEERS: usize = 8; +/// +/// Local constants are crate-private (decision K): +/// +/// ```compile_fail +/// let _ = tines_core::consts::MAX_PEERS; +/// ``` +pub(crate) const MAX_PEERS: usize = 8; /// LOCAL. Emission cadence; local scheduling only. -pub const SUMMARY_INTERVAL_MS: u64 = 2000; +pub(crate) const SUMMARY_INTERVAL_MS: u64 = 2000; /// LOCAL. Inbound reassembly timer; local only. -pub const REASSEMBLY_TIMEOUT_MS: u64 = 5000; +pub(crate) const REASSEMBLY_TIMEOUT_MS: u64 = 5000; /// LOCAL. Outbound delivery buffer size; local only. -pub const SEND_QUEUE_CAP: usize = 256; +pub(crate) const SEND_QUEUE_CAP: usize = 256; -/// LOCAL. BLE floor / MTU-sweep lower bound: below this a fragment header -/// leaves no room for payload. Used by the frag sweep -/// (`crate::frag`) and the BLE transport clamp; never carried on the wire. -pub const MIN_MTU: usize = 23; +/// LOCAL. Lower bound of the fragmentation MTU sweep (`crate::frag`): the +/// BLE ATT default MTU. It is **not** the smallest MTU core can use: that is +/// 85 bytes (see [`crate::Transport::mtu`] for every floor, pinned by +/// `tests::mtu_floors`). Between 23 and 85 core still never emits an +/// oversized frame; it just can't send everything. Only tests and the hidden +/// `conformance` module use it. +#[cfg(any(test, feature = "conformance"))] +pub(crate) const MIN_MTU: usize = 23; /// LOCAL. In-node `Exchange` `events` queue of `NodeEvent`s; never leaves the /// node (unlike the wire `ACK_IDS_CAP`). -pub const EVENT_QUEUE_CAP: usize = 64; +pub(crate) const EVENT_QUEUE_CAP: usize = 64; /// LOCAL. `Exchange` counted-drop table of distinct `(addr, DropReason)` /// pairs. Once full, further counted drops fold into a single overflow counter /// (reported with no address); see [`crate::exchange::NodeEvent::Dropped`]. -pub const DROP_TABLE_CAP: usize = 32; +pub(crate) const DROP_TABLE_CAP: usize = 32; /// LOCAL. `Exchange` per-peer `bcast_sent` table of broadcast ids dispatched /// to a peer. -pub const BCAST_SENT_CAP: usize = 256; +pub(crate) const BCAST_SENT_CAP: usize = 256; /// LOCAL. Pending ack-id buffer (`acks` for addressed, `bcast_acks` for /// broadcast) before the next Ack is emitted; overflow drops the oldest entry. -pub const ACK_BUFFER_CAP: usize = 256; +pub(crate) const ACK_BUFFER_CAP: usize = 256; /// LOCAL. Idle eviction window: a peer with no inbound frame for longer than /// this many ticks is removed as if Lost (`crate::exchange`). Never crosses /// the wire; local only. -pub const PEER_IDLE_MS: u64 = 30_000; +pub(crate) const PEER_IDLE_MS: u64 = 30_000; + +#[cfg(test)] +mod tests { + use super::{MAX_FRAME, MAX_PAYLOAD, MAX_SIG, MAX_TOPIC}; + use crate::frag::{fragment, plan, FRAG_OVERHEAD}; + use crate::wire::Frame; + use crate::{Dest, Observation, Payload, SigAlgo, Signature, Topic}; + + /// The MTU floors `Transport::mtu`'s rustdoc states (decision K: it + /// "states the smallest MTU core can use"). + #[test] + fn mtu_floors() { + let mut buf = [0u8; MAX_FRAME]; + + // A Summary with a 1-byte filter and a worst-case (5-byte varint) + // salt is 25 bytes, so summaries need mtu >= 26. + let summary = Frame::Summary { + from: [0xFF; 16], + salt: u32::MAX, + bits: heapless::Vec::from_slice(&[0xFF]).unwrap(), + }; + assert_eq!(summary.encode(&mut buf), Ok(25)); + + // An Ack naming one id is 35 bytes. + let ack = Frame::Ack { + from: [0xFF; 16], + ids: heapless::Vec::from_slice(&[[0xFF; 16]]).unwrap(), + }; + assert_eq!(ack.encode(&mut buf), Ok(35)); + + // Fragmentation needs room past the fragment header. + assert_eq!(plan(1, FRAG_OVERHEAD), None); + assert_eq!(plan(1, FRAG_OVERHEAD + 1), Some((1, 1))); + assert_eq!(FRAG_OVERHEAD + 1, 66); + + // The largest valid observation (signed, addressed, max topic and + // payload, worst-case varints) encodes to 1233 bytes and first fits + // in MAX_FRAGMENTS fragments at mtu 85. + let worst = Observation { + observer: [0xFF; 16], + seq: u64::MAX, + age_ms: u64::MAX, + observed_at: Some(u64::MAX), + topic: Topic::from_slice(&[0xFF; MAX_TOPIC]).unwrap(), + payload: Payload::from_slice(&[0xFF; MAX_PAYLOAD]).unwrap(), + dest: Some(Dest { + to: [0xFF; 16], + lifetime_ms: u64::MAX, + }), + sig: Some(Signature { + version: u8::MAX, + algo: SigAlgo::Ed25519, + bytes: heapless::Vec::from_slice(&[0xFF; MAX_SIG]).unwrap(), + }), + }; + let mut obs_buf = [0u8; MAX_FRAME * 2]; + let len = postcard::to_slice(&worst, &mut obs_buf).unwrap().len(); + assert_eq!(len, 1233); + assert!(fragment([0xFF; 16], &worst, 84, &mut |_| {}).is_err()); + assert!(fragment([0xFF; 16], &worst, 85, &mut |_| {}).is_ok()); + } +} diff --git a/tines-core/src/node.rs b/tines-core/src/node.rs index 41eaec74..749fb0f5 100644 --- a/tines-core/src/node.rs +++ b/tines-core/src/node.rs @@ -171,8 +171,8 @@ impl Node { /// counted drop. /// /// The returned [`heapless::Vec`] is capped at - /// [`consts::EVENT_QUEUE_CAP`] confirmed deliveries plus the entire - /// [`consts::DROP_TABLE_CAP`]-entry drop table and its overflow slot. Index + /// `EVENT_QUEUE_CAP` (64) confirmed deliveries plus the entire + /// `DROP_TABLE_CAP` (32)-entry drop table and its overflow slot. Index /// it directly — a [`NodeEvent::Delivered`] names the peer that held our /// broadcast and the broadcast's content-ID; a [`NodeEvent::Dropped`] names /// the peer and reason a dropped frame was rejected with. diff --git a/tines-core/src/transport.rs b/tines-core/src/transport.rs index 5800b14b..73e73deb 100644 --- a/tines-core/src/transport.rs +++ b/tines-core/src/transport.rs @@ -50,6 +50,17 @@ pub trait Transport { fn send(&mut self, to: &Self::Addr, frame: &[u8]) -> Result<(), SendError>; /// Maximum frame size in bytes this transport can send (must be `<= MAX_FRAME`). + /// + /// The smallest MTU core can fully use is **85 bytes**: at 85 every valid + /// observation, up to a maximum-size, signed, addressed one, fits in the + /// 64 fragments a peer will reassemble. Below that core never emits an + /// oversized frame, but it sends less: + /// - below 85, the largest observations can't be sent; + /// - below 66, nothing is fragmented, so only observations that fit + /// whole in one frame go out; + /// - below 35, no receipt fits, so none is sent and every sender keeps + /// custody of what it sent; + /// - below 26, no summary fits. fn mtu(&self) -> usize; } diff --git a/tines-core/tests/fuzz_twin.rs b/tines-core/tests/fuzz_twin.rs index 29da60e5..2cdb0d3c 100644 --- a/tines-core/tests/fuzz_twin.rs +++ b/tines-core/tests/fuzz_twin.rs @@ -22,7 +22,8 @@ mod fuzz; use proptest::prelude::*; -use tines_core::consts::{MAX_FRAME, MAX_PAYLOAD, MAX_PEERS, MAX_TOPIC, SUMMARY_BYTES}; +use tines_core::conformance::MAX_PEERS; +use tines_core::consts::{MAX_FRAME, MAX_PAYLOAD, MAX_TOPIC, SUMMARY_BYTES}; use tines_core::Store; // A single bounded case count keeps the twin fast under `cargo test` while diff --git a/tines-sim/tests/props.rs b/tines-sim/tests/props.rs index 9cfe89dc..6562999c 100644 --- a/tines-sim/tests/props.rs +++ b/tines-sim/tests/props.rs @@ -23,12 +23,28 @@ //! addressed candidate a turn. Pins §6's overflow-safety guarantee. use proptest::prelude::*; -use tines_core::{ - consts::{MAX_PEERS, SEND_QUEUE_CAP}, - FrameBuf, Store, -}; +use tines_core::{FrameBuf, Store}; use tines_sim::{CappedStore, Contact, ContactTrace, Sim, WorldConfig}; +/// Mirrors tines-core's peer-table capacity, a crate-private local constant +/// since 1.0 (decision K). `local_caps_match_core` checks the mirror. +const MAX_PEERS: usize = 8; + +/// Mirrors tines-core's send-queue / bounce-table capacity, a crate-private +/// local constant since 1.0 (decision K). `local_caps_match_core` checks the +/// mirror. +const SEND_QUEUE_CAP: usize = 256; + +/// The mirrored local caps still equal core's. Runs with the `conformance` +/// feature (as `scripts/ci.sh` does), which exposes core's values through its +/// hidden `conformance` module. +#[cfg(feature = "conformance")] +#[test] +fn local_caps_match_core() { + assert_eq!(MAX_PEERS, tines_core::conformance::MAX_PEERS); + assert_eq!(SEND_QUEUE_CAP, tines_core::conformance::SEND_QUEUE_CAP); +} + /// A fixed set of short observation topics the tests reuse so the generated /// observations round-trip cleanly through the core (`MAX_TOPIC` is 64 bytes). const TOPICS: [&[u8]; 8] = [ @@ -166,7 +182,7 @@ fn addressed_scenario() -> impl Strategy { /// graph plus a flood of distinct addressed observations past [`SEND_QUEUE_CAP`]. #[derive(Debug)] struct BounceScenario { - /// Node count (fixed at [`consts::MAX_PEERS`]). + /// Node count (fixed at [`MAX_PEERS`]). nodes: usize, /// Fully-connected trace: every pair in contact during one long window. trace: ContactTrace,