reth_network/transactions/
validation.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
//! Validation of [`NewPooledTransactionHashes66`](reth_eth_wire::NewPooledTransactionHashes66)
//! and [`NewPooledTransactionHashes68`](reth_eth_wire::NewPooledTransactionHashes68)
//! announcements. Validation and filtering of announcements is network dependent.

use crate::metrics::{AnnouncedTxTypesMetrics, TxTypesCounter};
use alloy_primitives::{PrimitiveSignature as Signature, TxHash};
use derive_more::{Deref, DerefMut};
use reth_eth_wire::{
    DedupPayload, Eth68TxMetadata, HandleMempoolData, PartiallyValidData, ValidAnnouncementData,
    MAX_MESSAGE_SIZE,
};
use reth_primitives::TxType;
use std::{fmt, fmt::Display, mem};
use tracing::trace;

/// The size of a decoded signature in bytes.
pub const SIGNATURE_DECODED_SIZE_BYTES: usize = mem::size_of::<Signature>();

/// Interface for validating a `(ty, size, hash)` tuple from a
/// [`NewPooledTransactionHashes68`](reth_eth_wire::NewPooledTransactionHashes68)..
pub trait ValidateTx68 {
    /// Validates a [`NewPooledTransactionHashes68`](reth_eth_wire::NewPooledTransactionHashes68)
    /// entry. Returns [`ValidationOutcome`] which signals to the caller whether to fetch the
    /// transaction or to drop it, and whether the sender of the announcement should be
    /// penalized.
    fn should_fetch(
        &self,
        ty: u8,
        hash: &TxHash,
        size: usize,
        tx_types_counter: &mut TxTypesCounter,
    ) -> ValidationOutcome;

    /// Returns the reasonable maximum encoded transaction length configured for this network, if
    /// any. This property is not spec'ed out but can be inferred by looking how much data can be
    /// packed into a transaction for any given transaction type.
    fn max_encoded_tx_length(&self, ty: TxType) -> Option<usize>;

    /// Returns the strict maximum encoded transaction length for the given transaction type, if
    /// any.
    fn strict_max_encoded_tx_length(&self, ty: TxType) -> Option<usize>;

    /// Returns the reasonable minimum encoded transaction length, if any. This property is not
    /// spec'ed out but can be inferred by looking at which
    /// [`reth_primitives::PooledTransaction`] will successfully pass decoding
    /// for any given transaction type.
    fn min_encoded_tx_length(&self, ty: TxType) -> Option<usize>;

    /// Returns the strict minimum encoded transaction length for the given transaction type, if
    /// any.
    fn strict_min_encoded_tx_length(&self, ty: TxType) -> Option<usize>;
}

/// Outcomes from validating a `(ty, hash, size)` entry from a
/// [`NewPooledTransactionHashes68`](reth_eth_wire::NewPooledTransactionHashes68). Signals to the
/// caller how to deal with an announcement entry and the peer who sent the announcement.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ValidationOutcome {
    /// Tells the caller to keep the entry in the announcement for fetch.
    Fetch,
    /// Tells the caller to filter out the entry from the announcement.
    Ignore,
    /// Tells the caller to filter out the entry from the announcement and penalize the peer. On
    /// this outcome, caller can drop the announcement, that is up to each implementation.
    ReportPeer,
}

/// Generic filter for announcements and responses. Checks for empty message and unique hashes/
/// transactions in message.
pub trait PartiallyFilterMessage {
    /// Removes duplicate entries from a mempool message. Returns [`FilterOutcome::ReportPeer`] if
    /// the caller should penalize the peer, otherwise [`FilterOutcome::Ok`].
    fn partially_filter_valid_entries<V>(
        &self,
        msg: impl DedupPayload<Value = V> + fmt::Debug,
    ) -> (FilterOutcome, PartiallyValidData<V>) {
        // 1. checks if the announcement is empty
        if msg.is_empty() {
            trace!(target: "net::tx",
                msg=?msg,
                "empty payload"
            );
            return (FilterOutcome::ReportPeer, PartiallyValidData::empty_eth66())
        }

        // 2. checks if announcement is spam packed with duplicate hashes
        let original_len = msg.len();
        let partially_valid_data = msg.dedup();

        (
            if partially_valid_data.len() == original_len {
                FilterOutcome::Ok
            } else {
                FilterOutcome::ReportPeer
            },
            partially_valid_data,
        )
    }
}

