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}