From 13f7146bb1403d3718271812bf2d56b77d0623c8 Mon Sep 17 00:00:00 2001 From: ljedrz Date: Thu, 11 Jun 2026 15:21:39 +0200 Subject: [PATCH 1/4] logs: fix a spelling error Signed-off-by: ljedrz --- node/bft/src/gateway.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/bft/src/gateway.rs b/node/bft/src/gateway.rs index 9ba9a0fc24..e273cdf9d2 100644 --- a/node/bft/src/gateway.rs +++ b/node/bft/src/gateway.rs @@ -1076,7 +1076,7 @@ impl Gateway { .collect(); if !handles.is_empty() { - info!("Reconnnecting to {} out of {} trusted validators", handles.len(), trusted_peers.len()); + info!("Reconnecting to {} out of {} trusted validators", handles.len(), trusted_peers.len()); } } From 41b3fbdb85d10817eee6180054253c281947f1b6 Mon Sep 17 00:00:00 2001 From: ljedrz Date: Thu, 11 Jun 2026 15:22:06 +0200 Subject: [PATCH 2/4] feat: record last known Aleo address for candidate peers Signed-off-by: ljedrz --- node/network/src/peer.rs | 16 +++++++++++++--- node/network/src/peering.rs | 4 ++-- node/router/src/heartbeat.rs | 2 +- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/node/network/src/peer.rs b/node/network/src/peer.rs index 98a3185fab..28ff9d79f4 100644 --- a/node/network/src/peer.rs +++ b/node/network/src/peer.rs @@ -23,7 +23,7 @@ use std::{fmt, net::SocketAddr, time::Instant}; #[derive(Clone, Debug)] pub enum Peer { /// A candidate peer that's currently not connected to. - Candidate(CandidatePeer), + Candidate(CandidatePeer), /// A peer that's currently being connected to (the handshake is in progress). Connecting(ConnectingPeer), /// A fully connected (post-handshake) peer. @@ -41,7 +41,7 @@ pub struct ConnectingPeer { /// A candidate peer. #[derive(Clone, Debug)] -pub struct CandidatePeer { +pub struct CandidatePeer { /// The listening address of a candidate peer. pub listener_addr: SocketAddr, /// Indicates whether the peer is considered trusted. @@ -53,6 +53,9 @@ pub struct CandidatePeer { pub last_connection_attempt: Option, /// The total number of connection attempts, since the peer was last connected. pub total_connection_attempts: u32, + /// The last known Aleo address of this peer, carried over from a prior connection. + /// Used to detect when a validator reconnects from a different IP address. + pub last_known_aleo_addr: Option>, } /// A fully connected peer. @@ -100,13 +103,14 @@ impl fmt::Display for ConnectionMode { impl Peer { /// Create a candidate peer. - pub const fn new_candidate(listener_addr: SocketAddr, trusted: bool) -> Self { + pub fn new_candidate(listener_addr: SocketAddr, trusted: bool) -> Self { Self::Candidate(CandidatePeer { listener_addr, trusted, last_height_seen: None, last_connection_attempt: None, total_connection_attempts: 0, + last_known_aleo_addr: None, }) } @@ -157,12 +161,18 @@ impl Peer { /// Demote a peer to candidate status, marking it as disconnected. pub fn downgrade_to_candidate(&mut self, listener_addr: SocketAddr) { + let last_known_aleo_addr = match self { + Self::Connected(p) => Some(p.aleo_addr), + _ => None, + }; + *self = Self::Candidate(CandidatePeer { listener_addr, trusted: self.is_trusted(), last_height_seen: self.last_height_seen(), last_connection_attempt: None, total_connection_attempts: 0, + last_known_aleo_addr, }); } diff --git a/node/network/src/peering.rs b/node/network/src/peering.rs index 5c9dffdc67..dd422b49fa 100644 --- a/node/network/src/peering.rs +++ b/node/network/src/peering.rs @@ -622,7 +622,7 @@ pub trait PeerPoolHandling: P2P { } /// Returns the list of candidate peers. - fn get_candidate_peers(&self) -> Vec { + fn get_candidate_peers(&self) -> Vec> { self.peer_pool() .read() .values() @@ -631,7 +631,7 @@ pub trait PeerPoolHandling: P2P { } /// Returns the list of trusted candidate peers. - fn get_trusted_candidate_peers(&self) -> Vec { + fn get_trusted_candidate_peers(&self) -> Vec> { self.peer_pool() .read() .values() diff --git a/node/router/src/heartbeat.rs b/node/router/src/heartbeat.rs index 2e44a29187..9b53d286bc 100644 --- a/node/router/src/heartbeat.rs +++ b/node/router/src/heartbeat.rs @@ -320,7 +320,7 @@ pub trait Heartbeat: Outbound { /// Helper function that attempts to connect the given peers. /// /// Used by [`Self::handle_trusted_peers`] and [`Self::handle_connected_peers`]. - async fn try_connect_to_peers(&self, peers: impl Iterator + Send + 'static) { + async fn try_connect_to_peers(&self, peers: impl Iterator> + Send + 'static) { let (peer_info, hdls): (Vec<_>, Vec<_>) = peers .filter_map(|peer| { let peer_type = if peer.trusted { "trusted peer" } else { "peer" }; From 891d9635b7a4fd53cca3e539cdd92c7dc8a9edb6 Mon Sep 17 00:00:00 2001 From: ljedrz Date: Thu, 11 Jun 2026 15:26:12 +0200 Subject: [PATCH 3/4] fmt: move a block of tests below non-test code Signed-off-by: ljedrz --- node/network/src/peering.rs | 326 ++++++++++++++++++------------------ 1 file changed, 163 insertions(+), 163 deletions(-) diff --git a/node/network/src/peering.rs b/node/network/src/peering.rs index dd422b49fa..2278bbdb1f 100644 --- a/node/network/src/peering.rs +++ b/node/network/src/peering.rs @@ -15,169 +15,6 @@ use crate::{CandidatePeer, ConnectedPeer, ConnectionMode, NodeType, Peer, Resolver}; -#[cfg(test)] -mod tests { - use super::*; - use crate::Peer; - use snarkos_node_tcp::{Config, P2P, Tcp}; - use snarkvm::{prelude::Rng, utilities::TestRng}; - - use std::{collections::HashMap, net::SocketAddr, time::Instant}; - - type CurrentNetwork = snarkvm::prelude::MainnetV0; - - struct MockPeerPool { - tcp: Tcp, - peer_pool: RwLock>>, - resolver: RwLock>, - } - - impl MockPeerPool { - fn new() -> Self { - let config = Config { listener_ip: None, ..Default::default() }; - Self { tcp: Tcp::new(config), peer_pool: Default::default(), resolver: Default::default() } - } - } - - impl P2P for MockPeerPool { - fn tcp(&self) -> &Tcp { - &self.tcp - } - } - - impl PeerPoolHandling for MockPeerPool { - const MAXIMUM_POOL_SIZE: usize = 100; - const OWNER: &str = "MockPeerPool"; - const PEER_SLASHING_COUNT: usize = 10; - - fn peer_pool(&self) -> &RwLock>> { - &self.peer_pool - } - - fn resolver(&self) -> &RwLock> { - &self.resolver - } - - fn is_dev(&self) -> bool { - false - } - - fn trusted_peers_only(&self) -> bool { - false - } - - fn node_type(&self) -> NodeType { - NodeType::Client - } - } - - fn make_connected_peer(port: u16, node_type: NodeType, rng: &mut TestRng) -> (SocketAddr, Peer) { - use snarkvm::prelude::Address; - let listener_addr = SocketAddr::from(([127, 0, 0, 1], port)); - let connected_addr = SocketAddr::from(([127, 0, 0, 1], port + 10000)); - let now = Instant::now(); - let peer = Peer::Connected(ConnectedPeer { - listener_addr, - connected_addr, - connection_mode: ConnectionMode::Router, - trusted: false, - aleo_addr: Address::::new(rng.random()), - node_type, - version: 1, - snarkos_sha: None, - last_height_seen: None, - first_seen: now, - last_seen: now, - }); - (listener_addr, peer) - } - - #[test] - fn test_peer_state_transitions() { - use snarkvm::prelude::Address; - - let pool = MockPeerPool::::new(); - let mut rng = TestRng::default(); - - let listener_addr = SocketAddr::from(([192, 0, 2, 1], 4000)); - let connected_addr = SocketAddr::from(([192, 0, 2, 1], 14000)); - let aleo_addr = Address::::new(rng.random()); - - // Step 1: insert as a candidate. - pool.peer_pool().write().insert(listener_addr, Peer::new_candidate(listener_addr, false)); - - assert_eq!(pool.number_of_candidate_peers(), 1); - assert_eq!(pool.number_of_connecting_peers(), Some(0)); - assert_eq!(pool.number_of_connected_peers(), 0); - assert!(!pool.is_connecting(listener_addr)); - assert!(!pool.is_connected(listener_addr)); - - // Step 2: promote to connecting. - assert!(pool.add_connecting_peer(listener_addr).is_ok()); - - assert_eq!(pool.number_of_candidate_peers(), 0); - assert_eq!(pool.number_of_connecting_peers(), Some(1)); - assert_eq!(pool.number_of_connected_peers(), 0); - assert!(pool.is_connecting(listener_addr)); - assert!(!pool.is_connected(listener_addr)); - - // Step 3: complete the handshake — upgrade to connected. - pool.peer_pool().write().get_mut(&listener_addr).unwrap().upgrade_to_connected( - connected_addr, - listener_addr.port(), - aleo_addr, - NodeType::Validator, - 1, - None, - ConnectionMode::Router, - ); - - assert_eq!(pool.number_of_candidate_peers(), 0); - assert_eq!(pool.number_of_connecting_peers(), Some(0)); - assert_eq!(pool.number_of_connected_peers(), 1); - assert!(!pool.is_connecting(listener_addr)); - assert!(pool.is_connected(listener_addr)); - assert_eq!(pool.number_of_connected_validators(), Some(1)); - - // Verify the connected peer's fields. - let connected = pool.get_connected_peer(listener_addr).expect("peer should be connected"); - assert_eq!(connected.listener_addr, listener_addr); - assert_eq!(connected.connected_addr, connected_addr); - assert_eq!(connected.aleo_addr, aleo_addr); - assert_eq!(connected.node_type, NodeType::Validator); - } - - #[test] - fn test_number_of_connected_validators() { - let pool = MockPeerPool::::new(); - let mut rng = TestRng::default(); - - // Empty pool: no validators. - assert_eq!(pool.number_of_connected_validators(), Some(0)); - - // Insert 2 validators and 1 client. - let (addr1, peer1) = make_connected_peer(3000, NodeType::Validator, &mut rng); - let (addr2, peer2) = make_connected_peer(3001, NodeType::Validator, &mut rng); - let (addr3, peer3) = make_connected_peer(3002, NodeType::Client, &mut rng); - { - let mut pool_write = pool.peer_pool().write(); - pool_write.insert(addr1, peer1); - pool_write.insert(addr2, peer2); - pool_write.insert(addr3, peer3); - } - - assert_eq!(pool.number_of_connected_validators(), Some(2)); - assert_eq!(pool.number_of_connected_peers(), 3); - - // A candidate peer should not be counted as a validator. - let candidate_addr = SocketAddr::from(([127, 0, 0, 1], 3003)); - pool.peer_pool().write().insert(candidate_addr, Peer::new_candidate(candidate_addr, false)); - - assert_eq!(pool.number_of_connected_validators(), Some(2)); - assert_eq!(pool.number_of_connected_peers(), 3); - } -} - use snarkos_node_tcp::{ConnectError, P2P, is_bogon_ip, is_unspecified_or_broadcast_ip}; use snarkvm::prelude::{Address, Network}; @@ -771,3 +608,166 @@ pub trait PeerPoolHandling: P2P { self.tcp().banned_peers().update_ip_ban(ip); } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::Peer; + use snarkos_node_tcp::{Config, P2P, Tcp}; + use snarkvm::{prelude::Rng, utilities::TestRng}; + + use std::{collections::HashMap, net::SocketAddr, time::Instant}; + + type CurrentNetwork = snarkvm::prelude::MainnetV0; + + struct MockPeerPool { + tcp: Tcp, + peer_pool: RwLock>>, + resolver: RwLock>, + } + + impl MockPeerPool { + fn new() -> Self { + let config = Config { listener_ip: None, ..Default::default() }; + Self { tcp: Tcp::new(config), peer_pool: Default::default(), resolver: Default::default() } + } + } + + impl P2P for MockPeerPool { + fn tcp(&self) -> &Tcp { + &self.tcp + } + } + + impl PeerPoolHandling for MockPeerPool { + const MAXIMUM_POOL_SIZE: usize = 100; + const OWNER: &str = "MockPeerPool"; + const PEER_SLASHING_COUNT: usize = 10; + + fn peer_pool(&self) -> &RwLock>> { + &self.peer_pool + } + + fn resolver(&self) -> &RwLock> { + &self.resolver + } + + fn is_dev(&self) -> bool { + false + } + + fn trusted_peers_only(&self) -> bool { + false + } + + fn node_type(&self) -> NodeType { + NodeType::Client + } + } + + fn make_connected_peer(port: u16, node_type: NodeType, rng: &mut TestRng) -> (SocketAddr, Peer) { + use snarkvm::prelude::Address; + let listener_addr = SocketAddr::from(([127, 0, 0, 1], port)); + let connected_addr = SocketAddr::from(([127, 0, 0, 1], port + 10000)); + let now = Instant::now(); + let peer = Peer::Connected(ConnectedPeer { + listener_addr, + connected_addr, + connection_mode: ConnectionMode::Router, + trusted: false, + aleo_addr: Address::::new(rng.random()), + node_type, + version: 1, + snarkos_sha: None, + last_height_seen: None, + first_seen: now, + last_seen: now, + }); + (listener_addr, peer) + } + + #[test] + fn test_peer_state_transitions() { + use snarkvm::prelude::Address; + + let pool = MockPeerPool::::new(); + let mut rng = TestRng::default(); + + let listener_addr = SocketAddr::from(([192, 0, 2, 1], 4000)); + let connected_addr = SocketAddr::from(([192, 0, 2, 1], 14000)); + let aleo_addr = Address::::new(rng.random()); + + // Step 1: insert as a candidate. + pool.peer_pool().write().insert(listener_addr, Peer::new_candidate(listener_addr, false)); + + assert_eq!(pool.number_of_candidate_peers(), 1); + assert_eq!(pool.number_of_connecting_peers(), Some(0)); + assert_eq!(pool.number_of_connected_peers(), 0); + assert!(!pool.is_connecting(listener_addr)); + assert!(!pool.is_connected(listener_addr)); + + // Step 2: promote to connecting. + assert!(pool.add_connecting_peer(listener_addr).is_ok()); + + assert_eq!(pool.number_of_candidate_peers(), 0); + assert_eq!(pool.number_of_connecting_peers(), Some(1)); + assert_eq!(pool.number_of_connected_peers(), 0); + assert!(pool.is_connecting(listener_addr)); + assert!(!pool.is_connected(listener_addr)); + + // Step 3: complete the handshake — upgrade to connected. + pool.peer_pool().write().get_mut(&listener_addr).unwrap().upgrade_to_connected( + connected_addr, + listener_addr.port(), + aleo_addr, + NodeType::Validator, + 1, + None, + ConnectionMode::Router, + ); + + assert_eq!(pool.number_of_candidate_peers(), 0); + assert_eq!(pool.number_of_connecting_peers(), Some(0)); + assert_eq!(pool.number_of_connected_peers(), 1); + assert!(!pool.is_connecting(listener_addr)); + assert!(pool.is_connected(listener_addr)); + assert_eq!(pool.number_of_connected_validators(), Some(1)); + + // Verify the connected peer's fields. + let connected = pool.get_connected_peer(listener_addr).expect("peer should be connected"); + assert_eq!(connected.listener_addr, listener_addr); + assert_eq!(connected.connected_addr, connected_addr); + assert_eq!(connected.aleo_addr, aleo_addr); + assert_eq!(connected.node_type, NodeType::Validator); + } + + #[test] + fn test_number_of_connected_validators() { + let pool = MockPeerPool::::new(); + let mut rng = TestRng::default(); + + // Empty pool: no validators. + assert_eq!(pool.number_of_connected_validators(), Some(0)); + + // Insert 2 validators and 1 client. + let (addr1, peer1) = make_connected_peer(3000, NodeType::Validator, &mut rng); + let (addr2, peer2) = make_connected_peer(3001, NodeType::Validator, &mut rng); + let (addr3, peer3) = make_connected_peer(3002, NodeType::Client, &mut rng); + { + let mut pool_write = pool.peer_pool().write(); + pool_write.insert(addr1, peer1); + pool_write.insert(addr2, peer2); + pool_write.insert(addr3, peer3); + } + + assert_eq!(pool.number_of_connected_validators(), Some(2)); + assert_eq!(pool.number_of_connected_peers(), 3); + + // A candidate peer should not be counted as a validator. + let candidate_addr = SocketAddr::from(([127, 0, 0, 1], 3003)); + pool.peer_pool().write().insert(candidate_addr, Peer::new_candidate(candidate_addr, false)); + + assert_eq!(pool.number_of_connected_validators(), Some(2)); + assert_eq!(pool.number_of_connected_peers(), 3); + } +} From 52111617b612c6875937fcc91ecaee6841aa999a Mon Sep 17 00:00:00 2001 From: ljedrz Date: Thu, 11 Jun 2026 15:43:51 +0200 Subject: [PATCH 4/4] fix: prune outdated validator candidates Signed-off-by: ljedrz --- node/bft/src/gateway.rs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/node/bft/src/gateway.rs b/node/bft/src/gateway.rs index e273cdf9d2..cedd81c301 100644 --- a/node/bft/src/gateway.rs +++ b/node/bft/src/gateway.rs @@ -1492,7 +1492,24 @@ impl Handshake for Gateway { NodeType::Validator }; - if let Some(peer) = self.peer_pool.write().get_mut(&addr) { + let mut peer_pool = self.peer_pool.write(); + + // Validators may change their listening address, but not the Aleo address; traverse + // the peer pool, and retain previously connected (the prior Aleo address is known) + // candidate peers with the same Aleo address only if their listening address is the + // same; otherwise, it may be concluded that a known validator has changed their + // listening address, and thus the old entry should be removed as outdated. + peer_pool.retain(|_, peer| { + if let Peer::Candidate(peer) = peer + && let Some(old_aleo_addr) = peer.last_known_aleo_addr + { + old_aleo_addr != cr.address || peer.listener_addr == addr + } else { + true + } + }); + + if let Some(peer) = peer_pool.get_mut(&addr) { self.resolver.write().insert_peer(addr, peer_addr, Some(cr.address)); peer.upgrade_to_connected( peer_addr,