/// Filters valid entries in
/// [`NewPooledTransactionHashes68`](reth_eth_wire::NewPooledTransactionHashes68) and
/// [`NewPooledTransactionHashes66`](reth_eth_wire::NewPooledTransactionHashes66) in place, and
/// flags misbehaving peers.
pub trait FilterAnnouncement {
    /// Removes invalid entries from a
    /// [`NewPooledTransactionHashes68`](reth_eth_wire::NewPooledTransactionHashes68) announcement.
    /// Returns [`FilterOutcome::ReportPeer`] if the caller should penalize the peer, otherwise
    /// [`FilterOutcome::Ok`].
    fn filter_valid_entries_68(
        &self,
        msg: PartiallyValidData<Eth68TxMetadata>,
    ) -> (FilterOutcome, ValidAnnouncementData)
    where
        Self: ValidateTx68;

    /// Removes invalid entries from a
    /// [`NewPooledTransactionHashes66`](reth_eth_wire::NewPooledTransactionHashes66) announcement.
    /// Returns [`FilterOutcome::ReportPeer`] if the caller should penalize the peer, otherwise
    /// [`FilterOutcome::Ok`].
    fn filter_valid_entries_66(
        &self,
        msg: PartiallyValidData<Eth68TxMetadata>,
    ) -> (FilterOutcome, ValidAnnouncementData);
}

/// Outcome from filtering
/// [`NewPooledTransactionHashes68`](reth_eth_wire::NewPooledTransactionHashes68). Signals to caller
/// whether to penalize the sender of the announcement or not.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FilterOutcome {
    /// Peer behaves appropriately.
    Ok,
    /// A penalty should be flagged for the peer. Peer sent an announcement with unacceptably
    /// invalid entries.
    ReportPeer,
}

/// Wrapper for types that implement [`FilterAnnouncement`]. The definition of a valid
/// announcement is network dependent. For example, different networks support different
/// [`TxType`]s, and different [`TxType`]s have different transaction size constraints. Defaults to
/// [`EthMessageFilter`].
#[derive(Debug, Default, Deref, DerefMut)]
pub struct MessageFilter<N = EthMessageFilter>(N);

/// Filter for announcements containing EIP [`TxType`]s.
#[derive(Debug, Default)]
pub struct EthMessageFilter {
    announced_tx_types_metrics: AnnouncedTxTypesMetrics,
}

impl Display for EthMessageFilter {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "EthMessageFilter")
    }
}

impl PartiallyFilterMessage for EthMessageFilter {}

impl ValidateTx68 for EthMessageFilter {
    fn should_fetch(
        &self,
        ty: u8,
        hash: &TxHash,
        size: usize,
        tx_types_counter: &mut TxTypesCounter,
    ) -> ValidationOutcome {
        //
        // 1. checks if tx type is valid value for this network
        //
        let tx_type = match TxType::try_from(ty) {
            Ok(ty) => ty,
            Err(_) => {
                trace!(target: "net::eth-wire",
                    ty=ty,
                    size=size,
                    hash=%hash,
                    network=%self,
                    "invalid tx type in eth68 announcement"
                );

                return ValidationOutcome::ReportPeer
            }
        };
        tx_types_counter.increase_by_tx_type(tx_type);

        //
        // 2. checks if tx's encoded length is within limits for this network
        //
        // transactions that are filtered out here, may not be spam, rather from benevolent peers
        // that are unknowingly sending announcements with invalid data.
        //
        if let Some(strict_min_encoded_tx_length) = self.strict_min_encoded_tx_length(tx_type) {
            if size < strict_min_encoded_tx_length {
                trace!(target: "net::eth-wire",
                    ty=ty,
                    size=size,
                    hash=%hash,
                    strict_min_encoded_tx_length=strict_min_encoded_tx_length,
                    network=%self,
                    "invalid tx size in eth68 announcement"
                );

                return ValidationOutcome::Ignore
            }
        }
        if let Some(reasonable_min_encoded_tx_length) = self.min_encoded_tx_length(tx_type) {
            if size < reasonable_min_encoded_tx_length {
                trace!(target: "net::eth-wire",
                    ty=ty,
                    size=size,
                    hash=%hash,
                    reasonable_min_encoded_tx_length=reasonable_min_encoded_tx_length,
                    strict_min_encoded_tx_length=self.strict_min_encoded_tx_length(tx_type),
                    network=%self,
                    "tx size in eth68 announcement, is unreasonably small"
                );

                // just log a tx which is smaller than the reasonable min encoded tx length, don't
                // filter it out
            }
        }
        // this network has no strict max encoded tx length for any tx type
        if let Some(reasonable_max_encoded_tx_length) = self.max_encoded_tx_length(tx_type) {
            if size > reasonable_max_encoded_tx_length {
                trace!(target: "net::eth-wire",
                    ty=ty,
                    size=size,
                    hash=%hash,
                    reasonable_max_encoded_tx_length=reasonable_max_encoded_tx_length,
                    strict_max_encoded_tx_length=self.strict_max_encoded_tx_length(tx_type),
                    network=%self,
                    "tx size in eth68 announcement, is unreasonably large"
                );

                // just log a tx which is bigger than the reasonable max encoded tx length, don't
                // filter it out
            }
        }

        ValidationOutcome::Fetch
    }

