Skip to main content

snarkos_node_tcp/protocols/
reading.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
16#[cfg(doc)]
17use crate::{Config, protocols::Handshake};
18use crate::{
19    Connection,
20    ConnectionSide,
21    P2P,
22    Stats,
23    Tcp,
24    connections::DisconnectOrigin,
25    protocols::{DisconnectOnDrop, ProtocolHandler, ReturnableConnection},
26};
27
28use async_trait::async_trait;
29use bytes::BytesMut;
30use futures_util::StreamExt;
31use std::{
32    io,
33    net::SocketAddr,
34    sync::Arc,
35    time::{Duration, Instant},
36};
37use tokio::{
38    io::AsyncRead,
39    sync::{mpsc, oneshot},
40    time::timeout,
41};
42use tokio_util::codec::{Decoder, FramedRead};
43use tracing::*;
44
45/// Can be used to specify and enable reading, i.e. receiving inbound messages. If the [`Handshake`]
46/// protocol is enabled too, it goes into force only after the handshake has been concluded.
47///
48/// Each inbound message is isolated by the user-supplied [`Reading::Codec`], creating a [`Reading::Message`],
49/// which is immediately queued (with a [`Reading::MESSAGE_QUEUE_DEPTH`] limit) to be processed by
50/// [`Reading::process_message`]. Errors result in an immediate disconnect (in order to e.g. avoid
51/// accidentally reading "borked" messages).
52#[async_trait]
53pub trait Reading: P2P
54where
55    Self: Clone + Send + Sync + 'static,
56{
57    /// The depth of per-connection queues used to process inbound messages; the greater it is, the more inbound
58    /// messages the node can enqueue, but setting it to a large value can make the node more susceptible to DoS
59    /// attacks.
60    ///
61    /// The default value is 1024.
62    fn message_queue_depth(&self) -> usize {
63        1024
64    }
65
66    /// The initial size of a per-connection buffer for reading inbound messages. Can be set to the maximum expected size
67    /// of the inbound message in order to only allocate it once.
68    ///
69    /// The default value is 1024KiB.
70    const INITIAL_BUFFER_SIZE: usize = 1024 * 1024;
71
72    /// The maximum time the node will wait for a new message before considering the connection dead.
73    const IDLE_TIMEOUT: Duration = Duration::from_secs(150);
74
75    /// The final (deserialized) type of inbound messages.
76    type Message: Send;
77
78    /// The user-supplied [`Decoder`] used to interpret inbound messages.
79    type Codec: Decoder<Item = Self::Message, Error = io::Error> + Send;
80
81    /// Prepares the node to receive messages.
82    async fn enable_reading(&self) {
83        let (conn_sender, mut conn_receiver) = mpsc::channel(self.tcp().config().max_connections as usize);
84
85        // use a channel to know when the reading task is ready
86        let (tx_reading, rx_reading) = oneshot::channel();
87
88        // the main task spawning per-connection tasks reading messages from their streams
89        let self_clone = self.clone();
90        let reading_task = tokio::spawn(async move {
91            trace!(parent: self_clone.tcp().span(), "spawned the Reading handler task");
92            tx_reading.send(()).unwrap(); // safe; the channel was just opened
93
94            // these objects are sent from `Tcp::adapt_stream`
95            while let Some(returnable_conn) = conn_receiver.recv().await {
96                self_clone.handle_new_connection(returnable_conn).await;
97            }
98        });
99        let _ = rx_reading.await;
100        self.tcp().tasks.lock().push(reading_task);
101
102        // register the Reading handler with the Tcp
103        let hdl = Box::new(ProtocolHandler(conn_sender));
104        assert!(self.tcp().protocols.reading.set(hdl).is_ok(), "the Reading protocol was enabled more than once!");
105    }
106
107    /// Creates a [`Decoder`] used to interpret messages from the network.
108    /// The `side` param indicates the connection side **from the node's perspective**.
109    fn codec(&self, addr: SocketAddr, side: ConnectionSide) -> Self::Codec;
110
111    /// Processes an inbound message. Can be used to update state, send replies etc.
112    async fn process_message(&self, source: SocketAddr, message: Self::Message) -> io::Result<()>;
113}
114
115/// This trait is used to restrict access to methods that would otherwise be public in [`Reading`].
116#[async_trait]
117trait ReadingInternal: Reading {
118    /// Applies the [`Reading`] protocol to a single connection.
119    async fn handle_new_connection(&self, (conn, conn_returner): ReturnableConnection);
120
121    /// Wraps the user-supplied [`Decoder`] ([`Reading::Codec`]) in another one used for message accounting.
122    fn map_codec<T: AsyncRead>(
123        &self,
124        framed: FramedRead<T, Self::Codec>,
125        conn: &Connection,
126    ) -> FramedRead<T, CountingCodec<Self::Codec>>;
127}
128
129#[async_trait]
130impl<R: Reading> ReadingInternal for R {
131    async fn handle_new_connection(&self, (mut conn, conn_returner): ReturnableConnection) {
132        let addr = conn.addr();
133        let codec = self.codec(addr, !conn.side());
134        let reader = conn.reader.take().expect("missing connection reader!");
135        let framed = FramedRead::new(reader, codec);
136        let mut framed = self.map_codec(framed, &conn);
137
138        // the connection will notify the reading task once it's fully ready
139        let (tx_conn_ready, rx_conn_ready) = oneshot::channel();
140        conn.readiness_notifier = Some(tx_conn_ready);
141
142        if Self::INITIAL_BUFFER_SIZE != 0 {
143            framed.read_buffer_mut().reserve(Self::INITIAL_BUFFER_SIZE);
144        }
145
146        let (inbound_message_sender, mut inbound_message_receiver) =
147            mpsc::channel::<(R::Message, QueuedMessageGuard)>(self.message_queue_depth());
148
149        // use a channel to know when the processing task is ready
150        let (tx_processing, rx_processing) = oneshot::channel::<()>();
151
152        // the task for processing parsed messages
153        let self_clone = self.clone();
154        let conn_span = conn.span().clone();
155        let inbound_processing_task = tokio::spawn(Box::pin(async move {
156            let node = self_clone.tcp();
157            trace!(parent: &conn_span, "spawned a task for processing messages");
158            tx_processing.send(()).unwrap(); // safe; the channel was just opened
159
160            // disconnect automatically regardless of how this task concludes
161            let _conn_cleanup = DisconnectOnDrop::new(node.clone(), addr, DisconnectOrigin::Reading);
162
163            while let Some((msg, _guard)) = inbound_message_receiver.recv().await {
164                if let Err(e) = self_clone.process_message(addr, msg).await {
165                    error!(parent: &conn_span, "can't process a message: {e}");
166                }
167                // _guard drops here, after process_message completes
168            }
169        }));
170        let _ = rx_processing.await;
171        conn.tasks.push(inbound_processing_task);
172
173        // use a channel to know when the reader task is ready
174        let (tx_reader, rx_reader) = oneshot::channel::<()>();
175
176        // the task for reading messages from a stream
177        let node = self.tcp().clone();
178        let conn_span = conn.span().clone();
179        let reader_task = tokio::spawn(Box::pin(async move {
180            trace!(parent: &conn_span, "spawned a task for reading messages");
181            tx_reader.send(()).unwrap(); // safe; the channel was just opened
182
183            // postpone reads until the connection is fully established; if the process fails,
184            // this task gets aborted, so there is no need for a dedicated timeout
185            let _ = rx_conn_ready.await;
186
187            // disconnect automatically regardless of how this task concludes
188            let _conn_cleanup = DisconnectOnDrop::new(node.clone(), addr, DisconnectOrigin::Reading);
189
190            // dropped message log suppression helpers
191            let mut dropped_count: usize = 0;
192            let mut last_drop_log = Instant::now();
193
194            loop {
195                let next_frame_future = framed.next();
196                let read_result = match timeout(Self::IDLE_TIMEOUT, next_frame_future).await {
197                    Ok(res) => res, // IO completed (success or error)
198                    Err(_) => {
199                        debug!(parent: &conn_span, "connection timed out due to inactivity");
200                        break;
201                    }
202                };
203                match read_result {
204                    Some(Ok(msg)) => {
205                        // send the message for further processing
206                        if let Err(e) = inbound_message_sender.try_send((msg, QueuedMessageGuard::new())) {
207                            match e {
208                                mpsc::error::TrySendError::Full(_) => {
209                                    // avoid log flooding
210                                    dropped_count += 1;
211                                    if last_drop_log.elapsed() >= Duration::from_secs(1) {
212                                        warn_about_dropped_messages(&conn_span, &mut dropped_count, &mut last_drop_log);
213                                    }
214                                }
215                                mpsc::error::TrySendError::Closed(_) => {
216                                    error!(parent: &conn_span, "inbound channel closed");
217                                    break;
218                                }
219                            }
220                        } else if dropped_count != 0 {
221                            warn_about_dropped_messages(&conn_span, &mut dropped_count, &mut last_drop_log);
222                            debug!(parent: &conn_span, "the inbound queue is no longer saturated");
223                        }
224                        #[cfg(feature = "metrics")]
225                        metrics::increment_gauge(metrics::tcp::TCP_TASKS, 1f64);
226                    }
227                    Some(Err(e)) => {
228                        error!(parent: &conn_span, "can't read: {e}");
229                        break;
230                    }
231                    None => break, // end of stream
232                }
233            }
234        }));
235        let _ = rx_reader.await;
236        conn.tasks.push(reader_task);
237
238        // return the Connection to the Tcp, resuming Tcp::adapt_stream
239        if conn_returner.send(Ok(conn)).is_err() {
240            unreachable!("couldn't return a Connection to the Tcp");
241        }
242    }
243
244    fn map_codec<T: AsyncRead>(
245        &self,
246        framed: FramedRead<T, Self::Codec>,
247        conn: &Connection,
248    ) -> FramedRead<T, CountingCodec<Self::Codec>> {
249        framed.map_decoder(|codec| CountingCodec {
250            codec,
251            node: self.tcp().clone(),
252            stats: Arc::clone(conn.stats()),
253            acc: 0,
254            span: conn.span().clone(),
255        })
256    }
257}
258
259/// A wrapper [`Decoder`] that also counts the inbound messages.
260struct CountingCodec<D: Decoder> {
261    codec: D,
262    node: Tcp,
263    stats: Arc<Stats>,
264    acc: usize,
265    span: Span,
266}
267
268impl<D: Decoder> Decoder for CountingCodec<D> {
269    type Error = D::Error;
270    type Item = D::Item;
271
272    fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
273        let initial_buf_len = src.len();
274        let ret = self.codec.decode(src)?;
275        let final_buf_len = src.len();
276        // defensive: the Decoder trait does not strictly forbid an inner codec from
277        // growing `src`; use saturating_sub to guard against such a possibility
278        let consumed = initial_buf_len.saturating_sub(final_buf_len);
279        let read_len = consumed + self.acc;
280
281        if read_len != 0 {
282            trace!(parent: &self.span, "read {read_len}B");
283
284            if ret.is_some() {
285                self.acc = 0;
286                self.stats.register_received_message(read_len);
287                self.node.stats().register_received_message(read_len);
288            } else {
289                self.acc = read_len;
290            }
291        }
292
293        Ok(ret)
294    }
295}
296
297/// Decrements the TCP_TASKS gauge on drop. Paired with each queued message so the gauge stays
298/// balanced whether the message is processed normally or discarded when the inbound channel is
299/// dropped (e.g. on connection abort). The caller must hold this guard until processing is
300/// complete; dropping it earlier will decrement the gauge prematurely.
301struct QueuedMessageGuard;
302
303impl QueuedMessageGuard {
304    fn new() -> Self {
305        #[cfg(feature = "metrics")]
306        metrics::increment_gauge(metrics::tcp::TCP_TASKS, 1f64);
307        Self
308    }
309}
310
311impl Drop for QueuedMessageGuard {
312    fn drop(&mut self) {
313        #[cfg(feature = "metrics")]
314        metrics::decrement_gauge(metrics::tcp::TCP_TASKS, 1f64);
315    }
316}
317
318/// Warns that some messages were dropped and resets the related counters.
319fn warn_about_dropped_messages(span: &Span, dropped_count: &mut usize, last_drop_log: &mut Instant) {
320    warn!(
321        parent: span,
322        "dropped {dropped_count} messages due\
323        to inbound queue saturation",
324    );
325    // reset counters
326    *dropped_count = 0;
327    *last_drop_log = Instant::now();
328}