alloy_consensus/transaction/
eip4844.rs

1use super::{RlpEcdsaDecodableTx, RlpEcdsaEncodableTx, TxEip4844Sidecar};
2use crate::{SignableTransaction, Signed, Transaction, TxType};
3use alloc::vec::Vec;
4use alloy_eips::{
5    eip2718::IsTyped2718,
6    eip2930::AccessList,
7    eip4844::{BlobTransactionSidecar, DATA_GAS_PER_BLOB},
8    eip7594::{Decodable7594, Encodable7594},
9    eip7702::SignedAuthorization,
10    Typed2718,
11};
12use alloy_primitives::{Address, Bytes, ChainId, Signature, TxKind, B256, U256};
13use alloy_rlp::{BufMut, Decodable, Encodable, Header};
14use core::{fmt, mem};
15
16#[cfg(feature = "kzg")]
17use alloy_eips::eip4844::BlobTransactionValidationError;
18
19/// [EIP-4844 Blob Transaction](https://eips.ethereum.org/EIPS/eip-4844#blob-transaction)
20///
21/// A transaction with blob hashes and max blob fee.
22/// It can either be a standalone transaction, mainly seen when retrieving historical transactions,
23/// or a transaction with a sidecar, which is used when submitting a transaction to the network and
24/// when receiving and sending transactions during the gossip stage.
25#[derive(Clone, Debug, PartialEq, Eq, Hash)]
26#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
27#[cfg_attr(feature = "serde", derive(serde::Serialize))]
28#[cfg_attr(feature = "serde", serde(untagged))]
29#[doc(alias = "Eip4844TransactionVariant")]
30pub enum TxEip4844Variant<T = BlobTransactionSidecar> {
31    /// A standalone transaction with blob hashes and max blob fee.
32    TxEip4844(TxEip4844),
33    /// A transaction with a sidecar, which contains the blob data, commitments, and proofs.
34    TxEip4844WithSidecar(TxEip4844WithSidecar<T>),
35}
36
37#[cfg(feature = "serde")]
38impl<'de, T: serde::Deserialize<'de>> serde::Deserialize<'de> for TxEip4844Variant<T> {
39    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
40    where
41        D: serde::Deserializer<'de>,
42    {
43        #[derive(serde::Deserialize)]
44        struct TxEip4844SerdeHelper<Sidecar> {
45            #[serde(flatten)]
46            #[doc(alias = "transaction")]
47            tx: TxEip4844,
48            #[serde(flatten)]
49            sidecar: Option<Sidecar>,
50        }
51
52        let tx = TxEip4844SerdeHelper::<T>::deserialize(deserializer)?;
53
54        if let Some(sidecar) = tx.sidecar {
55            Ok(TxEip4844WithSidecar::from_tx_and_sidecar(tx.tx, sidecar).into())
56        } else {
57            Ok(tx.tx.into())
58        }
59    }
60}
61
62impl<T> From<Signed<TxEip4844>> for Signed<TxEip4844Variant<T>> {
63    fn from(value: Signed<TxEip4844>) -> Self {
64        let (tx, signature, hash) = value.into_parts();
65        Self::new_unchecked(TxEip4844Variant::TxEip4844(tx), signature, hash)
66    }
67}
68
69impl<T: Encodable7594> From<Signed<TxEip4844WithSidecar<T>>> for Signed<TxEip4844Variant<T>> {
70    fn from(value: Signed<TxEip4844WithSidecar<T>>) -> Self {
71        let (tx, signature, hash) = value.into_parts();
72        Self::new_unchecked(TxEip4844Variant::TxEip4844WithSidecar(tx), signature, hash)
73    }
74}
75
76impl<T> From<TxEip4844WithSidecar<T>> for TxEip4844Variant<T> {
77    fn from(tx: TxEip4844WithSidecar<T>) -> Self {
78        Self::TxEip4844WithSidecar(tx)
79    }
80}
81
82impl<T> From<TxEip4844> for TxEip4844Variant<T> {
83    fn from(tx: TxEip4844) -> Self {
84        Self::TxEip4844(tx)
85    }
86}
87
88impl From<(TxEip4844, BlobTransactionSidecar)> for TxEip4844Variant<BlobTransactionSidecar> {
89    fn from((tx, sidecar): (TxEip4844, BlobTransactionSidecar)) -> Self {
90        TxEip4844WithSidecar::from_tx_and_sidecar(tx, sidecar).into()
91    }
92}
93
94impl<T> From<TxEip4844Variant<T>> for TxEip4844 {
95    fn from(tx: TxEip4844Variant<T>) -> Self {
96        match tx {
97            TxEip4844Variant::TxEip4844(tx) => tx,
98            TxEip4844Variant::TxEip4844WithSidecar(tx) => tx.tx,
99        }
100    }
101}
102
103impl<T> AsRef<TxEip4844> for TxEip4844Variant<T> {
104    fn as_ref(&self) -> &TxEip4844 {
105        match self {
106            Self::TxEip4844(tx) => tx,
107            Self::TxEip4844WithSidecar(tx) => &tx.tx,
108        }
109    }
110}
111
112impl<T> AsMut<TxEip4844> for TxEip4844Variant<T> {
113    fn as_mut(&mut self) -> &mut TxEip4844 {
114        match self {
115            Self::TxEip4844(tx) => tx,
116            Self::TxEip4844WithSidecar(tx) => &mut tx.tx,
117        }
118    }
119}
120
121impl AsRef<Self> for TxEip4844 {
122    fn as_ref(&self) -> &Self {
123        self
124    }
125}
126
127impl AsMut<Self> for TxEip4844 {
128    fn as_mut(&mut self) -> &mut Self {
129        self
130    }
131}
132
133impl<T> TxEip4844Variant<T> {
134    /// Get the transaction type.
135    #[doc(alias = "transaction_type")]
136    pub const fn tx_type() -> TxType {
137        TxType::Eip4844
138    }
139
140    /// Get access to the inner tx [TxEip4844].
141    #[doc(alias = "transaction")]
142    pub const fn tx(&self) -> &TxEip4844 {
143        match self {
144            Self::TxEip4844(tx) => tx,
145            Self::TxEip4844WithSidecar(tx) => tx.tx(),
146        }
147    }
148
149    /// Returns the [`TxEip4844WithSidecar`] if it has a sidecar
150    pub const fn as_with_sidecar(&self) -> Option<&TxEip4844WithSidecar<T>> {
151        match self {
152            Self::TxEip4844WithSidecar(tx) => Some(tx),
153            _ => None,
154        }
155    }
156
157    /// Tries to unwrap the [`TxEip4844WithSidecar`] returns the transaction as error if it is not a
158    /// [`TxEip4844WithSidecar`]
159    pub fn try_into_4844_with_sidecar(self) -> Result<TxEip4844WithSidecar<T>, Self> {
160        match self {
161            Self::TxEip4844WithSidecar(tx) => Ok(tx),
162            _ => Err(self),
163        }
164    }
165
166    /// Returns the sidecar if this is [`TxEip4844Variant::TxEip4844WithSidecar`].
167    pub const fn sidecar(&self) -> Option<&T> {
168        match self {
169            Self::TxEip4844WithSidecar(tx) => Some(tx.sidecar()),
170            _ => None,
171        }
172    }
173}
174
175impl<T: TxEip4844Sidecar> TxEip4844Variant<T> {
176    /// Verifies that the transaction's blob data, commitments, and proofs are all valid.
177    ///
178    /// See also [TxEip4844::validate_blob]
179    #[cfg(feature = "kzg")]
180    pub fn validate(
181        &self,
182        proof_settings: &c_kzg::KzgSettings,
183    ) -> Result<(), BlobTransactionValidationError> {
184        match self {
185            Self::TxEip4844(_) => Err(BlobTransactionValidationError::MissingSidecar),
186            Self::TxEip4844WithSidecar(tx) => tx.validate_blob(proof_settings),
187        }
188    }
189
190    /// Calculates a heuristic for the in-memory size of the [TxEip4844Variant] transaction.
191    #[inline]
192    pub fn size(&self) -> usize {
193        match self {
194            Self::TxEip4844(tx) => tx.size(),
195            Self::TxEip4844WithSidecar(tx) => tx.size(),
196        }
197    }
198}
199
200impl<T> Transaction for TxEip4844Variant<T>
201where
202    T: fmt::Debug + Send + Sync + 'static,
203{
204    #[inline]
205    fn chain_id(&self) -> Option<ChainId> {
206        match self {
207            Self::TxEip4844(tx) => Some(tx.chain_id),
208            Self::TxEip4844WithSidecar(tx) => Some(tx.tx().chain_id),
209        }
210    }
211
212    #[inline]
213    fn nonce(&self) -> u64 {
214        match self {
215            Self::TxEip4844(tx) => tx.nonce,
216            Self::TxEip4844WithSidecar(tx) => tx.tx().nonce,
217        }
218    }
219
220    #[inline]
221    fn gas_limit(&self) -> u64 {
222        match self {
223            Self::TxEip4844(tx) => tx.gas_limit,
224            Self::TxEip4844WithSidecar(tx) => tx.tx().gas_limit,
225        }
226    }
227
228    #[inline]
229    fn gas_price(&self) -> Option<u128> {
230        None
231    }
232
233    #[inline]
234    fn max_fee_per_gas(&self) -> u128 {
235        match self {
236            Self::TxEip4844(tx) => tx.max_fee_per_gas(),
237            Self::TxEip4844WithSidecar(tx) => tx.max_fee_per_gas(),
238        }
239    }
240
241    #[inline]
242    fn max_priority_fee_per_gas(&self) -> Option<u128> {
243        match self {
244            Self::TxEip4844(tx) => tx.max_priority_fee_per_gas(),
245            Self::TxEip4844WithSidecar(tx) => tx.max_priority_fee_per_gas(),
246        }
247    }
248
249    #[inline]
250    fn max_fee_per_blob_gas(&self) -> Option<u128> {
251        match self {
252            Self::TxEip4844(tx) => tx.max_fee_per_blob_gas(),
253            Self::TxEip4844WithSidecar(tx) => tx.max_fee_per_blob_gas(),
254        }
255    }
256
257    #[inline]
258    fn priority_fee_or_price(&self) -> u128 {
259        match self {
260            Self::TxEip4844(tx) => tx.priority_fee_or_price(),
261            Self::TxEip4844WithSidecar(tx) => tx.priority_fee_or_price(),
262        }
263    }
264
265    fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
266        match self {
267            Self::TxEip4844(tx) => tx.effective_gas_price(base_fee),
268            Self::TxEip4844WithSidecar(tx) => tx.effective_gas_price(base_fee),
269        }
270    }
271
272    #[inline]
273    fn is_dynamic_fee(&self) -> bool {
274        match self {
275            Self::TxEip4844(tx) => tx.is_dynamic_fee(),
276            Self::TxEip4844WithSidecar(tx) => tx.is_dynamic_fee(),
277        }
278    }
279
280    #[inline]
281    fn kind(&self) -> TxKind {
282        match self {
283            Self::TxEip4844(tx) => tx.to,
284            Self::TxEip4844WithSidecar(tx) => tx.tx.to,
285        }
286        .into()
287    }
288
289    #[inline]
290    fn is_create(&self) -> bool {
291        false
292    }
293
294    #[inline]
295    fn value(&self) -> U256 {
296        match self {
297            Self::TxEip4844(tx) => tx.value,
298            Self::TxEip4844WithSidecar(tx) => tx.tx.value,
299        }
300    }
301
302    #[inline]
303    fn input(&self) -> &Bytes {
304        match self {
305            Self::TxEip4844(tx) => tx.input(),
306            Self::TxEip4844WithSidecar(tx) => tx.tx().input(),
307        }
308    }
309
310    #[inline]
311    fn access_list(&self) -> Option<&AccessList> {
312        match self {
313            Self::TxEip4844(tx) => tx.access_list(),
314            Self::TxEip4844WithSidecar(tx) => tx.access_list(),
315        }
316    }
317
318    #[inline]
319    fn blob_versioned_hashes(&self) -> Option<&[B256]> {
320        match self {
321            Self::TxEip4844(tx) => tx.blob_versioned_hashes(),
322            Self::TxEip4844WithSidecar(tx) => tx.blob_versioned_hashes(),
323        }
324    }
325
326    #[inline]
327    fn authorization_list(&self) -> Option<&[SignedAuthorization]> {
328        None
329    }
330}
331impl Typed2718 for TxEip4844 {
332    fn ty(&self) -> u8 {
333        TxType::Eip4844 as u8
334    }
335}
336
337impl<T: Encodable7594> RlpEcdsaEncodableTx for TxEip4844Variant<T> {
338    fn rlp_encoded_fields_length(&self) -> usize {
339        match self {
340            Self::TxEip4844(inner) => inner.rlp_encoded_fields_length(),
341            Self::TxEip4844WithSidecar(inner) => inner.rlp_encoded_fields_length(),
342        }
343    }
344
345    fn rlp_encode_fields(&self, out: &mut dyn alloy_rlp::BufMut) {
346        match self {
347            Self::TxEip4844(inner) => inner.rlp_encode_fields(out),
348            Self::TxEip4844WithSidecar(inner) => inner.rlp_encode_fields(out),
349        }
350    }
351
352    fn rlp_header_signed(&self, signature: &Signature) -> Header {
353        match self {
354            Self::TxEip4844(inner) => inner.rlp_header_signed(signature),
355            Self::TxEip4844WithSidecar(inner) => inner.rlp_header_signed(signature),
356        }
357    }
358
359    fn rlp_encode_signed(&self, signature: &Signature, out: &mut dyn BufMut) {
360        match self {
361            Self::TxEip4844(inner) => inner.rlp_encode_signed(signature, out),
362            Self::TxEip4844WithSidecar(inner) => inner.rlp_encode_signed(signature, out),
363        }
364    }
365
366    fn tx_hash_with_type(&self, signature: &Signature, ty: u8) -> alloy_primitives::TxHash {
367        match self {
368            Self::TxEip4844(inner) => inner.tx_hash_with_type(signature, ty),
369            Self::TxEip4844WithSidecar(inner) => inner.tx_hash_with_type(signature, ty),
370        }
371    }
372}
373
374impl<T: Encodable7594 + Decodable7594> RlpEcdsaDecodableTx for TxEip4844Variant<T> {
375    const DEFAULT_TX_TYPE: u8 = { Self::tx_type() as u8 };
376
377    fn rlp_decode_fields(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
378        let needle = &mut &**buf;
379
380        // We also need to do a trial decoding of WithSidecar to see if it
381        // works. The trial ref is consumed to look for a WithSidecar.
382        let trial = &mut &**buf;
383
384        // If the next bytes are a header, one of 3 things is true:
385        // - If the header is a list, this is a WithSidecar tx
386        // - If there is no header, this is a non-sidecar tx with a single-byte chain ID.
387        // - If there is a string header, this is a non-sidecar tx with a multi-byte chain ID.
388        // To check these, we first try to decode the header. If it fails or is
389        // not a list, we lmow that it is a non-sidecar transaction.
390        if Header::decode(needle).is_ok_and(|h| h.list) {
391            if let Ok(tx) = TxEip4844WithSidecar::rlp_decode_fields(trial) {
392                *buf = *trial;
393                return Ok(tx.into());
394            }
395        }
396        TxEip4844::rlp_decode_fields(buf).map(Into::into)
397    }
398
399    fn rlp_decode_with_signature(buf: &mut &[u8]) -> alloy_rlp::Result<(Self, Signature)> {
400        // We need to determine if this has a sidecar tx or not. The needle ref
401        // is consumed to look for headers.
402        let needle = &mut &**buf;
403
404        // We also need to do a trial decoding of WithSidecar to see if it
405        // works. The original ref is consumed to look for a WithSidecar.
406        let trial = &mut &**buf;
407
408        // First we decode the outer header
409        Header::decode(needle)?;
410
411        // If the next bytes are a header, one of 3 things is true:
412        // - If the header is a list, this is a WithSidecar tx
413        // - If there is no header, this is a non-sidecar tx with a single-byte chain ID.
414        // - If there is a string header, this is a non-sidecar tx with a multi-byte chain ID.
415        // To check these, we first try to decode the header. If it fails or is
416        // not a list, we lmow that it is a non-sidecar transaction.
417        if Header::decode(needle).is_ok_and(|h| h.list) {
418            if let Ok((tx, signature)) = TxEip4844WithSidecar::rlp_decode_with_signature(trial) {
419                // If successful, we need to consume the trial buffer up to
420                // the same point.
421                *buf = *trial;
422                return Ok((tx.into(), signature));
423            }
424        }
425        TxEip4844::rlp_decode_with_signature(buf).map(|(tx, signature)| (tx.into(), signature))
426    }
427}
428
429impl<T> Typed2718 for TxEip4844Variant<T> {
430    fn ty(&self) -> u8 {
431        TxType::Eip4844 as u8
432    }
433}
434
435impl IsTyped2718 for TxEip4844 {
436    fn is_type(type_id: u8) -> bool {
437        matches!(type_id, 0x03)
438    }
439}
440
441impl<T> SignableTransaction<Signature> for TxEip4844Variant<T>
442where
443    T: fmt::Debug + Send + Sync + 'static,
444{
445    fn set_chain_id(&mut self, chain_id: ChainId) {
446        match self {
447            Self::TxEip4844(inner) => {
448                inner.set_chain_id(chain_id);
449            }
450            Self::TxEip4844WithSidecar(inner) => {
451                inner.set_chain_id(chain_id);
452            }
453        }
454    }
455
456    fn encode_for_signing(&self, out: &mut dyn alloy_rlp::BufMut) {
457        // A signature for a [TxEip4844WithSidecar] is a signature over the [TxEip4844Variant]
458        // EIP-2718 payload fields:
459        // (BLOB_TX_TYPE ||
460        //   rlp([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, to, value,
461        //     data, access_list, max_fee_per_blob_gas, blob_versioned_hashes]))
462        self.tx().encode_for_signing(out);
463    }
464
465    fn payload_len_for_signature(&self) -> usize {
466        self.tx().payload_len_for_signature()
467    }
468}
469
470/// [EIP-4844 Blob Transaction](https://eips.ethereum.org/EIPS/eip-4844#blob-transaction)
471///
472/// A transaction with blob hashes and max blob fee. It does not have the Blob sidecar.
473#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
474#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
475#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
476#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
477#[doc(alias = "Eip4844Transaction", alias = "TransactionEip4844", alias = "Eip4844Tx")]
478pub struct TxEip4844 {
479    /// Added as EIP-pub 155: Simple replay attack protection
480    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
481    pub chain_id: ChainId,
482    /// A scalar value equal to the number of transactions sent by the sender; formally Tn.
483    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
484    pub nonce: u64,
485    /// A scalar value equal to the maximum
486    /// amount of gas that should be used in executing
487    /// this transaction. This is paid up-front, before any
488    /// computation is done and may not be increased
489    /// later; formally Tg.
490    #[cfg_attr(
491        feature = "serde",
492        serde(with = "alloy_serde::quantity", rename = "gas", alias = "gasLimit")
493    )]
494    pub gas_limit: u64,
495    /// A scalar value equal to the maximum
496    /// amount of gas that should be used in executing
497    /// this transaction. This is paid up-front, before any
498    /// computation is done and may not be increased
499    /// later; formally Tg.
500    ///
501    /// As ethereum circulation is around 120mil eth as of 2022 that is around
502    /// 120000000000000000000000000 wei we are safe to use u128 as its max number is:
503    /// 340282366920938463463374607431768211455
504    ///
505    /// This is also known as `GasFeeCap`
506    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
507    pub max_fee_per_gas: u128,
508    /// Max Priority fee that transaction is paying
509    ///
510    /// As ethereum circulation is around 120mil eth as of 2022 that is around
511    /// 120000000000000000000000000 wei we are safe to use u128 as its max number is:
512    /// 340282366920938463463374607431768211455
513    ///
514    /// This is also known as `GasTipCap`
515    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
516    pub max_priority_fee_per_gas: u128,
517    /// The 160-bit address of the message call’s recipient.
518    pub to: Address,
519    /// A scalar value equal to the number of Wei to
520    /// be transferred to the message call’s recipient or,
521    /// in the case of contract creation, as an endowment
522    /// to the newly created account; formally Tv.
523    pub value: U256,
524    /// The accessList specifies a list of addresses and storage keys;
525    /// these addresses and storage keys are added into the `accessed_addresses`
526    /// and `accessed_storage_keys` global sets (introduced in EIP-2929).
527    /// A gas cost is charged, though at a discount relative to the cost of
528    /// accessing outside the list.
529    pub access_list: AccessList,
530
531    /// It contains a vector of fixed size hash(32 bytes)
532    pub blob_versioned_hashes: Vec<B256>,
533
534    /// Max fee per data gas
535    ///
536    /// aka BlobFeeCap or blobGasFeeCap
537    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
538    pub max_fee_per_blob_gas: u128,
539
540    /// Input has two uses depending if transaction is Create or Call (if `to` field is None or
541    /// Some). pub init: An unlimited size byte array specifying the
542    /// EVM-code for the account initialisation procedure CREATE,
543    /// data: An unlimited size byte array specifying the
544    /// input data of the message call, formally Td.
545    pub input: Bytes,
546}
547
548impl TxEip4844 {
549    /// Returns the total gas for all blobs in this transaction.
550    #[inline]
551    pub fn blob_gas(&self) -> u64 {
552        // SAFETY: we don't expect u64::MAX / DATA_GAS_PER_BLOB hashes in a single transaction
553        self.blob_versioned_hashes.len() as u64 * DATA_GAS_PER_BLOB
554    }
555
556    /// Verifies that the given blob data, commitments, and proofs are all valid for this
557    /// transaction.
558    ///
559    /// Takes as input the [KzgSettings](c_kzg::KzgSettings), which should contain the parameters
560    /// derived from the KZG trusted setup.
561    ///
562    /// This ensures that the blob transaction payload has the same number of blob data elements,
563    /// commitments, and proofs. Each blob data element is verified against its commitment and
564    /// proof.
565    ///
566    /// Returns [BlobTransactionValidationError::InvalidProof] if any blob KZG proof in the response
567    /// fails to verify, or if the versioned hashes in the transaction do not match the actual
568    /// commitment versioned hashes.
569    #[cfg(feature = "kzg")]
570    pub fn validate_blob<T: TxEip4844Sidecar>(
571        &self,
572        sidecar: &T,
573        proof_settings: &c_kzg::KzgSettings,
574    ) -> Result<(), BlobTransactionValidationError> {
575        sidecar.validate(&self.blob_versioned_hashes, proof_settings)
576    }
577
578    /// Get transaction type.
579    #[doc(alias = "transaction_type")]
580    pub const fn tx_type() -> TxType {
581        TxType::Eip4844
582    }
583
584    /// Attaches the blob sidecar to the transaction
585    pub const fn with_sidecar<T>(self, sidecar: T) -> TxEip4844WithSidecar<T> {
586        TxEip4844WithSidecar::from_tx_and_sidecar(self, sidecar)
587    }
588
589    /// Calculates a heuristic for the in-memory size of the [TxEip4844Variant] transaction.
590    #[inline]
591    pub fn size(&self) -> usize {
592        mem::size_of::<ChainId>() + // chain_id
593        mem::size_of::<u64>() + // nonce
594        mem::size_of::<u64>() + // gas_limit
595        mem::size_of::<u128>() + // max_fee_per_gas
596        mem::size_of::<u128>() + // max_priority_fee_per_gas
597        mem::size_of::<Address>() + // to
598        mem::size_of::<U256>() + // value
599        self.access_list.size() + // access_list
600        self.input.len() +  // input
601        self.blob_versioned_hashes.capacity() * mem::size_of::<B256>() + // blob hashes size
602        mem::size_of::<u128>() // max_fee_per_data_gas
603    }
604}
605
606impl RlpEcdsaEncodableTx for TxEip4844 {
607    fn rlp_encoded_fields_length(&self) -> usize {
608        self.chain_id.length()
609            + self.nonce.length()
610            + self.gas_limit.length()
611            + self.max_fee_per_gas.length()
612            + self.max_priority_fee_per_gas.length()
613            + self.to.length()
614            + self.value.length()
615            + self.access_list.length()
616            + self.blob_versioned_hashes.length()
617            + self.max_fee_per_blob_gas.length()
618            + self.input.0.length()
619    }
620
621    fn rlp_encode_fields(&self, out: &mut dyn alloy_rlp::BufMut) {
622        self.chain_id.encode(out);
623        self.nonce.encode(out);
624        self.max_priority_fee_per_gas.encode(out);
625        self.max_fee_per_gas.encode(out);
626        self.gas_limit.encode(out);
627        self.to.encode(out);
628        self.value.encode(out);
629        self.input.0.encode(out);
630        self.access_list.encode(out);
631        self.max_fee_per_blob_gas.encode(out);
632        self.blob_versioned_hashes.encode(out);
633    }
634}
635
636impl RlpEcdsaDecodableTx for TxEip4844 {
637    const DEFAULT_TX_TYPE: u8 = { Self::tx_type() as u8 };
638
639    fn rlp_decode_fields(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
640        Ok(Self {
641            chain_id: Decodable::decode(buf)?,
642            nonce: Decodable::decode(buf)?,
643            max_priority_fee_per_gas: Decodable::decode(buf)?,
644            max_fee_per_gas: Decodable::decode(buf)?,
645            gas_limit: Decodable::decode(buf)?,
646            to: Decodable::decode(buf)?,
647            value: Decodable::decode(buf)?,
648            input: Decodable::decode(buf)?,
649            access_list: Decodable::decode(buf)?,
650            max_fee_per_blob_gas: Decodable::decode(buf)?,
651            blob_versioned_hashes: Decodable::decode(buf)?,
652        })
653    }
654}
655
656impl SignableTransaction<Signature> for TxEip4844 {
657    fn set_chain_id(&mut self, chain_id: ChainId) {
658        self.chain_id = chain_id;
659    }
660
661    fn encode_for_signing(&self, out: &mut dyn alloy_rlp::BufMut) {
662        out.put_u8(Self::tx_type() as u8);
663        self.encode(out);
664    }
665
666    fn payload_len_for_signature(&self) -> usize {
667        self.length() + 1
668    }
669}
670
671impl Transaction for TxEip4844 {
672    #[inline]
673    fn chain_id(&self) -> Option<ChainId> {
674        Some(self.chain_id)
675    }
676
677    #[inline]
678    fn nonce(&self) -> u64 {
679        self.nonce
680    }
681
682    #[inline]
683    fn gas_limit(&self) -> u64 {
684        self.gas_limit
685    }
686
687    #[inline]
688    fn gas_price(&self) -> Option<u128> {
689        None
690    }
691
692    #[inline]
693    fn max_fee_per_gas(&self) -> u128 {
694        self.max_fee_per_gas
695    }
696
697    #[inline]
698    fn max_priority_fee_per_gas(&self) -> Option<u128> {
699        Some(self.max_priority_fee_per_gas)
700    }
701
702    #[inline]
703    fn max_fee_per_blob_gas(&self) -> Option<u128> {
704        Some(self.max_fee_per_blob_gas)
705    }
706
707    #[inline]
708    fn priority_fee_or_price(&self) -> u128 {
709        self.max_priority_fee_per_gas
710    }
711
712    fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
713        base_fee.map_or(self.max_fee_per_gas, |base_fee| {
714            // if the tip is greater than the max priority fee per gas, set it to the max
715            // priority fee per gas + base fee
716            let tip = self.max_fee_per_gas.saturating_sub(base_fee as u128);
717            if tip > self.max_priority_fee_per_gas {
718                self.max_priority_fee_per_gas + base_fee as u128
719            } else {
720                // otherwise return the max fee per gas
721                self.max_fee_per_gas
722            }
723        })
724    }
725
726    #[inline]
727    fn is_dynamic_fee(&self) -> bool {
728        true
729    }
730
731    #[inline]
732    fn kind(&self) -> TxKind {
733        self.to.into()
734    }
735
736    #[inline]
737    fn is_create(&self) -> bool {
738        false
739    }
740
741    #[inline]
742    fn value(&self) -> U256 {
743        self.value
744    }
745
746    #[inline]
747    fn input(&self) -> &Bytes {
748        &self.input
749    }
750
751    #[inline]
752    fn access_list(&self) -> Option<&AccessList> {
753        Some(&self.access_list)
754    }
755
756    #[inline]
757    fn blob_versioned_hashes(&self) -> Option<&[B256]> {
758        Some(&self.blob_versioned_hashes)
759    }
760
761    #[inline]
762    fn authorization_list(&self) -> Option<&[SignedAuthorization]> {
763        None
764    }
765}
766
767impl Encodable for TxEip4844 {
768    fn encode(&self, out: &mut dyn BufMut) {
769        self.rlp_encode(out);
770    }
771
772    fn length(&self) -> usize {
773        self.rlp_encoded_length()
774    }
775}
776
777impl Decodable for TxEip4844 {
778    fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
779        Self::rlp_decode(buf)
780    }
781}
782
783impl<T> From<TxEip4844WithSidecar<T>> for TxEip4844 {
784    /// Consumes the [TxEip4844WithSidecar] and returns the inner [TxEip4844].
785    fn from(tx_with_sidecar: TxEip4844WithSidecar<T>) -> Self {
786        tx_with_sidecar.tx
787    }
788}
789
790/// [EIP-4844 Blob Transaction](https://eips.ethereum.org/EIPS/eip-4844#blob-transaction)
791///
792/// A transaction with blob hashes and max blob fee, which also includes the
793/// [BlobTransactionSidecar]. This is the full type sent over the network as a raw transaction. It
794/// wraps a [TxEip4844] to include the sidecar and the ability to decode it properly.
795///
796/// This is defined in [EIP-4844](https://eips.ethereum.org/EIPS/eip-4844#networking) as an element
797/// of a `PooledTransactions` response, and is also used as the format for sending raw transactions
798/// through the network (eth_sendRawTransaction/eth_sendTransaction).
799#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
800#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
801#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
802#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
803#[doc(alias = "Eip4844TransactionWithSidecar", alias = "Eip4844TxWithSidecar")]
804pub struct TxEip4844WithSidecar<T = BlobTransactionSidecar> {
805    /// The actual transaction.
806    #[cfg_attr(feature = "serde", serde(flatten))]
807    #[doc(alias = "transaction")]
808    pub tx: TxEip4844,
809    /// The sidecar.
810    #[cfg_attr(feature = "serde", serde(flatten))]
811    pub sidecar: T,
812}
813
814impl<T> TxEip4844WithSidecar<T> {
815    /// Constructs a new [TxEip4844WithSidecar] from a [TxEip4844] and a sidecar.
816    #[doc(alias = "from_transaction_and_sidecar")]
817    pub const fn from_tx_and_sidecar(tx: TxEip4844, sidecar: T) -> Self {
818        Self { tx, sidecar }
819    }
820
821    /// Get the transaction type.
822    #[doc(alias = "transaction_type")]
823    pub const fn tx_type() -> TxType {
824        TxEip4844::tx_type()
825    }
826
827    /// Get access to the inner tx [TxEip4844].
828    #[doc(alias = "transaction")]
829    pub const fn tx(&self) -> &TxEip4844 {
830        &self.tx
831    }
832
833    /// Get access to the inner sidecar.
834    pub const fn sidecar(&self) -> &T {
835        &self.sidecar
836    }
837
838    /// Consumes the [TxEip4844WithSidecar] and returns the inner sidecar.
839    pub fn into_sidecar(self) -> T {
840        self.sidecar
841    }
842
843    /// Consumes the [TxEip4844WithSidecar] and returns the inner [TxEip4844] and a sidecar.
844    pub fn into_parts(self) -> (TxEip4844, T) {
845        (self.tx, self.sidecar)
846    }
847}
848
849impl<T: TxEip4844Sidecar> TxEip4844WithSidecar<T> {
850    /// Verifies that the transaction's blob data, commitments, and proofs are all valid.
851    ///
852    /// See also [TxEip4844::validate_blob]
853    #[cfg(feature = "kzg")]
854    pub fn validate_blob(
855        &self,
856        proof_settings: &c_kzg::KzgSettings,
857    ) -> Result<(), BlobTransactionValidationError> {
858        self.tx.validate_blob(&self.sidecar, proof_settings)
859    }
860
861    /// Calculates a heuristic for the in-memory size of the [TxEip4844WithSidecar] transaction.
862    #[inline]
863    pub fn size(&self) -> usize {
864        self.tx.size() + self.sidecar.size()
865    }
866}
867
868impl<T> SignableTransaction<Signature> for TxEip4844WithSidecar<T>
869where
870    T: fmt::Debug + Send + Sync + 'static,
871{
872    fn set_chain_id(&mut self, chain_id: ChainId) {
873        self.tx.chain_id = chain_id;
874    }
875
876    fn encode_for_signing(&self, out: &mut dyn alloy_rlp::BufMut) {
877        // A signature for a [TxEip4844WithSidecar] is a signature over the [TxEip4844] EIP-2718
878        // payload fields:
879        // (BLOB_TX_TYPE ||
880        //   rlp([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, to, value,
881        //     data, access_list, max_fee_per_blob_gas, blob_versioned_hashes]))
882        self.tx.encode_for_signing(out);
883    }
884
885    fn payload_len_for_signature(&self) -> usize {
886        // The payload length is the length of the `transaction_payload_body` list.
887        // The sidecar is NOT included.
888        self.tx.payload_len_for_signature()
889    }
890}
891
892impl<T> Transaction for TxEip4844WithSidecar<T>
893where
894    T: fmt::Debug + Send + Sync + 'static,
895{
896    #[inline]
897    fn chain_id(&self) -> Option<ChainId> {
898        self.tx.chain_id()
899    }
900
901    #[inline]
902    fn nonce(&self) -> u64 {
903        self.tx.nonce()
904    }
905
906    #[inline]
907    fn gas_limit(&self) -> u64 {
908        self.tx.gas_limit()
909    }
910
911    #[inline]
912    fn gas_price(&self) -> Option<u128> {
913        self.tx.gas_price()
914    }
915
916    #[inline]
917    fn max_fee_per_gas(&self) -> u128 {
918        self.tx.max_fee_per_gas()
919    }
920
921    #[inline]
922    fn max_priority_fee_per_gas(&self) -> Option<u128> {
923        self.tx.max_priority_fee_per_gas()
924    }
925
926    #[inline]
927    fn max_fee_per_blob_gas(&self) -> Option<u128> {
928        self.tx.max_fee_per_blob_gas()
929    }
930
931    #[inline]
932    fn priority_fee_or_price(&self) -> u128 {
933        self.tx.priority_fee_or_price()
934    }
935
936    fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
937        self.tx.effective_gas_price(base_fee)
938    }
939
940    #[inline]
941    fn is_dynamic_fee(&self) -> bool {
942        self.tx.is_dynamic_fee()
943    }
944
945    #[inline]
946    fn kind(&self) -> TxKind {
947        self.tx.kind()
948    }
949
950    #[inline]
951    fn is_create(&self) -> bool {
952        false
953    }
954
955    #[inline]
956    fn value(&self) -> U256 {
957        self.tx.value()
958    }
959
960    #[inline]
961    fn input(&self) -> &Bytes {
962        self.tx.input()
963    }
964
965    #[inline]
966    fn access_list(&self) -> Option<&AccessList> {
967        Some(&self.tx.access_list)
968    }
969
970    #[inline]
971    fn blob_versioned_hashes(&self) -> Option<&[B256]> {
972        self.tx.blob_versioned_hashes()
973    }
974
975    #[inline]
976    fn authorization_list(&self) -> Option<&[SignedAuthorization]> {
977        None
978    }
979}
980
981impl<T> Typed2718 for TxEip4844WithSidecar<T> {
982    fn ty(&self) -> u8 {
983        TxType::Eip4844 as u8
984    }
985}
986
987impl<T: Encodable7594> RlpEcdsaEncodableTx for TxEip4844WithSidecar<T> {
988    fn rlp_encoded_fields_length(&self) -> usize {
989        self.sidecar.encode_7594_len() + self.tx.rlp_encoded_length()
990    }
991
992    fn rlp_encode_fields(&self, out: &mut dyn alloy_rlp::BufMut) {
993        self.tx.rlp_encode(out);
994        self.sidecar.encode_7594(out);
995    }
996
997    fn rlp_header_signed(&self, signature: &Signature) -> Header {
998        let payload_length =
999            self.tx.rlp_encoded_length_with_signature(signature) + self.sidecar.encode_7594_len();
1000        Header { list: true, payload_length }
1001    }
1002
1003    fn rlp_encode_signed(&self, signature: &Signature, out: &mut dyn BufMut) {
1004        self.rlp_header_signed(signature).encode(out);
1005        self.tx.rlp_encode_signed(signature, out);
1006        self.sidecar.encode_7594(out);
1007    }
1008
1009    fn tx_hash_with_type(&self, signature: &Signature, ty: u8) -> alloy_primitives::TxHash {
1010        // eip4844 tx_hash is always based on the non-sidecar encoding
1011        self.tx.tx_hash_with_type(signature, ty)
1012    }
1013}
1014
1015impl<T: Encodable7594 + Decodable7594> RlpEcdsaDecodableTx for TxEip4844WithSidecar<T> {
1016    const DEFAULT_TX_TYPE: u8 = { Self::tx_type() as u8 };
1017
1018    fn rlp_decode_fields(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
1019        let tx = TxEip4844::rlp_decode(buf)?;
1020        let sidecar = T::decode_7594(buf)?;
1021        Ok(Self { tx, sidecar })
1022    }
1023
1024    fn rlp_decode_with_signature(buf: &mut &[u8]) -> alloy_rlp::Result<(Self, Signature)> {
1025        let header = Header::decode(buf)?;
1026        if !header.list {
1027            return Err(alloy_rlp::Error::UnexpectedString);
1028        }
1029        let remaining = buf.len();
1030
1031        let (tx, signature) = TxEip4844::rlp_decode_with_signature(buf)?;
1032        let sidecar = T::decode_7594(buf)?;
1033
1034        if buf.len() + header.payload_length != remaining {
1035            return Err(alloy_rlp::Error::UnexpectedLength);
1036        }
1037
1038        Ok((Self { tx, sidecar }, signature))
1039    }
1040}
1041
1042#[cfg(test)]
1043mod tests {
1044    use super::{BlobTransactionSidecar, TxEip4844, TxEip4844WithSidecar};
1045    use crate::{
1046        transaction::{eip4844::TxEip4844Variant, RlpEcdsaDecodableTx},
1047        SignableTransaction, TxEnvelope,
1048    };
1049    use alloy_eips::{
1050        eip2930::AccessList, eip4844::env_settings::EnvKzgSettings,
1051        eip7594::BlobTransactionSidecarVariant, Encodable2718 as _,
1052    };
1053    use alloy_primitives::{address, b256, bytes, hex, Signature, U256};
1054    use alloy_rlp::{Decodable, Encodable};
1055    use assert_matches::assert_matches;
1056    use std::path::PathBuf;
1057
1058    #[test]
1059    fn different_sidecar_same_hash() {
1060        // this should make sure that the hash calculated for the `into_signed` conversion does not
1061        // change if the sidecar is different
1062        let tx = TxEip4844 {
1063            chain_id: 1,
1064            nonce: 1,
1065            max_priority_fee_per_gas: 1,
1066            max_fee_per_gas: 1,
1067            gas_limit: 1,
1068            to: Default::default(),
1069            value: U256::from(1),
1070            access_list: Default::default(),
1071            blob_versioned_hashes: vec![Default::default()],
1072            max_fee_per_blob_gas: 1,
1073            input: Default::default(),
1074        };
1075        let sidecar = BlobTransactionSidecar {
1076            blobs: vec![[2; 131072].into()],
1077            commitments: vec![[3; 48].into()],
1078            proofs: vec![[4; 48].into()],
1079        };
1080        let mut tx = TxEip4844WithSidecar { tx, sidecar };
1081        let signature = Signature::test_signature();
1082
1083        // turn this transaction into_signed
1084        let expected_signed = tx.clone().into_signed(signature);
1085
1086        // change the sidecar, adding a single (blob, commitment, proof) pair
1087        tx.sidecar = BlobTransactionSidecar {
1088            blobs: vec![[1; 131072].into()],
1089            commitments: vec![[1; 48].into()],
1090            proofs: vec![[1; 48].into()],
1091        };
1092
1093        // turn this transaction into_signed
1094        let actual_signed = tx.into_signed(signature);
1095
1096        // the hashes should be the same
1097        assert_eq!(expected_signed.hash(), actual_signed.hash());
1098
1099        // convert to envelopes
1100        let expected_envelope: TxEnvelope = expected_signed.into();
1101        let actual_envelope: TxEnvelope = actual_signed.into();
1102
1103        // now encode the transaction and check the length
1104        let len = expected_envelope.length();
1105        let mut buf = Vec::with_capacity(len);
1106        expected_envelope.encode(&mut buf);
1107        assert_eq!(buf.len(), len);
1108
1109        // ensure it's also the same size that `actual` claims to be, since we just changed the
1110        // sidecar values.
1111        assert_eq!(buf.len(), actual_envelope.length());
1112
1113        // now decode the transaction and check the values
1114        let decoded = TxEnvelope::decode(&mut &buf[..]).unwrap();
1115        assert_eq!(decoded, expected_envelope);
1116    }
1117
1118    #[test]
1119    fn test_4844_variant_into_signed_correct_hash() {
1120        // Taken from <https://etherscan.io/tx/0x93fc9daaa0726c3292a2e939df60f7e773c6a6a726a61ce43f4a217c64d85e87>
1121        let tx =
1122            TxEip4844 {
1123                chain_id: 1,
1124                nonce: 15435,
1125                gas_limit: 8000000,
1126                max_fee_per_gas: 10571233596,
1127                max_priority_fee_per_gas: 1000000000,
1128                to: address!("a8cb082a5a689e0d594d7da1e2d72a3d63adc1bd"),
1129                value: U256::ZERO,
1130                access_list: AccessList::default(),
1131                blob_versioned_hashes: vec![
1132                    b256!("01e5276d91ac1ddb3b1c2d61295211220036e9a04be24c00f76916cc2659d004"),
1133                    b256!("0128eb58aff09fd3a7957cd80aa86186d5849569997cdfcfa23772811b706cc2"),
1134                ],
1135                max_fee_per_blob_gas: 1,
1136                input: bytes!("701f58c50000000000000000000000000000000000000000000000000000000000073fb1ed12e288def5b439ea074b398dbb4c967f2852baac3238c5fe4b62b871a59a6d00000000000000000000000000000000000000000000000000000000123971da000000000000000000000000000000000000000000000000000000000000000ac39b2a24e1dbdd11a1e7bd7c0f4dfd7d9b9cfa0997d033ad05f961ba3b82c6c83312c967f10daf5ed2bffe309249416e03ee0b101f2b84d2102b9e38b0e4dfdf0000000000000000000000000000000000000000000000000000000066254c8b538dcc33ecf5334bbd294469f9d4fd084a3090693599a46d6c62567747cbc8660000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000073fb20000000000000000000000000000000000000000000000000000000066254da10000000000000000000000000000000000000000000000000000000012397d5e20b09b263779fda4171c341e720af8fa469621ff548651f8dbbc06c2d320400c000000000000000000000000000000000000000000000000000000000000000b50a833bb11af92814e99c6ff7cf7ba7042827549d6f306a04270753702d897d8fc3c411b99159939ac1c16d21d3057ddc8b2333d1331ab34c938cff0eb29ce2e43241c170344db6819f76b1f1e0ab8206f3ec34120312d275c4f5bbea7f5c55700000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000480000000000000000000000000000000000000000000000000000000000000031800000000000000000000000000000000000000000000800b0000000000000000000000000000000000000000000000000000000000000004ed12e288def5b439ea074b398dbb4c967f2852baac3238c5fe4b62b871a59a6d00000ca8000000000000000000000000000000000000800b000000000000000000000000000000000000000000000000000000000000000300000000000000000000000066254da100000000000000000000000066254e9d00010ca80000000000000000000000000000000000008001000000000000000000000000000000000000000000000000000000000000000550a833bb11af92814e99c6ff7cf7ba7042827549d6f306a04270753702d897d800010ca800000000000000000000000000000000000080010000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000b00010ca8000000000000000000000000000000000000801100000000000000000000000000000000000000000000000000000000000000075c1cd5bd0fd333ce9d7c8edfc79f43b8f345b4a394f6aba12a2cc78ce4012ed700010ca80000000000000000000000000000000000008011000000000000000000000000000000000000000000000000000000000000000845392775318aa47beaafbdc827da38c9f1e88c3bdcabba2cb493062e17cbf21e00010ca800000000000000000000000000000000000080080000000000000000000000000000000000000000000000000000000000000000c094e20e7ac9b433f44a5885e3bdc07e51b309aeb993caa24ba84a661ac010c100010ca800000000000000000000000000000000000080080000000000000000000000000000000000000000000000000000000000000001ab42db8f4ed810bdb143368a2b641edf242af6e3d0de8b1486e2b0e7880d431100010ca8000000000000000000000000000000000000800800000000000000000000000000000000000000000000000000000000000000022d94e4cc4525e4e2d81e8227b6172e97076431a2cf98792d978035edd6e6f3100000000000000000000000000000000000000000000000000000000000000000000000000000012101c74dfb80a80fccb9a4022b2406f79f56305e6a7c931d30140f5d372fe793837e93f9ec6b8d89a9d0ab222eeb27547f66b90ec40fbbdd2a4936b0b0c19ca684ff78888fbf5840d7c8dc3c493b139471750938d7d2c443e2d283e6c5ee9fde3765a756542c42f002af45c362b4b5b1687a8fc24cbf16532b903f7bb289728170dcf597f5255508c623ba247735538376f494cdcdd5bd0c4cb067526eeda0f4745a28d8baf8893ecc1b8cee80690538d66455294a028da03ff2add9d8a88e6ee03ba9ffe3ad7d91d6ac9c69a1f28c468f00fe55eba5651a2b32dc2458e0d14b4dd6d0173df255cd56aa01e8e38edec17ea8933f68543cbdc713279d195551d4211bed5c91f77259a695e6768f6c4b110b2158fcc42423a96dcc4e7f6fddb3e2369d00000000000000000000000000000000000000000000000000000000000000") };
1137        let variant = TxEip4844Variant::<BlobTransactionSidecar>::TxEip4844(tx);
1138
1139        let signature = Signature::new(
1140            b256!("6c173c3c8db3e3299f2f728d293b912c12e75243e3aa66911c2329b58434e2a4").into(),
1141            b256!("7dd4d1c228cedc5a414a668ab165d9e888e61e4c3b44cd7daf9cdcc4cec5d6b2").into(),
1142            false,
1143        );
1144
1145        let signed = variant.into_signed(signature);
1146        assert_eq!(
1147            *signed.hash(),
1148            b256!("93fc9daaa0726c3292a2e939df60f7e773c6a6a726a61ce43f4a217c64d85e87")
1149        );
1150    }
1151
1152    #[test]
1153    fn decode_raw_7594_rlp() {
1154        let kzg_settings = EnvKzgSettings::default();
1155        let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("testdata/7594rlp");
1156        let dir = std::fs::read_dir(path).expect("Unable to read folder");
1157        for entry in dir {
1158            let entry = entry.unwrap();
1159            let content = std::fs::read_to_string(entry.path()).unwrap();
1160            let raw = hex::decode(content.trim()).unwrap();
1161            let tx = TxEip4844WithSidecar::<BlobTransactionSidecarVariant>::eip2718_decode(
1162                &mut raw.as_ref(),
1163            )
1164            .map_err(|err| {
1165                panic!("Failed to decode transaction: {:?} {:?}", err, entry.path());
1166            })
1167            .unwrap();
1168
1169            // Test roundtrip
1170            let encoded = tx.encoded_2718();
1171            assert_eq!(encoded.as_slice(), &raw[..], "{:?}", entry.path());
1172
1173            let TxEip4844WithSidecar { tx, sidecar } = tx.tx();
1174            assert_matches!(sidecar, BlobTransactionSidecarVariant::Eip7594(_));
1175
1176            let result = sidecar.validate(&tx.blob_versioned_hashes, kzg_settings.get());
1177            assert_matches!(result, Ok(()));
1178        }
1179    }
1180
1181    #[test]
1182    fn decode_raw_7594_rlp_invalid() {
1183        let kzg_settings = EnvKzgSettings::default();
1184        let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("testdata/7594rlp-invalid");
1185        let dir = std::fs::read_dir(path).expect("Unable to read folder");
1186        for entry in dir {
1187            let entry = entry.unwrap();
1188
1189            if entry.path().file_name().and_then(|f| f.to_str()) == Some("0.rlp") {
1190                continue;
1191            }
1192
1193            let content = std::fs::read_to_string(entry.path()).unwrap();
1194            let raw = hex::decode(content.trim()).unwrap();
1195            let tx = TxEip4844WithSidecar::<BlobTransactionSidecarVariant>::eip2718_decode(
1196                &mut raw.as_ref(),
1197            )
1198            .map_err(|err| {
1199                panic!("Failed to decode transaction: {:?} {:?}", err, entry.path());
1200            })
1201            .unwrap();
1202
1203            // Test roundtrip
1204            let encoded = tx.encoded_2718();
1205            assert_eq!(encoded.as_slice(), &raw[..], "{:?}", entry.path());
1206
1207            let TxEip4844WithSidecar { tx, sidecar } = tx.tx();
1208            assert_matches!(sidecar, BlobTransactionSidecarVariant::Eip7594(_));
1209
1210            let result = sidecar.validate(&tx.blob_versioned_hashes, kzg_settings.get());
1211            assert_matches!(result, Err(_));
1212        }
1213    }
1214}