Merge sole-store context change into the next-side

Bring the immutable-state single-store change onto the Noise XX line.
The shared NodeContext is now the sole store; this merge applies the
next-only adaptations the master-side change couldn't carry:

- Remove node_profile from the Node struct (next-only field) so it lives
  solely in NodeContext; migrate its readers (tree/bloom/discovery/
  handshake negotiation) onto the node_profile() accessor. The two FMP
  negotiation sites hoist node_profile() into a local to avoid borrowing
  &self while a connection is mutably borrowed.
- leaf_only sets both is_leaf_only and node_profile via the context swap.
- Preserve the XX handshake/rekey structure (no identity-in-msg1; XX/XK
  initiator/responder constructors) while applying the startup_epoch()
  accessor migration.
- Tests: route profile selection through a make_test_node_with_profile
  helper (profile is immutable, set via Config flags) instead of poking
  the removed field.

cargo test --lib 1369/0; clippy -D warnings and release build clean.
This commit is contained in:
Johnathan Corgan
2026-06-02 17:30:15 +00:00
19 changed files with 250 additions and 253 deletions
+2 -2
View File
@@ -179,7 +179,7 @@ impl Node {
/// Non-routing nodes do not send filters (they receive only).
pub(super) async fn send_pending_filter_announces(&mut self) {
// Non-routing and leaf nodes don't send bloom filters
if self.node_profile != crate::protocol::NodeProfile::Full {
if self.node_profile() != crate::protocol::NodeProfile::Full {
return;
}
@@ -402,7 +402,7 @@ impl Node {
/// and marks all peers for update.
fn check_adaptive_sizing(&mut self) {
// Only Full nodes participate in filter sizing
if self.node_profile != crate::protocol::NodeProfile::Full {
if self.node_profile() != crate::protocol::NodeProfile::Full {
return;
}
+10 -17
View File
@@ -1,16 +1,17 @@
//! Shared immutable context bundle.
//!
//! [`NodeContext`] groups the [`Node`](super::Node)'s effectively-immutable
//! fields behind a single `Arc` so that handlers can eventually borrow a
//! cheap `&NodeContext` clone instead of `&self`.
//! fields behind a single `Arc` so that handlers can borrow a cheap
//! `&NodeContext` clone instead of `&self`.
//!
//! During the migration it is a *parallel, authoritative* copy of the
//! corresponding `Node` fields: both are kept in lockstep at the only three
//! mutation points — the constructor, [`update_peers`](super::Node::update_peers),
//! and the test-only `set_max_*` setters — via
//! [`Node::rebuild_context`](super::Node::rebuild_context). Readers migrate
//! onto the bundle incrementally; the duplicated `Node` fields are removed
//! once the last reader has moved over.
//! It is the **sole store** of these fields: the `Node` no longer keeps
//! duplicate copies. The bundle itself is immutable; the rare mutation of a
//! bundled field (the constructors, [`leaf_only`](super::Node::leaf_only),
//! and [`update_peers`](super::Node::update_peers)) is done by building a fresh
//! `NodeContext` and swapping the whole `Arc` via
//! [`Node::replace_context`](super::Node::replace_context). Readers reach the
//! fields through the `Node` accessors (`config()`, `identity()`,
//! `startup_epoch()`, `is_leaf_only()`, `max_*()`, `uptime()`).
use std::sync::Arc;
@@ -28,8 +29,6 @@ pub(crate) struct NodeContext {
pub identity: Identity,
/// Random epoch generated at startup for peer restart detection.
// Consumed by readers migrating in a later sub-PR.
#[allow(dead_code)]
pub startup_epoch: [u8; 8],
/// Instant when the node was created, for uptime reporting.
@@ -42,18 +41,12 @@ pub(crate) struct NodeContext {
pub node_profile: NodeProfile,
/// Maximum connections (0 = unlimited).
// Consumed by readers migrating in a later sub-PR.
#[allow(dead_code)]
pub max_connections: usize,
/// Maximum peers (0 = unlimited).
// Consumed by readers migrating in a later sub-PR.
#[allow(dead_code)]
pub max_peers: usize,
/// Maximum links (0 = unlimited).
// Consumed by readers migrating in a later sub-PR.
#[allow(dead_code)]
pub max_links: usize,
}
+1 -1
View File
@@ -366,7 +366,7 @@ impl Node {
}
// Leaf nodes don't forward discovery requests
if self.node_profile == crate::protocol::NodeProfile::Leaf {
if self.node_profile() == crate::protocol::NodeProfile::Leaf {
return;
}
+16 -14
View File
@@ -179,13 +179,13 @@ impl Node {
);
// Create FMP negotiation payload for msg2 (includes profile, MMP bits, bloom TLV)
let neg_payload = NegotiationPayload::fmp(1, 1, self.node_profile).encode();
let neg_payload = NegotiationPayload::fmp(1, 1, self.node_profile()).encode();
let our_keypair = self.identity().keypair();
let noise_msg1 = &packet.data[header.noise_msg1_offset..];
let msg2_response = match conn.receive_handshake_init(
our_keypair,
self.startup_epoch,
self.startup_epoch(),
noise_msg1,
Some(&neg_payload),
packet.timestamp_ms,
@@ -492,6 +492,7 @@ impl Node {
return;
}
let our_profile = self.node_profile();
let (peer_identity, msg3_bytes, our_index) = {
let Some(conn) = self.connections.get_mut(&link_id) else {
warn!(link_id = %link_id, "Connection removed during msg2 processing");
@@ -502,7 +503,7 @@ impl Node {
};
// Create FMP negotiation payload for msg3 (includes profile, MMP bits, bloom TLV)
let neg_payload = NegotiationPayload::fmp(1, 1, self.node_profile).encode();
let neg_payload = NegotiationPayload::fmp(1, 1, our_profile).encode();
// Process Noise msg2 and generate msg3
let noise_msg2 = &packet.data[header.noise_msg2_offset..];
@@ -527,10 +528,10 @@ impl Node {
// Process peer's FMP negotiation payload from msg2
if let Some(neg_bytes) = &received_negotiation {
match process_fmp_negotiation(self.node_profile, conn, neg_bytes) {
match process_fmp_negotiation(our_profile, conn, neg_bytes) {
Ok(()) => {}
Err(e) => {
warn!(link_id = %link_id, our_profile = %self.node_profile, error = %e, "FMP negotiation failed");
warn!(link_id = %link_id, our_profile = %our_profile, error = %e, "FMP negotiation failed");
conn.mark_failed();
self.stats_mut()
.record_reject(RejectReason::Handshake(HandshakeReject::BadState));
@@ -878,6 +879,7 @@ impl Node {
}
};
let our_profile = self.node_profile();
let (peer_identity, our_index, remote_epoch) = {
// Get the pending connection
let conn = match self.connections.get_mut(&link_id) {
@@ -922,10 +924,10 @@ impl Node {
// Process peer's FMP negotiation payload from msg3
if let Some(neg_bytes) = &received_negotiation {
match process_fmp_negotiation(self.node_profile, conn, neg_bytes) {
match process_fmp_negotiation(our_profile, conn, neg_bytes) {
Ok(()) => {}
Err(e) => {
warn!(link_id = %link_id, our_profile = %self.node_profile, error = %e, "FMP negotiation failed");
warn!(link_id = %link_id, our_profile = %our_profile, error = %e, "FMP negotiation failed");
self.connections.remove(&link_id);
self.remove_link(&link_id);
self.stats_mut()
@@ -1025,7 +1027,7 @@ impl Node {
// admitting them doesn't grow peers.len(). The late cap check
// inside promote_connection() is intentionally left in place
// as defense-in-depth.
if self.max_peers > 0 && self.peers.len() >= self.max_peers {
if self.max_peers() > 0 && self.peers.len() >= self.max_peers() {
let is_known_active = self.peers.contains_key(&peer_node_addr);
let is_pending_outbound = self.connections.iter().any(|(_, conn)| {
conn.expected_identity()
@@ -1035,7 +1037,7 @@ impl Node {
if !is_known_active && !is_pending_outbound {
debug!(
peer = %self.peer_display_name(&peer_node_addr),
max = self.max_peers,
max = self.max_peers(),
"Silent-dropping Msg3 at max_peers cap (early gate; no promotion)"
);
// Capture our_index before removing the connection so
@@ -1497,7 +1499,7 @@ impl Node {
) -> Result<PromotionResult, NodeError> {
// Leaf nodes: reject if we already have a peer (single-peer enforcement)
let peer_node_addr_check = *verified_identity.node_addr();
if self.node_profile == crate::protocol::NodeProfile::Leaf
if self.node_profile() == crate::protocol::NodeProfile::Leaf
&& !self.peers.is_empty()
&& !self.peers.contains_key(&peer_node_addr_check)
{
@@ -1639,7 +1641,7 @@ impl Node {
is_outbound,
&self.config().node.mmp,
remote_epoch,
self.node_profile,
self.node_profile(),
peer_profile,
);
new_peer.set_tree_announce_min_interval_ms(
@@ -1720,10 +1722,10 @@ impl Node {
}
// Normal promotion
if self.max_peers > 0 && self.peers.len() >= self.max_peers {
if self.max_peers() > 0 && self.peers.len() >= self.max_peers() {
let _ = self.index_allocator.free(our_index);
return Err(NodeError::MaxPeersExceeded {
max: self.max_peers,
max: self.max_peers(),
});
}
@@ -1750,7 +1752,7 @@ impl Node {
is_outbound,
&self.config().node.mmp,
remote_epoch,
self.node_profile,
self.node_profile(),
peer_profile,
);
new_peer.set_tree_announce_min_interval_ms(
+2 -2
View File
@@ -204,7 +204,7 @@ impl Node {
// Create XX initiator handshake directly (no PeerConnection)
let our_keypair = self.identity().keypair();
let mut hs = HandshakeState::new_initiator(our_keypair);
hs.set_local_epoch(self.startup_epoch);
hs.set_local_epoch(self.startup_epoch());
let noise_msg1 = match hs.write_message_1() {
Ok(msg) => msg,
@@ -543,7 +543,7 @@ impl Node {
// Create Noise XX initiator handshake (rekey: no negotiation payload)
let our_keypair = self.identity().keypair();
let mut handshake = HandshakeState::new_initiator(our_keypair);
handshake.set_local_epoch(self.startup_epoch);
handshake.set_local_epoch(self.startup_epoch());
let msg1 = match handshake.write_message_1() {
Ok(m) => m,
+3 -3
View File
@@ -507,7 +507,7 @@ impl Node {
}
let our_keypair = self.identity().keypair();
let mut handshake = HandshakeState::new_responder(our_keypair);
handshake.set_local_epoch(self.startup_epoch);
handshake.set_local_epoch(self.startup_epoch());
if let Err(e) = handshake.read_message_1(&setup.handshake_payload) {
debug!(error = %e, "Failed to process rekey XX msg1");
@@ -558,7 +558,7 @@ impl Node {
// Create XX responder handshake and process msg1
let our_keypair = self.identity().keypair();
let mut handshake = HandshakeState::new_responder(our_keypair);
handshake.set_local_epoch(self.startup_epoch);
handshake.set_local_epoch(self.startup_epoch());
if let Err(e) = handshake.read_message_1(&setup.handshake_payload) {
debug!(error = %e, "Failed to process Noise XX msg1 in SessionSetup");
@@ -1419,7 +1419,7 @@ impl Node {
// Create Noise XX initiator handshake
let our_keypair = self.identity().keypair();
let mut handshake = HandshakeState::new_initiator(our_keypair);
handshake.set_local_epoch(self.startup_epoch);
handshake.set_local_epoch(self.startup_epoch());
let msg1 = handshake
.write_message_1()
.map_err(|e| NodeError::SendFailed {
+22 -18
View File
@@ -55,11 +55,14 @@ impl Node {
new_by_addr.insert(*identity.node_addr(), peer);
}
// Read the current peer set directly from the field: update_peers is the
// config-source mutation owner (it writes self.config.peers below and then
// rebuilds the context), so it manages config.peers directly rather than
// through the context accessor — same rationale as the write at line ~124.
// Read the current peer set from the context *before* the swap below:
// update_peers is the config-source mutation owner. It reads the current
// (pre-update) peer set here, builds a fresh Config + context, then swaps
// the whole Arc. Reading the live context Arc before the swap yields the
// pre-update set the diff needs (the `update_peers_races_*` canary depends
// on this ordering).
let current_by_addr: HashMap<NodeAddr, PeerConfig> = self
.context
.config
.peers()
.iter()
@@ -124,8 +127,9 @@ impl Node {
.map(|node_addr| new_by_addr[node_addr].clone())
.collect();
self.config.peers = new_by_addr.into_values().collect();
self.rebuild_context();
let mut new_config = (*self.context.config).clone();
new_config.peers = new_by_addr.into_values().collect();
self.replace_context(|ctx| ctx.config = std::sync::Arc::new(new_config));
for peer_config in added_configs {
outcome.added += 1;
@@ -502,7 +506,7 @@ impl Node {
// Start the Noise handshake and get message 1
let our_keypair = self.identity().keypair();
let noise_msg1 =
match connection.start_handshake(our_keypair, self.startup_epoch, current_time_ms) {
match connection.start_handshake(our_keypair, self.startup_epoch(), current_time_ms) {
Ok(msg) => msg,
Err(e) => {
// Clean up the index and link
@@ -755,7 +759,7 @@ impl Node {
debug!(
peer_npub = %traversal.peer_npub,
peers = self.peers.len(),
max_peers = self.max_peers,
max_peers = self.max_peers(),
"Dropping established NAT traversal: at capacity"
);
continue;
@@ -2128,16 +2132,16 @@ impl Node {
.connections
.len()
.saturating_add(self.pending_connects.len());
let connection_slots = if self.max_connections == 0 {
let connection_slots = if self.max_connections() == 0 {
usize::MAX
} else {
self.max_connections.saturating_sub(connection_used)
self.max_connections().saturating_sub(connection_used)
};
let peer_slots = if self.max_peers == 0 {
let peer_slots = if self.max_peers() == 0 {
usize::MAX
} else {
self.max_peers.saturating_sub(self.peers.len())
self.max_peers().saturating_sub(self.peers.len())
};
connection_slots.min(peer_slots)
@@ -2148,25 +2152,25 @@ impl Node {
.connections
.len()
.saturating_add(self.pending_connects.len());
if self.max_connections == 0 {
if self.max_connections() == 0 {
usize::MAX
} else {
self.max_connections.saturating_sub(used)
self.max_connections().saturating_sub(used)
}
}
fn outbound_link_slots(&self) -> usize {
if self.max_links == 0 {
if self.max_links() == 0 {
usize::MAX
} else {
self.max_links.saturating_sub(self.links.len())
self.max_links().saturating_sub(self.links.len())
}
}
fn path_candidate_attempt_budget(&self, peer_node_addr: &NodeAddr) -> usize {
if !self.peers.contains_key(peer_node_addr)
&& self.max_peers > 0
&& self.peers.len() >= self.max_peers
&& self.max_peers() > 0
&& self.peers.len() >= self.max_peers()
{
return 0;
}
+56 -106
View File
@@ -306,38 +306,18 @@ struct PendingConnect {
/// connection before authentication completes.
// Discovery lookup constants moved to config: node.discovery.attempt_timeouts_secs, node.discovery.ttl
pub struct Node {
// === Identity ===
/// This node's cryptographic identity.
identity: Identity,
/// Random epoch generated at startup for peer restart detection.
/// Exchanged inside Noise handshake messages so peers can detect restarts.
startup_epoch: [u8; 8],
/// Instant when the node was created, for uptime reporting.
started_at: std::time::Instant,
// === Configuration ===
/// Loaded configuration.
config: Config,
/// Shared immutable context bundle. A parallel, authoritative copy of the
/// effectively-immutable fields (config/identity/startup_epoch/started_at/
/// is_leaf_only/max_*), kept in lockstep with the `Node` fields via
/// `rebuild_context`. Readers migrate onto it in later sub-PRs; the
/// duplicated `Node` fields are removed once the last reader has moved.
// === Immutable Context ===
/// Shared immutable context bundle: the single source of truth for the
/// node's effectively-immutable state (config/identity/startup_epoch/
/// started_at/is_leaf_only/max_*). Mutated only by whole-`Arc` replacement
/// via `replace_context` at the constructors, `leaf_only`, and
/// `update_peers`; readers reach it through the accessors.
context: Arc<context::NodeContext>,
// === State ===
/// Node operational state.
state: NodeState,
/// Whether this is a leaf-only node.
is_leaf_only: bool,
/// Node profile derived from config (Full, NonRouting, Leaf).
node_profile: NodeProfile,
// === Spanning Tree ===
/// Local spanning tree state.
tree_state: TreeState,
@@ -404,14 +384,6 @@ pub struct Node {
/// initiation timestamp (Unix ms). Prevents duplicate flood queries.
pending_lookups: HashMap<NodeAddr, handlers::discovery::PendingLookup>,
// === Resource Limits ===
/// Maximum connections (0 = unlimited).
max_connections: usize,
/// Maximum peers (0 = unlimited).
max_peers: usize,
/// Maximum links (0 = unlimited).
max_links: usize,
// === Counters ===
/// Next link ID to allocate.
next_link_id: u64,
@@ -676,14 +648,8 @@ impl Node {
));
Ok(Self {
identity,
startup_epoch,
started_at,
config,
context,
state: NodeState::Created,
is_leaf_only,
node_profile,
tree_state,
bloom_state,
coord_cache,
@@ -700,9 +666,6 @@ impl Node {
identity_cache: HashMap::new(),
pending_tun_packets: HashMap::new(),
pending_lookups: HashMap::new(),
max_connections,
max_peers,
max_links,
next_link_id: 1,
next_transport_id: 1,
stats: stats::NodeStats::new(),
@@ -839,14 +802,8 @@ impl Node {
));
Ok(Self {
identity,
startup_epoch,
started_at,
config,
context,
state: NodeState::Created,
is_leaf_only: false,
node_profile: NodeProfile::Full,
tree_state,
bloom_state,
coord_cache,
@@ -863,9 +820,6 @@ impl Node {
identity_cache: HashMap::new(),
pending_tun_packets: HashMap::new(),
pending_lookups: HashMap::new(),
max_connections,
max_peers,
max_links,
next_link_id: 1,
next_transport_id: 1,
stats: stats::NodeStats::new(),
@@ -926,10 +880,11 @@ impl Node {
/// Create a leaf-only node (simplified state).
pub fn leaf_only(config: Config) -> Result<Self, NodeError> {
let mut node = Self::new(config)?;
node.is_leaf_only = true;
node.node_profile = NodeProfile::Leaf;
node.bloom_state = BloomState::leaf_only(*node.identity.node_addr());
node.rebuild_context();
node.bloom_state = BloomState::leaf_only(*node.node_addr());
node.replace_context(|ctx| {
ctx.is_leaf_only = true;
ctx.node_profile = NodeProfile::Leaf;
});
Ok(node)
}
@@ -941,7 +896,7 @@ impl Node {
// Collect UDP configs with optional names to avoid borrow conflicts
let udp_instances: Vec<_> = self
.config
.config()
.transports
.udp
.iter()
@@ -959,13 +914,12 @@ impl Node {
#[cfg(unix)]
{
let eth_instances: Vec<_> = self
.config
.config()
.transports
.ethernet
.iter()
.map(|(name, config)| (name.map(|s| s.to_string()), config.clone()))
.collect();
for (name, eth_config) in eth_instances {
let transport_id = self.allocate_transport_id();
let eth = EthernetTransport::new(transport_id, name, eth_config, packet_tx.clone());
@@ -975,7 +929,7 @@ impl Node {
// Create TCP transport instances
let tcp_instances: Vec<_> = self
.config
.config()
.transports
.tcp
.iter()
@@ -990,7 +944,7 @@ impl Node {
// Create Tor transport instances
let tor_instances: Vec<_> = self
.config
.config()
.transports
.tor
.iter()
@@ -1007,7 +961,7 @@ impl Node {
#[cfg(bluer_available)]
{
let ble_instances: Vec<_> = self
.config
.config()
.transports
.ble
.iter()
@@ -1149,12 +1103,17 @@ impl Node {
/// Get this node's NodeAddr.
pub fn node_addr(&self) -> &NodeAddr {
self.identity.node_addr()
self.context.identity.node_addr()
}
/// Get this node's npub.
pub fn npub(&self) -> String {
self.identity.npub()
self.context.identity.npub()
}
/// Get this node's startup epoch (random per-boot tag for restart detection).
pub fn startup_epoch(&self) -> [u8; 8] {
self.context.startup_epoch
}
/// Reload the host map if the backing `/etc/fips/hosts` file changed.
@@ -1197,23 +1156,16 @@ impl Node {
self.context.config.as_ref()
}
/// Rebuild the shared [`context::NodeContext`] from the current `Node`
/// fields. Called after any mutation of a bundled field (`update_peers`,
/// the `set_max_*` setters) so `self.context` stays equal to the `Node`
/// fields it mirrors. Cheap — the only deep copy is the (rare) `Config`
/// clone.
fn rebuild_context(&mut self) {
self.context = Arc::new(context::NodeContext::new(
Arc::new(self.config.clone()),
self.identity.clone(),
self.startup_epoch,
self.started_at,
self.is_leaf_only,
self.node_profile,
self.max_connections,
self.max_peers,
self.max_links,
));
/// Mutate the shared immutable context by building a fresh
/// [`context::NodeContext`] and swapping the whole `Arc`. The per-instance
/// context is never interior-mutated; this clone-edit-swap is the sole
/// runtime mutation path for the bundle (the constructors,
/// [`leaf_only`](Self::leaf_only), and [`update_peers`](Self::update_peers)).
/// Cheap — the only deep copy is the (rare) `Config` clone behind its `Arc`.
fn replace_context(&mut self, f: impl FnOnce(&mut context::NodeContext)) {
let mut ctx = (*self.context).clone();
f(&mut ctx);
self.context = Arc::new(ctx);
}
/// Calculate the effective IPv6 MTU that can be sent over FIPS.
@@ -1252,7 +1204,7 @@ impl Node {
return mtu;
}
// Fallback to config: try UDP first, then Ethernet
if let Some((_, cfg)) = self.config.transports.udp.iter().next() {
if let Some((_, cfg)) = self.config().transports.udp.iter().next() {
return cfg.mtu();
}
1280
@@ -1337,7 +1289,7 @@ impl Node {
let parent_id = *self.tree_state.my_declaration().parent_id();
let is_root = self.tree_state.is_root();
let max_fpr = self.config.node.bloom.max_inbound_fpr;
let max_fpr = self.config().node.bloom.max_inbound_fpr;
let mut total: f64 = 1.0; // count self
let mut child_count: u32 = 0;
let mut has_data = false;
@@ -1401,7 +1353,7 @@ impl Node {
None => true,
Some(last) => {
now.duration_since(last)
>= std::time::Duration::from_secs(self.config.node.mmp.log_interval_secs)
>= std::time::Duration::from_secs(self.config().node.mmp.log_interval_secs)
}
};
if should_log {
@@ -1522,16 +1474,19 @@ impl Node {
// === Resource Limits ===
/// Set the maximum number of connections (handshake phase).
pub fn set_max_connections(&mut self, max: usize) {
self.max_connections = max;
self.rebuild_context();
/// Maximum connections (handshake phase); 0 = unlimited.
pub fn max_connections(&self) -> usize {
self.context.max_connections
}
/// Set the maximum number of peers (authenticated).
pub fn set_max_peers(&mut self, max: usize) {
self.max_peers = max;
self.rebuild_context();
/// Maximum authenticated peers; 0 = unlimited.
pub fn max_peers(&self) -> usize {
self.context.max_peers
}
/// Maximum links; 0 = unlimited.
pub fn max_links(&self) -> usize {
self.context.max_links
}
/// Returns false when we are at or above the configured `max_peers`
@@ -1543,13 +1498,8 @@ impl Node {
/// Nostr-mediated NAT-traversal punch) from doing pointless work
/// when saturated.
pub(crate) fn outbound_admission_check(&self) -> bool {
self.max_peers == 0 || self.peers.len() < self.max_peers
}
/// Set the maximum number of links.
pub fn set_max_links(&mut self, max: usize) {
self.max_links = max;
self.rebuild_context();
let max_peers = self.context.max_peers;
max_peers == 0 || self.peers.len() < max_peers
}
// === Counts ===
@@ -1614,9 +1564,9 @@ impl Node {
/// Add a link.
pub fn add_link(&mut self, link: Link) -> Result<(), NodeError> {
if self.max_links > 0 && self.links.len() >= self.max_links {
if self.max_links() > 0 && self.links.len() >= self.max_links() {
return Err(NodeError::MaxLinksExceeded {
max: self.max_links,
max: self.max_links(),
});
}
let link_id = link.link_id();
@@ -1720,9 +1670,9 @@ impl Node {
return Err(NodeError::ConnectionAlreadyExists(link_id));
}
if self.max_connections > 0 && self.connections.len() >= self.max_connections {
if self.max_connections() > 0 && self.connections.len() >= self.max_connections() {
return Err(NodeError::MaxConnectionsExceeded {
max: self.max_connections,
max: self.max_connections(),
});
}
@@ -1857,7 +1807,7 @@ impl Node {
self.identity_cache
.insert(prefix, (node_addr, pubkey, Self::now_ms()));
// LRU eviction
let max = self.config.node.cache.identity_size;
let max = self.config().node.cache.identity_size;
if self.identity_cache.len() > max
&& let Some(oldest_key) = self
.identity_cache
@@ -1908,7 +1858,7 @@ impl Node {
/// Configured maximum identity cache size.
pub fn identity_cache_max(&self) -> usize {
self.config.node.cache.identity_size
self.config().node.cache.identity_size
}
/// Number of pending discovery lookups.
@@ -2389,7 +2339,7 @@ impl fmt::Debug for Node {
f.debug_struct("Node")
.field("node_addr", self.node_addr())
.field("state", &self.state)
.field("is_leaf_only", &self.is_leaf_only)
.field("is_leaf_only", &self.is_leaf_only())
.field("connections", &self.connection_count())
.field("peers", &self.peer_count())
.field("links", &self.link_count())
+1 -1
View File
@@ -235,7 +235,7 @@ impl Node {
if !self.outbound_admission_check() {
debug!(
peers = self.peers.len(),
max_peers = self.max_peers,
max_peers = self.max_peers(),
retry_pending = self.retry_pending.len(),
"Suppressing auto-reconnect retries: at capacity"
);
+5 -5
View File
@@ -53,13 +53,13 @@ async fn test_outbound_msg2_denied_after_acl_reload() {
let node_b = make_node();
let transport_id = TransportId::new(1);
let remote_addr = TransportAddr::from_string("127.0.0.1:5001");
let peer_b_identity = PeerIdentity::from_pubkey_full(node_b.identity.pubkey_full());
let peer_b_identity = PeerIdentity::from_pubkey_full(node_b.identity().pubkey_full());
let link_id_a = node_a.allocate_link_id();
let mut conn_a = PeerConnection::outbound(link_id_a, peer_b_identity, 1000);
let our_index_a = node_a.index_allocator.allocate().unwrap();
let noise_msg1 = conn_a
.start_handshake(node_a.identity.keypair(), node_a.startup_epoch, 1000)
.start_handshake(node_a.identity().keypair(), node_a.startup_epoch(), 1000)
.unwrap();
conn_a.set_our_index(our_index_a);
conn_a.set_transport_id(transport_id);
@@ -85,7 +85,7 @@ async fn test_outbound_msg2_denied_after_acl_reload() {
let responder_epoch = [0x11; 8];
let noise_msg2 = conn_b
.receive_handshake_init(
node_b.identity.keypair(),
node_b.identity().keypair(),
responder_epoch,
&noise_msg1,
None,
@@ -173,12 +173,12 @@ async fn test_inbound_msg3_denied_triggers_disconnect() {
// === A initiates the handshake ===
let peer_b_identity = PeerIdentity::from_pubkey_full(node_b.identity.pubkey_full());
let peer_b_identity = PeerIdentity::from_pubkey_full(node_b.identity().pubkey_full());
let link_id_a = node_a.allocate_link_id();
let mut conn_a = PeerConnection::outbound(link_id_a, peer_b_identity, 1000);
let our_index_a = node_a.index_allocator.allocate().unwrap();
let noise_msg1 = conn_a
.start_handshake(node_a.identity.keypair(), node_a.startup_epoch, 1000)
.start_handshake(node_a.identity().keypair(), node_a.startup_epoch(), 1000)
.unwrap();
conn_a.set_our_index(our_index_a);
conn_a.set_transport_id(transport_id_a);
+5 -5
View File
@@ -78,9 +78,9 @@ async fn test_adopted_udp_traversal_completes_handshake() {
node_b.handle_msg3(pkt_at_b).await;
let peer_a_node_addr =
*PeerIdentity::from_pubkey_full(node_a.identity.pubkey_full()).node_addr();
*PeerIdentity::from_pubkey_full(node_a.identity().pubkey_full()).node_addr();
let peer_b_node_addr =
*PeerIdentity::from_pubkey_full(node_b.identity.pubkey_full()).node_addr();
*PeerIdentity::from_pubkey_full(node_b.identity().pubkey_full()).node_addr();
assert_eq!(
node_a.peer_count(),
@@ -248,7 +248,7 @@ async fn test_third_peer_can_handshake_via_adopted_transport_socket() {
assert_eq!(pkt_at_a.data[0] & 0x0f, PHASE_MSG3);
node_a.handle_msg3(pkt_at_a).await;
let node_a_addr = *PeerIdentity::from_pubkey_full(node_a.identity.pubkey_full()).node_addr();
let node_a_addr = *PeerIdentity::from_pubkey_full(node_a.identity().pubkey_full()).node_addr();
assert!(
node_b.get_peer(&node_a_addr).is_some(),
"node_b should first be connected to node_a via adopted transport"
@@ -262,7 +262,7 @@ async fn test_third_peer_can_handshake_via_adopted_transport_socket() {
.transports
.insert(transport_id_c, TransportHandle::Udp(transport_c));
let peer_b_identity = PeerIdentity::from_pubkey_full(node_b.identity.pubkey_full());
let peer_b_identity = PeerIdentity::from_pubkey_full(node_b.identity().pubkey_full());
let adopted_addr = TransportAddr::from_string(&handoff_result.local_addr.to_string());
node_c
.initiate_connection(transport_id_c, adopted_addr, Some(peer_b_identity))
@@ -314,7 +314,7 @@ async fn test_third_peer_can_handshake_via_adopted_transport_socket() {
};
node_b.handle_msg3(pkt_at_b).await;
let node_c_addr = *PeerIdentity::from_pubkey_full(node_c.identity.pubkey_full()).node_addr();
let node_c_addr = *PeerIdentity::from_pubkey_full(node_c.identity().pubkey_full()).node_addr();
assert!(
node_b.get_peer(&node_c_addr).is_some(),
"node_b should promote node_c when node_c handshakes via adopted socket"
+1 -1
View File
@@ -1170,7 +1170,7 @@ async fn test_check_pending_lookups_default_sequence_unreachable() {
// Default attempt_timeouts_secs is [1, 2, 4, 8]. Confirm so the test
// cannot silently drift if the default changes.
assert_eq!(
node.config.node.discovery.attempt_timeouts_secs,
node.config().node.discovery.attempt_timeouts_secs,
vec![1, 2, 4, 8],
"test pins the [1,2,4,8] default; update the test if the default changes"
);
+29 -26
View File
@@ -1,6 +1,8 @@
//! Integration tests for end-to-end Noise XX handshake scenarios.
use super::spanning_tree::{cleanup_nodes, drain_all_packets, initiate_handshake, make_test_node};
use super::spanning_tree::{
cleanup_nodes, drain_all_packets, initiate_handshake, make_test_node_with_profile,
};
use super::*;
#[tokio::test]
@@ -50,7 +52,7 @@ async fn test_two_node_handshake_udp() {
// === Phase 1: Node A initiates handshake to Node B ===
// Create peer identity for B (must use full key for ECDH parity)
let peer_b_identity = PeerIdentity::from_pubkey_full(node_b.identity.pubkey_full());
let peer_b_identity = PeerIdentity::from_pubkey_full(node_b.identity().pubkey_full());
let peer_b_node_addr = *peer_b_identity.node_addr();
let link_id_a = node_a.allocate_link_id();
@@ -60,9 +62,9 @@ async fn test_two_node_handshake_udp() {
let our_index_a = node_a.index_allocator.allocate().unwrap();
// Start handshake (generates Noise XX msg1)
let our_keypair_a = node_a.identity.keypair();
let our_keypair_a = node_a.identity().keypair();
let noise_msg1 = conn_a
.start_handshake(our_keypair_a, node_a.startup_epoch, 1000)
.start_handshake(our_keypair_a, node_a.startup_epoch(), 1000)
.unwrap();
conn_a.set_our_index(our_index_a);
conn_a.set_transport_id(transport_id_a);
@@ -101,7 +103,7 @@ async fn test_two_node_handshake_udp() {
node_b.handle_msg1(packet_b).await;
let peer_a_node_addr =
*PeerIdentity::from_pubkey_full(node_a.identity.pubkey_full()).node_addr();
*PeerIdentity::from_pubkey_full(node_a.identity().pubkey_full()).node_addr();
// XX: B has NOT promoted yet (needs msg3)
assert_eq!(
@@ -313,16 +315,16 @@ async fn test_run_rx_loop_handshake() {
// === Phase 1: Node A initiates handshake to Node B ===
let peer_b_identity = PeerIdentity::from_pubkey_full(node_b.identity.pubkey_full());
let peer_b_identity = PeerIdentity::from_pubkey_full(node_b.identity().pubkey_full());
let peer_b_node_addr = *peer_b_identity.node_addr();
let link_id_a = node_a.allocate_link_id();
let mut conn_a = PeerConnection::outbound(link_id_a, peer_b_identity, 1000);
let our_index_a = node_a.index_allocator.allocate().unwrap();
let our_keypair_a = node_a.identity.keypair();
let our_keypair_a = node_a.identity().keypair();
let noise_msg1 = conn_a
.start_handshake(our_keypair_a, node_a.startup_epoch, 1000)
.start_handshake(our_keypair_a, node_a.startup_epoch(), 1000)
.unwrap();
conn_a.set_our_index(our_index_a);
conn_a.set_transport_id(transport_id_a);
@@ -494,9 +496,9 @@ async fn test_cross_connection_both_initiate() {
.insert(transport_id_b, TransportHandle::Udp(transport_b));
// Peer identities (must use full key for ECDH parity)
let peer_b_identity = PeerIdentity::from_pubkey_full(node_b.identity.pubkey_full());
let peer_b_identity = PeerIdentity::from_pubkey_full(node_b.identity().pubkey_full());
let peer_b_node_addr = *peer_b_identity.node_addr();
let peer_a_identity = PeerIdentity::from_pubkey_full(node_a.identity.pubkey_full());
let peer_a_identity = PeerIdentity::from_pubkey_full(node_a.identity().pubkey_full());
let peer_a_node_addr = *peer_a_identity.node_addr();
// === Phase 1: Both nodes initiate handshakes (simulate auto_connect) ===
@@ -505,9 +507,9 @@ async fn test_cross_connection_both_initiate() {
let link_id_a_out = node_a.allocate_link_id();
let mut conn_a = PeerConnection::outbound(link_id_a_out, peer_b_identity, 1000);
let our_index_a = node_a.index_allocator.allocate().unwrap();
let our_keypair_a = node_a.identity.keypair();
let our_keypair_a = node_a.identity().keypair();
let noise_msg1_a = conn_a
.start_handshake(our_keypair_a, node_a.startup_epoch, 1000)
.start_handshake(our_keypair_a, node_a.startup_epoch(), 1000)
.unwrap();
conn_a.set_our_index(our_index_a);
conn_a.set_transport_id(transport_id_a);
@@ -535,9 +537,9 @@ async fn test_cross_connection_both_initiate() {
let link_id_b_out = node_b.allocate_link_id();
let mut conn_b = PeerConnection::outbound(link_id_b_out, peer_a_identity, 1000);
let our_index_b = node_b.index_allocator.allocate().unwrap();
let our_keypair_b = node_b.identity.keypair();
let our_keypair_b = node_b.identity().keypair();
let noise_msg1_b = conn_b
.start_handshake(our_keypair_b, node_b.startup_epoch, 1000)
.start_handshake(our_keypair_b, node_b.startup_epoch(), 1000)
.unwrap();
conn_b.set_our_index(our_index_b);
conn_b.set_transport_id(transport_id_b);
@@ -705,9 +707,9 @@ async fn test_stale_connection_cleanup() {
// Allocate session index and set transport info
let our_index = node.index_allocator.allocate().unwrap();
let our_keypair = node.identity.keypair();
let our_keypair = node.identity().keypair();
let _noise_msg1 = conn
.start_handshake(our_keypair, node.startup_epoch, past_time_ms)
.start_handshake(our_keypair, node.startup_epoch(), past_time_ms)
.unwrap();
conn.set_our_index(our_index);
conn.set_transport_id(transport_id);
@@ -783,9 +785,9 @@ async fn test_failed_connection_cleanup() {
let mut conn = PeerConnection::outbound(link_id, peer_identity, now_ms);
let our_index = node.index_allocator.allocate().unwrap();
let our_keypair = node.identity.keypair();
let our_keypair = node.identity().keypair();
let _noise_msg1 = conn
.start_handshake(our_keypair, node.startup_epoch, now_ms)
.start_handshake(our_keypair, node.startup_epoch(), now_ms)
.unwrap();
conn.set_our_index(our_index);
conn.set_transport_id(transport_id);
@@ -843,9 +845,9 @@ async fn test_msg1_stored_for_resend() {
let mut conn = PeerConnection::outbound(link_id, peer_identity, now_ms);
let our_index = node.index_allocator.allocate().unwrap();
let our_keypair = node.identity.keypair();
let our_keypair = node.identity().keypair();
let noise_msg1 = conn
.start_handshake(our_keypair, node.startup_epoch, now_ms)
.start_handshake(our_keypair, node.startup_epoch(), now_ms)
.unwrap();
conn.set_our_index(our_index);
conn.set_transport_id(transport_id);
@@ -853,7 +855,7 @@ async fn test_msg1_stored_for_resend() {
// Build wire msg1 and store it (as initiate_peer_connection does)
let wire_msg1 = build_msg1(our_index, &noise_msg1);
let resend_interval = node.config.node.rate_limit.handshake_resend_interval_ms;
let resend_interval = node.config().node.rate_limit.handshake_resend_interval_ms;
conn.set_handshake_msg1(wire_msg1.clone(), now_ms + resend_interval);
// Verify stored msg1 matches what was built
@@ -876,9 +878,9 @@ async fn test_resend_scheduling() {
let mut conn = PeerConnection::outbound(link_id, peer_identity, now_ms);
let our_index = node.index_allocator.allocate().unwrap();
let our_keypair = node.identity.keypair();
let our_keypair = node.identity().keypair();
let noise_msg1 = conn
.start_handshake(our_keypair, node.startup_epoch, now_ms)
.start_handshake(our_keypair, node.startup_epoch(), now_ms)
.unwrap();
conn.set_our_index(our_index);
conn.set_transport_id(transport_id);
@@ -999,9 +1001,10 @@ async fn attempt_profile_handshake(
profile_a: crate::protocol::NodeProfile,
profile_b: crate::protocol::NodeProfile,
) -> (usize, usize) {
let mut nodes = vec![make_test_node().await, make_test_node().await];
nodes[0].node.node_profile = profile_a;
nodes[1].node.node_profile = profile_b;
let mut nodes = vec![
make_test_node_with_profile(profile_a).await,
make_test_node_with_profile(profile_b).await,
];
initiate_handshake(&mut nodes, 0, 1).await;
drain_all_packets(&mut nodes, false).await;
+21 -6
View File
@@ -28,14 +28,29 @@ pub(super) fn make_node() -> Node {
make_node_with(Config::new())
}
/// Build a test node from an explicit `Config`. Prefer this over poking
/// `node.config.*` after construction: immutable fields are mirrored into the
/// shared `NodeContext` at build time, so a post-construction field poke is
/// invisible to any reader that has migrated onto the `config()` accessor.
/// Build a test node from an explicit `Config`. Immutable state lives solely in
/// the shared `NodeContext`, built once at construction — there is no
/// post-construction field to poke, so set limits/config on the `Config` here.
pub(super) fn make_node_with(config: Config) -> Node {
Node::new(config).unwrap()
}
/// Build a test node with an explicit `max_peers` limit (replaces the removed
/// `set_max_peers` setter; resource limits are immutable post-construction).
pub(super) fn make_node_with_max_peers(max_peers: usize) -> Node {
let mut config = Config::new();
config.node.limits.max_peers = max_peers;
make_node_with(config)
}
/// Build a test node with an explicit `max_links` limit (replaces the removed
/// `set_max_links` setter; resource limits are immutable post-construction).
pub(super) fn make_node_with_max_links(max_links: usize) -> Node {
let mut config = Config::new();
config.node.limits.max_links = max_links;
make_node_with(config)
}
#[allow(dead_code)]
pub(super) fn make_node_addr(val: u8) -> NodeAddr {
let mut bytes = [0u8; 16];
@@ -66,9 +81,9 @@ pub(super) fn make_completed_connection(
let mut conn = PeerConnection::outbound(link_id, peer_identity, current_time_ms);
// Run initiator side of handshake
let our_keypair = node.identity.keypair();
let our_keypair = node.identity().keypair();
let msg1 = conn
.start_handshake(our_keypair, node.startup_epoch, current_time_ms)
.start_handshake(our_keypair, node.startup_epoch(), current_time_ms)
.unwrap();
// Run responder side to generate msg2
+7 -6
View File
@@ -1628,8 +1628,9 @@ fn test_coords_warmup_config_default() {
#[test]
fn test_identity_cache_lru_eviction() {
let mut node = make_node();
node.config.node.cache.identity_size = 2;
let mut config = crate::Config::new();
config.node.cache.identity_size = 2;
let mut node = make_node_with(config);
let id1 = Identity::generate();
let id2 = Identity::generate();
@@ -1791,7 +1792,7 @@ async fn test_session_handshake_timeout() {
let mut node = make_node();
let identity_b = Identity::generate();
let handshake = HandshakeState::new_initiator(node.identity.keypair());
let handshake = HandshakeState::new_initiator(node.identity().keypair());
let dest_addr = *identity_b.node_addr();
@@ -1808,7 +1809,7 @@ async fn test_session_handshake_timeout() {
assert!(node.sessions.contains_key(&dest_addr));
// Before timeout: session should remain
let timeout_secs = node.config.node.rate_limit.handshake_timeout_secs;
let timeout_secs = node.config().node.rate_limit.handshake_timeout_secs;
let before_timeout = 1000 + timeout_secs * 1000 - 1;
node.resend_pending_session_handshakes(before_timeout).await;
assert!(
@@ -1852,7 +1853,7 @@ async fn test_session_awaiting_msg3_timeout() {
assert!(node.sessions.contains_key(&src_addr));
// After timeout: session should be removed
let timeout_secs = node.config.node.rate_limit.handshake_timeout_secs;
let timeout_secs = node.config().node.rate_limit.handshake_timeout_secs;
let after_timeout = 1000 + timeout_secs * 1000 + 1;
node.resend_pending_session_handshakes(after_timeout).await;
assert!(
@@ -2298,7 +2299,7 @@ fn install_established_session_with_mmp(node: &mut Node, remote: &Identity) {
1000,
true,
);
entry.init_mmp(&node.config.node.session_mmp);
entry.init_mmp(&node.config().node.session_mmp);
node.sessions.insert(remote_addr, entry);
}
+21 -2
View File
@@ -30,10 +30,29 @@ pub(super) async fn make_test_node() -> TestNode {
/// Create a test node with a specific transport MTU.
pub(super) async fn make_test_node_with_mtu(mtu: u16) -> TestNode {
make_test_node_inner(Config::new(), mtu).await
}
/// Create a test node with a specific routing profile. Profile is immutable
/// (lives in the shared context), so it is set via the `Config` flags that
/// `Config::node_profile()` reads rather than poked post-construction.
pub(super) async fn make_test_node_with_profile(profile: crate::protocol::NodeProfile) -> TestNode {
use crate::protocol::NodeProfile;
let mut config = Config::new();
match profile {
NodeProfile::Leaf => config.node.leaf_only = true,
NodeProfile::NonRouting => config.node.disable_routing = true,
NodeProfile::Full => {}
}
make_test_node_inner(config, 1280).await
}
/// Shared builder: a test node from an explicit `Config` and transport MTU.
async fn make_test_node_inner(config: Config, mtu: u16) -> TestNode {
use crate::config::UdpConfig;
use crate::transport::udp::UdpTransport;
let mut node = make_node();
let mut node = make_node_with(config);
let transport_id = TransportId::new(1);
// recv_buf_size and packet_channel are sized for large-network harness
@@ -90,7 +109,7 @@ pub(super) async fn initiate_handshake(nodes: &mut [TestNode], i: usize, j: usiz
let our_index = initiator.node.index_allocator.allocate().unwrap();
let our_keypair = initiator.node.identity().keypair();
let noise_msg1 = conn
.start_handshake(our_keypair, initiator.node.startup_epoch, 1000)
.start_handshake(our_keypair, initiator.node.startup_epoch(), 1000)
.unwrap();
conn.set_our_index(our_index);
conn.set_transport_id(transport_id);
+32 -37
View File
@@ -250,8 +250,7 @@ fn test_node_link_management() {
#[test]
fn test_node_link_limit() {
let mut node = make_node();
node.set_max_links(2);
let mut node = make_node_with_max_links(2);
for i in 0..2 {
let link_id = node.allocate_link_id();
@@ -383,9 +382,8 @@ fn test_node_cross_connection_resolution() {
#[test]
fn test_node_peer_limit() {
let mut node = make_node();
let mut node = make_node_with_max_peers(2);
let transport_id = TransportId::new(1);
node.set_max_peers(2);
// Add two peers via promotion
for i in 0..2 {
@@ -601,9 +599,9 @@ fn test_promote_cleans_up_pending_outbound_to_same_peer() {
let mut pending_conn =
PeerConnection::outbound(pending_link_id, peer_b_identity, pending_time_ms);
let our_keypair = node.identity.keypair();
let our_keypair = node.identity().keypair();
let _msg1 = pending_conn
.start_handshake(our_keypair, node.startup_epoch, pending_time_ms)
.start_handshake(our_keypair, node.startup_epoch(), pending_time_ms)
.unwrap();
let pending_index = node.index_allocator.allocate().unwrap();
@@ -640,9 +638,9 @@ fn test_promote_cleans_up_pending_outbound_to_same_peer() {
let mut completing_conn =
PeerConnection::outbound(completing_link_id, peer_b_identity, completing_time_ms);
let our_keypair = node.identity.keypair();
let our_keypair = node.identity().keypair();
let msg1 = completing_conn
.start_handshake(our_keypair, node.startup_epoch, completing_time_ms)
.start_handshake(our_keypair, node.startup_epoch(), completing_time_ms)
.unwrap();
// B responds
@@ -989,7 +987,7 @@ fn active_peer_same_path_discovery_refreshes_stale_peer() {
let transport_id = TransportId::new(1);
let current_addr = TransportAddr::from_string("127.0.0.1:9");
let stale_at = Node::now_ms().saturating_sub(
node.config
node.config()
.node
.heartbeat_interval_secs
.saturating_add(1)
@@ -1040,7 +1038,20 @@ async fn node_context_mirrors_config_and_immutable_facades() {
#[tokio::test]
async fn update_peers_races_new_alternative_without_dropping_active_peer() {
let mut node = make_node();
// The node's *current* (pre-update) peer set must contain `old_peer`, so it
// is baked into the Config at construction (immutable context = sole store).
let peer_full = Identity::generate();
let old_peer = crate::config::PeerConfig {
npub: peer_full.npub(),
alias: None,
addresses: vec![crate::config::PeerAddress::new("udp", "127.0.0.1:9")],
connect_policy: crate::config::ConnectPolicy::AutoConnect,
auto_reconnect: true,
via_nostr: false,
};
let mut config = Config::new();
config.peers = vec![old_peer.clone()];
let mut node = make_node_with(config);
let (packet_tx, packet_rx) = packet_channel(64);
node.packet_tx = Some(packet_tx.clone());
node.packet_rx = Some(packet_rx);
@@ -1059,7 +1070,6 @@ async fn update_peers_races_new_alternative_without_dropping_active_peer() {
node.transports
.insert(transport_id, TransportHandle::Udp(udp));
let peer_full = Identity::generate();
let peer_identity = PeerIdentity::from_pubkey_full(peer_full.pubkey_full());
let peer_node_addr = *peer_identity.node_addr();
let current_addr = TransportAddr::from_string("127.0.0.1:9");
@@ -1079,14 +1089,6 @@ async fn update_peers_races_new_alternative_without_dropping_active_peer() {
),
);
let old_peer = crate::config::PeerConfig {
npub: peer_full.npub(),
alias: None,
addresses: vec![crate::config::PeerAddress::new("udp", "127.0.0.1:9")],
connect_policy: crate::config::ConnectPolicy::AutoConnect,
auto_reconnect: true,
via_nostr: false,
};
let new_peer = crate::config::PeerConfig {
addresses: vec![
crate::config::PeerAddress::new("udp", "127.0.0.1:9"),
@@ -1094,7 +1096,6 @@ async fn update_peers_races_new_alternative_without_dropping_active_peer() {
],
..old_peer.clone()
};
node.config.peers = vec![old_peer];
let outcome = node.update_peers(vec![new_peer]).await.unwrap();
@@ -1260,8 +1261,8 @@ fn test_schedule_reconnect_preserves_backoff() {
);
// With count=3, backoff should be 5s * 2^3 = 40s.
let base_ms = node.config.node.retry.base_interval_secs * 1000;
let max_ms = node.config.node.retry.max_backoff_secs * 1000;
let base_ms = node.config().node.retry.base_interval_secs * 1000;
let max_ms = node.config().node.retry.max_backoff_secs * 1000;
let expected_delay = state.backoff_ms(base_ms, max_ms);
assert_eq!(
state.retry_after_ms,
@@ -1296,8 +1297,8 @@ fn test_schedule_reconnect_fresh_state() {
"Fresh reconnect should start at count=0"
);
// Base delay: 5s * 2^0 = 5s
let base_ms = node.config.node.retry.base_interval_secs * 1000;
let max_ms = node.config.node.retry.max_backoff_secs * 1000;
let base_ms = node.config().node.retry.base_interval_secs * 1000;
let max_ms = node.config().node.retry.max_backoff_secs * 1000;
let expected_delay = state.backoff_ms(base_ms, max_ms);
assert_eq!(state.retry_after_ms, 1_000 + expected_delay);
}
@@ -1628,8 +1629,7 @@ fn inject_dummy_peers(node: &mut Node, count: usize) {
#[test]
fn outbound_admission_check_direct() {
// max_peers cap honored: above-cap returns false, below-cap returns true.
let mut node = make_node();
node.set_max_peers(3);
let mut node = make_node_with_max_peers(3);
assert!(node.outbound_admission_check(), "0/3 should be admissible");
inject_dummy_peers(&mut node, 2);
@@ -1646,8 +1646,7 @@ fn outbound_admission_check_direct() {
);
// No-cap sentinel: max_peers == 0 admits unconditionally.
let mut uncapped = make_node();
uncapped.set_max_peers(0);
let mut uncapped = make_node_with_max_peers(0);
assert!(uncapped.outbound_admission_check());
inject_dummy_peers(&mut uncapped, 50);
assert!(
@@ -1658,8 +1657,7 @@ fn outbound_admission_check_direct() {
#[tokio::test]
async fn process_pending_retries_gated_at_capacity() {
let mut node = make_node();
node.set_max_peers(2);
let mut node = make_node_with_max_peers(2);
inject_dummy_peers(&mut node, 2);
// Queue a retry that would otherwise be due.
@@ -1717,8 +1715,7 @@ async fn poll_nostr_discovery_established_gated_at_capacity() {
use crate::discovery::EstablishedTraversal;
use std::net::UdpSocket;
let mut node = make_node();
node.set_max_peers(2);
let mut node = make_node_with_max_peers(2);
inject_dummy_peers(&mut node, 2);
let bootstrap = Arc::new(NostrDiscovery::new_for_test());
@@ -1797,7 +1794,7 @@ async fn craft_and_send_msg1(
use crate::node::wire::build_msg1;
use crate::utils::index::SessionIndex;
let peer_b_identity = PeerIdentity::from_pubkey_full(node_b.identity.pubkey_full());
let peer_b_identity = PeerIdentity::from_pubkey_full(node_b.identity().pubkey_full());
let sender_pubkey_id = PeerIdentity::from_pubkey_full(sender_identity.pubkey_full());
let sender_node_addr = *sender_pubkey_id.node_addr();
@@ -1859,8 +1856,7 @@ async fn handle_msg1_silent_drops_at_cap_for_new_peer() {
use crate::config::UdpConfig;
use tokio::time::{Duration, timeout};
let mut node = make_node();
node.set_max_peers(2);
let mut node = make_node_with_max_peers(2);
inject_dummy_peers(&mut node, 2);
assert_eq!(node.peer_count(), 2, "precondition: at cap");
@@ -1941,8 +1937,7 @@ async fn handle_msg1_silent_drops_at_cap_for_new_peer() {
async fn handle_msg1_admits_existing_peer_at_cap() {
use crate::config::UdpConfig;
let mut node = make_node();
node.set_max_peers(2);
let mut node = make_node_with_max_peers(2);
inject_dummy_peers(&mut node, 1);
+1 -1
View File
@@ -37,7 +37,7 @@ impl Node {
&mut self,
peer_addr: &NodeAddr,
) -> Result<(), NodeError> {
if self.node_profile == crate::protocol::NodeProfile::Leaf {
if self.node_profile() == crate::protocol::NodeProfile::Leaf {
return Ok(());
}
let now_ms = std::time::SystemTime::now()
+15
View File
@@ -233,6 +233,21 @@ run_build() {
record "clippy" 1
return 1
fi
# Guard: the effectively-immutable state lives solely in NodeContext. The
# Node struct must not re-declare a bundled field (config/identity/
# startup_epoch/started_at/is_leaf_only/node_profile/max_*) — a shadow field
# would silently reopen dual-store divergence between the struct and the
# context. Checks the struct *declaration*, so it is wrap-insensitive.
info "node-context single-store guard"
if awk '/^pub struct Node \{/,/^\}/' src/node/mod.rs \
| grep -qE '^[[:space:]]+(config|identity|startup_epoch|started_at|is_leaf_only|node_profile|max_connections|max_peers|max_links):'; then
fail "Node struct re-declares a bundled immutable field; it must live solely in NodeContext"
record "node-context-guard" 1
return 1
else
record "node-context-guard" 0
fi
}
# ── Stage 2: Unit Tests ───────────────────────────────────────────────────