mirror of
https://github.com/hl-archive-node/nanoreth.git
synced 2025-12-06 10:59:55 +00:00
Verify tx response data against request (#6439)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de> Co-authored-by: Oliver Nordbjerg <onbjerg@users.noreply.github.com>
This commit is contained in:
@ -1,12 +1,12 @@
|
||||
//! Validation of [`NewPooledTransactionHashes66`] and [`NewPooledTransactionHashes68`]
|
||||
//! Validation of [`NewPooledTransactionHashes66`](reth_eth_wire::NewPooledTransactionHashes66)
|
||||
//! and [`NewPooledTransactionHashes68`](reth_eth_wire::NewPooledTransactionHashes68)
|
||||
//! announcements. Validation and filtering of announcements is network dependent.
|
||||
|
||||
use std::{collections::HashMap, mem};
|
||||
use std::{fmt, mem};
|
||||
|
||||
use derive_more::{Deref, DerefMut, Display};
|
||||
use itertools::izip;
|
||||
use reth_eth_wire::{
|
||||
NewPooledTransactionHashes66, NewPooledTransactionHashes68, ValidAnnouncementData,
|
||||
DedupPayload, Eth68TxMetadata, HandleMempoolData, PartiallyValidData, ValidAnnouncementData,
|
||||
MAX_MESSAGE_SIZE,
|
||||
};
|
||||
use reth_primitives::{Signature, TxHash, TxType};
|
||||
@ -15,12 +15,14 @@ use tracing::{debug, 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`].
|
||||
/// Interface for validating a `(ty, size, hash)` tuple from a
|
||||
/// [`NewPooledTransactionHashes68`](reth_eth_wire::NewPooledTransactionHashes68)..
|
||||
pub trait ValidateTx68 {
|
||||
/// Validates a [`NewPooledTransactionHashes68`] entry. Returns [`ValidationOutcome`] which
|
||||
/// signals to the caller wether to fetch the transaction or wether to drop it, and wether the
|
||||
/// sender of the announcement should be penalized.
|
||||
fn should_fetch(&self, ty: u8, hash: TxHash, size: usize) -> ValidationOutcome;
|
||||
/// Validates a [`NewPooledTransactionHashes68`](reth_eth_wire::NewPooledTransactionHashes68)
|
||||
/// entry. Returns [`ValidationOutcome`] which signals to the caller wether to fetch the
|
||||
/// transaction or wether to drop it, and wether the sender of the announcement should be
|
||||
/// penalized.
|
||||
fn should_fetch(&self, ty: u8, hash: &TxHash, size: usize) -> 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
|
||||
@ -42,9 +44,9 @@ pub trait ValidateTx68 {
|
||||
fn strict_min_encoded_tx_length(&self, ty: TxType) -> Option<usize>;
|
||||
}
|
||||
|
||||
/// Outcomes from validating a `(ty, hash, size)` entry from a [`NewPooledTransactionHashes68`].
|
||||
/// Signals to the caller how to deal with an announcement entry and the peer who sent the
|
||||
/// announcement.
|
||||
/// 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.
|
||||
@ -56,30 +58,68 @@ pub enum ValidationOutcome {
|
||||
ReportPeer,
|
||||
}
|
||||
|
||||
/// Filters valid entries in [`NewPooledTransactionHashes68`] and [`NewPooledTransactionHashes66`]
|
||||
/// in place, and flags misbehaving peers.
|
||||
/// 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() {
|
||||
debug!(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::ReportPeer
|
||||
} else {
|
||||
FilterOutcome::Ok
|
||||
},
|
||||
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`] announcement. Returns
|
||||
/// [`FilterOutcome::ReportPeer`] if the caller should penalize the peer, otherwise
|
||||
/// 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: NewPooledTransactionHashes68,
|
||||
msg: PartiallyValidData<Eth68TxMetadata>,
|
||||
) -> (FilterOutcome, ValidAnnouncementData)
|
||||
where
|
||||
Self: ValidateTx68;
|
||||
|
||||
/// Removes invalid entries from a [`NewPooledTransactionHashes66`] announcement. Returns
|
||||
/// [`FilterOutcome::ReportPeer`] if the caller should penalize the peer, otherwise
|
||||
/// 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: NewPooledTransactionHashes66,
|
||||
msg: PartiallyValidData<Eth68TxMetadata>,
|
||||
) -> (FilterOutcome, ValidAnnouncementData);
|
||||
}
|
||||
|
||||
/// Outcome from filtering [`NewPooledTransactionHashes68`]. Signals to caller whether to penalize
|
||||
/// the sender of the announcement or not.
|
||||
/// 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.
|
||||
@ -90,18 +130,20 @@ pub enum FilterOutcome {
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// [`EthAnnouncementFilter`].
|
||||
/// 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 AnnouncementFilter<N = EthAnnouncementFilter>(N);
|
||||
pub struct MessageFilter<N = EthMessageFilter>(N);
|
||||
|
||||
/// Filter for announcements containing EIP [`TxType`]s.
|
||||
#[derive(Debug, Display, Default)]
|
||||
pub struct EthAnnouncementFilter;
|
||||
pub struct EthMessageFilter;
|
||||
|
||||
impl ValidateTx68 for EthAnnouncementFilter {
|
||||
fn should_fetch(&self, ty: u8, hash: TxHash, size: usize) -> ValidationOutcome {
|
||||
impl PartiallyFilterMessage for EthMessageFilter {}
|
||||
|
||||
impl ValidateTx68 for EthMessageFilter {
|
||||
fn should_fetch(&self, ty: u8, hash: &TxHash, size: usize) -> ValidationOutcome {
|
||||
//
|
||||
// 1. checks if tx type is valid value for this network
|
||||
//
|
||||
@ -204,140 +246,74 @@ impl ValidateTx68 for EthAnnouncementFilter {
|
||||
}
|
||||
}
|
||||
|
||||
impl FilterAnnouncement for EthAnnouncementFilter {
|
||||
impl FilterAnnouncement for EthMessageFilter {
|
||||
fn filter_valid_entries_68(
|
||||
&self,
|
||||
msg: NewPooledTransactionHashes68,
|
||||
mut msg: PartiallyValidData<Eth68TxMetadata>,
|
||||
) -> (FilterOutcome, ValidAnnouncementData)
|
||||
where
|
||||
Self: ValidateTx68,
|
||||
{
|
||||
trace!(target: "net::tx::validation",
|
||||
types=?msg.types,
|
||||
sizes=?msg.sizes,
|
||||
hashes=?msg.hashes,
|
||||
msg=?*msg,
|
||||
network=%Self,
|
||||
"validating eth68 announcement data.."
|
||||
);
|
||||
|
||||
let NewPooledTransactionHashes68 { mut hashes, mut types, mut sizes } = msg;
|
||||
|
||||
debug_assert!(
|
||||
hashes.len() == types.len() && hashes.len() == sizes.len(), "`%hashes`, `%types` and `%sizes` should all be the same length, decoding of `NewPooledTransactionHashes68` should handle this,
|
||||
`%hashes`: {hashes:?},
|
||||
`%types`: {types:?},
|
||||
`%sizes: {sizes:?}`"
|
||||
);
|
||||
|
||||
//
|
||||
// 1. checks if the announcement is empty
|
||||
//
|
||||
if hashes.is_empty() {
|
||||
debug!(target: "net::tx",
|
||||
network=%Self,
|
||||
"empty eth68 announcement"
|
||||
);
|
||||
return (FilterOutcome::ReportPeer, ValidAnnouncementData::empty_eth68())
|
||||
}
|
||||
|
||||
let mut should_report_peer = false;
|
||||
let mut indices_to_remove = vec![];
|
||||
|
||||
//
|
||||
// 2. checks if eth68 announcement metadata is valid
|
||||
// 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.
|
||||
//
|
||||
for (i, (&ty, &hash, &size)) in izip!(&types, &hashes, &sizes).enumerate() {
|
||||
match self.should_fetch(ty, hash, size) {
|
||||
ValidationOutcome::Fetch => (),
|
||||
ValidationOutcome::Ignore => indices_to_remove.push(i),
|
||||
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) {
|
||||
ValidationOutcome::Fetch => true,
|
||||
ValidationOutcome::Ignore => false,
|
||||
ValidationOutcome::ReportPeer => {
|
||||
indices_to_remove.push(i);
|
||||
should_report_peer = true;
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for index in indices_to_remove.into_iter().rev() {
|
||||
hashes.remove(index);
|
||||
types.remove(index);
|
||||
sizes.remove(index);
|
||||
}
|
||||
|
||||
//
|
||||
// 3. checks if announcement is spam packed with duplicate hashes
|
||||
//
|
||||
let original_len = hashes.len();
|
||||
|
||||
let mut deduped_data = HashMap::with_capacity(hashes.len());
|
||||
for hash in hashes.into_iter().rev() {
|
||||
if let (Some(ty), Some(size)) = (types.pop(), sizes.pop()) {
|
||||
deduped_data.insert(hash, Some((ty, size)));
|
||||
}
|
||||
}
|
||||
deduped_data.shrink_to_fit();
|
||||
|
||||
if deduped_data.len() != original_len {
|
||||
should_report_peer = true
|
||||
}
|
||||
});
|
||||
(
|
||||
if should_report_peer { FilterOutcome::ReportPeer } else { FilterOutcome::Ok },
|
||||
ValidAnnouncementData::new_eth68(deduped_data),
|
||||
ValidAnnouncementData::from_partially_valid_data(msg),
|
||||
)
|
||||
}
|
||||
|
||||
fn filter_valid_entries_66(
|
||||
&self,
|
||||
msg: NewPooledTransactionHashes66,
|
||||
partially_valid_data: PartiallyValidData<Option<(u8, usize)>>,
|
||||
) -> (FilterOutcome, ValidAnnouncementData) {
|
||||
trace!(target: "net::tx::validation",
|
||||
hashes=?msg.0,
|
||||
hashes=?*partially_valid_data,
|
||||
network=%Self,
|
||||
"validating eth66 announcement data.."
|
||||
);
|
||||
|
||||
let NewPooledTransactionHashes66(hashes) = msg;
|
||||
|
||||
//
|
||||
// 1. checks if the announcement is empty
|
||||
//
|
||||
if hashes.is_empty() {
|
||||
debug!(target: "net::tx",
|
||||
network=%Self,
|
||||
"empty eth66 announcement"
|
||||
);
|
||||
return (FilterOutcome::ReportPeer, ValidAnnouncementData::empty_eth66())
|
||||
}
|
||||
|
||||
//
|
||||
// 2. checks if announcement is spam packed with duplicate hashes
|
||||
//
|
||||
let original_len = hashes.len();
|
||||
|
||||
let mut deduped_data = HashMap::with_capacity(hashes.len());
|
||||
for hash in hashes.into_iter().rev() {
|
||||
deduped_data.insert(hash, None);
|
||||
}
|
||||
deduped_data.shrink_to_fit();
|
||||
|
||||
(
|
||||
if deduped_data.len() != original_len {
|
||||
FilterOutcome::ReportPeer
|
||||
} else {
|
||||
FilterOutcome::Ok
|
||||
},
|
||||
ValidAnnouncementData::new_eth66(deduped_data),
|
||||
)
|
||||
(FilterOutcome::Ok, ValidAnnouncementData::from_partially_valid_data(partially_valid_data))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
use reth_eth_wire::{NewPooledTransactionHashes66, NewPooledTransactionHashes68};
|
||||
use reth_primitives::B256;
|
||||
use std::str::FromStr;
|
||||
use std::{collections::HashMap, str::FromStr};
|
||||
|
||||
#[test]
|
||||
fn eth68_empty_announcement() {
|
||||
@ -347,9 +323,9 @@ mod test {
|
||||
|
||||
let announcement = NewPooledTransactionHashes68 { types, sizes, hashes };
|
||||
|
||||
let filter = EthAnnouncementFilter;
|
||||
let filter = EthMessageFilter;
|
||||
|
||||
let (outcome, _data) = filter.filter_valid_entries_68(announcement);
|
||||
let (outcome, _partially_valid_data) = filter.partially_filter_valid_entries(announcement);
|
||||
|
||||
assert_eq!(outcome, FilterOutcome::ReportPeer);
|
||||
}
|
||||
@ -374,16 +350,20 @@ mod test {
|
||||
hashes: hashes.clone(),
|
||||
};
|
||||
|
||||
let filter = EthAnnouncementFilter;
|
||||
let filter = EthMessageFilter;
|
||||
|
||||
let (outcome, data) = filter.filter_valid_entries_68(announcement);
|
||||
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::new();
|
||||
expected_data.insert(hashes[1], Some((types[1], sizes[1])));
|
||||
|
||||
assert_eq!(expected_data, data.into_data())
|
||||
assert_eq!(expected_data, valid_data.into_data())
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -410,16 +390,20 @@ mod test {
|
||||
hashes: hashes.clone(),
|
||||
};
|
||||
|
||||
let filter = EthAnnouncementFilter;
|
||||
let filter = EthMessageFilter;
|
||||
|
||||
let (outcome, data) = filter.filter_valid_entries_68(announcement);
|
||||
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::new();
|
||||
expected_data.insert(hashes[2], Some((types[2], sizes[2])));
|
||||
|
||||
assert_eq!(expected_data, data.into_data())
|
||||
assert_eq!(expected_data, valid_data.into_data())
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -449,9 +433,9 @@ mod test {
|
||||
hashes: hashes.clone(),
|
||||
};
|
||||
|
||||
let filter = EthAnnouncementFilter;
|
||||
let filter = EthMessageFilter;
|
||||
|
||||
let (outcome, data) = filter.filter_valid_entries_68(announcement);
|
||||
let (outcome, partially_valid_data) = filter.partially_filter_valid_entries(announcement);
|
||||
|
||||
assert_eq!(outcome, FilterOutcome::ReportPeer);
|
||||
|
||||
@ -459,7 +443,7 @@ mod test {
|
||||
expected_data.insert(hashes[3], Some((types[3], sizes[3])));
|
||||
expected_data.insert(hashes[0], Some((types[0], sizes[0])));
|
||||
|
||||
assert_eq!(expected_data, data.into_data())
|
||||
assert_eq!(expected_data, partially_valid_data.into_data())
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -468,9 +452,9 @@ mod test {
|
||||
|
||||
let announcement = NewPooledTransactionHashes66(hashes);
|
||||
|
||||
let filter: AnnouncementFilter = AnnouncementFilter::default();
|
||||
let filter: MessageFilter = MessageFilter::default();
|
||||
|
||||
let (outcome, _data) = filter.filter_valid_entries_66(announcement);
|
||||
let (outcome, _partially_valid_data) = filter.partially_filter_valid_entries(announcement);
|
||||
|
||||
assert_eq!(outcome, FilterOutcome::ReportPeer);
|
||||
}
|
||||
@ -493,9 +477,9 @@ mod test {
|
||||
|
||||
let announcement = NewPooledTransactionHashes66(hashes.clone());
|
||||
|
||||
let filter: AnnouncementFilter = AnnouncementFilter::default();
|
||||
let filter: MessageFilter = MessageFilter::default();
|
||||
|
||||
let (outcome, data) = filter.filter_valid_entries_66(announcement);
|
||||
let (outcome, partially_valid_data) = filter.partially_filter_valid_entries(announcement);
|
||||
|
||||
assert_eq!(outcome, FilterOutcome::ReportPeer);
|
||||
|
||||
@ -503,12 +487,12 @@ mod test {
|
||||
expected_data.insert(hashes[1], None);
|
||||
expected_data.insert(hashes[0], None);
|
||||
|
||||
assert_eq!(expected_data, data.into_data())
|
||||
assert_eq!(expected_data, partially_valid_data.into_data())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_derive_more_display_for_zst() {
|
||||
let filter = EthAnnouncementFilter;
|
||||
assert_eq!("EthAnnouncementFilter", &filter.to_string());
|
||||
let filter = EthMessageFilter;
|
||||
assert_eq!("EthMessageFilter", &filter.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user