1use crate::metrics::{AnnouncedTxTypesMetrics, TxTypesCounter};
6use alloy_primitives::{PrimitiveSignature as Signature, TxHash};
7use derive_more::{Deref, DerefMut};
8use reth_eth_wire::{
9 DedupPayload, Eth68TxMetadata, HandleMempoolData, PartiallyValidData, ValidAnnouncementData,
10 MAX_MESSAGE_SIZE,
11};
12use reth_primitives::TxType;
13use std::{fmt, fmt::Display, mem};
14use tracing::trace;
15
16pub const SIGNATURE_DECODED_SIZE_BYTES: usize = mem::size_of::<Signature>();
18
19pub trait ValidateTx68 {
22 fn should_fetch(
27 &self,
28 ty: u8,
29 hash: &TxHash,
30 size: usize,
31 tx_types_counter: &mut TxTypesCounter,
32 ) -> ValidationOutcome;
33
34 fn max_encoded_tx_length(&self, ty: TxType) -> Option<usize>;
38
39 fn strict_max_encoded_tx_length(&self, ty: TxType) -> Option<usize>;
42
43 fn min_encoded_tx_length(&self, ty: TxType) -> Option<usize>;
48
49 fn strict_min_encoded_tx_length(&self, ty: TxType) -> Option<usize>;
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum ValidationOutcome {
59 Fetch,
61 Ignore,
63 ReportPeer,
66}
67
68pub trait PartiallyFilterMessage {
71 fn partially_filter_valid_entries<V>(
74 &self,
75 msg: impl DedupPayload<Value = V> + fmt::Debug,
76 ) -> (FilterOutcome, PartiallyValidData<V>) {
77 if msg.is_empty() {
79 trace!(target: "net::tx",
80 msg=?msg,
81 "empty payload"
82 );
83 return (FilterOutcome::ReportPeer, PartiallyValidData::empty_eth66())
84 }
85
86 let original_len = msg.len();
88 let partially_valid_data = msg.dedup();
89
90 (
91 if partially_valid_data.len() == original_len {
92 FilterOutcome::Ok
93 } else {
94 FilterOutcome::ReportPeer
95 },
96 partially_valid_data,
97 )
98 }
99}
100
101pub trait FilterAnnouncement {
106 fn filter_valid_entries_68(
111 &self,
112 msg: PartiallyValidData<Eth68TxMetadata>,
113 ) -> (FilterOutcome, ValidAnnouncementData)
114 where
115 Self: ValidateTx68;
116
117 fn filter_valid_entries_66(
122 &self,
123 msg: PartiallyValidData<Eth68TxMetadata>,
124 ) -> (FilterOutcome, ValidAnnouncementData);
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub enum FilterOutcome {
132 Ok,
134 ReportPeer,
137}
138
139#[derive(Debug, Default, Deref, DerefMut)]
144pub struct MessageFilter<N = EthMessageFilter>(N);
145
146#[derive(Debug, Default)]
148pub struct EthMessageFilter {
149 announced_tx_types_metrics: AnnouncedTxTypesMetrics,
150}
151
152impl Display for EthMessageFilter {
153 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154 write!(f, "EthMessageFilter")
155 }
156}
157
158impl PartiallyFilterMessage for EthMessageFilter {}
159
160impl ValidateTx68 for EthMessageFilter {
161 fn should_fetch(
162 &self,
163 ty: u8,
164 hash: &TxHash,
165 size: usize,
166 tx_types_counter: &mut TxTypesCounter,
167 ) -> ValidationOutcome {
168 let tx_type = match TxType::try_from(ty) {
172 Ok(ty) => ty,
173 Err(_) => {
174 trace!(target: "net::eth-wire",
175 ty=ty,
176 size=size,
177 hash=%hash,
178 network=%self,
179 "invalid tx type in eth68 announcement"
180 );
181
182 return ValidationOutcome::ReportPeer
183 }
184 };
185 tx_types_counter.increase_by_tx_type(tx_type);
186
187 if let Some(strict_min_encoded_tx_length) = self.strict_min_encoded_tx_length(tx_type) {
194 if size < strict_min_encoded_tx_length {
195 trace!(target: "net::eth-wire",
196 ty=ty,
197 size=size,
198 hash=%hash,
199 strict_min_encoded_tx_length=strict_min_encoded_tx_length,
200 network=%self,
201 "invalid tx size in eth68 announcement"
202 );
203
204 return ValidationOutcome::Ignore
205 }
206 }
207 if let Some(reasonable_min_encoded_tx_length) = self.min_encoded_tx_length(tx_type) {
208 if size < reasonable_min_encoded_tx_length {
209 trace!(target: "net::eth-wire",
210 ty=ty,
211 size=size,
212 hash=%hash,
213 reasonable_min_encoded_tx_length=reasonable_min_encoded_tx_length,
214 strict_min_encoded_tx_length=self.strict_min_encoded_tx_length(tx_type),
215 network=%self,
216 "tx size in eth68 announcement, is unreasonably small"
217 );
218
219 }
222 }
223 if let Some(reasonable_max_encoded_tx_length) = self.max_encoded_tx_length(tx_type) {
225 if size > reasonable_max_encoded_tx_length {
226 trace!(target: "net::eth-wire",
227 ty=ty,
228 size=size,
229 hash=%hash,
230 reasonable_max_encoded_tx_length=reasonable_max_encoded_tx_length,
231 strict_max_encoded_tx_length=self.strict_max_encoded_tx_length(tx_type),
232 network=%self,
233 "tx size in eth68 announcement, is unreasonably large"
234 );
235
236 }
239 }
240
241 ValidationOutcome::Fetch
242 }
243
244 fn max_encoded_tx_length(&self, ty: TxType) -> Option<usize> {
245 #[allow(unreachable_patterns, clippy::match_same_arms)]
248 match ty {
249 TxType::Legacy | TxType::Eip2930 | TxType::Eip1559 => Some(MAX_MESSAGE_SIZE),
250 TxType::Eip4844 => None,
251 _ => None,
252 }
253 }
254
255 fn strict_max_encoded_tx_length(&self, _ty: TxType) -> Option<usize> {
256 None
257 }
258
259 fn min_encoded_tx_length(&self, _ty: TxType) -> Option<usize> {
260 Some(SIGNATURE_DECODED_SIZE_BYTES)
263 }
264
265 fn strict_min_encoded_tx_length(&self, _ty: TxType) -> Option<usize> {
266 Some(1)
268 }
269}
270
271impl FilterAnnouncement for EthMessageFilter {
272 fn filter_valid_entries_68(
273 &self,
274 mut msg: PartiallyValidData<Eth68TxMetadata>,
275 ) -> (FilterOutcome, ValidAnnouncementData)
276 where
277 Self: ValidateTx68,
278 {
279 trace!(target: "net::tx::validation",
280 msg=?*msg,
281 network=%self,
282 "validating eth68 announcement data.."
283 );
284
285 let mut should_report_peer = false;
286 let mut tx_types_counter = TxTypesCounter::default();
287
288 msg.retain(|hash, metadata| {
294 debug_assert!(
295 metadata.is_some(),
296 "metadata should exist for `%hash` in eth68 announcement passed to `%filter_valid_entries_68`,
297`%hash`: {hash}"
298 );
299
300 let Some((ty, size)) = metadata else {
301 return false
302 };
303
304 match self.should_fetch(*ty, hash, *size, &mut tx_types_counter) {
305 ValidationOutcome::Fetch => true,
306 ValidationOutcome::Ignore => false,
307 ValidationOutcome::ReportPeer => {
308 should_report_peer = true;
309 false
310 }
311 }
312 });
313 self.announced_tx_types_metrics.update_eth68_announcement_metrics(tx_types_counter);
314 (
315 if should_report_peer { FilterOutcome::ReportPeer } else { FilterOutcome::Ok },
316 ValidAnnouncementData::from_partially_valid_data(msg),
317 )
318 }
319
320 fn filter_valid_entries_66(
321 &self,
322 partially_valid_data: PartiallyValidData<Option<(u8, usize)>>,
323 ) -> (FilterOutcome, ValidAnnouncementData) {
324 trace!(target: "net::tx::validation",
325 hashes=?*partially_valid_data,
326 network=%self,
327 "validating eth66 announcement data.."
328 );
329
330 (FilterOutcome::Ok, ValidAnnouncementData::from_partially_valid_data(partially_valid_data))
331 }
332}
333
334#[cfg(test)]
335mod test {
336 use super::*;
337 use alloy_primitives::B256;
338 use reth_eth_wire::{NewPooledTransactionHashes66, NewPooledTransactionHashes68};
339 use std::{collections::HashMap, str::FromStr};
340
341 #[test]
342 fn eth68_empty_announcement() {
343 let types = vec![];
344 let sizes = vec![];
345 let hashes = vec![];
346
347 let announcement = NewPooledTransactionHashes68 { types, sizes, hashes };
348
349 let filter = EthMessageFilter::default();
350
351 let (outcome, _partially_valid_data) = filter.partially_filter_valid_entries(announcement);
352
353 assert_eq!(outcome, FilterOutcome::ReportPeer);
354 }
355
356 #[test]
357 fn eth68_announcement_unrecognized_tx_type() {
358 let types = vec![
359 TxType::Eip7702 as u8 + 1, TxType::Legacy as u8,
361 ];
362 let sizes = vec![MAX_MESSAGE_SIZE, MAX_MESSAGE_SIZE];
363 let hashes = vec![
364 B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafa")
365 .unwrap(),
366 B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefbbbb")
367 .unwrap(),
368 ];
369
370 let announcement = NewPooledTransactionHashes68 {
371 types: types.clone(),
372 sizes: sizes.clone(),
373 hashes: hashes.clone(),
374 };
375
376 let filter = EthMessageFilter::default();
377
378 let (outcome, partially_valid_data) = filter.partially_filter_valid_entries(announcement);
379
380 assert_eq!(outcome, FilterOutcome::Ok);
381
382 let (outcome, valid_data) = filter.filter_valid_entries_68(partially_valid_data);
383
384 assert_eq!(outcome, FilterOutcome::ReportPeer);
385
386 let mut expected_data = HashMap::default();
387 expected_data.insert(hashes[1], Some((types[1], sizes[1])));
388
389 assert_eq!(expected_data, valid_data.into_data())
390 }
391
392 #[test]
393 fn eth68_announcement_too_small_tx() {
394 let types = vec![TxType::Eip7702 as u8, TxType::Legacy as u8, TxType::Eip2930 as u8];
395 let sizes = vec![
396 0, 0, 1,
399 ];
400 let hashes = vec![
401 B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafa")
402 .unwrap(),
403 B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefbbbb")
404 .unwrap(),
405 B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeef00bb")
406 .unwrap(),
407 ];
408
409 let announcement = NewPooledTransactionHashes68 {
410 types: types.clone(),
411 sizes: sizes.clone(),
412 hashes: hashes.clone(),
413 };
414
415 let filter = EthMessageFilter::default();
416
417 let (outcome, partially_valid_data) = filter.partially_filter_valid_entries(announcement);
418
419 assert_eq!(outcome, FilterOutcome::Ok);
420
421 let (outcome, valid_data) = filter.filter_valid_entries_68(partially_valid_data);
422
423 assert_eq!(outcome, FilterOutcome::Ok);
424
425 let mut expected_data = HashMap::default();
426 expected_data.insert(hashes[2], Some((types[2], sizes[2])));
427
428 assert_eq!(expected_data, valid_data.into_data())
429 }
430
431 #[test]
432 fn eth68_announcement_duplicate_tx_hash() {
433 let types = vec![
434 TxType::Eip1559 as u8,
435 TxType::Eip4844 as u8,
436 TxType::Eip1559 as u8,
437 TxType::Eip4844 as u8,
438 ];
439 let sizes = vec![1, 1, 1, MAX_MESSAGE_SIZE];
440 let hashes = vec![
442 B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafa") .unwrap(),
444 B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafa") .unwrap(),
446 B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafa") .unwrap(),
448 B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefbbbb")
449 .unwrap(),
450 ];
451
452 let announcement = NewPooledTransactionHashes68 {
453 types: types.clone(),
454 sizes: sizes.clone(),
455 hashes: hashes.clone(),
456 };
457
458 let filter = EthMessageFilter::default();
459
460 let (outcome, partially_valid_data) = filter.partially_filter_valid_entries(announcement);
461
462 assert_eq!(outcome, FilterOutcome::ReportPeer);
463
464 let mut expected_data = HashMap::default();
465 expected_data.insert(hashes[3], Some((types[3], sizes[3])));
466 expected_data.insert(hashes[0], Some((types[0], sizes[0])));
467
468 assert_eq!(expected_data, partially_valid_data.into_data())
469 }
470
471 #[test]
472 fn eth66_empty_announcement() {
473 let hashes = vec![];
474
475 let announcement = NewPooledTransactionHashes66(hashes);
476
477 let filter: MessageFilter = MessageFilter::default();
478
479 let (outcome, _partially_valid_data) = filter.partially_filter_valid_entries(announcement);
480
481 assert_eq!(outcome, FilterOutcome::ReportPeer);
482 }
483
484 #[test]
485 fn eth66_announcement_duplicate_tx_hash() {
486 let hashes = vec![
488 B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefbbbb") .unwrap(),
490 B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafa") .unwrap(),
492 B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafa") .unwrap(),
494 B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafa") .unwrap(),
496 B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefbbbb") .unwrap(),
498 ];
499
500 let announcement = NewPooledTransactionHashes66(hashes.clone());
501
502 let filter: MessageFilter = MessageFilter::default();
503
504 let (outcome, partially_valid_data) = filter.partially_filter_valid_entries(announcement);
505
506 assert_eq!(outcome, FilterOutcome::ReportPeer);
507
508 let mut expected_data = HashMap::default();
509 expected_data.insert(hashes[1], None);
510 expected_data.insert(hashes[0], None);
511
512 assert_eq!(expected_data, partially_valid_data.into_data())
513 }
514
515 #[test]
516 fn eth68_announcement_eip7702_tx() {
517 let types = vec![TxType::Eip7702 as u8, TxType::Legacy as u8];
518 let sizes = vec![MAX_MESSAGE_SIZE, MAX_MESSAGE_SIZE];
519 let hashes = vec![
520 B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafa")
521 .unwrap(),
522 B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefbbbb")
523 .unwrap(),
524 ];
525
526 let announcement = NewPooledTransactionHashes68 {
527 types: types.clone(),
528 sizes: sizes.clone(),
529 hashes: hashes.clone(),
530 };
531
532 let filter = EthMessageFilter::default();
533
534 let (outcome, partially_valid_data) = filter.partially_filter_valid_entries(announcement);
535 assert_eq!(outcome, FilterOutcome::Ok);
536
537 let (outcome, valid_data) = filter.filter_valid_entries_68(partially_valid_data);
538 assert_eq!(outcome, FilterOutcome::Ok);
539
540 let mut expected_data = HashMap::default();
541 expected_data.insert(hashes[0], Some((types[0], sizes[0])));
542 expected_data.insert(hashes[1], Some((types[1], sizes[1])));
543
544 assert_eq!(expected_data, valid_data.into_data());
545 }
546
547 #[test]
548 fn eth68_announcement_eip7702_tx_size_validation() {
549 let types = vec![TxType::Eip7702 as u8, TxType::Eip7702 as u8, TxType::Eip7702 as u8];
550 let sizes = vec![
552 1, MAX_MESSAGE_SIZE / 2, MAX_MESSAGE_SIZE + 1, ];
556 let hashes = vec![
557 B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafa")
558 .unwrap(),
559 B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefbbbb")
560 .unwrap(),
561 B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcccc")
562 .unwrap(),
563 ];
564
565 let announcement = NewPooledTransactionHashes68 {
566 types: types.clone(),
567 sizes: sizes.clone(),
568 hashes: hashes.clone(),
569 };
570
571 let filter = EthMessageFilter::default();
572
573 let (outcome, partially_valid_data) = filter.partially_filter_valid_entries(announcement);
574 assert_eq!(outcome, FilterOutcome::Ok);
575
576 let (outcome, valid_data) = filter.filter_valid_entries_68(partially_valid_data);
577 assert_eq!(outcome, FilterOutcome::Ok);
578
579 let mut expected_data = HashMap::default();
580
581 for i in 0..3 {
582 expected_data.insert(hashes[i], Some((types[i], sizes[i])));
583 }
584
585 assert_eq!(expected_data, valid_data.into_data());
586 }
587
588 #[test]
589 fn eth68_announcement_mixed_tx_types() {
590 let types = vec![
591 TxType::Legacy as u8,
592 TxType::Eip7702 as u8,
593 TxType::Eip1559 as u8,
594 TxType::Eip4844 as u8,
595 ];
596 let sizes = vec![MAX_MESSAGE_SIZE, MAX_MESSAGE_SIZE, MAX_MESSAGE_SIZE, MAX_MESSAGE_SIZE];
597 let hashes = vec![
598 B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafa")
599 .unwrap(),
600 B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefbbbb")
601 .unwrap(),
602 B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcccc")
603 .unwrap(),
604 B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefdddd")
605 .unwrap(),
606 ];
607
608 let announcement = NewPooledTransactionHashes68 {
609 types: types.clone(),
610 sizes: sizes.clone(),
611 hashes: hashes.clone(),
612 };
613
614 let filter = EthMessageFilter::default();
615
616 let (outcome, partially_valid_data) = filter.partially_filter_valid_entries(announcement);
617 assert_eq!(outcome, FilterOutcome::Ok);
618
619 let (outcome, valid_data) = filter.filter_valid_entries_68(partially_valid_data);
620 assert_eq!(outcome, FilterOutcome::Ok);
621
622 let mut expected_data = HashMap::default();
623 for i in 0..4 {
625 expected_data.insert(hashes[i], Some((types[i], sizes[i])));
626 }
627
628 assert_eq!(expected_data, valid_data.into_data());
629 }
630
631 #[test]
632 fn test_display_for_zst() {
633 let filter = EthMessageFilter::default();
634 assert_eq!("EthMessageFilter", &filter.to_string());
635 }
636}