Skip to main content

snarkos_node_network/
peer.rs

1// Copyright (c) 2019-2026 Provable Inc.
2// This file is part of the snarkOS library.
3
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at:
7
8// http://www.apache.org/licenses/LICENSE-2.0
9
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16use crate::NodeType;
17use snarkvm::prelude::{Address, FromBytes, Network, ToBytes, error};
18use tracing::*;
19
20use std::{fmt, io, net::SocketAddr, time::Instant};
21
22/// A peer of any connection status.
23#[derive(Clone, Debug)]
24pub enum Peer<N: Network> {
25    /// A candidate peer that's currently not connected to.
26    Candidate(CandidatePeer<N>),
27    /// A peer that's currently being connected to (the handshake is in progress).
28    Connecting(ConnectingPeer),
29    /// A fully connected (post-handshake) peer.
30    Connected(ConnectedPeer<N>),
31}
32
33/// A connecting peer.
34#[derive(Clone, Debug)]
35pub struct ConnectingPeer {
36    /// The listening address of a connecting peer.
37    pub listener_addr: SocketAddr,
38    /// Indicates whether the peer is considered trusted.
39    pub trusted: bool,
40    /// The last time we attempted to connect to the peer.
41    pub last_connection_attempt: Option<Instant>,
42    /// The total number of connection attempts, since the peer was last connected.
43    pub total_connection_attempts: u32,
44}
45
46/// A candidate peer.
47#[derive(Clone, Debug)]
48pub struct CandidatePeer<N: Network> {
49    /// The listening address of a candidate peer.
50    pub listener_addr: SocketAddr,
51    /// Indicates whether the peer is considered trusted.
52    pub trusted: bool,
53    /// The latest block height known to be associated with the peer.
54    pub last_height_seen: Option<u32>,
55    /// The last time we attempted to connect to the peer.
56    /// `None` if there was no attempt to connect since the peer was last connected, or no attempt at all.
57    pub last_connection_attempt: Option<Instant>,
58    /// The total number of connection attempts, since the peer was last connected.
59    pub total_connection_attempts: u32,
60    /// The last known Aleo address of this peer, carried over from a prior connection.
61    /// Used to detect when a validator reconnects from a different IP address.
62    pub last_known_aleo_addr: Option<Address<N>>,
63}
64
65/// A fully connected peer.
66#[derive(Clone, Debug)]
67pub struct ConnectedPeer<N: Network> {
68    /// The listener address of the peer.
69    pub listener_addr: SocketAddr,
70    /// The connected address of the peer.
71    pub connected_addr: SocketAddr,
72    /// Indicates whether this is a Router or a Gateway connection for the peer.
73    pub connection_mode: ConnectionMode,
74    /// Indicates whether the peer is considered trusted.
75    pub trusted: bool,
76    /// The Aleo address of the peer.
77    pub aleo_addr: Address<N>,
78    /// The node type of the peer.
79    pub node_type: NodeType,
80    /// The message version of the peer.
81    pub version: u32,
82    /// The snarkOS commit hash of the peer.
83    pub snarkos_sha: Option<[u8; 40]>,
84    /// The latest block height known to be associated with the peer.
85    pub last_height_seen: Option<u32>,
86    /// The timestamp of the first message received from the peer.
87    pub first_seen: Instant,
88    /// The timestamp of the last message received from this peer.
89    pub last_seen: Instant,
90}
91
92/// Indicates whether a peer is connected via the Gateway or the Router.
93///
94/// The mode is serialized as the first thing a Noise handshake's first message carries, because a
95/// node accepting more than one subprotocol on a single listener - the bootstrap clients take both
96/// the gateway's and the router's connections - has to know which payload it is looking at before it
97/// can parse it. The marker preceding the pattern cannot say, being shared, and neither can the node
98/// type, since a validator connects in both modes at once.
99///
100/// It is unauthenticated there, but does not need to be trusted: each subprotocol signs under its
101/// own binding domain, so a peer that claims one mode and then speaks another fails signature
102/// verification.
103#[derive(Clone, Copy, Debug, PartialEq, Eq)]
104#[repr(u8)]
105pub enum ConnectionMode {
106    Gateway = 0,
107    Router = 1,
108}
109
110impl ToBytes for ConnectionMode {
111    fn write_le<W: io::Write>(&self, writer: W) -> io::Result<()> {
112        (*self as u8).write_le(writer)
113    }
114}
115
116impl FromBytes for ConnectionMode {
117    fn read_le<R: io::Read>(reader: R) -> io::Result<Self> {
118        match u8::read_le(reader)? {
119            0 => Ok(Self::Gateway),
120            1 => Ok(Self::Router),
121            x => Err(error(format!("Invalid connection mode: expected 0..=1, got {x}."))),
122        }
123    }
124}
125
126impl fmt::Display for ConnectionMode {
127    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128        match self {
129            ConnectionMode::Gateway => write!(f, "Gateway"),
130            ConnectionMode::Router => write!(f, "Router"),
131        }
132    }
133}
134
135impl<N: Network> Peer<N> {
136    /// Create a candidate peer.
137    pub fn new_candidate(listener_addr: SocketAddr, trusted: bool) -> Self {
138        Self::Candidate(CandidatePeer {
139            listener_addr,
140            trusted,
141            last_height_seen: None,
142            last_connection_attempt: None,
143            total_connection_attempts: 0,
144            last_known_aleo_addr: None,
145        })
146    }
147
148    /// Create a connecting peer, with no prior connection attempts on record.
149    pub const fn new_connecting(listener_addr: SocketAddr, trusted: bool) -> Self {
150        Self::Connecting(ConnectingPeer {
151            listener_addr,
152            trusted,
153            last_connection_attempt: None,
154            total_connection_attempts: 0,
155        })
156    }
157
158    /// Promote a candidate peer to connecting status, preserving its connection-attempt metadata.
159    pub fn promote_to_connecting(&mut self) {
160        let listener_addr = self.listener_addr();
161        let trusted = self.is_trusted();
162
163        let (last_connection_attempt, total_connection_attempts) = match self {
164            Self::Candidate(peer) => (peer.last_connection_attempt, peer.total_connection_attempts),
165            Self::Connecting(_) | Self::Connected(_) => {
166                warn!(
167                    "Peer '{listener_addr}' is being promoted to Connecting, but is {}",
168                    if self.is_connected() { "already Connected" } else { "already Connecting" }
169                );
170                (None, 0)
171            }
172        };
173
174        *self = Self::Connecting(ConnectingPeer {
175            listener_addr,
176            trusted,
177            last_connection_attempt,
178            total_connection_attempts,
179        });
180    }
181
182    /// Promote a connecting peer to a fully connected one.
183    #[allow(clippy::too_many_arguments)]
184    pub fn upgrade_to_connected(
185        &mut self,
186        connected_addr: SocketAddr,
187        listener_port: u16,
188        aleo_address: Address<N>,
189        node_type: NodeType,
190        node_version: u32,
191        snarkos_sha: Option<[u8; 40]>,
192        connection_mode: ConnectionMode,
193    ) {
194        let timestamp = Instant::now();
195        let listener_addr = SocketAddr::from((connected_addr.ip(), listener_port));
196
197        // Logic check: this can only happen during the handshake. This isn't a fatal
198        // error, but should not be triggered.
199        if !matches!(self, Self::Connecting(_)) {
200            warn!(
201                "Peer '{listener_addr}' is being upgraded to Connected, but isn't Connecting \
202                - it is {}",
203                if self.is_connected() { "already Connected" } else { "only a Candidate" }
204            );
205        }
206
207        *self = Self::Connected(ConnectedPeer {
208            listener_addr,
209            connected_addr,
210            connection_mode,
211            aleo_addr: aleo_address,
212            node_type,
213            trusted: self.is_trusted(),
214            version: node_version,
215            snarkos_sha,
216            last_height_seen: None,
217            first_seen: timestamp,
218            last_seen: timestamp,
219        });
220    }
221
222    /// Demote a peer to candidate status, marking it as disconnected.
223    pub fn downgrade_to_candidate(&mut self, listener_addr: SocketAddr) {
224        let last_known_aleo_addr = match self {
225            Self::Connected(p) => Some(p.aleo_addr),
226            _ => None,
227        };
228
229        // Preserve the connection attempt info if the handshake didn't succeed.
230        let (last_connection_attempt, total_connection_attempts) = match self {
231            Self::Candidate(p) => (p.last_connection_attempt, p.total_connection_attempts),
232            Self::Connecting(p) => (p.last_connection_attempt, p.total_connection_attempts),
233            Self::Connected(_) => (None, 0),
234        };
235
236        *self = Self::Candidate(CandidatePeer {
237            listener_addr,
238            trusted: self.is_trusted(),
239            last_height_seen: self.last_height_seen(),
240            last_connection_attempt,
241            total_connection_attempts,
242            last_known_aleo_addr,
243        });
244    }
245
246    /// Returns the type of the node (only applicable to connected peers).
247    pub fn node_type(&self) -> Option<NodeType> {
248        match self {
249            Self::Candidate(_) => None,
250            Self::Connecting(_) => None,
251            Self::Connected(peer) => Some(peer.node_type),
252        }
253    }
254
255    /// The listener (public) address of this peer.
256    pub fn listener_addr(&self) -> SocketAddr {
257        match self {
258            Self::Candidate(p) => p.listener_addr,
259            Self::Connecting(p) => p.listener_addr,
260            Self::Connected(p) => p.listener_addr,
261        }
262    }
263
264    /// The listener (public) address of this peer.
265    pub fn last_height_seen(&self) -> Option<u32> {
266        match self {
267            Self::Candidate(_) => None,
268            Self::Connecting(_) => None,
269            Self::Connected(peer) => peer.last_height_seen,
270        }
271    }
272
273    /// The number of connection attempts made since this peer was last connected.
274    pub fn failed_connection_attempts(&self) -> u32 {
275        match self {
276            Self::Candidate(peer) => peer.total_connection_attempts,
277            Self::Connecting(peer) => peer.total_connection_attempts,
278            Self::Connected(_) => 0,
279        }
280    }
281
282    /// Returns `true` if the peer is not connected or connecting.
283    pub fn is_candidate(&self) -> bool {
284        matches!(self, Peer::Candidate(_))
285    }
286
287    /// Returns `true` if the peer is currently undergoing the network handshake.
288    pub fn is_connecting(&self) -> bool {
289        matches!(self, Peer::Connecting(_))
290    }
291
292    /// Returns `true` if the peer has concluded the network handshake.
293    pub fn is_connected(&self) -> bool {
294        matches!(self, Peer::Connected(_))
295    }
296
297    /// Returns `true` if the peer is considered trusted.
298    pub fn is_trusted(&self) -> bool {
299        match self {
300            Self::Candidate(peer) => peer.trusted,
301            Self::Connecting(peer) => peer.trusted,
302            Self::Connected(peer) => peer.trusted,
303        }
304    }
305
306    /// Updates the peer's `last_seen` timestamp.
307    pub fn update_last_seen(&mut self) {
308        if let Self::Connected(ConnectedPeer { last_seen, .. }) = self {
309            *last_seen = Instant::now();
310        }
311    }
312
313    /// Returns a reference to the underlying `ConnectedPeer` if it is connedcted,
314    /// otherwise `None`.
315    pub fn as_connected(&self) -> Option<&ConnectedPeer<N>> {
316        match self {
317            Self::Connected(peer) => Some(peer),
318            _ => None,
319        }
320    }
321}
322
323impl<N: Network> ConnectedPeer<N> {
324    /// Returns `true` if this peer is validator.
325    pub fn is_validator(&self) -> bool {
326        self.node_type == NodeType::Validator
327    }
328}