    fn max_encoded_tx_length(&self, ty: TxType) -> Option<usize> {
        // the biggest transaction so far is a blob transaction, which is currently max 2^17,
        // encoded length, nonetheless, the blob tx may become bigger in the future.
        #[allow(unreachable_patterns, clippy::match_same_arms)]
        match ty {
            TxType::Legacy | TxType::Eip2930 | TxType::Eip1559 => Some(MAX_MESSAGE_SIZE),
            TxType::Eip4844 => None,
            _ => None,
        }
    }

    fn strict_max_encoded_tx_length(&self, _ty: TxType) -> Option<usize> {
        None
    }

    fn min_encoded_tx_length(&self, _ty: TxType) -> Option<usize> {
        // a transaction will have at least a signature. the encoded signature encoded on the tx
        // is at least as big as the decoded type.
        Some(SIGNATURE_DECODED_SIZE_BYTES)
    }

    fn strict_min_encoded_tx_length(&self, _ty: TxType) -> Option<usize> {
        // decoding a tx will exit right away if it's not at least a byte
        Some(1)
    }
}

impl FilterAnnouncement for EthMessageFilter {
    fn filter_valid_entries_68(
        &self,
        mut msg: PartiallyValidData<Eth68TxMetadata>,
    ) -> (FilterOutcome, ValidAnnouncementData)
    where
        Self: ValidateTx68,
    {
        trace!(target: "net::tx::validation",
            msg=?*msg,
            network=%self,
            "validating eth68 announcement data.."
        );

        let mut should_report_peer = false;
        let mut tx_types_counter = TxTypesCounter::default();

        // checks if eth68 announcement metadata is valid
        //
        // transactions that are filtered out here, may not be spam, rather from benevolent peers
        // that are unknowingly sending announcements with invalid data.
        //
        msg.retain(|hash, metadata| {
            debug_assert!(
                metadata.is_some(),
                "metadata should exist for `%hash` in eth68 announcement passed to `%filter_valid_entries_68`,
`%hash`: {hash}"
            );

            let Some((ty, size)) = metadata else {
                return false
            };

            match self.should_fetch(*ty, hash, *size, &mut tx_types_counter) {
                ValidationOutcome::Fetch => true,
                ValidationOutcome::Ignore => false,
                ValidationOutcome::ReportPeer => {
                    should_report_peer = true;
                    false
                }
            }
        });
        self.announced_tx_types_metrics.update_eth68_announcement_metrics(tx_types_counter);
        (
            if should_report_peer { FilterOutcome::ReportPeer } else { FilterOutcome::Ok },
            ValidAnnouncementData::from_partially_valid_data(msg),
        )
    }

    fn filter_valid_entries_66(
        &self,
        partially_valid_data: PartiallyValidData<Option<(u8, usize)>>,
    ) -> (FilterOutcome, ValidAnnouncementData) {
        trace!(target: "net::tx::validation",
            hashes=?*partially_valid_data,
            network=%self,
            "validating eth66 announcement data.."
        );

        (FilterOutcome::Ok, ValidAnnouncementData::from_partially_valid_data(partially_valid_data))
    }
}

// TODO(eip7702): update tests as needed
#[cfg(test)]
mod test {
    use super::*;
    use alloy_primitives::B256;
    use reth_eth_wire::{NewPooledTransactionHashes66, NewPooledTransactionHashes68};
    use std::{collections::HashMap, str::FromStr};

    #[test]
    fn eth68_empty_announcement() {
        let types = vec![];
        let sizes = vec![];
        let hashes = vec![];

        let announcement = NewPooledTransactionHashes68 { types, sizes, hashes };

        let filter = EthMessageFilter::default();

        let (outcome, _partially_valid_data) = filter.partially_filter_valid_entries(announcement);

        assert_eq!(outcome, FilterOutcome::ReportPeer);
    }

