alloy_consensus/transaction/
eip4844.rs

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