alloy_eips/eip7594/
sidecar.rs

1use crate::{
2    eip4844::{
3        Blob, BlobAndProofV2, BlobTransactionSidecar, Bytes48, BYTES_PER_BLOB,
4        BYTES_PER_COMMITMENT, BYTES_PER_PROOF,
5    },
6    eip7594::{CELLS_PER_EXT_BLOB, EIP_7594_WRAPPER_VERSION},
7};
8use alloc::{boxed::Box, vec::Vec};
9use alloy_primitives::B256;
10use alloy_rlp::{BufMut, Decodable, Encodable, Header};
11
12use super::{Decodable7594, Encodable7594};
13#[cfg(feature = "kzg")]
14use crate::eip4844::BlobTransactionValidationError;
15use crate::eip4844::VersionedHashIter;
16
17/// This represents a set of blobs, and its corresponding commitments and proofs.
18/// Proof type depends on the sidecar variant.
19///
20/// This type encodes and decodes the fields without an rlp header.
21#[derive(Clone, PartialEq, Eq, Hash, Debug, derive_more::From)]
22#[cfg_attr(feature = "serde", derive(serde::Serialize))]
23#[cfg_attr(feature = "serde", serde(untagged))]
24#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
25pub enum BlobTransactionSidecarVariant {
26    /// EIP-4844 style blob transaction sidecar.
27    Eip4844(BlobTransactionSidecar),
28    /// EIP-7594 style blob transaction sidecar with cell proofs.
29    Eip7594(BlobTransactionSidecarEip7594),
30}
31
32impl BlobTransactionSidecarVariant {
33    /// Returns true if this is a [`BlobTransactionSidecarVariant::Eip4844`].
34    pub const fn is_eip4844(&self) -> bool {
35        matches!(self, Self::Eip4844(_))
36    }
37
38    /// Returns true if this is a [`BlobTransactionSidecarVariant::Eip7594`].
39    pub const fn is_eip7594(&self) -> bool {
40        matches!(self, Self::Eip7594(_))
41    }
42
43    /// Returns the EIP-4844 sidecar if it is [`Self::Eip4844`].
44    pub const fn as_eip4844(&self) -> Option<&BlobTransactionSidecar> {
45        match self {
46            Self::Eip4844(sidecar) => Some(sidecar),
47            _ => None,
48        }
49    }
50
51    /// Returns the EIP-7594 sidecar if it is [`Self::Eip7594`].
52    pub const fn as_eip7594(&self) -> Option<&BlobTransactionSidecarEip7594> {
53        match self {
54            Self::Eip7594(sidecar) => Some(sidecar),
55            _ => None,
56        }
57    }
58
59    /// Converts into EIP-4844 sidecar if it is [`Self::Eip4844`].
60    pub fn into_eip4844(self) -> Option<BlobTransactionSidecar> {
61        match self {
62            Self::Eip4844(sidecar) => Some(sidecar),
63            _ => None,
64        }
65    }
66
67    /// Converts the EIP-7594 sidecar if it is [`Self::Eip7594`].
68    pub fn into_eip7594(self) -> Option<BlobTransactionSidecarEip7594> {
69        match self {
70            Self::Eip7594(sidecar) => Some(sidecar),
71            _ => None,
72        }
73    }
74
75    /// Get a reference to the blobs
76    pub fn blobs(&self) -> &[Blob] {
77        match self {
78            Self::Eip4844(sidecar) => &sidecar.blobs,
79            Self::Eip7594(sidecar) => &sidecar.blobs,
80        }
81    }
82
83    /// Consume self and return the blobs
84    pub fn into_blobs(self) -> Vec<Blob> {
85        match self {
86            Self::Eip4844(sidecar) => sidecar.blobs,
87            Self::Eip7594(sidecar) => sidecar.blobs,
88        }
89    }
90
91    /// Calculates a size heuristic for the in-memory size of the [BlobTransactionSidecarVariant].
92    #[inline]
93    pub fn size(&self) -> usize {
94        match self {
95            Self::Eip4844(sidecar) => sidecar.size(),
96            Self::Eip7594(sidecar) => sidecar.size(),
97        }
98    }
99
100    /// Verifies that the sidecar is valid. See relevant methods for each variant for more info.
101    #[cfg(feature = "kzg")]
102    pub fn validate(
103        &self,
104        blob_versioned_hashes: &[B256],
105        proof_settings: &c_kzg::KzgSettings,
106    ) -> Result<(), BlobTransactionValidationError> {
107        match self {
108            Self::Eip4844(sidecar) => sidecar.validate(blob_versioned_hashes, proof_settings),
109            Self::Eip7594(sidecar) => sidecar.validate(blob_versioned_hashes, proof_settings),
110        }
111    }
112
113    /// Returns the commitments of the sidecar.
114    pub fn commitments(&self) -> &[Bytes48] {
115        match self {
116            Self::Eip4844(sidecar) => &sidecar.commitments,
117            Self::Eip7594(sidecar) => &sidecar.commitments,
118        }
119    }
120
121    /// Returns an iterator over the versioned hashes of the commitments.
122    pub fn versioned_hashes(&self) -> VersionedHashIter<'_> {
123        VersionedHashIter::new(self.commitments())
124    }
125
126    /// Returns the index of the versioned hash in the commitments vector.
127    pub fn versioned_hash_index(&self, hash: &B256) -> Option<usize> {
128        match self {
129            Self::Eip4844(s) => s.versioned_hash_index(hash),
130            Self::Eip7594(s) => s.versioned_hash_index(hash),
131        }
132    }
133
134    /// Returns the blob corresponding to the versioned hash, if it exists.
135    pub fn blob_by_versioned_hash(&self, hash: &B256) -> Option<&Blob> {
136        match self {
137            Self::Eip4844(s) => s.blob_by_versioned_hash(hash),
138            Self::Eip7594(s) => s.blob_by_versioned_hash(hash),
139        }
140    }
141
142    /// Outputs the RLP length of the [BlobTransactionSidecarVariant] fields, without a RLP header.
143    #[doc(hidden)]
144    pub fn rlp_encoded_fields_length(&self) -> usize {
145        match self {
146            Self::Eip4844(sidecar) => sidecar.rlp_encoded_fields_length(),
147            Self::Eip7594(sidecar) => sidecar.rlp_encoded_fields_length(),
148        }
149    }
150
151    /// Returns the [`Self::rlp_encode_fields`] RLP bytes.
152    #[inline]
153    #[doc(hidden)]
154    pub fn rlp_encoded_fields(&self) -> Vec<u8> {
155        let mut buf = Vec::with_capacity(self.rlp_encoded_fields_length());
156        self.rlp_encode_fields(&mut buf);
157        buf
158    }
159
160    /// Encodes the inner [BlobTransactionSidecarVariant] fields as RLP bytes, __without__ a RLP
161    /// header.
162    #[inline]
163    #[doc(hidden)]
164    pub fn rlp_encode_fields(&self, out: &mut dyn BufMut) {
165        match self {
166            Self::Eip4844(sidecar) => sidecar.rlp_encode_fields(out),
167            Self::Eip7594(sidecar) => sidecar.rlp_encode_fields(out),
168        }
169    }
170
171    /// RLP decode the fields of a [BlobTransactionSidecarVariant] based on the wrapper version.
172    #[doc(hidden)]
173    pub fn rlp_decode_fields(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
174        Self::decode_7594(buf)
175    }
176}
177
178impl Encodable for BlobTransactionSidecarVariant {
179    /// Encodes the [BlobTransactionSidecar] fields as RLP bytes, without a RLP header.
180    fn encode(&self, out: &mut dyn BufMut) {
181        match self {
182            Self::Eip4844(sidecar) => sidecar.encode(out),
183            Self::Eip7594(sidecar) => sidecar.encode(out),
184        }
185    }
186
187    fn length(&self) -> usize {
188        match self {
189            Self::Eip4844(sidecar) => sidecar.rlp_encoded_length(),
190            Self::Eip7594(sidecar) => sidecar.rlp_encoded_length(),
191        }
192    }
193}
194
195impl Decodable for BlobTransactionSidecarVariant {
196    /// Decodes the inner [BlobTransactionSidecar] fields from RLP bytes, without a RLP header.
197    fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
198        let header = Header::decode(buf)?;
199        if !header.list {
200            return Err(alloy_rlp::Error::UnexpectedString);
201        }
202        if buf.len() < header.payload_length {
203            return Err(alloy_rlp::Error::InputTooShort);
204        }
205        let remaining = buf.len();
206        let this = Self::rlp_decode_fields(buf)?;
207        if buf.len() + header.payload_length != remaining {
208            return Err(alloy_rlp::Error::UnexpectedLength);
209        }
210
211        Ok(this)
212    }
213}
214
215impl Encodable7594 for BlobTransactionSidecarVariant {
216    fn encode_7594_len(&self) -> usize {
217        self.rlp_encoded_fields_length()
218    }
219
220    fn encode_7594(&self, out: &mut dyn BufMut) {
221        self.rlp_encode_fields(out);
222    }
223}
224
225impl Decodable7594 for BlobTransactionSidecarVariant {
226    fn decode_7594(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
227        if buf.first() == Some(&EIP_7594_WRAPPER_VERSION) {
228            Ok(Self::Eip7594(Decodable7594::decode_7594(buf)?))
229        } else {
230            Ok(Self::Eip4844(Decodable7594::decode_7594(buf)?))
231        }
232    }
233}
234
235#[cfg(feature = "serde")]
236impl<'de> serde::Deserialize<'de> for BlobTransactionSidecarVariant {
237    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
238    where
239        D: serde::Deserializer<'de>,
240    {
241        use core::fmt;
242
243        #[derive(serde::Deserialize, fmt::Debug)]
244        #[serde(field_identifier, rename_all = "camelCase")]
245        enum Field {
246            Blobs,
247            Commitments,
248            Proofs,
249            CellProofs,
250        }
251
252        struct VariantVisitor;
253
254        impl<'de> serde::de::Visitor<'de> for VariantVisitor {
255            type Value = BlobTransactionSidecarVariant;
256
257            fn expecting(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
258                formatter
259                    .write_str("a valid blob transaction sidecar (EIP-4844 or EIP-7594 variant)")
260            }
261
262            fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
263            where
264                M: serde::de::MapAccess<'de>,
265            {
266                let mut blobs = None;
267                let mut commitments = None;
268                let mut proofs = None;
269                let mut cell_proofs = None;
270
271                while let Some(key) = map.next_key()? {
272                    match key {
273                        Field::Blobs => {
274                            blobs = Some(crate::eip4844::deserialize_blobs_map(&mut map)?);
275                        }
276                        Field::Commitments => commitments = Some(map.next_value()?),
277                        Field::Proofs => proofs = Some(map.next_value()?),
278                        Field::CellProofs => cell_proofs = Some(map.next_value()?),
279                    }
280                }
281
282                let blobs = blobs.ok_or_else(|| serde::de::Error::missing_field("blobs"))?;
283                let commitments =
284                    commitments.ok_or_else(|| serde::de::Error::missing_field("commitments"))?;
285
286                match (cell_proofs, proofs) {
287                    (Some(cp), None) => {
288                        Ok(BlobTransactionSidecarVariant::Eip7594(BlobTransactionSidecarEip7594 {
289                            blobs,
290                            commitments,
291                            cell_proofs: cp,
292                        }))
293                    }
294                    (None, Some(pf)) => {
295                        Ok(BlobTransactionSidecarVariant::Eip4844(BlobTransactionSidecar {
296                            blobs,
297                            commitments,
298                            proofs: pf,
299                        }))
300                    }
301                    (None, None) => {
302                        Err(serde::de::Error::custom("Missing 'cellProofs' or 'proofs'"))
303                    }
304                    (Some(_), Some(_)) => Err(serde::de::Error::custom(
305                        "Both 'cellProofs' and 'proofs' cannot be present",
306                    )),
307                }
308            }
309        }
310
311        const FIELDS: &[&str] = &["blobs", "commitments", "proofs", "cellProofs"];
312        deserializer.deserialize_struct("BlobTransactionSidecarVariant", FIELDS, VariantVisitor)
313    }
314}
315
316/// This represents a set of blobs, and its corresponding commitments and cell proofs.
317///
318/// This type encodes and decodes the fields without an rlp header.
319#[derive(Clone, Default, PartialEq, Eq, Hash)]
320#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
321#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
322#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
323pub struct BlobTransactionSidecarEip7594 {
324    /// The blob data.
325    #[cfg_attr(feature = "serde", serde(deserialize_with = "crate::eip4844::deserialize_blobs"))]
326    pub blobs: Vec<Blob>,
327    /// The blob commitments.
328    pub commitments: Vec<Bytes48>,
329    /// List of cell proofs for all blobs in the sidecar, including the proofs for the extension
330    /// indices, for a total of `CELLS_PER_EXT_BLOB` proofs per blob (`CELLS_PER_EXT_BLOB` is the
331    /// number of cells for an extended blob, defined in
332    /// [the consensus specs](https://github.com/ethereum/consensus-specs/tree/9d377fd53d029536e57cfda1a4d2c700c59f86bf/specs/fulu/polynomial-commitments-sampling.md#cells))
333    pub cell_proofs: Vec<Bytes48>,
334}
335
336impl core::fmt::Debug for BlobTransactionSidecarEip7594 {
337    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
338        f.debug_struct("BlobTransactionSidecarEip7594")
339            .field("blobs", &self.blobs.len())
340            .field("commitments", &self.commitments)
341            .field("cell_proofs", &self.cell_proofs)
342            .finish()
343    }
344}
345
346impl BlobTransactionSidecarEip7594 {
347    /// Constructs a new [BlobTransactionSidecarEip7594] from a set of blobs, commitments, and
348    /// cell proofs.
349    pub const fn new(
350        blobs: Vec<Blob>,
351        commitments: Vec<Bytes48>,
352        cell_proofs: Vec<Bytes48>,
353    ) -> Self {
354        Self { blobs, commitments, cell_proofs }
355    }
356
357    /// Calculates a size heuristic for the in-memory size of the [BlobTransactionSidecarEip7594].
358    #[inline]
359    pub fn size(&self) -> usize {
360        self.blobs.len() * BYTES_PER_BLOB + // blobs
361               self.commitments.len() * BYTES_PER_COMMITMENT + // commitments
362               self.cell_proofs.len() * BYTES_PER_PROOF // proofs
363    }
364
365    /// Verifies that the versioned hashes are valid for this sidecar's blob data, commitments, and
366    /// proofs.
367    ///
368    /// Takes as input the [KzgSettings](c_kzg::KzgSettings), which should contain the parameters
369    /// derived from the KZG trusted setup.
370    ///
371    /// This ensures that the blob transaction payload has the expected number of blob data
372    /// elements, commitments, and proofs. The cells are constructed from each blob and verified
373    /// against the commitments and proofs.
374    ///
375    /// Returns [BlobTransactionValidationError::InvalidProof] if any blob KZG proof in the response
376    /// fails to verify, or if the versioned hashes in the transaction do not match the actual
377    /// commitment versioned hashes.
378    #[cfg(feature = "kzg")]
379    pub fn validate(
380        &self,
381        blob_versioned_hashes: &[B256],
382        proof_settings: &c_kzg::KzgSettings,
383    ) -> Result<(), BlobTransactionValidationError> {
384        // Ensure the versioned hashes and commitments have the same length.
385        if blob_versioned_hashes.len() != self.commitments.len() {
386            return Err(c_kzg::Error::MismatchLength(format!(
387                "There are {} versioned commitment hashes and {} commitments",
388                blob_versioned_hashes.len(),
389                self.commitments.len()
390            ))
391            .into());
392        }
393
394        let blobs_len = self.blobs.len();
395        let expected_cell_proofs_len = blobs_len * CELLS_PER_EXT_BLOB;
396        if self.cell_proofs.len() != expected_cell_proofs_len {
397            return Err(c_kzg::Error::MismatchLength(format!(
398                "There are {} cell proofs and {} blobs. Expected {} cell proofs.",
399                self.cell_proofs.len(),
400                blobs_len,
401                expected_cell_proofs_len
402            ))
403            .into());
404        }
405
406        // calculate versioned hashes by zipping & iterating
407        for (versioned_hash, commitment) in
408            blob_versioned_hashes.iter().zip(self.commitments.iter())
409        {
410            // calculate & verify versioned hash
411            let calculated_versioned_hash =
412                crate::eip4844::kzg_to_versioned_hash(commitment.as_slice());
413            if *versioned_hash != calculated_versioned_hash {
414                return Err(BlobTransactionValidationError::WrongVersionedHash {
415                    have: *versioned_hash,
416                    expected: calculated_versioned_hash,
417                });
418            }
419        }
420
421        // Repeat cell ranges for each blob.
422        let cell_indices =
423            Vec::from_iter((0..blobs_len).flat_map(|_| 0..CELLS_PER_EXT_BLOB as u64));
424
425        // Repeat commitments for each cell.
426        let mut commitments = Vec::with_capacity(blobs_len * CELLS_PER_EXT_BLOB);
427        for commitment in &self.commitments {
428            commitments.extend(core::iter::repeat_n(*commitment, CELLS_PER_EXT_BLOB));
429        }
430
431        // SAFETY: ALL types have the same size
432        let res = unsafe {
433            let mut cells = Vec::with_capacity(blobs_len * CELLS_PER_EXT_BLOB);
434            for blob in &self.blobs {
435                let blob = core::mem::transmute::<&Blob, &c_kzg::Blob>(blob);
436                cells.extend(proof_settings.compute_cells(blob)?.into_iter());
437            }
438
439            proof_settings.verify_cell_kzg_proof_batch(
440                // commitments
441                core::mem::transmute::<&[Bytes48], &[c_kzg::Bytes48]>(&commitments),
442                // cell indices
443                &cell_indices,
444                // cells
445                &cells,
446                // proofs
447                core::mem::transmute::<&[Bytes48], &[c_kzg::Bytes48]>(self.cell_proofs.as_slice()),
448            )?
449        };
450
451        res.then_some(()).ok_or(BlobTransactionValidationError::InvalidProof)
452    }
453
454    /// Returns an iterator over the versioned hashes of the commitments.
455    pub fn versioned_hashes(&self) -> VersionedHashIter<'_> {
456        VersionedHashIter::new(&self.commitments)
457    }
458
459    /// Returns the index of the versioned hash in the commitments vector.
460    pub fn versioned_hash_index(&self, hash: &B256) -> Option<usize> {
461        self.commitments.iter().position(|commitment| {
462            crate::eip4844::kzg_to_versioned_hash(commitment.as_slice()) == *hash
463        })
464    }
465
466    /// Returns the blob corresponding to the versioned hash, if it exists.
467    pub fn blob_by_versioned_hash(&self, hash: &B256) -> Option<&Blob> {
468        self.versioned_hash_index(hash).and_then(|index| self.blobs.get(index))
469    }
470
471    /// Matches versioned hashes and returns an iterator of (index, [`BlobAndProofV2`]) pairs
472    /// where index is the position in `versioned_hashes` that matched the versioned hash in the
473    /// sidecar.
474    ///
475    /// This is used for the `engine_getBlobsV2` RPC endpoint of the engine API
476    pub fn match_versioned_hashes<'a>(
477        &'a self,
478        versioned_hashes: &'a [B256],
479    ) -> impl Iterator<Item = (usize, BlobAndProofV2)> + 'a {
480        self.versioned_hashes().enumerate().flat_map(move |(i, blob_versioned_hash)| {
481            versioned_hashes.iter().enumerate().filter_map(move |(j, target_hash)| {
482                if blob_versioned_hash == *target_hash {
483                    let maybe_blob = self.blobs.get(i);
484                    let proof_range = i * CELLS_PER_EXT_BLOB..(i + 1) * CELLS_PER_EXT_BLOB;
485                    let maybe_proofs = Some(&self.cell_proofs[proof_range])
486                        .filter(|proofs| proofs.len() == CELLS_PER_EXT_BLOB);
487                    if let Some((blob, proofs)) = maybe_blob.copied().zip(maybe_proofs) {
488                        return Some((
489                            j,
490                            BlobAndProofV2 { blob: Box::new(blob), proofs: proofs.to_vec() },
491                        ));
492                    }
493                }
494                None
495            })
496        })
497    }
498
499    /// Outputs the RLP length of [BlobTransactionSidecarEip7594] fields without a RLP header.
500    #[doc(hidden)]
501    pub fn rlp_encoded_fields_length(&self) -> usize {
502        // wrapper version + blobs + commitments + cell proofs
503        1 + self.blobs.length() + self.commitments.length() + self.cell_proofs.length()
504    }
505
506    /// Encodes the inner [BlobTransactionSidecarEip7594] fields as RLP bytes, __without__ a
507    /// RLP header.
508    ///
509    /// This encodes the fields in the following order:
510    /// - `wrapper_version`
511    /// - `blobs`
512    /// - `commitments`
513    /// - `cell_proofs`
514    #[inline]
515    #[doc(hidden)]
516    pub fn rlp_encode_fields(&self, out: &mut dyn BufMut) {
517        // Put version byte.
518        out.put_u8(EIP_7594_WRAPPER_VERSION);
519        // Encode the blobs, commitments, and cell proofs
520        self.blobs.encode(out);
521        self.commitments.encode(out);
522        self.cell_proofs.encode(out);
523    }
524
525    /// Creates an RLP header for the [BlobTransactionSidecarEip7594].
526    fn rlp_header(&self) -> Header {
527        Header { list: true, payload_length: self.rlp_encoded_fields_length() }
528    }
529
530    /// Calculates the length of the [BlobTransactionSidecarEip7594] when encoded as
531    /// RLP.
532    pub fn rlp_encoded_length(&self) -> usize {
533        self.rlp_header().length() + self.rlp_encoded_fields_length()
534    }
535
536    /// Encodes the [BlobTransactionSidecarEip7594] as RLP bytes.
537    pub fn rlp_encode(&self, out: &mut dyn BufMut) {
538        self.rlp_header().encode(out);
539        self.rlp_encode_fields(out);
540    }
541
542    /// RLP decode the fields of a [BlobTransactionSidecarEip7594].
543    #[doc(hidden)]
544    pub fn rlp_decode_fields(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
545        Ok(Self {
546            blobs: Decodable::decode(buf)?,
547            commitments: Decodable::decode(buf)?,
548            cell_proofs: Decodable::decode(buf)?,
549        })
550    }
551
552    /// Decodes the [BlobTransactionSidecarEip7594] from RLP bytes.
553    pub fn rlp_decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
554        let header = Header::decode(buf)?;
555        if !header.list {
556            return Err(alloy_rlp::Error::UnexpectedString);
557        }
558        if buf.len() < header.payload_length {
559            return Err(alloy_rlp::Error::InputTooShort);
560        }
561        let remaining = buf.len();
562
563        let this = Self::decode_7594(buf)?;
564        if buf.len() + header.payload_length != remaining {
565            return Err(alloy_rlp::Error::UnexpectedLength);
566        }
567
568        Ok(this)
569    }
570}
571
572impl Encodable for BlobTransactionSidecarEip7594 {
573    /// Encodes the inner [BlobTransactionSidecarEip7594] fields as RLP bytes, without a RLP header.
574    fn encode(&self, out: &mut dyn BufMut) {
575        self.rlp_encode(out);
576    }
577
578    fn length(&self) -> usize {
579        self.rlp_encoded_length()
580    }
581}
582
583impl Decodable for BlobTransactionSidecarEip7594 {
584    /// Decodes the inner [BlobTransactionSidecarEip7594] fields from RLP bytes, without a RLP
585    /// header.
586    fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
587        Self::rlp_decode(buf)
588    }
589}
590
591impl Encodable7594 for BlobTransactionSidecarEip7594 {
592    fn encode_7594_len(&self) -> usize {
593        self.rlp_encoded_fields_length()
594    }
595
596    fn encode_7594(&self, out: &mut dyn BufMut) {
597        self.rlp_encode_fields(out);
598    }
599}
600
601impl Decodable7594 for BlobTransactionSidecarEip7594 {
602    fn decode_7594(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
603        let wrapper_version: u8 = Decodable::decode(buf)?;
604        if wrapper_version != EIP_7594_WRAPPER_VERSION {
605            return Err(alloy_rlp::Error::Custom("invalid wrapper version"));
606        }
607        Self::rlp_decode_fields(buf)
608    }
609}
610
611#[cfg(test)]
612mod tests {
613    use super::*;
614
615    #[test]
616    fn sidecar_variant_rlp_roundtrip() {
617        let mut encoded = Vec::new();
618
619        // 4844
620        let empty_sidecar_4844 =
621            BlobTransactionSidecarVariant::Eip4844(BlobTransactionSidecar::default());
622        empty_sidecar_4844.encode(&mut encoded);
623        assert_eq!(
624            empty_sidecar_4844,
625            BlobTransactionSidecarVariant::decode(&mut &encoded[..]).unwrap()
626        );
627
628        let sidecar_4844 = BlobTransactionSidecarVariant::Eip4844(BlobTransactionSidecar::new(
629            vec![Blob::default()],
630            vec![Bytes48::ZERO],
631            vec![Bytes48::ZERO],
632        ));
633        encoded.clear();
634        sidecar_4844.encode(&mut encoded);
635        assert_eq!(sidecar_4844, BlobTransactionSidecarVariant::decode(&mut &encoded[..]).unwrap());
636
637        // 7594
638        let empty_sidecar_7594 =
639            BlobTransactionSidecarVariant::Eip7594(BlobTransactionSidecarEip7594::default());
640        encoded.clear();
641        empty_sidecar_7594.encode(&mut encoded);
642        assert_eq!(
643            empty_sidecar_7594,
644            BlobTransactionSidecarVariant::decode(&mut &encoded[..]).unwrap()
645        );
646
647        let sidecar_7594 =
648            BlobTransactionSidecarVariant::Eip7594(BlobTransactionSidecarEip7594::new(
649                vec![Blob::default()],
650                vec![Bytes48::ZERO],
651                core::iter::repeat_n(Bytes48::ZERO, CELLS_PER_EXT_BLOB).collect(),
652            ));
653        encoded.clear();
654        sidecar_7594.encode(&mut encoded);
655        assert_eq!(sidecar_7594, BlobTransactionSidecarVariant::decode(&mut &encoded[..]).unwrap());
656    }
657
658    #[test]
659    #[cfg(feature = "serde")]
660    fn sidecar_variant_json_deserialize_sanity() {
661        let mut eip4844 = BlobTransactionSidecar::default();
662        eip4844.blobs.push(Blob::repeat_byte(0x2));
663
664        let json = serde_json::to_string(&eip4844).unwrap();
665        let variant: BlobTransactionSidecarVariant = serde_json::from_str(&json).unwrap();
666        assert!(variant.is_eip4844());
667        let jsonvariant = serde_json::to_string(&variant).unwrap();
668        assert_eq!(json, jsonvariant);
669
670        let mut eip7594 = BlobTransactionSidecarEip7594::default();
671        eip7594.blobs.push(Blob::repeat_byte(0x4));
672        let json = serde_json::to_string(&eip7594).unwrap();
673        let variant: BlobTransactionSidecarVariant = serde_json::from_str(&json).unwrap();
674        assert!(variant.is_eip7594());
675        let jsonvariant = serde_json::to_string(&variant).unwrap();
676        assert_eq!(json, jsonvariant);
677    }
678
679    #[test]
680    fn rlp_7594_roundtrip() {
681        let mut encoded = Vec::new();
682
683        let sidecar_4844 = BlobTransactionSidecar::default();
684        sidecar_4844.encode_7594(&mut encoded);
685        assert_eq!(sidecar_4844, Decodable7594::decode_7594(&mut &encoded[..]).unwrap());
686
687        let sidecar_variant_4844 = BlobTransactionSidecarVariant::Eip4844(sidecar_4844);
688        assert_eq!(sidecar_variant_4844, Decodable7594::decode_7594(&mut &encoded[..]).unwrap());
689        encoded.clear();
690        sidecar_variant_4844.encode_7594(&mut encoded);
691        assert_eq!(sidecar_variant_4844, Decodable7594::decode_7594(&mut &encoded[..]).unwrap());
692
693        let sidecar_7594 = BlobTransactionSidecarEip7594::default();
694        encoded.clear();
695        sidecar_7594.encode_7594(&mut encoded);
696        assert_eq!(sidecar_7594, Decodable7594::decode_7594(&mut &encoded[..]).unwrap());
697
698        let sidecar_variant_7594 = BlobTransactionSidecarVariant::Eip7594(sidecar_7594);
699        assert_eq!(sidecar_variant_7594, Decodable7594::decode_7594(&mut &encoded[..]).unwrap());
700        encoded.clear();
701        sidecar_variant_7594.encode_7594(&mut encoded);
702        assert_eq!(sidecar_variant_7594, Decodable7594::decode_7594(&mut &encoded[..]).unwrap());
703    }
704}