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