Skip to main content

reth_network/transactions/
config.rs

1use core::fmt;
2use std::{fmt::Debug, str::FromStr};
3
4use super::{
5    PeerMetadata, DEFAULT_MAX_COUNT_TRANSACTIONS_SEEN_BY_PEER,
6    DEFAULT_SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESP_ON_PACK_GET_POOLED_TRANSACTIONS_REQ,
7    SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESPONSE,
8};
9use crate::transactions::constants::{
10    tx_fetcher::{
11        DEFAULT_MAX_CAPACITY_CACHE_PENDING_FETCH, DEFAULT_MAX_COUNT_ANNOUNCED_HASHES_PER_PEER,
12        DEFAULT_MAX_COUNT_CONCURRENT_REQUESTS, DEFAULT_MAX_COUNT_CONCURRENT_REQUESTS_PER_PEER,
13    },
14    tx_manager::{
15        DEFAULT_MAX_COUNT_PENDING_POOL_IMPORTS, DEFAULT_TX_MANAGER_CHANNEL_MEMORY_LIMIT_BYTES,
16    },
17};
18use alloy_eips::eip2718::IsTyped2718;
19use alloy_primitives::B256;
20use derive_more::Display;
21use reth_eth_wire::NetworkPrimitives;
22use reth_network_types::peers::kind::PeerKind;
23
24/// Configuration for managing transactions within the network.
25#[derive(Debug, Clone)]
26#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
27pub struct TransactionsManagerConfig {
28    /// Configuration for fetching transactions.
29    pub transaction_fetcher_config: TransactionFetcherConfig,
30    /// Max number of seen transactions to store for each peer.
31    pub max_transactions_seen_by_peer_history: u32,
32    /// Soft limit on concurrent transaction imports. A fetched response admitted while capacity
33    /// remains may exceed this limit by at most 255 transactions; broadcasts are truncated to fit.
34    #[cfg_attr(feature = "serde", serde(default = "default_max_pending_pool_imports"))]
35    pub max_pending_pool_imports: usize,
36    /// How new pending transactions are propagated.
37    #[cfg_attr(feature = "serde", serde(default))]
38    pub propagation_mode: TransactionPropagationMode,
39    /// Which peers we accept incoming transactions or announcements from.
40    #[cfg_attr(feature = "serde", serde(default))]
41    pub ingress_policy: TransactionIngressPolicy,
42    /// Memory limit (in bytes) for the channel that carries
43    /// `NetworkTransactionEvent`s from the `NetworkManager` to the `TransactionsManager`.
44    ///
45    /// When the budget is exhausted, new events are dropped.
46    #[cfg_attr(feature = "serde", serde(default = "default_tx_channel_memory_limit_bytes"))]
47    pub tx_channel_memory_limit_bytes: usize,
48}
49
50#[cfg(feature = "serde")]
51const fn default_max_pending_pool_imports() -> usize {
52    DEFAULT_MAX_COUNT_PENDING_POOL_IMPORTS
53}
54
55#[cfg(feature = "serde")]
56const fn default_tx_channel_memory_limit_bytes() -> usize {
57    DEFAULT_TX_MANAGER_CHANNEL_MEMORY_LIMIT_BYTES
58}
59
60impl Default for TransactionsManagerConfig {
61    fn default() -> Self {
62        Self {
63            transaction_fetcher_config: TransactionFetcherConfig::default(),
64            max_transactions_seen_by_peer_history: DEFAULT_MAX_COUNT_TRANSACTIONS_SEEN_BY_PEER,
65            max_pending_pool_imports: DEFAULT_MAX_COUNT_PENDING_POOL_IMPORTS,
66            propagation_mode: TransactionPropagationMode::default(),
67            ingress_policy: TransactionIngressPolicy::default(),
68            tx_channel_memory_limit_bytes: DEFAULT_TX_MANAGER_CHANNEL_MEMORY_LIMIT_BYTES,
69        }
70    }
71}
72
73/// Determines how new pending transactions are propagated to other peers in full.
74#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
75#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
76pub enum TransactionPropagationMode {
77    /// Send full transactions to sqrt of current peers.
78    #[default]
79    Sqrt,
80    /// Always send transactions in full.
81    All,
82    /// Send full transactions to a maximum number of peers
83    Max(usize),
84}
85
86impl TransactionPropagationMode {
87    /// Returns the number of peers full transactions should be propagated to.
88    pub(crate) fn full_peer_count(&self, peer_count: usize) -> usize {
89        match self {
90            Self::Sqrt => (peer_count as f64).sqrt().round() as usize,
91            Self::All => peer_count,
92            Self::Max(max) => peer_count.min(*max),
93        }
94    }
95}
96impl std::fmt::Display for TransactionPropagationMode {
97    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98        match self {
99            Self::Sqrt => write!(f, "sqrt"),
100            Self::All => write!(f, "all"),
101            Self::Max(max) => write!(f, "max:{max}"),
102        }
103    }
104}
105
106impl FromStr for TransactionPropagationMode {
107    type Err = String;
108
109    fn from_str(s: &str) -> Result<Self, Self::Err> {
110        let s = s.to_lowercase();
111        match s.as_str() {
112            "sqrt" => Ok(Self::Sqrt),
113            "all" => Ok(Self::All),
114            s => {
115                if let Some(num) = s.strip_prefix("max:") {
116                    num.parse::<usize>()
117                        .map(TransactionPropagationMode::Max)
118                        .map_err(|_| format!("Invalid number for Max variant: {num}"))
119                } else {
120                    Err(format!("Invalid transaction propagation mode: {s}"))
121                }
122            }
123        }
124    }
125}
126
127/// Configuration for fetching transactions.
128#[derive(Debug, Clone)]
129#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
130pub struct TransactionFetcherConfig {
131    /// Max inflight [`GetPooledTransactions`](reth_eth_wire::GetPooledTransactions) requests.
132    pub max_inflight_requests: u32,
133    /// Max inflight [`GetPooledTransactions`](reth_eth_wire::GetPooledTransactions) requests per
134    /// peer.
135    pub max_inflight_requests_per_peer: u8,
136    /// Soft limit for the byte size of a
137    /// [`PooledTransactions`](reth_eth_wire::PooledTransactions) response on assembling a
138    /// [`GetPooledTransactions`](reth_eth_wire::GetPooledTransactions) request. Spec'd at 2
139    /// MiB.
140    pub soft_limit_byte_size_pooled_transactions_response: usize,
141    /// Soft limit for the byte size of the expected
142    /// [`PooledTransactions`](reth_eth_wire::PooledTransactions) response on packing a
143    /// [`GetPooledTransactions`](reth_eth_wire::GetPooledTransactions) request with hashes.
144    pub soft_limit_byte_size_pooled_transactions_response_on_pack_request: usize,
145    /// Max number of announced transaction hashes to keep track of, i.e. hashes that were
146    /// announced but not fetched yet, both pending and inflight. Once reached, the peer tracking
147    /// the most hashes gives up its oldest unshared pending hash for a newly announced one.
148    /// If none is found, the oldest pending hash (including shared hashes) is evicted instead.
149    /// Inflight hashes are preserved; if no victim is found, the announcement is dropped.
150    pub max_capacity_cache_txns_pending_fetch: u32,
151    /// Max number of tracked hashes a single peer can be a candidate for. Announcements from a
152    /// peer that exceed this limit are dropped.
153    #[cfg_attr(feature = "serde", serde(default = "default_max_announced_hashes_per_peer"))]
154    pub max_announced_hashes_per_peer: u32,
155}
156
157#[cfg(feature = "serde")]
158const fn default_max_announced_hashes_per_peer() -> u32 {
159    DEFAULT_MAX_COUNT_ANNOUNCED_HASHES_PER_PEER
160}
161
162impl TransactionFetcherConfig {
163    /// Creates a new config with the default limit on announced hashes per peer.
164    pub const fn new(
165        max_inflight_requests: u32,
166        max_inflight_requests_per_peer: u8,
167        soft_limit_byte_size_pooled_transactions_response: usize,
168        soft_limit_byte_size_pooled_transactions_response_on_pack_request: usize,
169        max_capacity_cache_txns_pending_fetch: u32,
170    ) -> Self {
171        Self {
172            max_inflight_requests,
173            max_inflight_requests_per_peer,
174            soft_limit_byte_size_pooled_transactions_response,
175            soft_limit_byte_size_pooled_transactions_response_on_pack_request,
176            max_capacity_cache_txns_pending_fetch,
177            max_announced_hashes_per_peer: DEFAULT_MAX_COUNT_ANNOUNCED_HASHES_PER_PEER,
178        }
179    }
180}
181
182impl Default for TransactionFetcherConfig {
183    fn default() -> Self {
184        Self::new(
185            DEFAULT_MAX_COUNT_CONCURRENT_REQUESTS,
186            DEFAULT_MAX_COUNT_CONCURRENT_REQUESTS_PER_PEER,
187            SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESPONSE,
188            DEFAULT_SOFT_LIMIT_BYTE_SIZE_POOLED_TRANSACTIONS_RESP_ON_PACK_GET_POOLED_TRANSACTIONS_REQ,
189            DEFAULT_MAX_CAPACITY_CACHE_PENDING_FETCH,
190        )
191    }
192}
193
194/// A policy defining which peers pending transactions are gossiped to.
195pub trait TransactionPropagationPolicy<N: NetworkPrimitives>:
196    Send + Sync + Unpin + fmt::Debug + 'static
197{
198    /// Filter a given peer based on the policy.
199    ///
200    /// This determines whether transactions can be propagated to this peer.
201    fn can_propagate(&self, peer: &mut PeerMetadata<N>) -> bool;
202
203    /// A callback on the policy when a new peer session is established.
204    fn on_session_established(&mut self, peer: &mut PeerMetadata<N>);
205
206    /// A callback on the policy when a peer session is closed.
207    fn on_session_closed(&mut self, peer: &mut PeerMetadata<N>);
208}
209
210/// Determines which peers pending transactions are propagated to.
211#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Display)]
212#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
213pub enum TransactionPropagationKind {
214    /// Propagate transactions to all peers.
215    ///
216    /// No restrictions
217    #[default]
218    All,
219    /// Propagate transactions to only trusted peers.
220    Trusted,
221    /// Do not propagate transactions
222    None,
223}
224
225impl<N: NetworkPrimitives> TransactionPropagationPolicy<N> for TransactionPropagationKind {
226    fn can_propagate(&self, peer: &mut PeerMetadata<N>) -> bool {
227        match self {
228            Self::All => true,
229            Self::Trusted => peer.peer_kind.is_trusted(),
230            Self::None => false,
231        }
232    }
233
234    fn on_session_established(&mut self, _peer: &mut PeerMetadata<N>) {}
235
236    fn on_session_closed(&mut self, _peer: &mut PeerMetadata<N>) {}
237}
238
239impl FromStr for TransactionPropagationKind {
240    type Err = String;
241
242    fn from_str(s: &str) -> Result<Self, Self::Err> {
243        match s {
244            "All" | "all" => Ok(Self::All),
245            "Trusted" | "trusted" => Ok(Self::Trusted),
246            "None" | "none" => Ok(Self::None),
247            _ => Err(format!("Invalid transaction propagation policy: {s}")),
248        }
249    }
250}
251
252/// Determines which peers we will accept incoming transactions or announcements from.
253#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Display)]
254#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
255pub enum TransactionIngressPolicy {
256    /// Accept transactions from any peer.
257    #[default]
258    All,
259    /// Accept transactions only from trusted peers.
260    Trusted,
261    /// Drop all incoming transactions.
262    None,
263}
264
265impl TransactionIngressPolicy {
266    /// Returns true if the ingress policy allows the provided peer kind.
267    pub const fn allows(&self, peer_kind: PeerKind) -> bool {
268        match self {
269            Self::All => true,
270            Self::Trusted => peer_kind.is_trusted(),
271            Self::None => false,
272        }
273    }
274
275    /// Returns true if the ingress policy accepts transactions from any peer.
276    pub const fn allows_all(&self) -> bool {
277        matches!(self, Self::All)
278    }
279}
280
281impl FromStr for TransactionIngressPolicy {
282    type Err = String;
283
284    fn from_str(s: &str) -> Result<Self, Self::Err> {
285        match s {
286            "All" | "all" => Ok(Self::All),
287            "Trusted" | "trusted" => Ok(Self::Trusted),
288            "None" | "none" => Ok(Self::None),
289            _ => Err(format!("Invalid transaction ingress policy: {s}")),
290        }
291    }
292}
293
294/// Defines the outcome of evaluating a transaction against an `AnnouncementFilteringPolicy`.
295///
296/// Dictates how the `TransactionManager` should proceed on an announced transaction.
297#[derive(Debug, Clone, Copy, PartialEq, Eq)]
298pub enum AnnouncementAcceptance {
299    /// Accept the transaction announcement.
300    Accept,
301    /// Log the transaction but not fetching the transaction or penalizing the peer.
302    Ignore,
303    /// Reject
304    Reject {
305        /// If true, the peer sending this announcement should be penalized.
306        penalize_peer: bool,
307    },
308}
309
310/// A policy that defines how to handle incoming transaction announcements,
311/// particularly concerning transaction types and other announcement metadata.
312pub trait AnnouncementFilteringPolicy<N: NetworkPrimitives>:
313    Send + Sync + Unpin + fmt::Debug + 'static
314{
315    /// Decides how to handle a transaction announcement based on its type, hash, and size.
316    fn decide_on_announcement(&self, ty: u8, hash: &B256, size: usize) -> AnnouncementAcceptance;
317}
318
319/// A generic `AnnouncementFilteringPolicy` that enforces strict validation
320/// of transaction type based on a generic type `T`.
321#[derive(Debug, Clone, Default)]
322#[non_exhaustive]
323pub struct TypedStrictFilter;
324
325impl<N: NetworkPrimitives> AnnouncementFilteringPolicy<N> for TypedStrictFilter {
326    fn decide_on_announcement(&self, ty: u8, hash: &B256, size: usize) -> AnnouncementAcceptance {
327        if N::PooledTransaction::is_type(ty) {
328            AnnouncementAcceptance::Accept
329        } else {
330            tracing::trace!(target: "net::tx::policy::strict_typed",
331                %ty,
332                %size,
333                %hash,
334                "Invalid or unrecognized transaction type byte. Rejecting entry and recommending peer penalization."
335            );
336            AnnouncementAcceptance::Reject { penalize_peer: true }
337        }
338    }
339}
340
341/// Type alias for a `TypedStrictFilter`. This is the default strict announcement filter.
342pub type StrictEthAnnouncementFilter = TypedStrictFilter;
343
344/// An [`AnnouncementFilteringPolicy`] that permissively handles unknown type bytes
345/// based on a given type `T` using `T::try_from(u8)`.
346///
347/// If `T::try_from(ty)` succeeds, the announcement is accepted. Otherwise, it's ignored.
348#[derive(Debug, Clone, Default)]
349#[non_exhaustive]
350pub struct TypedRelaxedFilter;
351
352impl<N: NetworkPrimitives> AnnouncementFilteringPolicy<N> for TypedRelaxedFilter {
353    fn decide_on_announcement(&self, ty: u8, hash: &B256, size: usize) -> AnnouncementAcceptance {
354        if N::PooledTransaction::is_type(ty) {
355            AnnouncementAcceptance::Accept
356        } else {
357            tracing::trace!(target: "net::tx::policy::relaxed_typed",
358                %ty,
359                %size,
360                %hash,
361                "Unknown transaction type byte. Ignoring entry."
362            );
363            AnnouncementAcceptance::Ignore
364        }
365    }
366}
367
368/// Type alias for `TypedRelaxedFilter`. This filter accepts known Ethereum transaction types and
369/// ignores unknown ones without penalizing the peer.
370pub type RelaxedEthAnnouncementFilter = TypedRelaxedFilter;
371
372#[cfg(test)]
373mod tests {
374    use super::*;
375
376    #[test]
377    fn test_transaction_propagation_mode_from_str() {
378        // Test "sqrt" variant
379        assert_eq!(
380            TransactionPropagationMode::from_str("sqrt").unwrap(),
381            TransactionPropagationMode::Sqrt
382        );
383        assert_eq!(
384            TransactionPropagationMode::from_str("SQRT").unwrap(),
385            TransactionPropagationMode::Sqrt
386        );
387        assert_eq!(
388            TransactionPropagationMode::from_str("Sqrt").unwrap(),
389            TransactionPropagationMode::Sqrt
390        );
391
392        // Test "all" variant
393        assert_eq!(
394            TransactionPropagationMode::from_str("all").unwrap(),
395            TransactionPropagationMode::All
396        );
397        assert_eq!(
398            TransactionPropagationMode::from_str("ALL").unwrap(),
399            TransactionPropagationMode::All
400        );
401        assert_eq!(
402            TransactionPropagationMode::from_str("All").unwrap(),
403            TransactionPropagationMode::All
404        );
405
406        // Test "max:N" variant
407        assert_eq!(
408            TransactionPropagationMode::from_str("max:10").unwrap(),
409            TransactionPropagationMode::Max(10)
410        );
411        assert_eq!(
412            TransactionPropagationMode::from_str("MAX:42").unwrap(),
413            TransactionPropagationMode::Max(42)
414        );
415        assert_eq!(
416            TransactionPropagationMode::from_str("Max:100").unwrap(),
417            TransactionPropagationMode::Max(100)
418        );
419
420        // Test invalid inputs
421        assert!(TransactionPropagationMode::from_str("invalid").is_err());
422        assert!(TransactionPropagationMode::from_str("max:not_a_number").is_err());
423        assert!(TransactionPropagationMode::from_str("max:").is_err());
424        assert!(TransactionPropagationMode::from_str("max").is_err());
425        assert!(TransactionPropagationMode::from_str("").is_err());
426    }
427}