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. Peers that are
201    /// filtered out also don't receive the pending pool announcement on session establishment and
202    /// are served empty responses to `GetPooledTransactions` requests.
203    fn can_propagate(&self, peer: &mut PeerMetadata<N>) -> bool;
204
205    /// A callback on the policy when a new peer session is established.
206    fn on_session_established(&mut self, peer: &mut PeerMetadata<N>);
207
208    /// A callback on the policy when a peer session is closed.
209    fn on_session_closed(&mut self, peer: &mut PeerMetadata<N>);
210}
211
212/// Determines which peers pending transactions are propagated to.
213#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Display)]
214#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
215pub enum TransactionPropagationKind {
216    /// Propagate transactions to all peers.
217    ///
218    /// No restrictions
219    #[default]
220    All,
221    /// Propagate transactions to only trusted peers.
222    Trusted,
223    /// Do not propagate transactions
224    None,
225}
226
227impl<N: NetworkPrimitives> TransactionPropagationPolicy<N> for TransactionPropagationKind {
228    fn can_propagate(&self, peer: &mut PeerMetadata<N>) -> bool {
229        match self {
230            Self::All => true,
231            Self::Trusted => peer.peer_kind.is_trusted(),
232            Self::None => false,
233        }
234    }
235
236    fn on_session_established(&mut self, _peer: &mut PeerMetadata<N>) {}
237
238    fn on_session_closed(&mut self, _peer: &mut PeerMetadata<N>) {}
239}
240
241impl FromStr for TransactionPropagationKind {
242    type Err = String;
243
244    fn from_str(s: &str) -> Result<Self, Self::Err> {
245        match s {
246            "All" | "all" => Ok(Self::All),
247            "Trusted" | "trusted" => Ok(Self::Trusted),
248            "None" | "none" => Ok(Self::None),
249            _ => Err(format!("Invalid transaction propagation policy: {s}")),
250        }
251    }
252}
253
254/// Determines which peers we will accept incoming transactions or announcements from.
255#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Display)]
256#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
257pub enum TransactionIngressPolicy {
258    /// Accept transactions from any peer.
259    #[default]
260    All,
261    /// Accept transactions only from trusted peers.
262    Trusted,
263    /// Drop all incoming transactions.
264    None,
265}
266
267impl TransactionIngressPolicy {
268    /// Returns true if the ingress policy allows the provided peer kind.
269    pub const fn allows(&self, peer_kind: PeerKind) -> bool {
270        match self {
271            Self::All => true,
272            Self::Trusted => peer_kind.is_trusted(),
273            Self::None => false,
274        }
275    }
276
277    /// Returns true if the ingress policy accepts transactions from any peer.
278    pub const fn allows_all(&self) -> bool {
279        matches!(self, Self::All)
280    }
281}
282
283impl FromStr for TransactionIngressPolicy {
284    type Err = String;
285
286    fn from_str(s: &str) -> Result<Self, Self::Err> {
287        match s {
288            "All" | "all" => Ok(Self::All),
289            "Trusted" | "trusted" => Ok(Self::Trusted),
290            "None" | "none" => Ok(Self::None),
291            _ => Err(format!("Invalid transaction ingress policy: {s}")),
292        }
293    }
294}
295
296/// Defines the outcome of evaluating a transaction against an `AnnouncementFilteringPolicy`.
297///
298/// Dictates how the `TransactionManager` should proceed on an announced transaction.
299#[derive(Debug, Clone, Copy, PartialEq, Eq)]
300pub enum AnnouncementAcceptance {
301    /// Accept the transaction announcement.
302    Accept,
303    /// Log the transaction but not fetching the transaction or penalizing the peer.
304    Ignore,
305    /// Reject
306    Reject {
307        /// If true, the peer sending this announcement should be penalized.
308        penalize_peer: bool,
309    },
310}
311
312/// A policy that defines how to handle incoming transaction announcements,
313/// particularly concerning transaction types and other announcement metadata.
314pub trait AnnouncementFilteringPolicy<N: NetworkPrimitives>:
315    Send + Sync + Unpin + fmt::Debug + 'static
316{
317    /// Decides how to handle a transaction announcement based on its type, hash, and size.
318    fn decide_on_announcement(&self, ty: u8, hash: &B256, size: usize) -> AnnouncementAcceptance;
319}
320
321/// A generic `AnnouncementFilteringPolicy` that enforces strict validation
322/// of transaction type based on a generic type `T`.
323#[derive(Debug, Clone, Default)]
324#[non_exhaustive]
325pub struct TypedStrictFilter;
326
327impl<N: NetworkPrimitives> AnnouncementFilteringPolicy<N> for TypedStrictFilter {
328    fn decide_on_announcement(&self, ty: u8, hash: &B256, size: usize) -> AnnouncementAcceptance {
329        if N::PooledTransaction::is_type(ty) {
330            AnnouncementAcceptance::Accept
331        } else {
332            tracing::trace!(target: "net::tx::policy::strict_typed",
333                %ty,
334                %size,
335                %hash,
336                "Invalid or unrecognized transaction type byte. Rejecting entry and recommending peer penalization."
337            );
338            AnnouncementAcceptance::Reject { penalize_peer: true }
339        }
340    }
341}
342
343/// Type alias for a `TypedStrictFilter`. This is the default strict announcement filter.
344pub type StrictEthAnnouncementFilter = TypedStrictFilter;
345
346/// An [`AnnouncementFilteringPolicy`] that permissively handles unknown type bytes
347/// based on a given type `T` using `T::try_from(u8)`.
348///
349/// If `T::try_from(ty)` succeeds, the announcement is accepted. Otherwise, it's ignored.
350#[derive(Debug, Clone, Default)]
351#[non_exhaustive]
352pub struct TypedRelaxedFilter;
353
354impl<N: NetworkPrimitives> AnnouncementFilteringPolicy<N> for TypedRelaxedFilter {
355    fn decide_on_announcement(&self, ty: u8, hash: &B256, size: usize) -> AnnouncementAcceptance {
356        if N::PooledTransaction::is_type(ty) {
357            AnnouncementAcceptance::Accept
358        } else {
359            tracing::trace!(target: "net::tx::policy::relaxed_typed",
360                %ty,
361                %size,
362                %hash,
363                "Unknown transaction type byte. Ignoring entry."
364            );
365            AnnouncementAcceptance::Ignore
366        }
367    }
368}
369
370/// Type alias for `TypedRelaxedFilter`. This filter accepts known Ethereum transaction types and
371/// ignores unknown ones without penalizing the peer.
372pub type RelaxedEthAnnouncementFilter = TypedRelaxedFilter;
373
374#[cfg(test)]
375mod tests {
376    use super::*;
377
378    #[test]
379    fn test_transaction_propagation_mode_from_str() {
380        // Test "sqrt" variant
381        assert_eq!(
382            TransactionPropagationMode::from_str("sqrt").unwrap(),
383            TransactionPropagationMode::Sqrt
384        );
385        assert_eq!(
386            TransactionPropagationMode::from_str("SQRT").unwrap(),
387            TransactionPropagationMode::Sqrt
388        );
389        assert_eq!(
390            TransactionPropagationMode::from_str("Sqrt").unwrap(),
391            TransactionPropagationMode::Sqrt
392        );
393
394        // Test "all" variant
395        assert_eq!(
396            TransactionPropagationMode::from_str("all").unwrap(),
397            TransactionPropagationMode::All
398        );
399        assert_eq!(
400            TransactionPropagationMode::from_str("ALL").unwrap(),
401            TransactionPropagationMode::All
402        );
403        assert_eq!(
404            TransactionPropagationMode::from_str("All").unwrap(),
405            TransactionPropagationMode::All
406        );
407
408        // Test "max:N" variant
409        assert_eq!(
410            TransactionPropagationMode::from_str("max:10").unwrap(),
411            TransactionPropagationMode::Max(10)
412        );
413        assert_eq!(
414            TransactionPropagationMode::from_str("MAX:42").unwrap(),
415            TransactionPropagationMode::Max(42)
416        );
417        assert_eq!(
418            TransactionPropagationMode::from_str("Max:100").unwrap(),
419            TransactionPropagationMode::Max(100)
420        );
421
422        // Test invalid inputs
423        assert!(TransactionPropagationMode::from_str("invalid").is_err());
424        assert!(TransactionPropagationMode::from_str("max:not_a_number").is_err());
425        assert!(TransactionPropagationMode::from_str("max:").is_err());
426        assert!(TransactionPropagationMode::from_str("max").is_err());
427        assert!(TransactionPropagationMode::from_str("").is_err());
428    }
429}