Skip to main content

zakura_network/peer/
error.rs

1//! Peer-related errors.
2
3use std::{borrow::Cow, sync::Arc};
4
5use thiserror::Error;
6
7use tracing_error::TracedError;
8use zakura_chain::serialization::SerializationError;
9
10use crate::{
11    protocol::external::InventoryHash,
12    zakura::{ZakuraProtocolError, ZakuraUpgradeError},
13};
14
15/// A wrapper around `Arc<PeerError>` that implements `Error`.
16///
17/// The second field is a [`NotFoundClass`] discriminant computed at construction, so callers can
18/// branch on the `notfound` kind without string-matching `Debug` output (which a variant rename
19/// would silently break, disabling the syncer's retry paths).
20#[derive(Debug, Clone)]
21pub struct SharedPeerError(Arc<TracedError<PeerError>>, NotFoundClass);
22
23/// Typed classification of `notfound`-style peer errors, computed when a [`SharedPeerError`] is
24/// constructed (the only construction path is the `From` impl below, so this stays in sync with
25/// [`PeerError`] β€” a new or renamed variant is a compile error here, not a silent regression).
26#[derive(Copy, Clone, Debug, PartialEq, Eq)]
27pub enum NotFoundClass {
28    /// [`PeerError::NotFoundResponse`] β€” a specific peer lacked the item; retry on another peer.
29    Response,
30    /// [`PeerError::NotFoundRegistry`] β€” no ready peer has it; needs fresh tips/peers.
31    Registry,
32    /// Any other error (not a `notfound`-style failure).
33    Other,
34}
35
36impl std::fmt::Display for SharedPeerError {
37    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38        // Preserves the previous `#[error(transparent)]` Display.
39        std::fmt::Display::fmt(self.0.as_ref(), f)
40    }
41}
42
43impl std::error::Error for SharedPeerError {
44    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
45        // Preserves the previous `#[error(transparent)]` source chain.
46        std::error::Error::source(self.0.as_ref())
47    }
48}
49
50impl<E> From<E> for SharedPeerError
51where
52    PeerError: From<E>,
53{
54    fn from(source: E) -> Self {
55        let inner = PeerError::from(source);
56        let class = match &inner {
57            PeerError::NotFoundResponse(_) => NotFoundClass::Response,
58            PeerError::NotFoundRegistry(_) => NotFoundClass::Registry,
59            _ => NotFoundClass::Other,
60        };
61        Self(Arc::new(TracedError::from(inner)), class)
62    }
63}
64
65impl SharedPeerError {
66    /// Returns a debug-formatted string describing the inner [`PeerError`].
67    ///
68    /// Unfortunately, [`TracedError`] makes it impossible to get a reference to the original error.
69    pub fn inner_debug(&self) -> String {
70        format!("{:?}", self.0.as_ref())
71    }
72
73    /// The `notfound`-style classification of this error, computed at construction.
74    ///
75    /// Returns `None` for errors that aren't a `notfound`-style failure.
76    pub fn not_found_class(&self) -> Option<NotFoundClass> {
77        (self.1 != NotFoundClass::Other).then_some(self.1)
78    }
79}
80
81/// An error related to peer connection handling.
82#[derive(Error, Debug)]
83#[allow(dead_code)]
84pub enum PeerError {
85    /// The remote peer closed the connection.
86    #[error("Peer closed connection")]
87    ConnectionClosed,
88
89    /// Zebra dropped the [`Connection`](crate::peer::Connection).
90    #[error("Internal connection dropped")]
91    ConnectionDropped,
92
93    /// Zebra dropped the [`Client`](crate::peer::Client).
94    #[error("Internal client dropped")]
95    ClientDropped,
96
97    /// A [`Client`](crate::peer::Client)'s internal connection task exited.
98    #[error("Internal peer connection task exited")]
99    ConnectionTaskExited,
100
101    /// Zebra's [`Client`](crate::peer::Client) cancelled its heartbeat task.
102    #[error("Internal client cancelled its heartbeat task")]
103    ClientCancelledHeartbeatTask,
104
105    /// Zebra's internal heartbeat task exited.
106    #[error("Internal heartbeat task exited with message: {0:?}")]
107    HeartbeatTaskExited(String),
108
109    /// Sending a message to a remote peer took too long.
110    #[error("Sending Client request timed out")]
111    ConnectionSendTimeout,
112
113    /// Receiving a response to a [`Client`](crate::peer::Client) request took too long.
114    #[error("Receiving client response timed out")]
115    ConnectionReceiveTimeout,
116
117    /// A serialization error occurred while reading or writing a message.
118    #[error("Serialization error: {0}")]
119    Serialization(#[from] SerializationError),
120
121    /// A badly-behaved remote peer sent a handshake message after the handshake was
122    /// already complete.
123    #[error("Remote peer sent handshake messages after handshake")]
124    DuplicateHandshake,
125
126    /// This node's internal services were overloaded, so the connection was dropped
127    /// to shed load.
128    #[error("Internal services over capacity")]
129    Overloaded,
130
131    /// There are no ready remote peers.
132    #[error("No ready peers available")]
133    NoReadyPeers,
134
135    /// This peer request's caused an internal service timeout, so the connection was dropped
136    /// to shed load or prevent attacks.
137    #[error("Internal services timed out")]
138    InboundTimeout,
139
140    /// This node's internal services are no longer able to service requests.
141    #[error("Internal services have failed or shutdown")]
142    ServiceShutdown,
143
144    /// We requested data, but the peer replied with a `notfound` message.
145    /// (Or it didn't respond before the request finished.)
146    ///
147    /// This error happens when the peer doesn't have any of the requested data,
148    /// so that the original request can be retried.
149    ///
150    /// This is a temporary error.
151    ///
152    /// Zebra can try different peers if the request is retried,
153    /// or peers can download and verify the missing data.
154    ///
155    /// If the peer has some of the data, the request returns an [`Ok`] response,
156    /// with any `notfound` data is marked as [`Missing`][1].
157    ///
158    /// [1]: crate::protocol::internal::InventoryResponse::Missing
159    #[error("Remote peer could not find any of the items: {0:?}")]
160    NotFoundResponse(Vec<InventoryHash>),
161
162    /// We requested data, but all our ready peers are marked as recently
163    /// [`Missing`][1] that data in our local inventory registry.
164    ///
165    /// This is a temporary error.
166    ///
167    /// Peers with the inventory can finish their requests and become ready, or
168    /// other peers can download and verify the missing data.
169    ///
170    /// # Correctness
171    ///
172    /// This error is produced using Zebra's local inventory registry, without
173    /// contacting any peers.
174    ///
175    /// Client responses containing this error must not be used to update the
176    /// inventory registry. This makes sure that we eventually expire our local
177    /// cache of missing inventory, and send requests to peers again.
178    ///
179    /// [1]: crate::protocol::internal::InventoryResponse::Missing
180    #[error("All ready peers are registered as recently missing these items: {0:?}")]
181    NotFoundRegistry(Vec<InventoryHash>),
182}
183
184impl PeerError {
185    /// Returns the Zebra internal handler type as a string.
186    pub fn kind(&self) -> Cow<'static, str> {
187        match self {
188            PeerError::ConnectionClosed => "ConnectionClosed".into(),
189            PeerError::ConnectionDropped => "ConnectionDropped".into(),
190            PeerError::ClientDropped => "ClientDropped".into(),
191            PeerError::ClientCancelledHeartbeatTask => "ClientCancelledHeartbeatTask".into(),
192            PeerError::HeartbeatTaskExited(_) => "HeartbeatTaskExited".into(),
193            PeerError::ConnectionTaskExited => "ConnectionTaskExited".into(),
194            PeerError::ConnectionSendTimeout => "ConnectionSendTimeout".into(),
195            PeerError::ConnectionReceiveTimeout => "ConnectionReceiveTimeout".into(),
196            // TODO: add error kinds or summaries to `SerializationError`
197            PeerError::Serialization(inner) => format!("Serialization({inner})").into(),
198            PeerError::DuplicateHandshake => "DuplicateHandshake".into(),
199            PeerError::Overloaded => "Overloaded".into(),
200            PeerError::NoReadyPeers => "NoReadyPeers".into(),
201            PeerError::InboundTimeout => "InboundTimeout".into(),
202            PeerError::ServiceShutdown => "ServiceShutdown".into(),
203            PeerError::NotFoundResponse(_) => "NotFoundResponse".into(),
204            PeerError::NotFoundRegistry(_) => "NotFoundRegistry".into(),
205        }
206    }
207}
208
209/// A shared error slot for peer errors.
210///
211/// # Correctness
212///
213/// Error slots are shared between sync and async code. In async code, the error
214/// mutex should be held for as short a time as possible. This avoids blocking
215/// the async task thread on acquiring the mutex.
216///
217/// > If the value behind the mutex is just data, it’s usually appropriate to use a blocking mutex
218/// > ...
219/// > wrap the `Arc<Mutex<...>>` in a struct
220/// > that provides non-async methods for performing operations on the data within,
221/// > and only lock the mutex inside these methods
222///
223/// <https://docs.rs/tokio/1.15.0/tokio/sync/struct.Mutex.html#which-kind-of-mutex-should-you-use>
224#[derive(Default, Clone)]
225pub struct ErrorSlot(Arc<std::sync::Mutex<Option<SharedPeerError>>>);
226
227impl std::fmt::Debug for ErrorSlot {
228    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
229        // don't hang if the mutex is locked
230        // show the panic if the mutex was poisoned
231        f.debug_struct("ErrorSlot")
232            .field("error", &self.0.try_lock())
233            .finish()
234    }
235}
236
237impl ErrorSlot {
238    /// Read the current error in the slot.
239    ///
240    /// Returns `None` if there is no error in the slot.
241    ///
242    /// # Correctness
243    ///
244    /// Briefly locks the error slot's threaded `std::sync::Mutex`, to get a
245    /// reference to the error in the slot.
246    #[allow(clippy::unwrap_in_result)]
247    pub fn try_get_error(&self) -> Option<SharedPeerError> {
248        self.0
249            .lock()
250            .expect("error mutex should be unpoisoned")
251            .as_ref()
252            .cloned()
253    }
254
255    /// Update the current error in the slot.
256    ///
257    /// Returns `Err(AlreadyErrored)` if there was already an error in the slot.
258    ///
259    /// # Correctness
260    ///
261    /// Briefly locks the error slot's threaded `std::sync::Mutex`, to check for
262    /// a previous error, then update the error in the slot.
263    #[allow(clippy::unwrap_in_result)]
264    pub fn try_update_error(&self, e: SharedPeerError) -> Result<(), AlreadyErrored> {
265        let mut guard = self.0.lock().expect("error mutex should be unpoisoned");
266
267        if let Some(original_error) = guard.clone() {
268            Err(AlreadyErrored { original_error })
269        } else {
270            *guard = Some(e);
271            Ok(())
272        }
273    }
274}
275
276/// Error returned when the [`ErrorSlot`] already contains an error.
277#[derive(Clone, Debug)]
278pub struct AlreadyErrored {
279    /// The original error in the error slot.
280    pub original_error: SharedPeerError,
281}
282
283/// An error during a handshake with a remote peer.
284#[derive(Error, Debug)]
285pub enum HandshakeError {
286    /// The remote peer sent an unexpected message during the handshake.
287    #[error("The remote peer sent an unexpected message: {0:?}")]
288    UnexpectedMessage(Box<crate::protocol::external::Message>),
289    /// The peer connector detected handshake nonce reuse, possibly indicating self-connection.
290    #[error("Detected nonce reuse, possible self-connection")]
291    RemoteNonceReuse,
292    /// The peer connector created a duplicate random nonce. This is very unlikely,
293    /// because the range of the data type is 2^64.
294    #[error("Unexpectedly created a duplicate random local nonce")]
295    LocalDuplicateNonce,
296    /// The remote peer closed the connection.
297    #[error("Peer closed connection")]
298    ConnectionClosed,
299    /// An error occurred while performing an IO operation.
300    #[error("Underlying IO error: {0}")]
301    Io(#[from] std::io::Error),
302    /// A serialization error occurred while reading or writing a message.
303    #[error("Serialization error: {0}")]
304    Serialization(#[from] SerializationError),
305    /// The remote peer offered a version older than our minimum version.
306    #[error("Peer offered obsolete version: {0:?}")]
307    ObsoleteVersion(crate::protocol::external::types::Version),
308    /// Sending or receiving a message timed out.
309    #[error("Timeout when sending or receiving a message to peer")]
310    Timeout,
311    /// A mutually P2P-v2-capable peer was routed to the Zakura upgrade path.
312    #[error("Zakura P2P v2 upgrade selected")]
313    ZakuraUpgradeSelected,
314    /// The Zakura upgrade hook returned an error.
315    #[error("Zakura P2P v2 upgrade failed: {0}")]
316    ZakuraUpgrade(#[from] ZakuraUpgradeError),
317    /// A mutually P2P-v2-capable peer framed a Zakura upgrade prelude whose
318    /// payload failed to decode.
319    ///
320    /// The peer advertised `NODE_P2P_V2` and sent a `p2pv2up` message, but its
321    /// bytes were malformed (oversized, trailing, truncated, or a bad
322    /// discriminator). This is a protocol violation, so we disconnect the peer
323    /// on the first offense (SR-7 fail-closed) instead of silently falling back
324    /// to legacy, which a peer could otherwise use to force a downgrade. Unlike
325    /// the neutral upgrade outcomes, this is a real peer failure.
326    #[error("Malformed Zakura P2P v2 upgrade prelude: {0}")]
327    ZakuraUpgradePreludeMalformed(#[source] ZakuraProtocolError),
328}
329
330impl HandshakeError {
331    /// Returns true if this error is an intentional neutral disconnect rather than peer failure.
332    pub fn is_neutral_disconnect(&self) -> bool {
333        matches!(
334            self,
335            HandshakeError::ZakuraUpgradeSelected | HandshakeError::ZakuraUpgrade(_)
336        )
337    }
338}
339
340impl From<tokio::time::error::Elapsed> for HandshakeError {
341    fn from(_source: tokio::time::error::Elapsed) -> Self {
342        HandshakeError::Timeout
343    }
344}
345
346#[cfg(test)]
347mod tests {
348    use super::{NotFoundClass, PeerError, SharedPeerError};
349    use crate::protocol::external::InventoryHash;
350    use zakura_chain::block;
351
352    fn block_inv() -> Vec<InventoryHash> {
353        vec![InventoryHash::Block(block::Hash([0; 32]))]
354    }
355
356    /// The `notfound` classification is computed from the `PeerError` variant at construction, so
357    /// `not_found_class()` must stay in lock-step with the variants and never fall back to
358    /// `Debug`-string matching (which a rename would silently break, disabling the syncer's retry
359    /// paths).
360    #[test]
361    fn not_found_class_matches_peer_error_variant() {
362        assert_eq!(
363            SharedPeerError::from(PeerError::NotFoundResponse(block_inv())).not_found_class(),
364            Some(NotFoundClass::Response),
365        );
366        assert_eq!(
367            SharedPeerError::from(PeerError::NotFoundRegistry(block_inv())).not_found_class(),
368            Some(NotFoundClass::Registry),
369        );
370        // An unrelated peer error is not a `notfound`-style failure.
371        assert_eq!(
372            SharedPeerError::from(PeerError::NoReadyPeers).not_found_class(),
373            None,
374        );
375    }
376}