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(_) => {
366                matches!(version, EthVersion::Eth72)
367            }
368        }
369    }
370
371    /// Returns an iterator over all transaction hashes.
372    pub fn iter_hashes(&self) -> impl Iterator<Item = &B256> + '_ {
373        match self {
374            Self::Eth66(msg) => msg.iter(),
375            Self::Eth68(msg) => msg.hashes.iter(),
376            Self::Eth72(msg) => msg.hashes.iter(),
377        }
378    }
379
380    /// Returns an immutable reference to transaction hashes.
381    pub const fn hashes(&self) -> &Vec<B256> {
382        match self {
383            Self::Eth66(msg) => &msg.0,
384            Self::Eth68(msg) => &msg.hashes,
385            Self::Eth72(msg) => &msg.hashes,
386        }
387    }
388
389    /// Returns a mutable reference to transaction hashes.
390    pub const fn hashes_mut(&mut self) -> &mut Vec<B256> {
391        match self {
392            Self::Eth66(msg) => &mut msg.0,
393            Self::Eth68(msg) => &mut msg.hashes,
394            Self::Eth72(msg) => &mut msg.hashes,
395        }
396    }
397
398    /// Consumes the type and returns all hashes
399    pub fn into_hashes(self) -> Vec<B256> {
400        match self {
401            Self::Eth66(msg) => msg.0,
402            Self::Eth68(msg) => msg.hashes,
403            Self::Eth72(msg) => msg.hashes,
404        }
405    }
406
407    /// Returns an iterator over all transaction hashes.
408    pub fn into_iter_hashes(self) -> impl Iterator<Item = B256> {
409        match self {
410            Self::Eth66(msg) => msg.into_iter(),
411            Self::Eth68(msg) => msg.hashes.into_iter(),
412            Self::Eth72(msg) => msg.hashes.into_iter(),
413        }
414    }
415
416    /// Shortens the number of hashes in the message, keeping the first `len` hashes and dropping
417    /// the rest. If `len` is greater than the number of hashes, this has no effect.
418    pub fn truncate(&mut self, len: usize) {
419        match self {
420            Self::Eth66(msg) => msg.truncate(len),
421            Self::Eth68(msg) => {
422                msg.types.truncate(len);
423                msg.sizes.truncate(len);
424                msg.hashes.truncate(len);
425            }
426            Self::Eth72(msg) => {
427                msg.types.truncate(len);
428                msg.sizes.truncate(len);
429                msg.hashes.truncate(len);
430            }
431        }
432    }
433
434    /// Returns true if the message is empty
435    pub const fn is_empty(&self) -> bool {
436        match self {
437            Self::Eth66(msg) => msg.0.is_empty(),
438            Self::Eth68(msg) => msg.hashes.is_empty(),
439            Self::Eth72(msg) => msg.hashes.is_empty(),
440        }
441    }
442
443    /// Returns the number of hashes in the message
444    pub const fn len(&self) -> usize {
445        match self {
446            Self::Eth66(msg) => msg.0.len(),
447            Self::Eth68(msg) => msg.hashes.len(),
448            Self::Eth72(msg) => msg.hashes.len(),
449        }
450    }
451
452    /// Returns an immutable reference to the inner type if this is an eth68 announcement.
453    pub const fn as_eth72(&self) -> Option<&NewPooledTransactionHashes72> {
454        match self {
455            Self::Eth66(_) | Self::Eth68(_) => None,
456            Self::Eth72(msg) => Some(msg),
457        }
458    }
459
460    /// Returns a mutable reference to the inner type if this is an eth68 announcement.
461    pub const fn as_eth72_mut(&mut self) -> Option<&mut NewPooledTransactionHashes72> {
462        match self {
463            Self::Eth66(_) | Self::Eth68(_) => None,
464            Self::Eth72(msg) => Some(msg),
465        }
466    }
467
468    /// Returns an immutable reference to the inner type if this is an eth68 announcement.
469    pub const fn as_eth68(&self) -> Option<&NewPooledTransactionHashes68> {
470        match self {
471            Self::Eth66(_) | Self::Eth72(_) => None,
472            Self::Eth68(msg) => Some(msg),
473        }
474    }
475
476    /// Returns a mutable reference to the inner type if this is an eth68 announcement.
477    pub const fn as_eth68_mut(&mut self) -> Option<&mut NewPooledTransactionHashes68> {
478        match self {
479            Self::Eth66(_) | Self::Eth72(_) => None,
480            Self::Eth68(msg) => Some(msg),
481        }
482    }
483
484    /// Returns a mutable reference to the inner type if this is an eth66 announcement.
485    pub const fn as_eth66_mut(&mut self) -> Option<&mut NewPooledTransactionHashes66> {
486        match self {
487            Self::Eth66(msg) => Some(msg),
488            Self::Eth68(_) | Self::Eth72(_) => None,
489        }
490    }
491
492    /// Returns the inner type if this is an eth68 announcement.
493    pub fn take_eth68(&mut self) -> Option<NewPooledTransactionHashes68> {
494        match self {
495            Self::Eth66(_) | Self::Eth72(_) => None,
496            Self::Eth68(msg) => Some(mem::take(msg)),
497        }
498    }
499
500    /// Returns the inner type if this is an eth66 announcement.
501    pub fn take_eth66(&mut self) -> Option<NewPooledTransactionHashes66> {
502        match self {
503            Self::Eth66(msg) => Some(mem::take(msg)),
504            Self::Eth68(_) | Self::Eth72(_) => None,
505        }
506    }
507}
508
509impl<N: NetworkPrimitives> From<NewPooledTransactionHashes> for EthMessage<N> {
510    fn from(value: NewPooledTransactionHashes) -> Self {
511        match value {
512            NewPooledTransactionHashes::Eth66(msg) => Self::NewPooledTransactionHashes66(msg),
513            NewPooledTransactionHashes::Eth68(msg) => Self::NewPooledTransactionHashes68(msg),
514            NewPooledTransactionHashes::Eth72(msg) => Self::NewPooledTransactionHashes72(msg),
515        }
516    }
517}
518
519impl From<NewPooledTransactionHashes66> for NewPooledTransactionHashes {
520    fn from(hashes: NewPooledTransactionHashes66) -> Self {
521        Self::Eth66(hashes)
522    }
523}
524
525impl From<NewPooledTransactionHashes68> for NewPooledTransactionHashes {
526    fn from(hashes: NewPooledTransactionHashes68) -> Self {
527        Self::Eth68(hashes)
528    }
529}
530
531impl From<NewPooledTransactionHashes72> for NewPooledTransactionHashes {
532    fn from(hashes: NewPooledTransactionHashes72) -> Self {
533        Self::Eth72(hashes)
534    }
535}
536
537/// This informs peers of transaction hashes for transactions that have appeared on the network,
538/// but have not been included in a block.
539#[derive(
540    Clone,
541    Debug,
542    PartialEq,
543    Eq,
544    RlpEncodableWrapper,
545    RlpDecodableWrapper,
546    Default,
547    Deref,
548    DerefMut,
549    IntoIterator,
550)]
551#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
552#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
553#[add_arbitrary_tests(rlp)]
554pub struct NewPooledTransactionHashes66(
555    /// Transaction hashes for new transactions that have appeared on the network.
556    /// Clients should request the transactions with the given hashes using a
557    /// [`GetPooledTransactions`](crate::GetPooledTransactions) message.
558    pub Vec<B256>,
559);
560
561impl NewPooledTransactionHashes66 {
562    /// Returns a new instance with capacity for `capacity` hashes.
563    pub fn with_capacity(capacity: usize) -> Self {
564        Self(Vec::with_capacity(capacity))
565    }
566}
567
568impl From<Vec<B256>> for NewPooledTransactionHashes66 {
569    fn from(v: Vec<B256>) -> Self {
570        Self(v)
571    }
572}
573
574/// Same as [`NewPooledTransactionHashes66`] but extends that beside the transaction hashes,
575/// the node sends the transaction types and their sizes (as defined in EIP-2718) as well.
576#[derive(Clone, Debug, PartialEq, Eq, Default)]
577#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
578pub struct NewPooledTransactionHashes68 {
579    /// Transaction types for new transactions that have appeared on the network.
580    ///
581    /// ## Note on RLP encoding and decoding
582    ///
583    /// In the [eth/68 spec](https://eips.ethereum.org/EIPS/eip-5793#specification) this is defined
584    /// the following way:
585    ///  * `[type_0: B_1, type_1: B_1, ...]`
586    ///
587    /// This would make it seem like the [`Encodable`] and
588    /// [`Decodable`] implementations should directly use a `Vec<u8>` for
589    /// encoding and decoding, because it looks like this field should be encoded as a _list_ of
590    /// bytes.
591    ///
592    /// However, [this is implemented in geth as a `[]byte`
593    /// type](https://github.com/ethereum/go-ethereum/blob/82d934b1dd80cdd8190803ea9f73ed2c345e2576/eth/protocols/eth/protocol.go#L308-L313),
594    /// which [ends up being encoded as a RLP
595    /// string](https://github.com/ethereum/go-ethereum/blob/82d934b1dd80cdd8190803ea9f73ed2c345e2576/rlp/encode_test.go#L171-L176),
596    /// **not** a RLP list.
597    ///
598    /// Because of this, we do not directly use the `Vec<u8>` when encoding and decoding, and
599    /// instead use the [`Encodable`] and [`Decodable`]
600    /// implementations for `&[u8]` instead, which encodes into a RLP string, and expects an RLP
601    /// string when decoding.
602    pub types: Vec<u8>,
603    /// Transaction sizes for new transactions that have appeared on the network.
604    pub sizes: Vec<usize>,
605    /// Transaction hashes for new transactions that have appeared on the network.
606    pub hashes: Vec<B256>,
607}
608
609#[cfg(feature = "arbitrary")]
610impl proptest::prelude::Arbitrary for NewPooledTransactionHashes68 {
611    type Parameters = ();
612    fn arbitrary_with(_args: ()) -> Self::Strategy {
613        use proptest::{collection::vec, prelude::*};
614        // Generate a single random length for all vectors
615        let vec_length = any::<usize>().prop_map(|x| x % 100 + 1); // Lengths between 1 and 100
616
617        vec_length
618            .prop_flat_map(|len| {
619                // Use the generated length to create vectors of TxType, usize, and B256
620                let types_vec = vec(
621                    proptest_arbitrary_interop::arb::<reth_ethereum_primitives::TxType>()
622                        .prop_map(|ty| ty as u8),
623                    len..=len,
624                );
625
626                // Map the usize values to the range 0..131072(0x20000)
627                let sizes_vec = vec(proptest::num::usize::ANY.prop_map(|x| x % 131072), len..=len);
628                let hashes_vec = vec(any::<B256>(), len..=len);
629
630                (types_vec, sizes_vec, hashes_vec)
631            })
632            .prop_map(|(types, sizes, hashes)| Self { types, sizes, hashes })
633            .boxed()
634    }
635
636    type Strategy = proptest::prelude::BoxedStrategy<Self>;
637}
638
639impl NewPooledTransactionHashes68 {
640    /// Returns the number of announced hashes.
641    pub const fn len(&self) -> usize {
642        self.hashes.len()
643    }
644
645    /// Returns whether there are no announced hashes.
646    pub const fn is_empty(&self) -> bool {
647        self.hashes.is_empty()
648    }
649
650    /// Returns a new instance with capacity for `capacity` entries.
651    pub fn with_capacity(capacity: usize) -> Self {
652        Self {
653            types: Vec::with_capacity(capacity),
654            sizes: Vec::with_capacity(capacity),
655            hashes: Vec::with_capacity(capacity),
656        }
657    }
658
659    /// Returns an iterator over tx hashes zipped with corresponding metadata.
660    pub fn metadata_iter(&self) -> impl Iterator<Item = (&B256, (u8, usize))> {
661        self.hashes.iter().zip(self.types.iter().copied().zip(self.sizes.iter().copied()))
662    }
663
664    /// Appends a transaction
665    pub fn push<T: SignedTransaction>(&mut self, tx: &T) {
666        self.hashes.push(*tx.tx_hash());
667        self.sizes.push(tx.encode_2718_len());
668        self.types.push(tx.ty());
669    }
670
671    /// Appends the provided transactions
672    pub fn extend<'a, T: SignedTransaction>(&mut self, txs: impl IntoIterator<Item = &'a T>) {
673        for tx in txs {
674            self.push(tx);
675        }
676    }
677
678    /// Shrinks the capacity of the message vectors as much as possible.
679    pub fn shrink_to_fit(&mut self) {
680        self.hashes.shrink_to_fit();
681        self.sizes.shrink_to_fit();
682        self.types.shrink_to_fit()
683    }
684
685    /// Consumes and appends a transaction
686    pub fn with_transaction<T: SignedTransaction>(mut self, tx: &T) -> Self {
687        self.push(tx);
688        self
689    }
690
691    /// Consumes and appends the provided transactions
692    pub fn with_transactions<'a, T: SignedTransaction>(
693        mut self,
694        txs: impl IntoIterator<Item = &'a T>,
695    ) -> Self {
696        self.extend(txs);
697        self
698    }
699}
700
701impl Encodable for NewPooledTransactionHashes68 {
702    fn encode(&self, out: &mut dyn bytes::BufMut) {
703        #[derive(RlpEncodable)]
704        struct EncodableNewPooledTransactionHashes68<'a> {
705            types: &'a [u8],
706            sizes: &'a Vec<usize>,
707            hashes: &'a Vec<B256>,
708        }
709
710        let encodable = EncodableNewPooledTransactionHashes68 {
711            types: &self.types[..],
712            sizes: &self.sizes,
713            hashes: &self.hashes,
714        };
715
716        encodable.encode(out);
717    }
718    fn length(&self) -> usize {
719        #[derive(RlpEncodable)]
720        struct EncodableNewPooledTransactionHashes68<'a> {
721            types: &'a [u8],
722            sizes: &'a Vec<usize>,
723            hashes: &'a Vec<B256>,
724        }
725
726        let encodable = EncodableNewPooledTransactionHashes68 {
727            types: &self.types[..],
728            sizes: &self.sizes,
729            hashes: &self.hashes,
730        };
731
732        encodable.length()
733    }
734}
735
736impl Decodable for NewPooledTransactionHashes68 {
737    fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
738        let Header { list, payload_length } = Header::decode(buf)?;
739        if !list {
740            return Err(alloy_rlp::Error::UnexpectedString)
741        }
742        // Payload length checked by Header::decode.
743        let (mut payload, rest) = buf.split_at(payload_length);
744        let (types, sizes, hashes) = decode_pooled_transaction_hashes_payload(&mut payload)?;
745
746        if !payload.is_empty() {
747            return Err(alloy_rlp::Error::ListLengthMismatch {
748                expected: payload_length,
749                got: payload_length - payload.len(),
750            })
751        }
752
753        ensure_pooled_transaction_hashes_lengths(hashes.len(), types.len(), sizes.len())?;
754
755        *buf = rest;
756        Ok(Self { types, sizes, hashes })
757    }
758}
759
760/// Same as [`NewPooledTransactionHashes68`] but adds the eth/72 `cell_mask` field from
761/// [EIP-8070](https://eips.ethereum.org/EIPS/eip-8070).
762#[derive(Clone, Debug, PartialEq, Eq, Default)]
763#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
764pub struct NewPooledTransactionHashes72 {
765    /// Transaction types for new transactions that have appeared on the network.
766    ///
767    /// ## Note on RLP encoding and decoding
768    ///
769    /// In the [eth/72 spec](https://eips.ethereum.org/EIPS/eip-8070#specification) this is defined
770    /// the following way:
771    ///  * `[types: B, [size_0: P, size_1: P, ...], [hash_0: B_32, hash_1: B_32, ...], cell_mask:
772    ///    B_16]`
773    pub types: Vec<u8>,
774    /// Transaction sizes for new transactions that have appeared on the network.
775    pub sizes: Vec<usize>,
776    /// Transaction hashes for new transactions that have appeared on the network.
777    pub hashes: Vec<B256>,
778    /// Cell availability mask for type 3 (blob) transactions announced by this message.
779    ///
780    /// Per [EIP-8070](https://eips.ethereum.org/EIPS/eip-8070), this is a `B_16`
781    /// bitarray over `CELLS_PER_EXT_BLOB`; bit `i` is set when the announcer has column
782    /// `i` available for every type 3 transaction in the message.
783    ///
784    /// On the wire this field is always encoded as a 16 byte string, zero-filled when no
785    /// type 3 transactions are announced: go-ethereum decodes the mask into a fixed
786    /// `[16]byte` and rejects the RLP `nil` encoding the EIP text describes, so the
787    /// always-present form is the de facto network format. `None` is equivalent to a zero
788    /// mask; decoding additionally accepts the spec's `nil` encoding for compatibility.
789    pub cell_mask: Option<B128>,
790}
791
792#[cfg(feature = "arbitrary")]
793impl proptest::prelude::Arbitrary for NewPooledTransactionHashes72 {
794    type Parameters = ();
795    fn arbitrary_with(_args: ()) -> Self::Strategy {
796        use proptest::{collection::vec, prelude::*};
797        // Generate a single random length for all vectors
798        let vec_length = any::<usize>().prop_map(|x| x % 100 + 1); // Lengths between 1 and 100
799
800        vec_length
801            .prop_flat_map(|len| {
802                // Use the generated length to create vectors of TxType, usize, and B256
803                let types_vec = vec(
804                    proptest_arbitrary_interop::arb::<reth_ethereum_primitives::TxType>()
805                        .prop_map(|ty| ty as u8),
806                    len..=len,
807                );
808
809                // Map the usize values to the range 0..131072(0x20000)
810                let sizes_vec = vec(proptest::num::usize::ANY.prop_map(|x| x % 131072), len..=len);
811                let hashes_vec = vec(any::<B256>(), len..=len);
812                // A zero mask is spelled `None`, so generating `Some(ZERO)` would produce a
813                // value that cannot survive its own encoding.
814                let cell_mask =
815                    any::<Option<B128>>().prop_map(|mask| mask.filter(|mask| !mask.is_zero()));
816
817                (types_vec, sizes_vec, hashes_vec, cell_mask)
818            })
819            .prop_map(|(types, sizes, hashes, cell_mask)| Self { types, sizes, hashes, cell_mask })
820            .boxed()
821    }
822
823    type Strategy = proptest::prelude::BoxedStrategy<Self>;
824}
825
826impl NewPooledTransactionHashes72 {
827    /// Returns the number of announced hashes.
828    pub const fn len(&self) -> usize {
829        self.hashes.len()
830    }
831
832    /// Returns whether there are no announced hashes.
833    pub const fn is_empty(&self) -> bool {
834        self.hashes.is_empty()
835    }
836
837    /// Cell mask advertising availability of every cell.
838    ///
839    /// Used when announcing blob transactions whose full sidecar is available locally, since
840    /// every cell can be computed from the complete blob data.
841    pub const ALL_CELLS_MASK: B128 = B128::repeat_byte(0xff);
842
843    /// Returns a new instance with capacity for `capacity` entries and no cell mask.
844    pub fn with_capacity(capacity: usize) -> Self {
845        Self {
846            types: Vec::with_capacity(capacity),
847            sizes: Vec::with_capacity(capacity),
848            hashes: Vec::with_capacity(capacity),
849            cell_mask: None,
850        }
851    }
852
853    /// Returns an iterator over tx hashes zipped with corresponding metadata.
854    pub fn metadata_iter(&self) -> impl Iterator<Item = (&B256, (u8, usize))> {
855        self.hashes.iter().zip(self.types.iter().copied().zip(self.sizes.iter().copied()))
856    }
857
858    /// Appends a transaction
859    pub fn push<T: SignedTransaction>(&mut self, tx: &T) {
860        self.hashes.push(*tx.tx_hash());
861        self.sizes.push(tx.encode_2718_len());
862        self.types.push(tx.ty());
863        if tx.is_eip4844() {
864            self.cell_mask = Some(Self::ALL_CELLS_MASK);
865        }
866    }
867
868    /// Appends the provided transactions
869    pub fn extend<'a, T: SignedTransaction>(&mut self, txs: impl IntoIterator<Item = &'a T>) {
870        for tx in txs {
871            self.push(tx);
872        }
873    }
874
875    /// Shrinks the capacity of the message vectors as much as possible.
876    pub fn shrink_to_fit(&mut self) {
877        self.hashes.shrink_to_fit();
878        self.sizes.shrink_to_fit();
879        self.types.shrink_to_fit()
880    }
881
882    /// Consumes and appends a transaction
883    pub fn with_transaction<T: SignedTransaction>(mut self, tx: &T) -> Self {
884        self.push(tx);
885        self
886    }
887
888    /// Consumes and appends the provided transactions
889    pub fn with_transactions<'a, T: SignedTransaction>(
890        mut self,
891        txs: impl IntoIterator<Item = &'a T>,
892    ) -> Self {
893        self.extend(txs);
894        self
895    }
896
897    fn payload_length(&self) -> usize {
898        self.types.as_slice().length() +
899            self.sizes.length() +
900            self.hashes.length() +
901            self.cell_mask.unwrap_or_default().length()
902    }
903}
904
905impl Encodable for NewPooledTransactionHashes72 {
906    fn encode(&self, out: &mut dyn bytes::BufMut) {
907        Header { list: true, payload_length: self.payload_length() }.encode(out);
908        self.types.as_slice().encode(out);
909        self.sizes.encode(out);
910        self.hashes.encode(out);
911        // A zero-filled mask when no cells are available, see the `cell_mask` field docs.
912        self.cell_mask.unwrap_or_default().encode(out);
913    }
914
915    fn length(&self) -> usize {
916        Header { list: true, payload_length: self.payload_length() }.length_with_payload()
917    }
918}
919
920impl Decodable for NewPooledTransactionHashes72 {
921    fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
922        let Header { list, payload_length } = Header::decode(buf)?;
923        if !list {
924            return Err(alloy_rlp::Error::UnexpectedString)
925        }
926        // Payload length checked by Header::decode.
927        let (mut payload, rest) = buf.split_at(payload_length);
928        let (types, sizes, hashes) = decode_pooled_transaction_hashes_payload(&mut payload)?;
929        let Some(first_byte) = payload.first().copied() else {
930            return Err(alloy_rlp::Error::InputTooShort)
931        };
932        let cell_mask = if first_byte == alloy_rlp::EMPTY_STRING_CODE {
933            // The EIP-8070 `nil` encoding, tolerated for compatibility.
934            payload = &payload[1..];
935            None
936        } else {
937            // A zero mask is the wire representation of "no cells available", see the
938            // `cell_mask` field docs.
939            Some(B128::decode(&mut payload)?).filter(|mask| !mask.is_zero())
940        };
941
942        if !payload.is_empty() {
943            return Err(alloy_rlp::Error::ListLengthMismatch {
944                expected: payload_length,
945                got: payload_length - payload.len(),
946            })
947        }
948
949        ensure_pooled_transaction_hashes_lengths(hashes.len(), types.len(), sizes.len())?;
950
951        *buf = rest;
952        Ok(Self { types, sizes, hashes, cell_mask })
953    }
954}
955
956/// Twice the spec'd soft limit for `NewPooledTransactionHashes` announcements.
957///
958/// This keeps capacity hints bounded when a malformed packet spends most of its bytes on the
959/// one-byte `types` string before the size and hash lists are validated.
960const NEW_POOLED_TRANSACTION_HASHES_DECODE_CAP: usize =
961    2 * SOFT_LIMIT_COUNT_HASHES_IN_NEW_POOLED_TRANSACTIONS_BROADCAST_MESSAGE;
962
963#[inline]
964fn decode_pooled_transaction_hashes_payload(
965    payload: &mut &[u8],
966) -> alloy_rlp::Result<(Vec<u8>, Vec<usize>, Vec<B256>)> {
967    let types = Bytes::decode(payload)?;
968    let capacity = types.len().min(NEW_POOLED_TRANSACTION_HASHES_DECODE_CAP);
969
970    let mut sizes = Vec::with_capacity(capacity);
971    decode_append(payload, &mut sizes)?;
972
973    let mut hashes = Vec::with_capacity(capacity);
974    decode_append(payload, &mut hashes)?;
975
976    Ok((types.into(), sizes, hashes))
977}
978
979#[inline]
980const fn ensure_pooled_transaction_hashes_lengths(
981    hashes_len: usize,
982    types_len: usize,
983    sizes_len: usize,
984) -> alloy_rlp::Result<()> {
985    if hashes_len != types_len {
986        return Err(alloy_rlp::Error::ListLengthMismatch { expected: hashes_len, got: types_len })
987    }
988    if hashes_len != sizes_len {
989        return Err(alloy_rlp::Error::ListLengthMismatch { expected: hashes_len, got: sizes_len })
990    }
991
992    Ok(())
993}
994
995/// Interface for handling mempool message data. Used in various filters in pipelines in
996/// `TransactionsManager` and in queries to `TransactionPool`.
997pub trait HandleMempoolData {
998    /// The announcement contains no entries.
999    fn is_empty(&self) -> bool;
1000
1001    /// Returns the number of entries.
1002    fn len(&self) -> usize;
1003
1004    /// Retain only entries for which the hash in the entry satisfies a given predicate.
1005    fn retain_by_hash(&mut self, f: impl FnMut(&TxHash) -> bool);
1006}
1007
1008impl<T: SignedTransaction> HandleMempoolData for Vec<T> {
1009    fn is_empty(&self) -> bool {
1010        self.is_empty()
1011    }
1012
1013    fn len(&self) -> usize {
1014        self.len()
1015    }
1016
1017    fn retain_by_hash(&mut self, mut f: impl FnMut(&TxHash) -> bool) {
1018        self.retain(|tx| f(tx.tx_hash()))
1019    }
1020}
1021
1022/// The earliest block, the latest block and hash of the latest block which can be provided.
1023/// See [BlockRangeUpdate](https://github.com/ethereum/devp2p/blob/master/caps/eth.md#blockrangeupdate-0x11).
1024#[derive(Clone, Debug, PartialEq, Eq, Default, RlpEncodable, RlpDecodable)]
1025#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1026#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
1027pub struct BlockRangeUpdate {
1028    /// The earliest block which is available.
1029    pub earliest: u64,
1030    /// The latest block which is available.
1031    pub latest: u64,
1032    /// Latest available block's hash.
1033    pub latest_hash: B256,
1034}
1035
1036impl InMemorySize for NewPooledTransactionHashes {
1037    fn size(&self) -> usize {
1038        match self {
1039            Self::Eth66(msg) => msg.0.len() * core::mem::size_of::<B256>(),
1040            Self::Eth68(msg) => {
1041                msg.types.len() * core::mem::size_of::<u8>() +
1042                    msg.sizes.len() * core::mem::size_of::<usize>() +
1043                    msg.hashes.len() * core::mem::size_of::<B256>()
1044            }
1045            Self::Eth72(msg) => {
1046                msg.types.len() * core::mem::size_of::<u8>() +
1047                    msg.sizes.len() * core::mem::size_of::<usize>() +
1048                    msg.hashes.len() * core::mem::size_of::<B256>() +
1049                    core::mem::size_of::<B128>()
1050            }
1051        }
1052    }
1053}
1054
1055#[cfg(test)]
1056mod tests {
1057    use super::*;
1058    use alloy_consensus::{transaction::TxHashRef, Typed2718};
1059    use alloy_eips::eip2718::Encodable2718;
1060    use alloy_primitives::{hex, Bytes, Signature, U256};
1061    use alloy_rlp::{RlpDecodable, RlpEncodable};
1062    use proptest::prelude::*;
1063    use reth_ethereum_primitives::{Transaction, TransactionSigned};
1064    use std::str::FromStr;
1065
1066    /// Takes as input a struct / encoded hex message pair, ensuring that we encode to the exact hex
1067    /// message, and decode to the exact struct.
1068    fn test_encoding_vector<T: Encodable + Decodable + PartialEq + core::fmt::Debug>(
1069        input: (T, &[u8]),
1070    ) {
1071        let (expected_decoded, expected_encoded) = input;
1072        let mut encoded = Vec::new();
1073        expected_decoded.encode(&mut encoded);
1074
1075        assert_eq!(hex::encode(&encoded), hex::encode(expected_encoded));
1076
1077        let decoded = T::decode(&mut encoded.as_ref()).unwrap();
1078        assert_eq!(expected_decoded, decoded);
1079    }
1080
1081    fn encoded<T: Encodable>(value: &T) -> Vec<u8> {
1082        let mut out = Vec::new();
1083        value.encode(&mut out);
1084        out
1085    }
1086
1087    #[derive(RlpEncodable, RlpDecodable)]
1088    struct EncodableNewPooledTransactionHashes68 {
1089        types: Bytes,
1090        sizes: Vec<usize>,
1091        hashes: Vec<B256>,
1092    }
1093
1094    type NewPooledTransactionHashes68Fields = (Vec<u8>, Vec<usize>, Vec<B256>);
1095
1096    fn decode_eth68_hashes_derived(
1097        buf: &mut &[u8],
1098    ) -> alloy_rlp::Result<NewPooledTransactionHashes68> {
1099        let encodable = EncodableNewPooledTransactionHashes68::decode(buf)?;
1100        let msg = NewPooledTransactionHashes68 {
1101            types: encodable.types.into(),
1102            sizes: encodable.sizes,
1103            hashes: encodable.hashes,
1104        };
1105
1106        ensure_pooled_transaction_hashes_lengths(
1107            msg.hashes.len(),
1108            msg.types.len(),
1109            msg.sizes.len(),
1110        )?;
1111
1112        Ok(msg)
1113    }
1114
1115    fn eth68_hash_fields_strategy() -> impl Strategy<Value = NewPooledTransactionHashes68Fields> {
1116        (0usize..128, 0usize..128, 0usize..128).prop_flat_map(
1117            |(types_len, sizes_len, hashes_len)| {
1118                (
1119                    proptest::collection::vec(any::<u8>(), types_len),
1120                    proptest::collection::vec(0usize..131_072, sizes_len),
1121                    proptest::collection::vec(any::<B256>(), hashes_len),
1122                )
1123            },
1124        )
1125    }
1126
1127    proptest! {
1128        #[test]
1129        fn broadcast_pool_transactions_match_shared_transactions_encoding(
1130            txs in proptest::collection::vec(
1131                proptest_arbitrary_interop::arb::<TransactionSigned>(),
1132                0..32,
1133            )
1134        ) {
1135            let shared = SharedTransactions::<TransactionSigned>(
1136                txs.iter().cloned().map(Arc::new).collect(),
1137            );
1138            let broadcast = BroadcastPoolTransactions(
1139                txs.iter().cloned().map(LazyEncoded::new).collect(),
1140            );
1141
1142            prop_assert_eq!(broadcast.length(), shared.length());
1143
1144            let shared_encoded = encoded(&shared);
1145            let broadcast_encoded = encoded(&broadcast);
1146            prop_assert_eq!(&broadcast_encoded, &shared_encoded);
1147
1148            let broadcast_encoded_cached = encoded(&broadcast);
1149            prop_assert_eq!(&broadcast_encoded_cached, &shared_encoded);
1150
1151            let mut shared_bytes = shared_encoded.as_slice();
1152            let decoded_shared = SharedTransactions::<TransactionSigned>::decode(&mut shared_bytes)
1153                .expect("shared transactions decode");
1154            prop_assert!(shared_bytes.is_empty());
1155
1156            let mut broadcast_bytes = broadcast_encoded.as_slice();
1157            let decoded_broadcast =
1158                SharedTransactions::<TransactionSigned>::decode(&mut broadcast_bytes)
1159                    .expect("broadcast pool transactions decode as shared transactions");
1160            prop_assert!(broadcast_bytes.is_empty());
1161
1162            prop_assert_eq!(decoded_broadcast, decoded_shared);
1163        }
1164
1165        #[test]
1166        fn eth_68_handrolled_decode_matches_derived_implementation(
1167            (types, sizes, hashes) in eth68_hash_fields_strategy()
1168        ) {
1169            let encodable = EncodableNewPooledTransactionHashes68 {
1170                types: Bytes::from(types),
1171                sizes,
1172                hashes,
1173            };
1174            let encoded = encoded(&encodable);
1175
1176            let mut derived_buf = encoded.as_slice();
1177            let derived = decode_eth68_hashes_derived(&mut derived_buf);
1178
1179            let mut handrolled_buf = encoded.as_slice();
1180            let handrolled = NewPooledTransactionHashes68::decode(&mut handrolled_buf);
1181
1182            let handrolled_is_ok = handrolled.is_ok();
1183            prop_assert_eq!(&handrolled, &derived);
1184            if handrolled_is_ok {
1185                prop_assert!(derived_buf.is_empty());
1186                prop_assert!(handrolled_buf.is_empty());
1187            }
1188        }
1189    }
1190
1191    #[test]
1192    fn decode_error_preserves_payload_position() {
1193        let encoded = [0xc1, 0x80, 0xaa];
1194
1195        let mut input = encoded.as_slice();
1196        assert!(
1197            decode_list_with_memory_budget::<TransactionSigned>(&mut input, usize::MAX).is_err()
1198        );
1199        assert_eq!(input, &encoded[1..]);
1200
1201        let mut input = encoded.as_slice();
1202        assert!(NewPooledTransactionHashes68::decode(&mut input).is_err());
1203        assert_eq!(input, &encoded[1..]);
1204
1205        let mut input = encoded.as_slice();
1206        assert!(NewPooledTransactionHashes72::decode(&mut input).is_err());
1207        assert_eq!(input, &encoded[1..]);
1208    }
1209
1210    #[test]
1211    fn can_return_latest_block() {
1212        let mut blocks = NewBlockHashes(vec![BlockHashNumber { hash: B256::random(), number: 0 }]);
1213        let latest = blocks.latest().unwrap();
1214        assert_eq!(latest.number, 0);
1215
1216        blocks.push(BlockHashNumber { hash: B256::random(), number: 100 });
1217        blocks.push(BlockHashNumber { hash: B256::random(), number: 2 });
1218        let latest = blocks.latest().unwrap();
1219        assert_eq!(latest.number, 100);
1220    }
1221
1222    #[test]
1223    fn eth_68_tx_hash_roundtrip() {
1224        let vectors = vec![
1225            (
1226                NewPooledTransactionHashes68 { types: vec![], sizes: vec![], hashes: vec![] },
1227                &hex!("c380c0c0")[..],
1228            ),
1229            (
1230                NewPooledTransactionHashes68 {
1231                    types: vec![0x00],
1232                    sizes: vec![0x00],
1233                    hashes: vec![
1234                        B256::from_str(
1235                            "0x0000000000000000000000000000000000000000000000000000000000000000",
1236                        )
1237                        .unwrap(),
1238                    ],
1239                },
1240                &hex!(
1241                    "e500c180e1a00000000000000000000000000000000000000000000000000000000000000000"
1242                )[..],
1243            ),
1244            (
1245                NewPooledTransactionHashes68 {
1246                    types: vec![0x00, 0x00],
1247                    sizes: vec![0x00, 0x00],
1248                    hashes: vec![
1249                        B256::from_str(
1250                            "0x0000000000000000000000000000000000000000000000000000000000000000",
1251                        )
1252                        .unwrap(),
1253                        B256::from_str(
1254                            "0x0000000000000000000000000000000000000000000000000000000000000000",
1255                        )
1256                        .unwrap(),
1257                    ],
1258                },
1259                &hex!(
1260                    "f84a820000c28080f842a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000"
1261                )[..],
1262            ),
1263            (
1264                NewPooledTransactionHashes68 {
1265                    types: vec![0x02],
1266                    sizes: vec![0xb6],
1267                    hashes: vec![
1268                        B256::from_str(
1269                            "0xfecbed04c7b88d8e7221a0a3f5dc33f220212347fc167459ea5cc9c3eb4c1124",
1270                        )
1271                        .unwrap(),
1272                    ],
1273                },
1274                &hex!(
1275                    "e602c281b6e1a0fecbed04c7b88d8e7221a0a3f5dc33f220212347fc167459ea5cc9c3eb4c1124"
1276                )[..],
1277            ),
1278            (
1279                NewPooledTransactionHashes68 {
1280                    types: vec![0xff, 0xff],
1281                    sizes: vec![0xffffffff, 0xffffffff],
1282                    hashes: vec![
1283                        B256::from_str(
1284                            "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
1285                        )
1286                        .unwrap(),
1287                        B256::from_str(
1288                            "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
1289                        )
1290                        .unwrap(),
1291                    ],
1292                },
1293                &hex!(
1294                    "f85282ffffca84ffffffff84fffffffff842a0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
1295                )[..],
1296            ),
1297            (
1298                NewPooledTransactionHashes68 {
1299                    types: vec![0xff, 0xff],
1300                    sizes: vec![0xffffffff, 0xffffffff],
1301                    hashes: vec![
1302                        B256::from_str(
1303                            "0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafe",
1304                        )
1305                        .unwrap(),
1306                        B256::from_str(
1307                            "0xbeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafe",
1308                        )
1309                        .unwrap(),
1310                    ],
1311                },
1312                &hex!(
1313                    "f85282ffffca84ffffffff84fffffffff842a0beefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafea0beefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafebeefcafe"
1314                )[..],
1315            ),
1316            (
1317                NewPooledTransactionHashes68 {
1318                    types: vec![0x10, 0x10],
1319                    sizes: vec![0xdeadc0de, 0xdeadc0de],
1320                    hashes: vec![
1321                        B256::from_str(
1322                            "0x3b9aca00f0671c9a2a1b817a0a78d3fe0c0f776cccb2a8c3c1b412a4f4e4d4e2",
1323                        )
1324                        .unwrap(),
1325                        B256::from_str(
1326                            "0x3b9aca00f0671c9a2a1b817a0a78d3fe0c0f776cccb2a8c3c1b412a4f4e4d4e2",
1327                        )
1328                        .unwrap(),
1329                    ],
1330                },
1331                &hex!(
1332                    "f852821010ca84deadc0de84deadc0def842a03b9aca00f0671c9a2a1b817a0a78d3fe0c0f776cccb2a8c3c1b412a4f4e4d4e2a03b9aca00f0671c9a2a1b817a0a78d3fe0c0f776cccb2a8c3c1b412a4f4e4d4e2"
1333                )[..],
1334            ),
1335            (
1336                NewPooledTransactionHashes68 {
1337                    types: vec![0x6f, 0x6f],
1338                    sizes: vec![0x7fffffff, 0x7fffffff],
1339                    hashes: vec![
1340                        B256::from_str(
1341                            "0x0000000000000000000000000000000000000000000000000000000000000002",
1342                        )
1343                        .unwrap(),
1344                        B256::from_str(
1345                            "0x0000000000000000000000000000000000000000000000000000000000000002",
1346                        )
1347                        .unwrap(),
1348                    ],
1349                },
1350                &hex!(
1351                    "f852826f6fca847fffffff847ffffffff842a00000000000000000000000000000000000000000000000000000000000000002a00000000000000000000000000000000000000000000000000000000000000002"
1352                )[..],
1353            ),
1354        ];
1355
1356        for vector in vectors {
1357            test_encoding_vector(vector);
1358        }
1359    }
1360
1361    #[test]
1362    fn eth_72_zero_cell_mask_decodes_as_none() {
1363        // The wire cannot tell a zero mask from an absent one, so `None` is the only
1364        // representation that survives a round trip.
1365        let zero = NewPooledTransactionHashes72 {
1366            types: vec![],
1367            sizes: vec![],
1368            hashes: vec![],
1369            cell_mask: Some(B128::ZERO),
1370        };
1371
1372        let mut encoded = Vec::new();
1373        zero.encode(&mut encoded);
1374
1375        let decoded = NewPooledTransactionHashes72::decode(&mut &encoded[..]).unwrap();
1376
1377        assert_eq!(decoded.cell_mask, None);
1378        assert_eq!(encoded, hex!("d480c0c09000000000000000000000000000000000"));
1379    }
1380
1381    #[test]
1382    fn eth_72_tx_hash_roundtrip() {
1383        let vectors = vec![
1384            // `None` is always encoded as a zero-filled 16 byte mask, matching go-ethereum's
1385            // non-optional `[16]byte` field.
1386            (
1387                NewPooledTransactionHashes72 {
1388                    types: vec![],
1389                    sizes: vec![],
1390                    hashes: vec![],
1391                    cell_mask: None,
1392                },
1393                &hex!("d480c0c09000000000000000000000000000000000")[..],
1394            ),
1395            (
1396                NewPooledTransactionHashes72 {
1397                    types: vec![],
1398                    sizes: vec![],
1399                    hashes: vec![],
1400                    cell_mask: Some(B128::repeat_byte(0x11)),
1401                },
1402                &hex!("d480c0c09011111111111111111111111111111111")[..],
1403            ),
1404        ];
1405
1406        for vector in vectors {
1407            test_encoding_vector(vector);
1408        }
1409    }
1410
1411    #[test]
1412    fn eth_72_decodes_spec_nil_cell_mask() {
1413        // The EIP-8070 text encodes an absent mask as the RLP empty string; decoding stays
1414        // lenient even though reth never produces this form.
1415        let encoded = hex!("c480c0c080");
1416
1417        let decoded = NewPooledTransactionHashes72::decode(&mut encoded.as_ref()).unwrap();
1418
1419        assert_eq!(decoded.cell_mask, None);
1420    }
1421
1422    #[test]
1423    fn eth_72_rejects_missing_cell_mask() {
1424        let encoded_eth68_payload = hex!("c380c0c0");
1425
1426        let result = NewPooledTransactionHashes72::decode(&mut encoded_eth68_payload.as_ref());
1427
1428        assert!(matches!(result, Err(alloy_rlp::Error::InputTooShort)));
1429    }
1430
1431    fn signed_transaction() -> impl SignedTransaction {
1432        TransactionSigned::new_unhashed(
1433            Transaction::Legacy(Default::default()),
1434            Signature::new(
1435                U256::from_str(
1436                    "0x64b1702d9298fee62dfeccc57d322a463ad55ca201256d01f62b45b2e1c21c12",
1437                )
1438                .unwrap(),
1439                U256::from_str(
1440                    "0x64b1702d9298fee62dfeccc57d322a463ad55ca201256d01f62b45b2e1c21c10",
1441                )
1442                .unwrap(),
1443                false,
1444            ),
1445        )
1446    }
1447
1448    #[test]
1449    fn test_pooled_tx_hashes_68_push() {
1450        let tx = signed_transaction();
1451        let mut tx_hashes =
1452            NewPooledTransactionHashes68 { types: vec![], sizes: vec![], hashes: vec![] };
1453        tx_hashes.push(&tx);
1454        assert_eq!(tx_hashes.types.len(), 1);
1455        assert_eq!(tx_hashes.sizes.len(), 1);
1456        assert_eq!(tx_hashes.hashes.len(), 1);
1457        assert_eq!(tx_hashes.types[0], tx.ty());
1458        assert_eq!(tx_hashes.sizes[0], tx.encode_2718_len());
1459        assert_eq!(tx_hashes.hashes[0], *tx.tx_hash());
1460    }
1461
1462    #[test]
1463    fn test_pooled_tx_hashes_68_extend() {
1464        let tx = signed_transaction();
1465        let txs = vec![tx.clone(), tx.clone()];
1466        let mut tx_hashes =
1467            NewPooledTransactionHashes68 { types: vec![], sizes: vec![], hashes: vec![] };
1468        tx_hashes.extend(&txs);
1469        assert_eq!(tx_hashes.types.len(), 2);
1470        assert_eq!(tx_hashes.sizes.len(), 2);
1471        assert_eq!(tx_hashes.hashes.len(), 2);
1472        assert_eq!(tx_hashes.types[0], tx.ty());
1473        assert_eq!(tx_hashes.sizes[0], tx.encode_2718_len());
1474        assert_eq!(tx_hashes.hashes[0], *tx.tx_hash());
1475        assert_eq!(tx_hashes.types[1], tx.ty());
1476        assert_eq!(tx_hashes.sizes[1], tx.encode_2718_len());
1477        assert_eq!(tx_hashes.hashes[1], *tx.tx_hash());
1478    }
1479
1480    #[test]
1481    fn test_pooled_tx_hashes_68_with_transaction() {
1482        let tx = signed_transaction();
1483        let tx_hashes =
1484            NewPooledTransactionHashes68 { types: vec![], sizes: vec![], hashes: vec![] }
1485                .with_transaction(&tx);
1486        assert_eq!(tx_hashes.types.len(), 1);
1487        assert_eq!(tx_hashes.sizes.len(), 1);
1488        assert_eq!(tx_hashes.hashes.len(), 1);
1489        assert_eq!(tx_hashes.types[0], tx.ty());
1490        assert_eq!(tx_hashes.sizes[0], tx.encode_2718_len());
1491        assert_eq!(tx_hashes.hashes[0], *tx.tx_hash());
1492    }
1493
1494    #[test]
1495    fn test_pooled_tx_hashes_68_with_transactions() {
1496        let tx = signed_transaction();
1497        let txs = vec![tx.clone(), tx.clone()];
1498        let tx_hashes =
1499            NewPooledTransactionHashes68 { types: vec![], sizes: vec![], hashes: vec![] }
1500                .with_transactions(&txs);
1501        assert_eq!(tx_hashes.types.len(), 2);
1502        assert_eq!(tx_hashes.sizes.len(), 2);
1503        assert_eq!(tx_hashes.hashes.len(), 2);
1504        assert_eq!(tx_hashes.types[0], tx.ty());
1505        assert_eq!(tx_hashes.sizes[0], tx.encode_2718_len());
1506        assert_eq!(tx_hashes.hashes[0], *tx.tx_hash());
1507        assert_eq!(tx_hashes.types[1], tx.ty());
1508        assert_eq!(tx_hashes.sizes[1], tx.encode_2718_len());
1509        assert_eq!(tx_hashes.hashes[1], *tx.tx_hash());
1510    }
1511}