Skip to main content

reth_eth_wire_types/
broadcast.rs

1//! Types for broadcasting new data.
2
3use crate::{EthMessage, EthVersion, NetworkPrimitives};
4use alloc::{sync::Arc, vec::Vec};
5use alloy_consensus::transaction::TxHashRef;
6use alloy_eips::eip2718::Typed2718;
7use alloy_primitives::{bytes::BufMut, Bytes, TxHash, B128, B256, U128};
8use alloy_rlp::{
9    decode_append, Decodable, Encodable, Header, RlpDecodable, RlpDecodableWrapper, RlpEncodable,
10    RlpEncodableWrapper,
11};
12use core::{fmt::Debug, mem};
13use derive_more::{Deref, DerefMut, IntoIterator};
14use reth_codecs_derive::{add_arbitrary_tests, generate_tests};
15use reth_ethereum_primitives::TransactionSigned;
16use reth_primitives_traits::{sync::OnceLock, Block, InMemorySize, SignedTransaction};
17
18/// Soft limit for the number of hashes in a
19/// [`NewPooledTransactionHashes`] broadcast message.
20///
21/// Spec'd at 4096 hashes.
22///
23/// <https://github.com/ethereum/devp2p/blob/master/caps/eth.md#newpooledtransactionhashes-0x08>
24pub const SOFT_LIMIT_COUNT_HASHES_IN_NEW_POOLED_TRANSACTIONS_BROADCAST_MESSAGE: usize = 4096;
25
26/// This informs peers of new blocks that have appeared on the network.
27#[derive(
28    Clone,
29    Debug,
30    PartialEq,
31    Eq,
32    RlpEncodableWrapper,
33    RlpDecodableWrapper,
34    Default,
35    Deref,
36    DerefMut,
37    IntoIterator,
38)]
39#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
40#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
41#[add_arbitrary_tests(rlp)]
42pub struct NewBlockHashes(
43    /// New block hashes and the block number for each blockhash.
44    /// Clients should request blocks using a [`GetBlockBodies`](crate::GetBlockBodies) message.
45    pub Vec<BlockHashNumber>,
46);
47
48// === impl NewBlockHashes ===
49
50impl NewBlockHashes {
51    /// Returns the latest block in the list of blocks.
52    pub fn latest(&self) -> Option<&BlockHashNumber> {
53        self.iter().max_by_key(|b| b.number)
54    }
55}
56
57/// A block hash _and_ a block number.
58#[derive(Clone, Debug, PartialEq, Eq, RlpEncodable, RlpDecodable, Default)]
59#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
60#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
61#[add_arbitrary_tests(rlp)]
62pub struct BlockHashNumber {
63    /// The block hash
64    pub hash: B256,
65    /// The block number
66    pub number: u64,
67}
68
69impl From<Vec<BlockHashNumber>> for NewBlockHashes {
70    fn from(v: Vec<BlockHashNumber>) -> Self {
71        Self(v)
72    }
73}
74
75impl From<NewBlockHashes> for Vec<BlockHashNumber> {
76    fn from(v: NewBlockHashes) -> Self {
77        v.0
78    }
79}
80
81/// A trait for block payloads transmitted through p2p.
82pub trait NewBlockPayload:
83    Encodable + Decodable + Clone + Eq + Debug + Send + Sync + Unpin + 'static
84{
85    /// The block type.
86    type Block: Block;
87
88    /// Returns a reference to the block.
89    fn block(&self) -> &Self::Block;
90}
91
92/// A new block with the current total difficulty, which includes the difficulty of the returned
93/// block.
94#[derive(Clone, Debug, PartialEq, Eq, RlpEncodable, RlpDecodable, Default)]
95#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
96#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
97pub struct NewBlock<B = reth_ethereum_primitives::Block> {
98    /// A new block.
99    pub block: B,
100    /// The current total difficulty.
101    pub td: U128,
102}
103
104impl<B: Block + 'static> NewBlockPayload for NewBlock<B> {
105    type Block = B;
106
107    fn block(&self) -> &Self::Block {
108        &self.block
109    }
110}
111
112generate_tests!(#[rlp, 25] NewBlock<reth_ethereum_primitives::Block>, EthNewBlockTests);
113
114/// This informs peers of transactions that have appeared on the network and are not yet included
115/// in a block.
116#[derive(
117    Clone,
118    Debug,
119    PartialEq,
120    Eq,
121    RlpEncodableWrapper,
122    RlpDecodableWrapper,
123    Default,
124    Deref,
125    IntoIterator,
126)]
127#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
128#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
129#[add_arbitrary_tests(rlp, 10)]
130pub struct Transactions<T = TransactionSigned>(
131    /// New transactions for the peer to include in its mempool.
132    pub Vec<T>,
133);
134
135impl<T: SignedTransaction> Transactions<T> {
136    /// Returns `true` if the list of transactions contains any blob transactions.
137    pub fn has_eip4844(&self) -> bool {
138        self.iter().any(|tx| tx.is_eip4844())
139    }
140}
141
142impl<T> From<Vec<T>> for Transactions<T> {
143    fn from(txs: Vec<T>) -> Self {
144        Self(txs)
145    }
146}
147
148impl<T> From<Transactions<T>> for Vec<T> {
149    fn from(txs: Transactions<T>) -> Self {
150        txs.0
151    }
152}
153
154impl<T: Decodable + InMemorySize> Transactions<T> {
155    /// Decodes the RLP list of transactions, stopping once the cumulative
156    /// [`InMemorySize`] of decoded transactions exceeds `memory_budget` bytes.
157    /// Any remaining transactions in the payload are skipped.
158    pub fn decode_with_memory_budget(
159        buf: &mut &[u8],
160        memory_budget: usize,
161    ) -> alloy_rlp::Result<Self> {
162        decode_list_with_memory_budget(buf, memory_budget).map(Self)
163    }
164}
165
166/// Decodes an RLP list, stopping once the cumulative [`InMemorySize`] of decoded items exceeds
167/// `memory_budget` bytes. Any remaining items in the payload are skipped.
168pub fn decode_list_with_memory_budget<T: Decodable + InMemorySize>(
169    buf: &mut &[u8],
170    memory_budget: usize,
171) -> alloy_rlp::Result<Vec<T>> {
172    let Header { list, payload_length } = Header::decode(buf)?;
173    if !list {
174        return Err(alloy_rlp::Error::UnexpectedString)
175    }
176    // Payload length checked by Header::decode.
177    let (mut payload, rest) = buf.split_at(payload_length);
178
179    let mut txs = Vec::with_capacity(estimated_transaction_list_capacity(payload_length));
180    let mut total_size = 0usize;
181
182    while !payload.is_empty() {
183        let item = T::decode(&mut payload)?;
184        total_size = total_size.saturating_add(item.size());
185
186        if total_size > memory_budget {
187            break;
188        }
189
190        txs.push(item);
191    }
192
193    *buf = rest;
194    Ok(txs)
195}
196
197// Keep this as a conservative hint: small lists stay allocation-free until the first push, while
198// large untrusted payloads cannot force an outsized preallocation.
199const MIN_TRANSACTION_RLP_SIZE_ESTIMATE: usize = 128;
200const MIN_PREALLOCATED_TRANSACTIONS: usize = 4;
201const MAX_PREALLOCATED_TRANSACTIONS: usize = 1024;
202
203const fn estimated_transaction_list_capacity(payload_length: usize) -> usize {
204    let estimate = payload_length / MIN_TRANSACTION_RLP_SIZE_ESTIMATE;
205    if estimate < MIN_PREALLOCATED_TRANSACTIONS {
206        0
207    } else if estimate > MAX_PREALLOCATED_TRANSACTIONS {
208        MAX_PREALLOCATED_TRANSACTIONS
209    } else {
210        estimate
211    }
212}
213
214/// Same as [`Transactions`] but this is intended as egress message send from local to _many_ peers.
215///
216/// The list of transactions is constructed on per-peers basis, but the underlying transaction
217/// objects are shared.
218#[derive(
219    Clone, Debug, PartialEq, Eq, RlpEncodableWrapper, RlpDecodableWrapper, Deref, IntoIterator,
220)]
221#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
222#[add_arbitrary_tests(rlp, 20)]
223pub struct SharedTransactions<T = TransactionSigned>(
224    /// New transactions for the peer to include in its mempool.
225    pub Vec<Arc<T>>,
226);
227
228/// A transaction that can be lazily encoded for pool-backed outbound propagation.
229pub trait BroadcastPoolTransaction:
230    Encodable + TxHashRef + Typed2718 + Send + Sync + 'static
231{
232}
233
234impl<T> BroadcastPoolTransaction for T where
235    T: Encodable + TxHashRef + Typed2718 + Send + Sync + 'static
236{
237}
238
239/// Shared cached encoding for an outbound transaction.
240///
241/// This keeps the transaction object and its encoded bytes behind shared references so cloned
242/// per-peer messages reuse the same EIP-2718 encoding.
243pub struct LazyEncoded<T: ?Sized> {
244    value: Arc<T>,
245    encoded: Arc<OnceLock<Bytes>>,
246}
247
248impl<T: ?Sized> Clone for LazyEncoded<T> {
249    fn clone(&self) -> Self {
250        Self { value: Arc::clone(&self.value), encoded: Arc::clone(&self.encoded) }
251    }
252}
253
254impl LazyEncoded<dyn BroadcastPoolTransaction> {
255    /// Wraps a transaction-like value and lazily caches its encoded bytes.
256    pub fn new<T>(value: T) -> Self
257    where
258        T: BroadcastPoolTransaction,
259    {
260        let value: Arc<dyn BroadcastPoolTransaction> = Arc::new(value);
261        Self { value, encoded: Arc::new(OnceLock::new()) }
262    }
263}
264
265impl<T: Encodable + ?Sized> Encodable for LazyEncoded<T> {
266    fn encode(&self, out: &mut dyn BufMut) {
267        let encoded = self.encoded.get_or_init(|| self.encode_uncached());
268        out.put_slice(encoded);
269    }
270
271    fn length(&self) -> usize {
272        self.encoded.get_or_init(|| self.encode_uncached()).len()
273    }
274}
275
276impl<T: Encodable + ?Sized> LazyEncoded<T> {
277    fn encode_uncached(&self) -> Bytes {
278        alloy_rlp::encode(&self.value).into()
279    }
280}
281
282impl<T: TxHashRef + ?Sized> TxHashRef for LazyEncoded<T> {
283    fn tx_hash(&self) -> &TxHash {
284        self.value.tx_hash()
285    }
286}
287
288impl<T: Typed2718 + ?Sized> Typed2718 for LazyEncoded<T> {
289    fn ty(&self) -> u8 {
290        self.value.ty()
291    }
292}
293
294impl<T: ?Sized> Debug for LazyEncoded<T> {
295    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
296        f.debug_struct("LazyEncoded")
297            .field("is_cached", &self.encoded.get().is_some())
298            .finish_non_exhaustive()
299    }
300}
301
302/// A lazily encoded transaction used for pool-backed full transaction propagation.
303pub type LazyEncodedTransaction = LazyEncoded<dyn BroadcastPoolTransaction>;
304
305/// Outbound-only full transaction propagation message backed by pool transactions.
306///
307/// This encodes to the same `Transactions` wire payload as [`SharedTransactions`], but is used by
308/// the transaction manager when the source is the pool. Unlike [`SharedTransactions`], it can wrap
309/// pool transaction references directly and cache each transaction's encoded bytes across per-peer
310/// messages. Queued messages retain the pool-backed value and the shared cached bytes until they
311/// are sent.
312#[derive(Clone, Debug, Deref, RlpEncodableWrapper)]
313pub struct BroadcastPoolTransactions(pub Vec<LazyEncodedTransaction>);
314
315impl BroadcastPoolTransactions {
316    /// Returns an iterator over the transaction hashes.
317    pub fn iter_hashes(&self) -> impl Iterator<Item = &TxHash> + '_ {
318        self.0.iter().map(TxHashRef::tx_hash)
319    }
320}
321
322/// A wrapper type for all different new pooled transaction types
323#[derive(Clone, Debug, PartialEq, Eq)]
324#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
325pub enum NewPooledTransactionHashes {
326    /// A list of transaction hashes valid for [66-68)
327    Eth66(NewPooledTransactionHashes66),
328    /// A list of transaction hashes valid for [68-72)
329    ///
330    /// Note: it is assumed that the payload is valid (all vectors have the same length)
331    Eth68(NewPooledTransactionHashes68),
332    /// A list of transaction hashes valid from [72..]
333    ///
334    /// This extends the eth/68 announcement payload with the `cell_mask` field introduced by
335    /// [EIP-8070](https://eips.ethereum.org/EIPS/eip-8070).
336    ///
337    /// Note: it is assumed that the payload is valid (all vectors have the same length)
338    Eth72(NewPooledTransactionHashes72),
339}
340
341// === impl NewPooledTransactionHashes ===
342
343impl NewPooledTransactionHashes {
344    /// Returns the message [`EthVersion`].
345    pub const fn version(&self) -> EthVersion {
346        match self {
347            Self::Eth66(_) => EthVersion::Eth66,
348            Self::Eth68(_) => EthVersion::Eth68,
349            Self::Eth72(_) => EthVersion::Eth72,
350        }
351    }
352
353    /// Returns `true` if the payload is valid for the given version
354    pub const fn is_valid_for_version(&self, version: EthVersion) -> bool {
355        match self {
356            Self::Eth66(_) => {
357                matches!(version, EthVersion::Eth67 | EthVersion::Eth66)
358            }
359            Self::Eth68(_) => {
360                matches!(
361                    version,
362                    EthVersion::Eth68 | EthVersion::Eth69 | EthVersion::Eth70 | EthVersion::Eth71
363                )
364            }
365            Self::Eth72(_) => version.is_eth72(),
366        }
367    }
368
369    /// Returns an iterator over all transaction hashes.
370    pub fn iter_hashes(&self) -> impl Iterator<Item = &B256> + '_ {
371        match self {
372            Self::Eth66(msg) => msg.iter(),
373            Self::Eth68(msg) => msg.hashes.iter(),
374            Self::Eth72(msg) => msg.hashes.iter(),
375        }
376    }
377
378    /// Returns an immutable reference to transaction hashes.
379    pub const fn hashes(&self) -> &Vec<B256> {
380        match self {
381            Self::Eth66(msg) => &msg.0,
382            Self::Eth68(msg) => &msg.hashes,
383            Self::Eth72(msg) => &msg.hashes,
384        }
385    }
386
387    /// Returns a mutable reference to transaction hashes.
388    pub const fn hashes_mut(&mut self) -> &mut Vec<B256> {
389        match self {
390            Self::Eth66(msg) => &mut msg.0,
391            Self::Eth68(msg) => &mut msg.hashes,
392            Self::Eth72(msg) => &mut msg.hashes,
393        }
394    }
395
396    /// Consumes the type and returns all hashes
397    pub fn into_hashes(self) -> Vec<B256> {
398        match self {
399            Self::Eth66(msg) => msg.0,
400            Self::Eth68(msg) => msg.hashes,
401            Self::Eth72(msg) => msg.hashes,
402        }
403    }
404
405    /// Returns an iterator over all transaction hashes.
406    pub fn into_iter_hashes(self) -> impl Iterator<Item = B256> {
407        match self {
408            Self::Eth66(msg) => msg.into_iter(),
409            Self::Eth68(msg) => msg.hashes.into_iter(),
410            Self::Eth72(msg) => msg.hashes.into_iter(),
411        }
412    }
413
414    /// Shortens the number of hashes in the message, keeping the first `len` hashes and dropping
415    /// the rest. If `len` is greater than the number of hashes, this has no effect.
416    pub fn truncate(&mut self, len: usize) {
417        match self {
418            Self::Eth66(msg) => msg.truncate(len),
419            Self::Eth68(msg) => {
420                msg.types.truncate(len);
421                msg.sizes.truncate(len);
422                msg.hashes.truncate(len);
423            }
424            Self::Eth72(msg) => {
425                msg.types.truncate(len);
426                msg.sizes.truncate(len);
427                msg.hashes.truncate(len);
428            }
429        }
430    }
431
432    /// Returns true if the message is empty
433    pub const fn is_empty(&self) -> bool {
434        match self {
435            Self::Eth66(msg) => msg.0.is_empty(),
436            Self::Eth68(msg) => msg.hashes.is_empty(),
437            Self::Eth72(msg) => msg.hashes.is_empty(),
438        }
439    }
440
441    /// Returns the number of hashes in the message
442    pub const fn len(&self) -> usize {
443        match self {
444            Self::Eth66(msg) => msg.0.len(),
445            Self::Eth68(msg) => msg.hashes.len(),
446            Self::Eth72(msg) => msg.hashes.len(),
447        }
448    }
449
450    /// Returns an immutable reference to the inner type if this is an eth68 announcement.
451    pub const fn as_eth72(&self) -> Option<&NewPooledTransactionHashes72> {
452        match self {
453            Self::Eth66(_) | Self::Eth68(_) => None,
454            Self::Eth72(msg) => Some(msg),
455        }
456    }
457
458    /// Returns a mutable reference to the inner type if this is an eth68 announcement.
459    pub const fn as_eth72_mut(&mut self) -> Option<&mut NewPooledTransactionHashes72> {
460        match self {
461            Self::Eth66(_) | Self::Eth68(_) => None,
462            Self::Eth72(msg) => Some(msg),
463        }
464    }
465
466    /// Returns an immutable reference to the inner type if this is an eth68 announcement.
467    pub const fn as_eth68(&self) -> Option<&NewPooledTransactionHashes68> {
468        match self {
469            Self::Eth66(_) | Self::Eth72(_) => None,
470            Self::Eth68(msg) => Some(msg),
471        }
472    }
473
474    /// Returns a mutable reference to the inner type if this is an eth68 announcement.
475    pub const fn as_eth68_mut(&mut self) -> Option<&mut NewPooledTransactionHashes68> {
476        match self {
477            Self::Eth66(_) | Self::Eth72(_) => None,
478            Self::Eth68(msg) => Some(msg),
479        }
480    }
481
482    /// Returns a mutable reference to the inner type if this is an eth66 announcement.
483    pub const fn as_eth66_mut(&mut self) -> Option<&mut NewPooledTransactionHashes66> {
484        match self {
485            Self::Eth66(msg) => Some(msg),
486            Self::Eth68(_) | Self::Eth72(_) => None,
487        }
488    }
489
490    /// Returns the inner type if this is an eth68 announcement.
491    pub fn take_eth68(&mut self) -> Option<NewPooledTransactionHashes68> {
492        match self {
493            Self::Eth66(_) | Self::Eth72(_) => None,
494            Self::Eth68(msg) => Some(mem::take(msg)),
495        }
496    }
497
498    /// Returns the inner type if this is an eth66 announcement.
499    pub fn take_eth66(&mut self) -> Option<NewPooledTransactionHashes66> {
500        match self {
501            Self::Eth66(msg) => Some(mem::take(msg)),
502            Self::Eth68(_) | Self::Eth72(_) => None,
503        }
504    }
505}
506
507impl<N: NetworkPrimitives> From<NewPooledTransactionHashes> for EthMessage<N> {
508    fn from(value: NewPooledTransactionHashes) -> Self {
509        match value {
510            NewPooledTransactionHashes::Eth66(msg) => Self::NewPooledTransactionHashes66(msg),
511            NewPooledTransactionHashes::Eth68(msg) => Self::NewPooledTransactionHashes68(msg),
512            NewPooledTransactionHashes::Eth72(msg) => Self::NewPooledTransactionHashes72(msg),
513        }
514    }
515}
516
517impl From<NewPooledTransactionHashes66> for NewPooledTransactionHashes {
518    fn from(hashes: NewPooledTransactionHashes66) -> Self {
519        Self::Eth66(hashes)
520    }
521}
522
523impl From<NewPooledTransactionHashes68> for NewPooledTransactionHashes {
524    fn from(hashes: NewPooledTransactionHashes68) -> Self {
525        Self::Eth68(hashes)
526    }
527}
528
529impl From<NewPooledTransactionHashes72> for NewPooledTransactionHashes {
530    fn from(hashes: NewPooledTransactionHashes72) -> Self {
531        Self::Eth72(hashes)
532    }
533}
534
535/// This informs peers of transaction hashes for transactions that have appeared on the network,
536/// but have not been included in a block.
537#[derive(
538    Clone,
539    Debug,
540    PartialEq,
541    Eq,
542    RlpEncodableWrapper,
543    RlpDecodableWrapper,
544    Default,
545    Deref,
546    DerefMut,
547    IntoIterator,
548)]
549#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
550#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
551#[add_arbitrary_tests(rlp)]
552pub struct NewPooledTransactionHashes66(
553    /// Transaction hashes for new transactions that have appeared on the network.
554    /// Clients should request the transactions with the given hashes using a
555    /// [`GetPooledTransactions`](crate::GetPooledTransactions) message.
556    pub Vec<B256>,
557);
558
559impl NewPooledTransactionHashes66 {
560    /// Returns a new instance with capacity for `capacity` hashes.
561    pub fn with_capacity(capacity: usize) -> Self {
562        Self(Vec::with_capacity(capacity))
563    }
564}
565
566impl From<Vec<B256>> for NewPooledTransactionHashes66 {
567    fn from(v: Vec<B256>) -> Self {
568        Self(v)
569    }
570}
571
572/// Same as [`NewPooledTransactionHashes66`] but extends that beside the transaction hashes,
573/// the node sends the transaction types and their sizes (as defined in EIP-2718) as well.
574#[derive(Clone, Debug, PartialEq, Eq, Default)]
575#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
576pub struct NewPooledTransactionHashes68 {
577    /// Transaction types for new transactions that have appeared on the network.
578    ///
579    /// ## Note on RLP encoding and decoding
580    ///
581    /// In the [eth/68 spec](https://eips.ethereum.org/EIPS/eip-5793#specification) this is defined
582    /// the following way:
583    ///  * `[type_0: B_1, type_1: B_1, ...]`
584    ///
585    /// This would make it seem like the [`Encodable`] and
586    /// [`Decodable`] implementations should directly use a `Vec<u8>` for
587    /// encoding and decoding, because it looks like this field should be encoded as a _list_ of
588    /// bytes.
589    ///
590    /// However, [this is implemented in geth as a `[]byte`
591    /// type](https://github.com/ethereum/go-ethereum/blob/82d934b1dd80cdd8190803ea9f73ed2c345e2576/eth/protocols/eth/protocol.go#L308-L313),
592    /// which [ends up being encoded as a RLP
593    /// string](https://github.com/ethereum/go-ethereum/blob/82d934b1dd80cdd8190803ea9f73ed2c345e2576/rlp/encode_test.go#L171-L176),
594    /// **not** a RLP list.
595    ///
596    /// Because of this, we do not directly use the `Vec<u8>` when encoding and decoding, and
597    /// instead use the [`Encodable`] and [`Decodable`]
598    /// implementations for `&[u8]` instead, which encodes into a RLP string, and expects an RLP
599    /// string when decoding.
600    pub types: Vec<u8>,
601    /// Transaction sizes for new transactions that have appeared on the network.
602    pub sizes: Vec<usize>,
603    /// Transaction hashes for new transactions that have appeared on the network.
604    pub hashes: Vec<B256>,
605}
606
607#[cfg(feature = "arbitrary")]
608impl proptest::prelude::Arbitrary for NewPooledTransactionHashes68 {
609    type Parameters = ();
610    fn arbitrary_with(_args: ()) -> Self::Strategy {
611        use proptest::{collection::vec, prelude::*};
612        // Generate a single random length for all vectors
613        let vec_length = any::<usize>().prop_map(|x| x % 100 + 1); // Lengths between 1 and 100
614
615        vec_length
616            .prop_flat_map(|len| {
617                // Use the generated length to create vectors of TxType, usize, and B256
618                let types_vec = vec(
619                    proptest_arbitrary_interop::arb::<reth_ethereum_primitives::TxType>()
620                        .prop_map(|ty| ty as u8),
621                    len..=len,
622                );
623
624                // Map the usize values to the range 0..131072(0x20000)
625                let sizes_vec = vec(proptest::num::usize::ANY.prop_map(|x| x % 131072), len..=len);
626                let hashes_vec = vec(any::<B256>(), len..=len);
627
628                (types_vec, sizes_vec, hashes_vec)
629            })
630            .prop_map(|(types, sizes, hashes)| Self { types, sizes, hashes })
631            .boxed()
632    }
633
634    type Strategy = proptest::prelude::BoxedStrategy<Self>;
635}
636
637impl NewPooledTransactionHashes68 {
638    /// Returns the number of announced hashes.
639    pub const fn len(&self) -> usize {
640        self.hashes.len()
641    }
642
643    /// Returns whether there are no announced hashes.
644    pub const fn is_empty(&self) -> bool {
645        self.hashes.is_empty()
646    }
647
648    /// Returns a new instance with capacity for `capacity` entries.
649    pub fn with_capacity(capacity: usize) -> Self {
650        Self {
651            types: Vec::with_capacity(capacity),
652            sizes: Vec::with_capacity(capacity),
653            hashes: Vec::with_capacity(capacity),
654        }
655    }
656
657    /// Returns an iterator over tx hashes zipped with corresponding metadata.
658    pub fn metadata_iter(&self) -> impl Iterator<Item = (&B256, (u8, usize))> {
659        self.hashes.iter().zip(self.types.iter().copied().zip(self.sizes.iter().copied()))
660    }
661
662    /// Appends a transaction
663    pub fn push<T: SignedTransaction>(&mut self, tx: &T) {
664        self.hashes.push(*tx.tx_hash());
665        self.sizes.push(tx.encode_2718_len());
666        self.types.push(tx.ty());
667    }
668
669    /// Appends the provided transactions
670    pub fn extend<'a, T: SignedTransaction>(&mut self, txs: impl IntoIterator<Item = &'a T>) {
671        for tx in txs {
672            self.push(tx);
673        }
674    }
675
676    /// Shrinks the capacity of the message vectors as much as possible.
677    pub fn shrink_to_fit(&mut self) {
678        self.hashes.shrink_to_fit();
679        self.sizes.shrink_to_fit();
680        self.types.shrink_to_fit()
681    }
682
683    /// Consumes and appends a transaction
684    pub fn with_transaction<T: SignedTransaction>(mut self, tx: &T) -> Self {
685        self.push(tx);
686        self
687    }
688
689    /// Consumes and appends the provided transactions
690    pub fn with_transactions<'a, T: SignedTransaction>(
691        mut self,
692        txs: impl IntoIterator<Item = &'a T>,
693    ) -> Self {
694        self.extend(txs);
695        self
696    }
697}
698
699impl Encodable for NewPooledTransactionHashes68 {
700    fn encode(&self, out: &mut dyn bytes::BufMut) {
701        #[derive(RlpEncodable)]
702        struct EncodableNewPooledTransactionHashes68<'a> {
703            types: &'a [u8],
704            sizes: &'a Vec<usize>,
705            hashes: &'a Vec<B256>,
706        }
707
708        let encodable = EncodableNewPooledTransactionHashes68 {
709            types: &self.types[..],
710            sizes: &self.sizes,
711            hashes: &self.hashes,
712        };
713
714        encodable.encode(out);
715    }
716    fn length(&self) -> usize {
717        #[derive(RlpEncodable)]
718        struct EncodableNewPooledTransactionHashes68<'a> {
719            types: &'a [u8],
720            sizes: &'a Vec<usize>,
721            hashes: &'a Vec<B256>,
722        }
723
724        let encodable = EncodableNewPooledTransactionHashes68 {
725            types: &self.types[..],
726            sizes: &self.sizes,
727            hashes: &self.hashes,
728        };
729
730        encodable.length()
731    }
732}
733
734impl Decodable for NewPooledTransactionHashes68 {
735    fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
736        let Header { list, payload_length } = Header::decode(buf)?;
737        if !list {
738            return Err(alloy_rlp::Error::UnexpectedString)
739        }
740        // Payload length checked by Header::decode.
741        let (mut payload, rest) = buf.split_at(payload_length);
742        let (types, sizes, hashes) = decode_pooled_transaction_hashes_payload(&mut payload)?;
743
744        if !payload.is_empty() {
745            return Err(alloy_rlp::Error::ListLengthMismatch {
746                expected: payload_length,
747                got: payload_length - payload.len(),
748            })
749        }
750
751        ensure_pooled_transaction_hashes_lengths(hashes.len(), types.len(), sizes.len())?;
752
753        *buf = rest;
754        Ok(Self { types, sizes, hashes })
755    }
756}
757
758/// Same as [`NewPooledTransactionHashes68`] but adds the eth/72 `cell_mask` field from
759/// [EIP-8070](https://eips.ethereum.org/EIPS/eip-8070).
760#[derive(Clone, Debug, PartialEq, Eq, Default)]
761#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
762pub struct NewPooledTransactionHashes72 {
763    /// Transaction types for new transactions that have appeared on the network.
764    ///
765    /// ## Note on RLP encoding and decoding
766    ///
767    /// In the [eth/72 spec](https://eips.ethereum.org/EIPS/eip-8070#specification) this is defined
768    /// the following way:
769    ///  * `[types: B, [size_0: P, size_1: P, ...], [hash_0: B_32, hash_1: B_32, ...], cell_mask:
770    ///    B_16]`
771    pub types: Vec<u8>,
772    /// Transaction sizes for new transactions that have appeared on the network.
773    pub sizes: Vec<usize>,
774    /// Transaction hashes for new transactions that have appeared on the network.
775    pub hashes: Vec<B256>,
776    /// Cell availability mask for type 3 (blob) transactions announced by this message.
777    ///
778    /// Per [EIP-8070](https://eips.ethereum.org/EIPS/eip-8070), this is a `B_16`
779    /// bitarray over `CELLS_PER_EXT_BLOB`; bit `i` is set when the announcer has column
780    /// `i` available for every type 3 transaction in the message.
781    ///
782    /// On the wire this field is always encoded as a 16 byte string, zero-filled when no
783    /// type 3 transactions are announced: go-ethereum decodes the mask into a fixed
784    /// `[16]byte` and rejects the RLP `nil` encoding the EIP text describes, so the
785    /// always-present form is the de facto network format. `None` is equivalent to a zero
786    /// mask; decoding additionally accepts the spec's `nil` encoding for compatibility.
787    pub cell_mask: Option<B128>,
788}
789
790#[cfg(feature = "arbitrary")]
791impl proptest::prelude::Arbitrary for NewPooledTransactionHashes72 {
792    type Parameters = ();
793    fn arbitrary_with(_args: ()) -> Self::Strategy {
794        use proptest::{collection::vec, prelude::*};
795        // Generate a single random length for all vectors
796        let vec_length = any::<usize>().prop_map(|x| x % 100 + 1); // Lengths between 1 and 100
797
798        vec_length
799            .prop_flat_map(|len| {
800                // Use the generated length to create vectors of TxType, usize, and B256
801                let types_vec = vec(
802                    proptest_arbitrary_interop::arb::<reth_ethereum_primitives::TxType>()
803                        .prop_map(|ty| ty as u8),
804                    len..=len,
805                );
806
807                // Map the usize values to the range 0..131072(0x20000)
808                let sizes_vec = vec(proptest::num::usize::ANY.prop_map(|x| x % 131072), len..=len);
809                let hashes_vec = vec(any::<B256>(), len..=len);
810                // A zero mask is spelled `None`, so generating `Some(ZERO)` would produce a
811                // value that cannot survive its own encoding.
812                let cell_mask =
813                    any::<Option<B128>>().prop_map(|mask| mask.filter(|mask| !mask.is_zero()));
814
815                (types_vec, sizes_vec, hashes_vec, cell_mask)
816            })
817            .prop_map(|(types, sizes, hashes, cell_mask)| Self { types, sizes, hashes, cell_mask })
818            .boxed()
819    }
820
821    type Strategy = proptest::prelude::BoxedStrategy<Self>;
822}
823
824impl NewPooledTransactionHashes72 {
825    /// Returns the number of announced hashes.
826    pub const fn len(&self) -> usize {
827        self.hashes.len()
828    }
829
830    /// Returns whether there are no announced hashes.
831    pub const fn is_empty(&self) -> bool {
832        self.hashes.is_empty()
833    }
834
835    /// Cell mask advertising availability of every cell.
836    ///
837    /// Used when announcing blob transactions whose full sidecar is available locally, since
838    /// every cell can be computed from the complete blob data.
839    pub const ALL_CELLS_MASK: B128 = B128::repeat_byte(0xff);
840
841    /// Returns a new instance with capacity for `capacity` entries and no cell mask.
842    pub fn with_capacity(capacity: usize) -> Self {
843        Self {
844            types: Vec::with_capacity(capacity),
845            sizes: Vec::with_capacity(capacity),
846            hashes: Vec::with_capacity(capacity),
847            cell_mask: None,
848        }
849    }
850
851    /// Returns an iterator over tx hashes zipped with corresponding metadata.
852    pub fn metadata_iter(&self) -> impl Iterator<Item = (&B256, (u8, usize))> {
853        self.hashes.iter().zip(self.types.iter().copied().zip(self.sizes.iter().copied()))
854    }
855
856    /// Appends a transaction
857    pub fn push<T: SignedTransaction>(&mut self, tx: &T) {
858        self.hashes.push(*tx.tx_hash());
859        self.sizes.push(tx.encode_2718_len());
860        self.types.push(tx.ty());
861        if tx.is_eip4844() {
862            self.cell_mask = Some(Self::ALL_CELLS_MASK);
863        }
864    }
865
866    /// Appends the provided transactions
867    pub fn extend<'a, T: SignedTransaction>(&mut self, txs: impl IntoIterator<Item = &'a T>) {
868        for tx in txs {
869            self.push(tx);
870        }
871    }
872
873    /// Shrinks the capacity of the message vectors as much as possible.
874    pub fn shrink_to_fit(&mut self) {
875        self.hashes.shrink_to_fit();
876        self.sizes.shrink_to_fit();
877        self.types.shrink_to_fit()
878    }
879
880    /// Consumes and appends a transaction
881    pub fn with_transaction<T: SignedTransaction>(mut self, tx: &T) -> Self {
882        self.push(tx);
883        self
884    }
885
886    /// Consumes and appends the provided transactions
887    pub fn with_transactions<'a, T: SignedTransaction>(
888        mut self,
889        txs: impl IntoIterator<Item = &'a T>,
890    ) -> Self {
891        self.extend(txs);
892        self
893    }
894
895    fn payload_length(&self) -> usize {
896        self.types.as_slice().length() +
897            self.sizes.length() +
898            self.hashes.length() +
899            self.cell_mask.unwrap_or_default().length()
900    }
901}
902
903impl Encodable for NewPooledTransactionHashes72 {
904    fn encode(&self, out: &mut dyn bytes::BufMut) {
905        Header { list: true, payload_length: self.payload_length() }.encode(out);
906        self.types.as_slice().encode(out);
907        self.sizes.encode(out);
908        self.hashes.encode(out);
909        // A zero-filled mask when no cells are available, see the `cell_mask` field docs.
910        self.cell_mask.unwrap_or_default().encode(out);
911    }
912
913    fn length(&self) -> usize {
914        Header { list: true, payload_length: self.payload_length() }.length_with_payload()
915    }
916}
917
918impl Decodable for NewPooledTransactionHashes72 {
919    fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
920        let Header { list, payload_length } = Header::decode(buf)?;
921        if !list {
922            return Err(alloy_rlp::Error::UnexpectedString)
923        }
924        // Payload length checked by Header::decode.
925        let (mut payload, rest) = buf.split_at(payload_length);
926        let (types, sizes, hashes) = decode_pooled_transaction_hashes_payload(&mut payload)?;
927        let Some(first_byte) = payload.first().copied() else {
928            return Err(alloy_rlp::Error::InputTooShort)
929        };
930        let cell_mask = if first_byte == alloy_rlp::EMPTY_STRING_CODE {
931            // The EIP-8070 `nil` encoding, tolerated for compatibility.
932            payload = &payload[1..];
933            None
934        } else {
935            // A zero mask is the wire representation of "no cells available", see the
936            // `cell_mask` field docs.
937            Some(B128::decode(&mut payload)?).filter(|mask| !mask.is_zero())
938        };
939
940        if !payload.is_empty() {
941            return Err(alloy_rlp::Error::ListLengthMismatch {
942                expected: payload_length,
943                got: payload_length - payload.len(),
944            })
945        }
946
947        ensure_pooled_transaction_hashes_lengths(hashes.len(), types.len(), sizes.len())?;
948
949        *buf = rest;
950        Ok(Self { types, sizes, hashes, cell_mask })
951    }
952}
953
954/// Twice the spec'd soft limit for `NewPooledTransactionHashes` announcements.
955///
956/// This keeps capacity hints bounded when a malformed packet spends most of its bytes on the
957/// one-byte `types` string before the size and hash lists are validated.
958const NEW_POOLED_TRANSACTION_HASHES_DECODE_CAP: usize =
959    2 * SOFT_LIMIT_COUNT_HASHES_IN_NEW_POOLED_TRANSACTIONS_BROADCAST_MESSAGE;
960
961#[inline]
962fn decode_pooled_transaction_hashes_payload(
963    payload: &mut &[u8],
964) -> alloy_rlp::Result<(Vec<u8>, Vec<usize>, Vec<B256>)> {
965    let types = Bytes::decode(payload)?;
966    let capacity = types.len().min(NEW_POOLED_TRANSACTION_HASHES_DECODE_CAP);
967
968    let mut sizes = Vec::with_capacity(capacity);
969    decode_append(payload, &mut sizes)?;
970
971    let mut hashes = Vec::with_capacity(capacity);
972    decode_append(payload, &mut hashes)?;
973
974    Ok((types.into(), sizes, hashes))
975}
976
977#[inline]
978const fn ensure_pooled_transaction_hashes_lengths(
979    hashes_len: usize,
980    types_len: usize,
981    sizes_len: usize,
982) -> alloy_rlp::Result<()> {
983    if hashes_len != types_len {
984        return Err(alloy_rlp::Error::ListLengthMismatch { expected: hashes_len, got: types_len })
985    }
986    if hashes_len != sizes_len {
987        return Err(alloy_rlp::Error::ListLengthMismatch { expected: hashes_len, got: sizes_len })
988    }
989
990    Ok(())
991}
992
993/// Interface for handling mempool message data. Used in various filters in pipelines in
994/// `TransactionsManager` and in queries to `TransactionPool`.
995pub trait HandleMempoolData {
996    /// The announcement contains no entries.
997    fn is_empty(&self) -> bool;
998
999    /// Returns the number of entries.
1000    fn len(&self) -> usize;
1001
1002    /// Retain only entries for which the hash in the entry satisfies a given predicate.
1003    fn retain_by_hash(&mut self, f: impl FnMut(&TxHash) -> bool);
1004}
1005
1006impl<T: SignedTransaction> HandleMempoolData for Vec<T> {
1007    fn is_empty(&self) -> bool {
1008        self.is_empty()
1009    }
1010
1011    fn len(&self) -> usize {
1012        self.len()
1013    }
1014
1015    fn retain_by_hash(&mut self, mut f: impl FnMut(&TxHash) -> bool) {
1016        self.retain(|tx| f(tx.tx_hash()))
1017    }
1018}
1019
1020/// The earliest block, the latest block and hash of the latest block which can be provided.
1021/// See [BlockRangeUpdate](https://github.com/ethereum/devp2p/blob/master/caps/eth.md#blockrangeupdate-0x11).
1022#[derive(Clone, Debug, PartialEq, Eq, Default, RlpEncodable, RlpDecodable)]
1023#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1024#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
1025pub struct BlockRangeUpdate {
1026    /// The earliest block which is available.
1027    pub earliest: u64,
1028    /// The latest block which is available.
1029    pub latest: u64,
1030    /// Latest available block's hash.
1031    pub latest_hash: B256,
1032}
1033
1034impl InMemorySize for NewPooledTransactionHashes {
1035    fn size(&self) -> usize {
1036        match self {
1037            Self::Eth66(msg) => msg.0.len() * core::mem::size_of::<B256>(),
1038            Self::Eth68(msg) => {
1039                msg.types.len() * core::mem::size_of::<u8>() +
1040                    msg.sizes.len() * core::mem::size_of::<usize>() +
1041                    msg.hashes.len() * core::mem::size_of::<B256>()
1042            }
1043            Self::Eth72(msg) => {
1044                msg.types.len() * core::mem::size_of::<u8>() +
1045                    msg.sizes.len() * core::mem::size_of::<usize>() +
1046                    msg.hashes.len() * core::mem::size_of::<B256>() +
1047                    core::mem::size_of::<B128>()
1048            }
1049        }
1050    }
1051}
1052
1053#[cfg(test)]
1054mod tests {
1055    use super::*;
1056    use alloy_consensus::{transaction::TxHashRef, Typed2718};
1057    use alloy_eips::eip2718::Encodable2718;
1058    use alloy_primitives::{b256, hex, Bytes, Signature, U256};
1059    use alloy_rlp::{RlpDecodable, RlpEncodable};
1060    use proptest::prelude::*;
1061    use reth_ethereum_primitives::{Transaction, TransactionSigned};
1062    use std::str::FromStr;
1063
1064    /// Takes as input a struct / encoded hex message pair, ensuring that we encode to the exact hex
1065    /// message, and decode to the exact struct.
1066    fn test_encoding_vector<T: Encodable + Decodable + PartialEq + core::fmt::Debug>(
1067        input: (T, &[u8]),
1068    ) {
1069        let (expected_decoded, expected_encoded) = input;
1070        let mut encoded = Vec::new();
1071        expected_decoded.encode(&mut encoded);
1072
1073        assert_eq!(hex::encode(&encoded), hex::encode(expected_encoded));
1074
1075        let decoded = T::decode(&mut encoded.as_ref()).unwrap();
1076        assert_eq!(expected_decoded, decoded);
1077    }
1078
1079    fn encoded<T: Encodable>(value: &T) -> Vec<u8> {
1080        let mut out = Vec::new();
1081        value.encode(&mut out);
1082        out
1083    }
1084
1085    #[derive(RlpEncodable, RlpDecodable)]
1086    struct EncodableNewPooledTransactionHashes68 {
1087        types: Bytes,
1088        sizes: Vec<usize>,
1089        hashes: Vec<B256>,
1090    }
1091
1092    type NewPooledTransactionHashes68Fields = (Vec<u8>, Vec<usize>, Vec<B256>);
1093
1094    fn decode_eth68_hashes_derived(
1095        buf: &mut &[u8],
1096    ) -> alloy_rlp::Result<NewPooledTransactionHashes68> {
1097        let encodable = EncodableNewPooledTransactionHashes68::decode(buf)?;
1098        let msg = NewPooledTransactionHashes68 {
1099            types: encodable.types.into(),
1100            sizes: encodable.sizes,
1101            hashes: encodable.hashes,
1102        };
1103
1104        ensure_pooled_transaction_hashes_lengths(
1105            msg.hashes.len(),
1106            msg.types.len(),
1107            msg.sizes.len(),
1108        )?;
1109
1110        Ok(msg)
1111    }
1112
1113    fn eth68_hash_fields_strategy() -> impl Strategy<Value = NewPooledTransactionHashes68Fields> {
1114        (0usize..128, 0usize..128, 0usize..128).prop_flat_map(
1115            |(types_len, sizes_len, hashes_len)| {
1116                (
1117                    proptest::collection::vec(any::<u8>(), types_len),
1118                    proptest::collection::vec(0usize..131_072, sizes_len),
1119                    proptest::collection::vec(any::<B256>(), hashes_len),
1120                )
1121            },
1122        )
1123    }
1124
1125    proptest! {
1126        #[test]
1127        fn broadcast_pool_transactions_match_shared_transactions_encoding(
1128            txs in proptest::collection::vec(
1129                proptest_arbitrary_interop::arb::<TransactionSigned>(),
1130                0..32,
1131            )
1132        ) {
1133            let shared = SharedTransactions::<TransactionSigned>(
1134                txs.iter().cloned().map(Arc::new).collect(),
1135            );
1136            let broadcast = BroadcastPoolTransactions(
1137                txs.iter().cloned().map(LazyEncoded::new).collect(),
1138            );
1139
1140            prop_assert_eq!(broadcast.length(), shared.length());
1141
1142            let shared_encoded = encoded(&shared);
1143            let broadcast_encoded = encoded(&broadcast);
1144            prop_assert_eq!(&broadcast_encoded, &shared_encoded);
1145
1146            let broadcast_encoded_cached = encoded(&broadcast);
1147            prop_assert_eq!(&broadcast_encoded_cached, &shared_encoded);
1148
1149            let mut shared_bytes = shared_encoded.as_slice();
1150            let decoded_shared = SharedTransactions::<TransactionSigned>::decode(&mut shared_bytes)
1151                .expect("shared transactions decode");
1152            prop_assert!(shared_bytes.is_empty());
1153
1154            let mut broadcast_bytes = broadcast_encoded.as_slice();
1155            let decoded_broadcast =
1156                SharedTransactions::<TransactionSigned>::decode(&mut broadcast_bytes)
1157                    .expect("broadcast pool transactions decode as shared transactions");
1158            prop_assert!(broadcast_bytes.is_empty());
1159
1160            prop_assert_eq!(decoded_broadcast, decoded_shared);
1161        }
1162
1163        #[test]
1164        fn eth_68_handrolled_decode_matches_derived_implementation(
1165            (types, sizes, hashes) in eth68_hash_fields_strategy()
1166        ) {
1167            let encodable = EncodableNewPooledTransactionHashes68 {
1168                types: Bytes::from(types),
1169                sizes,
1170                hashes,
1171            };
1172            let encoded = encoded(&encodable);
1173
1174            let mut derived_buf = encoded.as_slice();
1175            let derived = decode_eth68_hashes_derived(&mut derived_buf);
1176
1177            let mut handrolled_buf = encoded.as_slice();
1178            let handrolled = NewPooledTransactionHashes68::decode(&mut handrolled_buf);
1179
1180            let handrolled_is_ok = handrolled.is_ok();
1181            prop_assert_eq!(&handrolled, &derived);
1182            if handrolled_is_ok {
1183                prop_assert!(derived_buf.is_empty());
1184                prop_assert!(handrolled_buf.is_empty());
1185            }
1186        }
1187    }
1188
1189    #[test]
1190    fn can_return_latest_block() {
1191        let mut blocks = NewBlockHashes(vec![BlockHashNumber { hash: B256::random(), number: 0 }]);
1192        let latest = blocks.latest().unwrap();
1193        assert_eq!(latest.number, 0);
1194
1195        blocks.push(BlockHashNumber { hash: B256::random(), number: 100 });
1196        blocks.push(BlockHashNumber { hash: B256::random(), number: 2 });
1197        let latest = blocks.latest().unwrap();
1198        assert_eq!(latest.number, 100);
1199    }
1200
1201    #[test]
1202    fn eth_68_tx_hash_roundtrip() {
1203        let vectors = vec![
1204            (
1205                NewPooledTransactionHashes68 { types: vec![], sizes: vec![], hashes: vec![] },
1206                &hex!("c380c0c0")[..],
1207            ),
1208            (
1209                NewPooledTransactionHashes68 {
1210                    types: vec![0x00],
1211                    sizes: vec![0x00],
1212                    hashes: vec![
1213                        B256::ZERO,
1214                    ],
1215                },
1216                &hex!(
1217                    "e500c180e1a00000000000000000000000000000000000000000000000000000000000000000"
1218                )[..],
1219            ),
1220            (
1221                NewPooledTransactionHashes68 {
1222                    types: vec![0x00, 0x00],
1223                    sizes: vec![0x00, 0x00],
1224                    hashes: vec![
1225                        B256::ZERO,
1226                        B256::ZERO,
1227                    ],
1228                },
1229                &hex!(
1230                    "f84a820000c28080f842a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000"
1231                )[..],
1232            ),
1233            (
1234                NewPooledTransactionHashes68 {
1235                    types: vec![0x02],
1236                    sizes: vec![0xb6],
1237                    hashes: vec![
1238                        b256!("0xfecbed04c7b88d8e7221a0a3f5dc33f220212347fc167459ea5cc9c3eb4c1124"),
1239                    ],
1240                },
1241                &hex!(
1242                    "e602c281b6e1a0fecbed04c7b88d8e7221a0a3f5dc33f220212347fc167459ea5cc9c3eb4c1124"
1243                )[..],
1244            ),
1245            (
1246                NewPooledTransactionHashes68 {
1247                    types: vec![0xff, 0xff],
1248                    sizes: vec![0xffffffff, 0xffffffff],
1249                    hashes: vec![
1250                        B256::repeat_byte(0xff),
1251                        B256::repeat_byte(0xff),
1252                    ],
1253                },
1254                &hex!(
1255                    "f85282ffffca84ffffffff84fffffffff842a0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
1256                )[..],
1257            ),
1258            (
1259                NewPooledTransactionHashes68 {
1260                    types: vec![0xff, 0xff],
1261                    sizes: vec![0xffffffff, 0xffffffff],
1262                    hashes: vec![
1263                        b256!("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafe"),
1264                        b256!("0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafe"),
1265                    ],
1266                },
1267                &hex!(
1268                    "f85282ffffca84ffffffff84fffffffff842a0beefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafea0beefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafe"
1269                )[..],
1270            ),
1271            (
1272                NewPooledTransactionHashes68 {
1273                    types: vec![0x10, 0x10],
1274                    sizes: vec![0xdeadc0de, 0xdeadc0de],
1275                    hashes: vec![
1276                        b256!("0x3b9aca00f0671c9a2a1b817a0a78d3fe0c0f776cccb2a8c3c1b412a4f4e4d4e2"),
1277                        b256!("0x3b9aca00f0671c9a2a1b817a0a78d3fe0c0f776cccb2a8c3c1b412a4f4e4d4e2"),
1278                    ],
1279                },
1280                &hex!(
1281                    "f852821010ca84deadc0de84deadc0def842a03b9aca00f0671c9a2a1b817a0a78d3fe0c0f776cccb2a8c3c1b412a4f4e4d4e2a03b9aca00f0671c9a2a1b817a0a78d3fe0c0f776cccb2a8c3c1b412a4f4e4d4e2"
1282                )[..],
1283            ),
1284            (
1285                NewPooledTransactionHashes68 {
1286                    types: vec![0x6f, 0x6f],
1287                    sizes: vec![0x7fffffff, 0x7fffffff],
1288                    hashes: vec![
1289                        B256::with_last_byte(2),
1290                        B256::with_last_byte(2),
1291                    ],
1292                },
1293                &hex!(
1294                    "f852826f6fca847fffffff847ffffffff842a00000000000000000000000000000000000000000000000000000000000000002a00000000000000000000000000000000000000000000000000000000000000002"
1295                )[..],
1296            ),
1297        ];
1298
1299        for vector in vectors {
1300            test_encoding_vector(vector);
1301        }
1302    }
1303
1304    #[test]
1305    fn eth_72_zero_cell_mask_decodes_as_none() {
1306        // The wire cannot tell a zero mask from an absent one, so `None` is the only
1307        // representation that survives a round trip.
1308        let zero = NewPooledTransactionHashes72 {
1309            types: vec![],
1310            sizes: vec![],
1311            hashes: vec![],
1312            cell_mask: Some(B128::ZERO),
1313        };
1314
1315        let mut encoded = Vec::new();
1316        zero.encode(&mut encoded);
1317
1318        let decoded = NewPooledTransactionHashes72::decode(&mut &encoded[..]).unwrap();
1319
1320        assert_eq!(decoded.cell_mask, None);
1321        assert_eq!(encoded, hex!("d480c0c09000000000000000000000000000000000"));
1322    }
1323
1324    #[test]
1325    fn eth_72_tx_hash_roundtrip() {
1326        let vectors = vec![
1327            // `None` is always encoded as a zero-filled 16 byte mask, matching go-ethereum's
1328            // non-optional `[16]byte` field.
1329            (
1330                NewPooledTransactionHashes72 {
1331                    types: vec![],
1332                    sizes: vec![],
1333                    hashes: vec![],
1334                    cell_mask: None,
1335                },
1336                &hex!("d480c0c09000000000000000000000000000000000")[..],
1337            ),
1338            (
1339                NewPooledTransactionHashes72 {
1340                    types: vec![],
1341                    sizes: vec![],
1342                    hashes: vec![],
1343                    cell_mask: Some(B128::repeat_byte(0x11)),
1344                },
1345                &hex!("d480c0c09011111111111111111111111111111111")[..],
1346            ),
1347        ];
1348
1349        for vector in vectors {
1350            test_encoding_vector(vector);
1351        }
1352    }
1353
1354    #[test]
1355    fn eth_72_decodes_spec_nil_cell_mask() {
1356        // The EIP-8070 text encodes an absent mask as the RLP empty string; decoding stays
1357        // lenient even though reth never produces this form.
1358        let encoded = hex!("c480c0c080");
1359
1360        let decoded = NewPooledTransactionHashes72::decode(&mut encoded.as_ref()).unwrap();
1361
1362        assert_eq!(decoded.cell_mask, None);
1363    }
1364
1365    #[test]
1366    fn eth_72_rejects_missing_cell_mask() {
1367        let encoded_eth68_payload = hex!("c380c0c0");
1368
1369        let result = NewPooledTransactionHashes72::decode(&mut encoded_eth68_payload.as_ref());
1370
1371        assert!(matches!(result, Err(alloy_rlp::Error::InputTooShort)));
1372    }
1373
1374    fn signed_transaction() -> impl SignedTransaction {
1375        TransactionSigned::new_unhashed(
1376            Transaction::Legacy(Default::default()),
1377            Signature::new(
1378                U256::from_str(
1379                    "0x64b1702d9298fee62dfeccc57d322a463ad55ca201256d01f62b45b2e1c21c12",
1380                )
1381                .unwrap(),
1382                U256::from_str(
1383                    "0x64b1702d9298fee62dfeccc57d322a463ad55ca201256d01f62b45b2e1c21c10",
1384                )
1385                .unwrap(),
1386                false,
1387            ),
1388        )
1389    }
1390
1391    #[test]
1392    fn test_pooled_tx_hashes_68_push() {
1393        let tx = signed_transaction();
1394        let mut tx_hashes =
1395            NewPooledTransactionHashes68 { types: vec![], sizes: vec![], hashes: vec![] };
1396        tx_hashes.push(&tx);
1397        assert_eq!(tx_hashes.types.len(), 1);
1398        assert_eq!(tx_hashes.sizes.len(), 1);
1399        assert_eq!(tx_hashes.hashes.len(), 1);
1400        assert_eq!(tx_hashes.types[0], tx.ty());
1401        assert_eq!(tx_hashes.sizes[0], tx.encode_2718_len());
1402        assert_eq!(tx_hashes.hashes[0], *tx.tx_hash());
1403    }
1404
1405    #[test]
1406    fn test_pooled_tx_hashes_68_extend() {
1407        let tx = signed_transaction();
1408        let txs = vec![tx.clone(), tx.clone()];
1409        let mut tx_hashes =
1410            NewPooledTransactionHashes68 { types: vec![], sizes: vec![], hashes: vec![] };
1411        tx_hashes.extend(&txs);
1412        assert_eq!(tx_hashes.types.len(), 2);
1413        assert_eq!(tx_hashes.sizes.len(), 2);
1414        assert_eq!(tx_hashes.hashes.len(), 2);
1415        assert_eq!(tx_hashes.types[0], tx.ty());
1416        assert_eq!(tx_hashes.sizes[0], tx.encode_2718_len());
1417        assert_eq!(tx_hashes.hashes[0], *tx.tx_hash());
1418        assert_eq!(tx_hashes.types[1], tx.ty());
1419        assert_eq!(tx_hashes.sizes[1], tx.encode_2718_len());
1420        assert_eq!(tx_hashes.hashes[1], *tx.tx_hash());
1421    }
1422
1423    #[test]
1424    fn test_pooled_tx_hashes_68_with_transaction() {
1425        let tx = signed_transaction();
1426        let tx_hashes =
1427            NewPooledTransactionHashes68 { types: vec![], sizes: vec![], hashes: vec![] }
1428                .with_transaction(&tx);
1429        assert_eq!(tx_hashes.types.len(), 1);
1430        assert_eq!(tx_hashes.sizes.len(), 1);
1431        assert_eq!(tx_hashes.hashes.len(), 1);
1432        assert_eq!(tx_hashes.types[0], tx.ty());
1433        assert_eq!(tx_hashes.sizes[0], tx.encode_2718_len());
1434        assert_eq!(tx_hashes.hashes[0], *tx.tx_hash());
1435    }
1436
1437    #[test]
1438    fn test_pooled_tx_hashes_68_with_transactions() {
1439        let tx = signed_transaction();
1440        let txs = vec![tx.clone(), tx.clone()];
1441        let tx_hashes =
1442            NewPooledTransactionHashes68 { types: vec![], sizes: vec![], hashes: vec![] }
1443                .with_transactions(&txs);
1444        assert_eq!(tx_hashes.types.len(), 2);
1445        assert_eq!(tx_hashes.sizes.len(), 2);
1446        assert_eq!(tx_hashes.hashes.len(), 2);
1447        assert_eq!(tx_hashes.types[0], tx.ty());
1448        assert_eq!(tx_hashes.sizes[0], tx.encode_2718_len());
1449        assert_eq!(tx_hashes.hashes[0], *tx.tx_hash());
1450        assert_eq!(tx_hashes.types[1], tx.ty());
1451        assert_eq!(tx_hashes.sizes[1], tx.encode_2718_len());
1452        assert_eq!(tx_hashes.hashes[1], *tx.tx_hash());
1453    }
1454}