    #[test]
    fn eth68_announcement_unrecognized_tx_type() {
        let types = vec![
            TxType::MAX_RESERVED_EIP as u8 + 1, // the first type isn't valid
            TxType::Legacy as u8,
        ];
        let sizes = vec![MAX_MESSAGE_SIZE, MAX_MESSAGE_SIZE];
        let hashes = vec![
            B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafa")
                .unwrap(),
            B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefbbbb")
                .unwrap(),
        ];

        let announcement = NewPooledTransactionHashes68 {
            types: types.clone(),
            sizes: sizes.clone(),
            hashes: hashes.clone(),
        };

        let filter = EthMessageFilter::default();

        let (outcome, partially_valid_data) = filter.partially_filter_valid_entries(announcement);

        assert_eq!(outcome, FilterOutcome::Ok);

        let (outcome, valid_data) = filter.filter_valid_entries_68(partially_valid_data);

        assert_eq!(outcome, FilterOutcome::ReportPeer);

        let mut expected_data = HashMap::default();
        expected_data.insert(hashes[1], Some((types[1], sizes[1])));

        assert_eq!(expected_data, valid_data.into_data())
    }

    #[test]
    fn eth68_announcement_too_small_tx() {
        let types =
            vec![TxType::MAX_RESERVED_EIP as u8, TxType::Legacy as u8, TxType::Eip2930 as u8];
        let sizes = vec![
            0, // the first length isn't valid
            0, // neither is the second
            1,
        ];
        let hashes = vec![
            B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafa")
                .unwrap(),
            B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefbbbb")
                .unwrap(),
            B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeef00bb")
                .unwrap(),
        ];

        let announcement = NewPooledTransactionHashes68 {
            types: types.clone(),
            sizes: sizes.clone(),
            hashes: hashes.clone(),
        };

        let filter = EthMessageFilter::default();

        let (outcome, partially_valid_data) = filter.partially_filter_valid_entries(announcement);

        assert_eq!(outcome, FilterOutcome::Ok);

        let (outcome, valid_data) = filter.filter_valid_entries_68(partially_valid_data);

        assert_eq!(outcome, FilterOutcome::Ok);

        let mut expected_data = HashMap::default();
        expected_data.insert(hashes[2], Some((types[2], sizes[2])));

        assert_eq!(expected_data, valid_data.into_data())
    }

    #[test]
    fn eth68_announcement_duplicate_tx_hash() {
        let types = vec![
            TxType::Eip1559 as u8,
            TxType::Eip4844 as u8,
            TxType::Eip1559 as u8,
            TxType::Eip4844 as u8,
        ];
        let sizes = vec![1, 1, 1, MAX_MESSAGE_SIZE];
        // first three or the same
        let hashes = vec![
            B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafa") // dup
                .unwrap(),
            B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafa") // removed dup
                .unwrap(),
            B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafa") // removed dup
                .unwrap(),
            B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefbbbb")
                .unwrap(),
        ];

        let announcement = NewPooledTransactionHashes68 {
            types: types.clone(),
            sizes: sizes.clone(),
            hashes: hashes.clone(),
        };

        let filter = EthMessageFilter::default();

        let (outcome, partially_valid_data) = filter.partially_filter_valid_entries(announcement);

        assert_eq!(outcome, FilterOutcome::ReportPeer);

        let mut expected_data = HashMap::default();
        expected_data.insert(hashes[3], Some((types[3], sizes[3])));
        expected_data.insert(hashes[0], Some((types[0], sizes[0])));

        assert_eq!(expected_data, partially_valid_data.into_data())
    }

    #[test]
    fn eth66_empty_announcement() {
        let hashes = vec![];

        let announcement = NewPooledTransactionHashes66(hashes);

        let filter: MessageFilter = MessageFilter::default();

        let (outcome, _partially_valid_data) = filter.partially_filter_valid_entries(announcement);

        assert_eq!(outcome, FilterOutcome::ReportPeer);
    }

    #[test]
    fn eth66_announcement_duplicate_tx_hash() {
        // first three or the same
        let hashes = vec![
            B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefbbbb") // dup1
                .unwrap(),
            B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafa") // dup2
                .unwrap(),
            B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafa") // removed dup2
                .unwrap(),
            B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafa") // removed dup2
                .unwrap(),
            B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefbbbb") // removed dup1
                .unwrap(),
        ];

        let announcement = NewPooledTransactionHashes66(hashes.clone());

        let filter: MessageFilter = MessageFilter::default();

        let (outcome, partially_valid_data) = filter.partially_filter_valid_entries(announcement);

        assert_eq!(outcome, FilterOutcome::ReportPeer);

        let mut expected_data = HashMap::default();
        expected_data.insert(hashes[1], None);
        expected_data.insert(hashes[0], None);

        assert_eq!(expected_data, partially_valid_data.into_data())
    }

    #[test]
    fn test_display_for_zst() {
        let filter = EthMessageFilter::default();
        assert_eq!("EthMessageFilter", &filter.to_string());
    }
}