reth_network/transactions/
validation.rsuse 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;
pub const SIGNATURE_DECODED_SIZE_BYTES: usize = mem::size_of::<Signature>();
pub trait ValidateTx68 {
fn should_fetch(
&self,
ty: u8,
hash: &TxHash,
size: usize,
tx_types_counter: &mut TxTypesCounter,
) -> ValidationOutcome;
fn max_encoded_tx_length(&self, ty: TxType) -> Option<usize>;
fn strict_max_encoded_tx_length(&self, ty: TxType) -> Option<usize>;
fn min_encoded_tx_length(&self, ty: TxType) -> Option<usize>;
fn strict_min_encoded_tx_length(&self, ty: TxType) -> Option<usize>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ValidationOutcome {
Fetch,
Ignore,
ReportPeer,
}
pub trait PartiallyFilterMessage {
fn partially_filter_valid_entries<V>(
&self,
msg: impl DedupPayload<Value = V> + fmt::Debug,
) -> (FilterOutcome, PartiallyValidData<V>) {
if msg.is_empty() {
trace!(target: "net::tx",
msg=?msg,
"empty payload"
);
return (FilterOutcome::ReportPeer, PartiallyValidData::empty_eth66())
}
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,
)
}
}
pub trait FilterAnnouncement {
fn filter_valid_entries_68(
&self,
msg: PartiallyValidData<Eth68TxMetadata>,
) -> (FilterOutcome, ValidAnnouncementData)
where
Self: ValidateTx68;
fn filter_valid_entries_66(
&self,
msg: PartiallyValidData<Eth68TxMetadata>,
) -> (FilterOutcome, ValidAnnouncementData);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FilterOutcome {
Ok,
ReportPeer,
}
#[derive(Debug, Default, Deref, DerefMut)]
pub struct MessageFilter<N = EthMessageFilter>(N);
#[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 {
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);
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"
);
}
}
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"
);
}
}
ValidationOutcome::Fetch
}
fn max_encoded_tx_length(&self, ty: TxType) -> Option<usize> {
#[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> {
Some(SIGNATURE_DECODED_SIZE_BYTES)
}
fn strict_min_encoded_tx_length(&self, _ty: TxType) -> Option<usize> {
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();
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))
}
}
#[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, 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, 0, 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];
let hashes = vec![
B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafa") .unwrap(),
B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafa") .unwrap(),
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::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() {
let hashes = vec![
B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefbbbb") .unwrap(),
B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafa") .unwrap(),
B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafa") .unwrap(),
B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafa") .unwrap(),
B256::from_str("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefbbbb") .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());
}
}