reth_network/transactions/
validation.rs

1//! Validation of [`NewPooledTransactionHashes66`](reth_eth_wire::NewPooledTransactionHashes66)
2//! and [`NewPooledTransactionHashes68`](reth_eth_wire::NewPooledTransactionHashes68)
3//! announcements. Validation and filtering of announcements is network dependent.
4
5use crate::metrics::{AnnouncedTxTypesMetrics, TxTypesCounter};
6use alloy_primitives::Signature;
7use derive_more::{Deref, DerefMut};
8use reth_eth_wire::{
9    DedupPayload, Eth68TxMetadata, HandleMempoolData, PartiallyValidData, ValidAnnouncementData,
10};
11use reth_ethereum_primitives::TxType;
12use std::{fmt, fmt::Display, mem};
13use tracing::trace;
14
15/// The size of a decoded signature in bytes.
16pub const SIGNATURE_DECODED_SIZE_BYTES: usize = mem::size_of::<Signature>();
17
18/// Outcomes from validating a `(ty, hash, size)` entry from a
19/// [`NewPooledTransactionHashes68`](reth_eth_wire::NewPooledTransactionHashes68). Signals to the
20/// caller how to deal with an announcement entry and the peer who sent the announcement.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum ValidationOutcome {
23    /// Tells the caller to keep the entry in the announcement for fetch.
24    Fetch,
25    /// Tells the caller to filter out the entry from the announcement.
26    Ignore,
27    /// Tells the caller to filter out the entry from the announcement and penalize the peer. On
28    /// this outcome, caller can drop the announcement, that is up to each implementation.
29    ReportPeer,
30}
31
32/// Generic filter for announcements and responses. Checks for empty message and unique hashes/
33/// transactions in message.
34pub trait PartiallyFilterMessage {
35    /// Removes duplicate entries from a mempool message. Returns [`FilterOutcome::ReportPeer`] if
36    /// the caller should penalize the peer, otherwise [`FilterOutcome::Ok`].
37    fn partially_filter_valid_entries<V>(
38        &self,
39        msg: impl DedupPayload<Value = V> + fmt::Debug,
40    ) -> (FilterOutcome, PartiallyValidData<V>) {
41        // 1. checks if the announcement is empty
42        if msg.is_empty() {
43            trace!(target: "net::tx",
44                msg=?msg,
45                "empty payload"
46            );
47            return (FilterOutcome::ReportPeer, PartiallyValidData::empty_eth66())
48        }
49
50        // 2. checks if announcement is spam packed with duplicate hashes
51        let original_len = msg.len();
52        let partially_valid_data = msg.dedup();
53
54        (
55            if partially_valid_data.len() == original_len {
56                FilterOutcome::Ok
57            } else {
58                FilterOutcome::ReportPeer
59            },
60            partially_valid_data,
61        )
62    }
63}
64
65/// Filters valid entries in
66/// [`NewPooledTransactionHashes68`](reth_eth_wire::NewPooledTransactionHashes68) and
67/// [`NewPooledTransactionHashes66`](reth_eth_wire::NewPooledTransactionHashes66) in place, and
68/// flags misbehaving peers.
69pub trait FilterAnnouncement {
70    /// Removes invalid entries from a
71    /// [`NewPooledTransactionHashes68`](reth_eth_wire::NewPooledTransactionHashes68) announcement.
72    /// Returns [`FilterOutcome::ReportPeer`] if the caller should penalize the peer, otherwise
73    /// [`FilterOutcome::Ok`].
74    fn filter_valid_entries_68(
75        &self,
76        msg: PartiallyValidData<Eth68TxMetadata>,
77    ) -> (FilterOutcome, ValidAnnouncementData);
78
79    /// Removes invalid entries from a
80    /// [`NewPooledTransactionHashes66`](reth_eth_wire::NewPooledTransactionHashes66) announcement.
81    /// Returns [`FilterOutcome::ReportPeer`] if the caller should penalize the peer, otherwise
82    /// [`FilterOutcome::Ok`].
83    fn filter_valid_entries_66(
84        &self,
85        msg: PartiallyValidData<Eth68TxMetadata>,
86    ) -> (FilterOutcome, ValidAnnouncementData);
87}
88
89/// Outcome from filtering
90/// [`NewPooledTransactionHashes68`](reth_eth_wire::NewPooledTransactionHashes68). Signals to caller
91/// whether to penalize the sender of the announcement or not.
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub enum FilterOutcome {
94    /// Peer behaves appropriately.
95    Ok,
96    /// A penalty should be flagged for the peer. Peer sent an announcement with unacceptably
97    /// invalid entries.
98    ReportPeer,
99}
100
101/// Wrapper for types that implement [`FilterAnnouncement`]. The definition of a valid
102/// announcement is network dependent. For example, different networks support different
103/// [`TxType`]s, and different [`TxType`]s have different transaction size constraints. Defaults to
104/// [`EthMessageFilter`].
105#[derive(Debug, Default, Deref, DerefMut)]
106pub struct MessageFilter<N = EthMessageFilter>(N);
107
108/// Filter for announcements containing EIP [`TxType`]s.
109#[derive(Debug, Default)]
110pub struct EthMessageFilter {
111    announced_tx_types_metrics: AnnouncedTxTypesMetrics,
112}
113
114impl Display for EthMessageFilter {
115    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116        write!(f, "EthMessageFilter")
117    }
118}
119
120impl PartiallyFilterMessage for EthMessageFilter {}
121
122impl FilterAnnouncement for EthMessageFilter {
123    fn filter_valid_entries_68(
124        &self,
125        mut msg: PartiallyValidData<Eth68TxMetadata>,
126    ) -> (FilterOutcome, ValidAnnouncementData) {
127        trace!(target: "net::tx::validation",
128            msg=?*msg,
129            network=%self,
130            "validating eth68 announcement data.."
131        );
132
133        let mut should_report_peer = false;
134        let mut tx_types_counter = TxTypesCounter::default();
135
136        // checks if eth68 announcement metadata is valid
137        //
138        // transactions that are filtered out here, may not be spam, rather from benevolent peers
139        // that are unknowingly sending announcements with invalid data.
140        //
141        msg.retain(|hash, metadata| {
142            debug_assert!(
143                metadata.is_some(),
144                "metadata should exist for `%hash` in eth68 announcement passed to `%filter_valid_entries_68`,
145`%hash`: {hash}"
146            );
147
148            let Some((ty, size)) = metadata else {
149                return false
150            };
151
152            //
153            //  checks if tx type is valid value for this network
154            //
155            let tx_type = match TxType::try_from(*ty) {
156                Ok(ty) => ty,
157                Err(_) => {
158                    trace!(target: "net::eth-wire",
159                        ty=ty,
160                        size=size,
161                        hash=%hash,
162                        network=%self,
163                        "invalid tx type in eth68 announcement"
164                    );
165
166                    should_report_peer = true;
167                    return false;
168            }
169
170            };
171            tx_types_counter.increase_by_tx_type(tx_type);
172
173            true
174        });
175        self.announced_tx_types_metrics.update_eth68_announcement_metrics(tx_types_counter);
176        (
177            if should_report_peer { FilterOutcome::ReportPeer } else { FilterOutcome::Ok },
178            ValidAnnouncementData::from_partially_valid_data(msg),
179        )
180    }
181
182    fn filter_valid_entries_66(
183        &self,
184        partially_valid_data: PartiallyValidData<Option<(u8, usize)>>,
185    ) -> (FilterOutcome, ValidAnnouncementData) {
186        trace!(target: "net::tx::validation",
187            hashes=?*partially_valid_data,
188            network=%self,
189            "validating eth66 announcement data.."
190        );
191
192        (FilterOutcome::Ok, ValidAnnouncementData::from_partially_valid_data(partially_valid_data))
193    }
194}
195
196#[cfg(test)]
197mod test {
198    use super::*;
199    use alloy_primitives::B256;
200    use reth_eth_wire::{
201        NewPooledTransactionHashes66, NewPooledTransactionHashes68, MAX_MESSAGE_SIZE,
202    };
203    use std::{collections::HashMap, str::FromStr};
204
205    #[test]
206    fn eth68_empty_announcement() {
207        let types = vec![];
208        let sizes = vec![];
209        let hashes = vec![];
210
211        let announcement = NewPooledTransactionHashes68 { types, sizes, hashes };
212
213        let filter = EthMessageFilter::default();
214
215        let (outcome, _partially_valid_data) = filter.partially_filter_valid_entries(announcement);
216
217        assert_eq!(outcome, FilterOutcome::ReportPeer);
218    }
219
220    #[test]
221    fn eth68_announcement_unrecognized_tx_type() {
222        let types = vec![
223            TxType::Eip7702 as u8 + 1, // the first type isn't valid
224            TxType::Legacy as u8,
225        ];
226        let sizes = vec![MAX_MESSAGE_SIZE, MAX_MESSAGE_SIZE];
227        let hashes = vec![
228            B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafa")
229                .unwrap(),
230            B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefbbbb")
231                .unwrap(),
232        ];
233
234        let announcement = NewPooledTransactionHashes68 {
235            types: types.clone(),
236            sizes: sizes.clone(),
237            hashes: hashes.clone(),
238        };
239
240        let filter = EthMessageFilter::default();
241
242        let (outcome, partially_valid_data) = filter.partially_filter_valid_entries(announcement);
243
244        assert_eq!(outcome, FilterOutcome::Ok);
245
246        let (outcome, valid_data) = filter.filter_valid_entries_68(partially_valid_data);
247
248        assert_eq!(outcome, FilterOutcome::ReportPeer);
249
250        let mut expected_data = HashMap::default();
251        expected_data.insert(hashes[1], Some((types[1], sizes[1])));
252
253        assert_eq!(expected_data, valid_data.into_data())
254    }
255
256    #[test]
257    fn eth68_announcement_duplicate_tx_hash() {
258        let types = vec![
259            TxType::Eip1559 as u8,
260            TxType::Eip4844 as u8,
261            TxType::Eip1559 as u8,
262            TxType::Eip4844 as u8,
263        ];
264        let sizes = vec![1, 1, 1, MAX_MESSAGE_SIZE];
265        // first three or the same
266        let hashes = vec![
267            B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafa") // dup
268                .unwrap(),
269            B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafa") // removed dup
270                .unwrap(),
271            B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafa") // removed dup
272                .unwrap(),
273            B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefbbbb")
274                .unwrap(),
275        ];
276
277        let announcement = NewPooledTransactionHashes68 {
278            types: types.clone(),
279            sizes: sizes.clone(),
280            hashes: hashes.clone(),
281        };
282
283        let filter = EthMessageFilter::default();
284
285        let (outcome, partially_valid_data) = filter.partially_filter_valid_entries(announcement);
286
287        assert_eq!(outcome, FilterOutcome::ReportPeer);
288
289        let mut expected_data = HashMap::default();
290        expected_data.insert(hashes[3], Some((types[3], sizes[3])));
291        expected_data.insert(hashes[0], Some((types[0], sizes[0])));
292
293        assert_eq!(expected_data, partially_valid_data.into_data())
294    }
295
296    #[test]
297    fn eth66_empty_announcement() {
298        let hashes = vec![];
299
300        let announcement = NewPooledTransactionHashes66(hashes);
301
302        let filter: MessageFilter = MessageFilter::default();
303
304        let (outcome, _partially_valid_data) = filter.partially_filter_valid_entries(announcement);
305
306        assert_eq!(outcome, FilterOutcome::ReportPeer);
307    }
308
309    #[test]
310    fn eth66_announcement_duplicate_tx_hash() {
311        // first three or the same
312        let hashes = vec![
313            B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefbbbb") // dup1
314                .unwrap(),
315            B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafa") // dup2
316                .unwrap(),
317            B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafa") // removed dup2
318                .unwrap(),
319            B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafa") // removed dup2
320                .unwrap(),
321            B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefbbbb") // removed dup1
322                .unwrap(),
323        ];
324
325        let announcement = NewPooledTransactionHashes66(hashes.clone());
326
327        let filter: MessageFilter = MessageFilter::default();
328
329        let (outcome, partially_valid_data) = filter.partially_filter_valid_entries(announcement);
330
331        assert_eq!(outcome, FilterOutcome::ReportPeer);
332
333        let mut expected_data = HashMap::default();
334        expected_data.insert(hashes[1], None);
335        expected_data.insert(hashes[0], None);
336
337        assert_eq!(expected_data, partially_valid_data.into_data())
338    }
339
340    #[test]
341    fn eth68_announcement_eip7702_tx() {
342        let types = vec![TxType::Eip7702 as u8, TxType::Legacy as u8];
343        let sizes = vec![MAX_MESSAGE_SIZE, MAX_MESSAGE_SIZE];
344        let hashes = vec![
345            B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafa")
346                .unwrap(),
347            B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefbbbb")
348                .unwrap(),
349        ];
350
351        let announcement = NewPooledTransactionHashes68 {
352            types: types.clone(),
353            sizes: sizes.clone(),
354            hashes: hashes.clone(),
355        };
356
357        let filter = EthMessageFilter::default();
358
359        let (outcome, partially_valid_data) = filter.partially_filter_valid_entries(announcement);
360        assert_eq!(outcome, FilterOutcome::Ok);
361
362        let (outcome, valid_data) = filter.filter_valid_entries_68(partially_valid_data);
363        assert_eq!(outcome, FilterOutcome::Ok);
364
365        let mut expected_data = HashMap::default();
366        expected_data.insert(hashes[0], Some((types[0], sizes[0])));
367        expected_data.insert(hashes[1], Some((types[1], sizes[1])));
368
369        assert_eq!(expected_data, valid_data.into_data());
370    }
371
372    #[test]
373    fn eth68_announcement_eip7702_tx_size_validation() {
374        let types = vec![TxType::Eip7702 as u8, TxType::Eip7702 as u8, TxType::Eip7702 as u8];
375        // Test with different sizes: too small, reasonable, too large
376        let sizes = vec![
377            1,                    // too small
378            MAX_MESSAGE_SIZE / 2, // reasonable size
379            MAX_MESSAGE_SIZE + 1, // too large
380        ];
381        let hashes = vec![
382            B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafa")
383                .unwrap(),
384            B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefbbbb")
385                .unwrap(),
386            B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcccc")
387                .unwrap(),
388        ];
389
390        let announcement = NewPooledTransactionHashes68 {
391            types: types.clone(),
392            sizes: sizes.clone(),
393            hashes: hashes.clone(),
394        };
395
396        let filter = EthMessageFilter::default();
397
398        let (outcome, partially_valid_data) = filter.partially_filter_valid_entries(announcement);
399        assert_eq!(outcome, FilterOutcome::Ok);
400
401        let (outcome, valid_data) = filter.filter_valid_entries_68(partially_valid_data);
402        assert_eq!(outcome, FilterOutcome::Ok);
403
404        let mut expected_data = HashMap::default();
405
406        for i in 0..3 {
407            expected_data.insert(hashes[i], Some((types[i], sizes[i])));
408        }
409
410        assert_eq!(expected_data, valid_data.into_data());
411    }
412
413    #[test]
414    fn eth68_announcement_mixed_tx_types() {
415        let types = vec![
416            TxType::Legacy as u8,
417            TxType::Eip7702 as u8,
418            TxType::Eip1559 as u8,
419            TxType::Eip4844 as u8,
420        ];
421        let sizes = vec![MAX_MESSAGE_SIZE, MAX_MESSAGE_SIZE, MAX_MESSAGE_SIZE, MAX_MESSAGE_SIZE];
422        let hashes = vec![
423            B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafa")
424                .unwrap(),
425            B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefbbbb")
426                .unwrap(),
427            B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcccc")
428                .unwrap(),
429            B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefdddd")
430                .unwrap(),
431        ];
432
433        let announcement = NewPooledTransactionHashes68 {
434            types: types.clone(),
435            sizes: sizes.clone(),
436            hashes: hashes.clone(),
437        };
438
439        let filter = EthMessageFilter::default();
440
441        let (outcome, partially_valid_data) = filter.partially_filter_valid_entries(announcement);
442        assert_eq!(outcome, FilterOutcome::Ok);
443
444        let (outcome, valid_data) = filter.filter_valid_entries_68(partially_valid_data);
445        assert_eq!(outcome, FilterOutcome::Ok);
446
447        let mut expected_data = HashMap::default();
448        // All transaction types should be included as they are valid
449        for i in 0..4 {
450            expected_data.insert(hashes[i], Some((types[i], sizes[i])));
451        }
452
453        assert_eq!(expected_data, valid_data.into_data());
454    }
455
456    #[test]
457    fn test_display_for_zst() {
458        let filter = EthMessageFilter::default();
459        assert_eq!("EthMessageFilter", &filter.to_string());
460    }
461}