Skip to main content

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::{B128, B256};
10use alloy_rlp::{BufMut, Decodable, Encodable, Header};
11
12use super::{Decodable7594, Encodable7594};
13use crate::eip4844::VersionedHashIter;
14#[cfg(feature = "kzg")]
15use crate::eip4844::{AsAlloy, AsCkzg, BlobTransactionValidationError};
16
17/// This represents a set of blobs, and its corresponding commitments and proofs.
18/// Proof type depends on the sidecar variant.
19///
20/// Its [`Encodable`] and [`Decodable`] implementations include an outer RLP list header. The
21/// field-level [`Encodable7594`] and [`Decodable7594`] codecs omit that header.
22#[derive(Clone, PartialEq, Eq, Hash, Debug, derive_more::From)]
23#[cfg_attr(feature = "serde", derive(serde::Serialize))]
24#[cfg_attr(feature = "serde", serde(untagged))]
25#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
26pub enum BlobTransactionSidecarVariant {
27    /// EIP-4844 style blob transaction sidecar.
28    Eip4844(BlobTransactionSidecar),
29    /// EIP-7594 style blob transaction sidecar with cell proofs.
30    Eip7594(BlobTransactionSidecarEip7594),
31}
32
33impl Default for BlobTransactionSidecarVariant {
34    fn default() -> Self {
35        Self::Eip4844(BlobTransactionSidecar::default())
36    }
37}
38
39impl BlobTransactionSidecarVariant {
40    /// Returns true if this is a [`BlobTransactionSidecarVariant::Eip4844`].
41    pub const fn is_eip4844(&self) -> bool {
42        matches!(self, Self::Eip4844(_))
43    }
44
45    /// Returns true if this is a [`BlobTransactionSidecarVariant::Eip7594`].
46    pub const fn is_eip7594(&self) -> bool {
47        matches!(self, Self::Eip7594(_))
48    }
49
50    /// Returns the EIP-4844 sidecar if it is [`Self::Eip4844`].
51    pub const fn as_eip4844(&self) -> Option<&BlobTransactionSidecar> {
52        match self {
53            Self::Eip4844(sidecar) => Some(sidecar),
54            _ => None,
55        }
56    }
57
58    /// Returns the EIP-7594 sidecar if it is [`Self::Eip7594`].
59    pub const fn as_eip7594(&self) -> Option<&BlobTransactionSidecarEip7594> {
60        match self {
61            Self::Eip7594(sidecar) => Some(sidecar),
62            _ => None,
63        }
64    }
65
66    /// Converts into EIP-4844 sidecar if it is [`Self::Eip4844`].
67    pub fn into_eip4844(self) -> Option<BlobTransactionSidecar> {
68        match self {
69            Self::Eip4844(sidecar) => Some(sidecar),
70            _ => None,
71        }
72    }
73
74    /// Converts the EIP-7594 sidecar if it is [`Self::Eip7594`].
75    pub fn into_eip7594(self) -> Option<BlobTransactionSidecarEip7594> {
76        match self {
77            Self::Eip7594(sidecar) => Some(sidecar),
78            _ => None,
79        }
80    }
81
82    /// Get a reference to the blobs
83    pub fn blobs(&self) -> &[Blob] {
84        match self {
85            Self::Eip4844(sidecar) => &sidecar.blobs,
86            Self::Eip7594(sidecar) => &sidecar.blobs,
87        }
88    }
89
90    /// Consume self and return the blobs
91    pub fn into_blobs(self) -> Vec<Blob> {
92        match self {
93            Self::Eip4844(sidecar) => sidecar.blobs,
94            Self::Eip7594(sidecar) => sidecar.blobs,
95        }
96    }
97
98    /// Clears EIP-7594 blob payloads while retaining commitments and cell proofs.
99    ///
100    /// This prepares the sidecar for inclusion in an eth/72 `PooledTransactions` response as
101    /// specified by [EIP-8070]. This has no effect on EIP-4844 sidecars.
102    ///
103    /// [EIP-8070]: https://eips.ethereum.org/EIPS/eip-8070
104    pub fn clear_eip7594_blobs(&mut self) {
105        if let Self::Eip7594(sidecar) = self {
106            sidecar.clear_eip7594_blobs();
107        }
108    }
109
110    /// Calculates a size heuristic for the in-memory size of the [BlobTransactionSidecarVariant].
111    #[inline]
112    pub const fn size(&self) -> usize {
113        match self {
114            Self::Eip4844(sidecar) => sidecar.size(),
115            Self::Eip7594(sidecar) => sidecar.size(),
116        }
117    }
118
119    /// Attempts to convert this sidecar into the EIP-7594 format using default KZG settings.
120    ///
121    /// This method converts an EIP-4844 sidecar to EIP-7594 by computing cell KZG proofs from
122    /// the blob data. If the sidecar is already in EIP-7594 format, it returns itself unchanged.
123    ///
124    /// The conversion requires computing `CELLS_PER_EXT_BLOB` cell proofs for each blob using
125    /// the KZG trusted setup. The default KZG settings are loaded from the environment.
126    ///
127    /// # Returns
128    ///
129    /// - `Ok(Self)` - The sidecar in EIP-7594 format (either converted or unchanged)
130    /// - `Err(c_kzg::Error)` - If KZG proof computation fails
131    ///
132    /// # Examples
133    ///
134    /// ```no_run
135    /// # use alloy_eips::eip7594::BlobTransactionSidecarVariant;
136    /// # use alloy_eips::eip4844::BlobTransactionSidecar;
137    /// # fn example(sidecar: BlobTransactionSidecarVariant) -> Result<(), c_kzg::Error> {
138    /// // Convert an EIP-4844 sidecar to EIP-7594 format
139    /// let eip7594_sidecar = sidecar.try_convert_into_eip7594()?;
140    ///
141    /// // Verify it's now in EIP-7594 format
142    /// assert!(eip7594_sidecar.is_eip7594());
143    /// # Ok(())
144    /// # }
145    /// ```
146    #[cfg(feature = "kzg")]
147    pub fn try_convert_into_eip7594(self) -> Result<Self, c_kzg::Error> {
148        self.try_convert_into_eip7594_with_settings(
149            crate::eip4844::env_settings::EnvKzgSettings::Default.get(),
150        )
151    }
152
153    /// Attempts to convert this sidecar into the EIP-7594 format using custom KZG settings.
154    ///
155    /// This method converts an EIP-4844 sidecar to EIP-7594 by computing cell KZG proofs from
156    /// the blob data using the provided KZG settings. If the sidecar is already in EIP-7594
157    /// format, it returns itself unchanged.
158    ///
159    /// The conversion requires computing `CELLS_PER_EXT_BLOB` cell proofs for each blob using
160    /// the provided KZG trusted setup parameters.
161    ///
162    /// Use this method when you need to specify custom KZG settings rather than using the
163    /// defaults. For most use cases, [`try_convert_into_eip7594`](Self::try_convert_into_eip7594)
164    /// is sufficient.
165    ///
166    /// # Arguments
167    ///
168    /// * `settings` - The KZG settings to use for computing cell proofs
169    ///
170    /// # Returns
171    ///
172    /// - `Ok(Self)` - The sidecar in EIP-7594 format (either converted or unchanged)
173    /// - `Err(c_kzg::Error)` - If KZG proof computation fails
174    ///
175    /// # Examples
176    ///
177    /// ```no_run
178    /// # use alloy_eips::eip7594::BlobTransactionSidecarVariant;
179    /// # use alloy_eips::eip4844::BlobTransactionSidecar;
180    /// # use alloy_eips::eip4844::env_settings::EnvKzgSettings;
181    /// # fn example(sidecar: BlobTransactionSidecarVariant) -> Result<(), c_kzg::Error> {
182    /// // Load custom KZG settings
183    /// let kzg_settings = EnvKzgSettings::Default.get();
184    ///
185    /// // Convert using custom settings
186    /// let eip7594_sidecar = sidecar.try_convert_into_eip7594_with_settings(kzg_settings)?;
187    ///
188    /// // Verify it's now in EIP-7594 format
189    /// assert!(eip7594_sidecar.is_eip7594());
190    /// # Ok(())
191    /// # }
192    /// ```
193    #[cfg(feature = "kzg")]
194    pub fn try_convert_into_eip7594_with_settings(
195        self,
196        settings: &c_kzg::KzgSettings,
197    ) -> Result<Self, c_kzg::Error> {
198        match self {
199            Self::Eip4844(legacy) => legacy.try_into_7594(settings).map(Self::Eip7594),
200            sidecar @ Self::Eip7594(_) => Ok(sidecar),
201        }
202    }
203
204    /// Consumes this sidecar and returns a [`BlobTransactionSidecarEip7594`] using default KZG
205    /// settings.
206    ///
207    /// This method converts an EIP-4844 sidecar to EIP-7594 by computing cell KZG proofs from
208    /// the blob data. If the sidecar is already in EIP-7594 format, it extracts and returns the
209    /// inner [`BlobTransactionSidecarEip7594`].
210    ///
211    /// Unlike [`try_convert_into_eip7594`](Self::try_convert_into_eip7594), this method returns
212    /// the concrete [`BlobTransactionSidecarEip7594`] type rather than the enum variant.
213    ///
214    /// The conversion requires computing `CELLS_PER_EXT_BLOB` cell proofs for each blob using
215    /// the KZG trusted setup. The default KZG settings are loaded from the environment.
216    ///
217    /// # Returns
218    ///
219    /// - `Ok(BlobTransactionSidecarEip7594)` - The sidecar in EIP-7594 format
220    /// - `Err(c_kzg::Error)` - If KZG proof computation fails
221    ///
222    /// # Examples
223    ///
224    /// ```no_run
225    /// # use alloy_eips::eip7594::BlobTransactionSidecarVariant;
226    /// # use alloy_eips::eip4844::BlobTransactionSidecar;
227    /// # fn example(sidecar: BlobTransactionSidecarVariant) -> Result<(), c_kzg::Error> {
228    /// // Convert and extract the EIP-7594 sidecar
229    /// let eip7594_sidecar = sidecar.try_into_eip7594()?;
230    ///
231    /// // Now we have the concrete BlobTransactionSidecarEip7594 type
232    /// assert_eq!(eip7594_sidecar.blobs.len(), eip7594_sidecar.commitments.len());
233    /// # Ok(())
234    /// # }
235    /// ```
236    #[cfg(feature = "kzg")]
237    pub fn try_into_eip7594(self) -> Result<BlobTransactionSidecarEip7594, c_kzg::Error> {
238        self.try_into_eip7594_with_settings(
239            crate::eip4844::env_settings::EnvKzgSettings::Default.get(),
240        )
241    }
242
243    /// Consumes this sidecar and returns a [`BlobTransactionSidecarEip7594`] using custom KZG
244    /// settings.
245    ///
246    /// This method converts an EIP-4844 sidecar to EIP-7594 by computing cell KZG proofs from
247    /// the blob data using the provided KZG settings. If the sidecar is already in EIP-7594
248    /// format, it extracts and returns the inner [`BlobTransactionSidecarEip7594`].
249    ///
250    /// Unlike [`try_convert_into_eip7594_with_settings`](Self::try_convert_into_eip7594_with_settings),
251    /// this method returns the concrete [`BlobTransactionSidecarEip7594`] type rather than the
252    /// enum variant.
253    ///
254    /// The conversion requires computing `CELLS_PER_EXT_BLOB` cell proofs for each blob using
255    /// the provided KZG trusted setup parameters.
256    ///
257    /// Use this method when you need to specify custom KZG settings rather than using the
258    /// defaults. For most use cases, [`try_into_eip7594`](Self::try_into_eip7594) is sufficient.
259    ///
260    /// # Arguments
261    ///
262    /// * `settings` - The KZG settings to use for computing cell proofs
263    ///
264    /// # Returns
265    ///
266    /// - `Ok(BlobTransactionSidecarEip7594)` - The sidecar in EIP-7594 format
267    /// - `Err(c_kzg::Error)` - If KZG proof computation fails
268    ///
269    /// # Examples
270    ///
271    /// ```no_run
272    /// # use alloy_eips::eip7594::BlobTransactionSidecarVariant;
273    /// # use alloy_eips::eip4844::BlobTransactionSidecar;
274    /// # use alloy_eips::eip4844::env_settings::EnvKzgSettings;
275    /// # fn example(sidecar: BlobTransactionSidecarVariant) -> Result<(), c_kzg::Error> {
276    /// // Load custom KZG settings
277    /// let kzg_settings = EnvKzgSettings::Default.get();
278    ///
279    /// // Convert and extract using custom settings
280    /// let eip7594_sidecar = sidecar.try_into_eip7594_with_settings(kzg_settings)?;
281    ///
282    /// // Now we have the concrete BlobTransactionSidecarEip7594 type
283    /// assert_eq!(eip7594_sidecar.blobs.len(), eip7594_sidecar.commitments.len());
284    /// # Ok(())
285    /// # }
286    /// ```
287    #[cfg(feature = "kzg")]
288    pub fn try_into_eip7594_with_settings(
289        self,
290        settings: &c_kzg::KzgSettings,
291    ) -> Result<BlobTransactionSidecarEip7594, c_kzg::Error> {
292        match self {
293            Self::Eip4844(legacy) => legacy.try_into_7594(settings),
294            Self::Eip7594(sidecar) => Ok(sidecar),
295        }
296    }
297
298    /// Verifies that the sidecar is valid. See relevant methods for each variant for more info.
299    #[cfg(feature = "kzg")]
300    pub fn validate(
301        &self,
302        blob_versioned_hashes: &[B256],
303        proof_settings: &c_kzg::KzgSettings,
304    ) -> Result<(), BlobTransactionValidationError> {
305        match self {
306            Self::Eip4844(sidecar) => sidecar.validate(blob_versioned_hashes, proof_settings),
307            Self::Eip7594(sidecar) => sidecar.validate(blob_versioned_hashes, proof_settings),
308        }
309    }
310
311    /// Returns the commitments of the sidecar.
312    pub fn commitments(&self) -> &[Bytes48] {
313        match self {
314            Self::Eip4844(sidecar) => &sidecar.commitments,
315            Self::Eip7594(sidecar) => &sidecar.commitments,
316        }
317    }
318
319    /// Returns an iterator over the versioned hashes of the commitments.
320    pub fn versioned_hashes(&self) -> VersionedHashIter<'_> {
321        VersionedHashIter::new(self.commitments())
322    }
323
324    /// Returns the index of the versioned hash in the commitments vector.
325    pub fn versioned_hash_index(&self, hash: &B256) -> Option<usize> {
326        match self {
327            Self::Eip4844(s) => s.versioned_hash_index(hash),
328            Self::Eip7594(s) => s.versioned_hash_index(hash),
329        }
330    }
331
332    /// Returns the blob corresponding to the versioned hash, if it exists.
333    pub fn blob_by_versioned_hash(&self, hash: &B256) -> Option<&Blob> {
334        match self {
335            Self::Eip4844(s) => s.blob_by_versioned_hash(hash),
336            Self::Eip7594(s) => s.blob_by_versioned_hash(hash),
337        }
338    }
339
340    /// Outputs the RLP length of the [BlobTransactionSidecarVariant] fields, without a RLP header.
341    #[doc(hidden)]
342    pub fn rlp_encoded_fields_length(&self) -> usize {
343        match self {
344            Self::Eip4844(sidecar) => sidecar.rlp_encoded_fields_length(),
345            Self::Eip7594(sidecar) => sidecar.rlp_encoded_fields_length(),
346        }
347    }
348
349    /// Returns the [`Self::rlp_encode_fields`] RLP bytes.
350    #[inline]
351    #[doc(hidden)]
352    pub fn rlp_encoded_fields(&self) -> Vec<u8> {
353        let mut buf = Vec::with_capacity(self.rlp_encoded_fields_length());
354        self.rlp_encode_fields(&mut buf);
355        buf
356    }
357
358    /// Encodes the inner [BlobTransactionSidecarVariant] fields as RLP bytes, __without__ a RLP
359    /// header.
360    #[inline]
361    #[doc(hidden)]
362    pub fn rlp_encode_fields(&self, out: &mut dyn BufMut) {
363        match self {
364            Self::Eip4844(sidecar) => sidecar.rlp_encode_fields(out),
365            Self::Eip7594(sidecar) => sidecar.rlp_encode_fields(out),
366        }
367    }
368
369    /// RLP decode the fields of a [BlobTransactionSidecarVariant] based on the wrapper version.
370    #[doc(hidden)]
371    pub fn rlp_decode_fields(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
372        Self::decode_7594(buf)
373    }
374}
375
376impl Encodable for BlobTransactionSidecarVariant {
377    /// Encodes the selected sidecar as an RLP list, including its outer header.
378    fn encode(&self, out: &mut dyn BufMut) {
379        match self {
380            Self::Eip4844(sidecar) => sidecar.encode(out),
381            Self::Eip7594(sidecar) => sidecar.encode(out),
382        }
383    }
384
385    fn length(&self) -> usize {
386        match self {
387            Self::Eip4844(sidecar) => sidecar.rlp_encoded_length(),
388            Self::Eip7594(sidecar) => sidecar.rlp_encoded_length(),
389        }
390    }
391}
392
393impl Decodable for BlobTransactionSidecarVariant {
394    /// Decodes an RLP list, including its outer header.
395    fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
396        let header = Header::decode(buf)?;
397        if !header.list {
398            return Err(alloy_rlp::Error::UnexpectedString);
399        }
400        if buf.len() < header.payload_length {
401            return Err(alloy_rlp::Error::InputTooShort);
402        }
403        let remaining = buf.len();
404        let this = Self::rlp_decode_fields(buf)?;
405        if buf.len() + header.payload_length != remaining {
406            return Err(alloy_rlp::Error::UnexpectedLength);
407        }
408
409        Ok(this)
410    }
411}
412
413impl Encodable7594 for BlobTransactionSidecarVariant {
414    fn encode_7594_len(&self) -> usize {
415        self.rlp_encoded_fields_length()
416    }
417
418    fn encode_7594(&self, out: &mut dyn BufMut) {
419        self.rlp_encode_fields(out);
420    }
421}
422
423impl Decodable7594 for BlobTransactionSidecarVariant {
424    fn decode_7594(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
425        if buf.first() == Some(&EIP_7594_WRAPPER_VERSION) {
426            Ok(Self::Eip7594(Decodable7594::decode_7594(buf)?))
427        } else {
428            Ok(Self::Eip4844(Decodable7594::decode_7594(buf)?))
429        }
430    }
431}
432
433#[cfg(feature = "kzg")]
434impl TryFrom<BlobTransactionSidecarVariant> for BlobTransactionSidecarEip7594 {
435    type Error = c_kzg::Error;
436
437    fn try_from(value: BlobTransactionSidecarVariant) -> Result<Self, Self::Error> {
438        value.try_into_eip7594()
439    }
440}
441
442#[cfg(feature = "serde")]
443impl<'de> serde::Deserialize<'de> for BlobTransactionSidecarVariant {
444    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
445    where
446        D: serde::Deserializer<'de>,
447    {
448        use core::fmt;
449
450        #[derive(serde::Deserialize, fmt::Debug)]
451        #[serde(field_identifier, rename_all = "camelCase")]
452        enum Field {
453            Blobs,
454            Commitments,
455            Proofs,
456            CellProofs,
457        }
458
459        struct VariantVisitor;
460
461        impl<'de> serde::de::Visitor<'de> for VariantVisitor {
462            type Value = BlobTransactionSidecarVariant;
463
464            fn expecting(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
465                formatter
466                    .write_str("a valid blob transaction sidecar (EIP-4844 or EIP-7594 variant)")
467            }
468
469            fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
470            where
471                M: serde::de::MapAccess<'de>,
472            {
473                let mut blobs = None;
474                let mut commitments = None;
475                let mut proofs = None;
476                let mut cell_proofs = None;
477
478                while let Some(key) = map.next_key()? {
479                    match key {
480                        Field::Blobs => {
481                            blobs = Some(crate::eip4844::deserialize_blobs_map(&mut map)?);
482                        }
483                        Field::Commitments => commitments = Some(map.next_value()?),
484                        Field::Proofs => proofs = Some(map.next_value()?),
485                        Field::CellProofs => cell_proofs = Some(map.next_value()?),
486                    }
487                }
488
489                let blobs = blobs.ok_or_else(|| serde::de::Error::missing_field("blobs"))?;
490                let commitments =
491                    commitments.ok_or_else(|| serde::de::Error::missing_field("commitments"))?;
492
493                match (cell_proofs, proofs) {
494                    (Some(cp), None) => {
495                        Ok(BlobTransactionSidecarVariant::Eip7594(BlobTransactionSidecarEip7594 {
496                            blobs,
497                            commitments,
498                            cell_proofs: cp,
499                        }))
500                    }
501                    (None, Some(pf)) => {
502                        Ok(BlobTransactionSidecarVariant::Eip4844(BlobTransactionSidecar {
503                            blobs,
504                            commitments,
505                            proofs: pf,
506                        }))
507                    }
508                    (None, None) => {
509                        Err(serde::de::Error::custom("Missing 'cellProofs' or 'proofs'"))
510                    }
511                    (Some(_), Some(_)) => Err(serde::de::Error::custom(
512                        "Both 'cellProofs' and 'proofs' cannot be present",
513                    )),
514                }
515            }
516        }
517
518        const FIELDS: &[&str] = &["blobs", "commitments", "proofs", "cellProofs"];
519        deserializer.deserialize_struct("BlobTransactionSidecarVariant", FIELDS, VariantVisitor)
520    }
521}
522
523/// This represents a set of blobs, and its corresponding commitments and cell proofs.
524///
525/// A well-formed sidecar has one commitment per blob and `CELLS_PER_EXT_BLOB` cell proofs per
526/// blob. Public fields and [`Self::new`] do not enforce these cardinalities or validate proofs.
527/// With the `kzg` feature, prefer `try_from_blobs_with_settings` or call `validate` before
528/// use.
529///
530/// Its [`Encodable`] and [`Decodable`] implementations include an outer RLP list header. The
531/// field-level [`Encodable7594`] and [`Decodable7594`] codecs omit that header.
532#[derive(Clone, Default, PartialEq, Eq, Hash)]
533#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
534#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
535#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
536pub struct BlobTransactionSidecarEip7594 {
537    /// The blob data.
538    #[cfg_attr(feature = "serde", serde(deserialize_with = "crate::eip4844::deserialize_blobs"))]
539    pub blobs: Vec<Blob>,
540    /// The blob commitments.
541    pub commitments: Vec<Bytes48>,
542    /// List of cell proofs for all blobs in the sidecar, including the proofs for the extension
543    /// indices, for a total of `CELLS_PER_EXT_BLOB` proofs per blob (`CELLS_PER_EXT_BLOB` is the
544    /// number of cells for an extended blob, defined in
545    /// [the consensus specs](https://github.com/ethereum/consensus-specs/tree/9d377fd53d029536e57cfda1a4d2c700c59f86bf/specs/fulu/polynomial-commitments-sampling.md#cells))
546    pub cell_proofs: Vec<Bytes48>,
547}
548
549impl core::fmt::Debug for BlobTransactionSidecarEip7594 {
550    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
551        f.debug_struct("BlobTransactionSidecarEip7594")
552            .field("blobs", &self.blobs.len())
553            .field("commitments", &self.commitments)
554            .field("cell_proofs", &self.cell_proofs)
555            .finish()
556    }
557}
558
559impl BlobTransactionSidecarEip7594 {
560    /// Constructs a sidecar without validating cardinalities, commitments, or cell proofs.
561    pub const fn new(
562        blobs: Vec<Blob>,
563        commitments: Vec<Bytes48>,
564        cell_proofs: Vec<Bytes48>,
565    ) -> Self {
566        Self { blobs, commitments, cell_proofs }
567    }
568
569    /// Recovers a sidecar from a common set of EIP-7594 cells for every blob.
570    ///
571    /// `cell_mask` identifies the cells supplied for each commitment. `cells` must be flattened in
572    /// blob-major order: all selected cells for `commitments[0]`, followed by all selected cells
573    /// for `commitments[1]`, and so on. At least half of the 128 extended blob cells must be
574    /// selected. The recovered sidecar contains the complete blob data and all 128 cell proofs.
575    ///
576    /// Recovery authenticates each reconstructed blob by recomputing and comparing its
577    /// commitment. It does not verify proofs for the input cells; callers accepting untrusted
578    /// cells and proofs should batch-verify them before recovery when early rejection is useful.
579    ///
580    /// This uses the default KZG settings.
581    #[cfg(feature = "kzg")]
582    pub fn try_recover_from_cells(
583        commitments: Vec<Bytes48>,
584        cell_mask: BlobCellMask,
585        cells: &[crate::eip7594::Cell],
586    ) -> Result<Self, BlobCellRecoveryError> {
587        use crate::eip4844::env_settings::EnvKzgSettings;
588
589        Self::try_recover_from_cells_with_settings(
590            commitments,
591            cell_mask,
592            cells,
593            EnvKzgSettings::Default.get(),
594        )
595    }
596
597    /// Recovers a sidecar from EIP-7594 cells using custom KZG settings.
598    ///
599    /// See [`Self::try_recover_from_cells`] for the expected cell layout and verification
600    /// boundary.
601    #[cfg(feature = "kzg")]
602    pub fn try_recover_from_cells_with_settings(
603        commitments: Vec<Bytes48>,
604        cell_mask: BlobCellMask,
605        cells: &[crate::eip7594::Cell],
606        settings: &c_kzg::KzgSettings,
607    ) -> Result<Self, BlobCellRecoveryError> {
608        let cells_per_blob = cell_mask.count();
609        if !commitments.is_empty() && cells_per_blob < CELLS_PER_EXT_BLOB / 2 {
610            return Err(BlobCellRecoveryError::InsufficientCells {
611                provided: cells_per_blob,
612                required: CELLS_PER_EXT_BLOB / 2,
613            });
614        }
615
616        let expected_cells = commitments
617            .len()
618            .checked_mul(cells_per_blob)
619            .ok_or(BlobCellRecoveryError::CellCountOverflow)?;
620        if cells.len() != expected_cells {
621            return Err(BlobCellRecoveryError::CellCountMismatch {
622                provided: cells.len(),
623                expected: expected_cells,
624            });
625        }
626        if commitments.is_empty() {
627            return Ok(Self::new(Vec::new(), commitments, Vec::new()));
628        }
629
630        let cell_indices =
631            cell_mask.selected_indices().map(|index| index as u64).collect::<Vec<_>>();
632        let mut blobs = Vec::with_capacity(commitments.len());
633        let cell_proof_capacity = commitments
634            .len()
635            .checked_mul(CELLS_PER_EXT_BLOB)
636            .ok_or(BlobCellRecoveryError::CellCountOverflow)?;
637        let mut cell_proofs = Vec::with_capacity(cell_proof_capacity);
638
639        for (blob_index, (blob_cells, expected_commitment)) in
640            cells.chunks_exact(cells_per_blob).zip(&commitments).enumerate()
641        {
642            let ckzg_cells = crate::eip7594::Cell::slice_as_ckzg(blob_cells);
643            let (recovered_cells, recovered_proofs) =
644                settings.recover_cells_and_kzg_proofs(&cell_indices, ckzg_cells)?;
645            let blob = reconstruct_blob(recovered_cells.as_ref());
646
647            let commitment = settings.blob_to_kzg_commitment(blob.as_ckzg())?;
648            let commitment = Bytes48::from_ckzg(commitment.to_bytes());
649            if commitment != *expected_commitment {
650                return Err(BlobCellRecoveryError::CommitmentMismatch { blob_index });
651            }
652
653            blobs.push(blob);
654            cell_proofs
655                .extend_from_slice(c_kzg::KzgProof::slice_as_alloy(recovered_proofs.as_ref()));
656        }
657
658        Ok(Self::new(blobs, commitments, cell_proofs))
659    }
660
661    /// Clears blob payloads while retaining commitments and cell proofs.
662    ///
663    /// This prepares the sidecar for inclusion in an eth/72 `PooledTransactions` response as
664    /// specified by [EIP-8070].
665    ///
666    /// [EIP-8070]: https://eips.ethereum.org/EIPS/eip-8070
667    pub fn clear_eip7594_blobs(&mut self) {
668        self.blobs.clear();
669    }
670
671    /// Calculates a size heuristic for the in-memory size of the [BlobTransactionSidecarEip7594].
672    #[inline]
673    pub const fn size(&self) -> usize {
674        self.blobs.capacity() * BYTES_PER_BLOB
675            + self.commitments.capacity() * BYTES_PER_COMMITMENT
676            + self.cell_proofs.capacity() * BYTES_PER_PROOF
677    }
678
679    /// Shrinks the sidecar vectors to fit their current contents.
680    #[inline]
681    pub fn shrink_to_fit(&mut self) {
682        self.blobs.shrink_to_fit();
683        self.commitments.shrink_to_fit();
684        self.cell_proofs.shrink_to_fit();
685    }
686
687    /// Tries to create a new [`BlobTransactionSidecarEip7594`] from the hex encoded blob str.
688    ///
689    /// See also [`Blob::from_hex`](c_kzg::Blob::from_hex)
690    #[cfg(all(feature = "kzg", any(test, feature = "arbitrary")))]
691    pub fn try_from_blobs_hex<I, B>(blobs: I) -> Result<Self, c_kzg::Error>
692    where
693        I: IntoIterator<Item = B>,
694        B: AsRef<str>,
695    {
696        let mut converted = Vec::new();
697        for blob in blobs {
698            converted.push(crate::eip4844::utils::hex_to_blob(blob)?);
699        }
700        Self::try_from_blobs(converted)
701    }
702
703    /// Tries to create a new [`BlobTransactionSidecarEip7594`] from the given blob
704    /// bytes.
705    ///
706    /// See also [`Blob::from_bytes`](c_kzg::Blob::from_bytes)
707    #[cfg(all(feature = "kzg", any(test, feature = "arbitrary")))]
708    pub fn try_from_blobs_bytes<I, B>(blobs: I) -> Result<Self, c_kzg::Error>
709    where
710        I: IntoIterator<Item = B>,
711        B: AsRef<[u8]>,
712    {
713        let mut converted = Vec::new();
714        for blob in blobs {
715            converted.push(crate::eip4844::utils::bytes_to_blob(blob)?);
716        }
717        Self::try_from_blobs(converted)
718    }
719
720    /// Tries to create a new [`BlobTransactionSidecarEip7594`] from the given
721    /// blobs and KZG settings.
722    #[cfg(feature = "kzg")]
723    pub fn try_from_blobs_with_settings(
724        blobs: Vec<Blob>,
725        settings: &c_kzg::KzgSettings,
726    ) -> Result<Self, c_kzg::Error> {
727        if let [blob] = blobs.as_slice() {
728            let blob = blob.as_ckzg();
729            let commitment = settings.blob_to_kzg_commitment(blob)?;
730            let (_cells, kzg_proofs) = settings.compute_cells_and_kzg_proofs(blob)?;
731            let commitments = vec![Bytes48::from_ckzg(commitment.to_bytes())];
732            let proofs = c_kzg::KzgProof::boxed_slice_as_alloy(kzg_proofs).into();
733            return Ok(Self::new(blobs, commitments, proofs));
734        }
735
736        let mut commitments = Vec::with_capacity(blobs.len());
737        let mut proofs = Vec::with_capacity(blobs.len() * CELLS_PER_EXT_BLOB);
738        for blob in &blobs {
739            let blob = blob.as_ckzg();
740            let commitment = settings.blob_to_kzg_commitment(blob)?;
741            let (_cells, kzg_proofs) = settings.compute_cells_and_kzg_proofs(blob)?;
742
743            commitments.push(Bytes48::from_ckzg(commitment.to_bytes()));
744            proofs.extend_from_slice(c_kzg::KzgProof::slice_as_alloy(kzg_proofs.as_ref()));
745        }
746
747        Ok(Self::new(blobs, commitments, proofs))
748    }
749
750    /// Tries to create a new [`BlobTransactionSidecarEip7594`] from the given
751    /// blobs.
752    ///
753    /// This uses the global/default KZG settings, see also
754    /// [`EnvKzgSettings::Default`](crate::eip4844::env_settings::EnvKzgSettings).
755    #[cfg(feature = "kzg")]
756    pub fn try_from_blobs(blobs: Vec<Blob>) -> Result<Self, c_kzg::Error> {
757        use crate::eip4844::env_settings::EnvKzgSettings;
758
759        Self::try_from_blobs_with_settings(blobs, EnvKzgSettings::Default.get())
760    }
761
762    /// Computes the EIP-7594 cells for all blobs using the default KZG settings.
763    ///
764    /// The returned cells use the same blob-major flattened layout as [`Self::cell_proofs`]:
765    /// every blob contributes one contiguous [`CELLS_PER_EXT_BLOB`]-cell chunk. For blob index
766    /// `i` and cell index `j`, the cell is at `i * CELLS_PER_EXT_BLOB + j`.
767    ///
768    /// In other words, the layout is `[blob0_cell0, ..., blob0_cell127, blob1_cell0, ...]`.
769    #[cfg(feature = "kzg")]
770    pub fn compute_cells(&self) -> Result<Vec<crate::eip7594::Cell>, c_kzg::Error> {
771        use crate::eip4844::env_settings::EnvKzgSettings;
772
773        self.compute_cells_with_settings(EnvKzgSettings::Default.get())
774    }
775
776    /// Computes the EIP-7594 cells for all blobs using the given KZG settings.
777    ///
778    /// The returned cells use the same blob-major flattened layout as [`Self::cell_proofs`]:
779    /// every blob contributes one contiguous [`CELLS_PER_EXT_BLOB`]-cell chunk. For blob index
780    /// `i` and cell index `j`, the cell is at `i * CELLS_PER_EXT_BLOB + j`.
781    ///
782    /// In other words, the layout is `[blob0_cell0, ..., blob0_cell127, blob1_cell0, ...]`.
783    #[cfg(feature = "kzg")]
784    pub fn compute_cells_with_settings(
785        &self,
786        settings: &c_kzg::KzgSettings,
787    ) -> Result<Vec<crate::eip7594::Cell>, c_kzg::Error> {
788        if let [blob] = self.blobs.as_slice() {
789            let blob_cells = settings.compute_cells(blob.as_ckzg())?;
790            return Ok(c_kzg::Cell::boxed_slice_as_alloy(blob_cells).into());
791        }
792
793        let mut cells = Vec::with_capacity(self.blobs.len() * CELLS_PER_EXT_BLOB);
794        for blob in &self.blobs {
795            let blob_cells = settings.compute_cells(blob.as_ckzg())?;
796            cells.extend_from_slice(c_kzg::Cell::slice_as_alloy(blob_cells.as_ref()));
797        }
798        Ok(cells)
799    }
800
801    /// Computes the EIP-7594 cells for all blobs and returns only the cells selected by
802    /// `cell_mask`.
803    ///
804    /// The returned cells keep the blob-major order from [`Self::compute_cells`] but omit cells
805    /// whose indices are not selected by `cell_mask`.
806    ///
807    /// This uses the default KZG settings.
808    #[cfg(feature = "kzg")]
809    pub fn compute_matching_cells(
810        &self,
811        cell_mask: BlobCellMask,
812    ) -> Result<Vec<crate::eip7594::Cell>, c_kzg::Error> {
813        use crate::eip4844::env_settings::EnvKzgSettings;
814
815        self.compute_matching_cells_with_settings(cell_mask, EnvKzgSettings::Default.get())
816    }
817
818    /// Computes the EIP-7594 cells for all blobs with the given KZG settings and returns only the
819    /// cells selected by `cell_mask`.
820    ///
821    /// The returned cells keep the blob-major order from [`Self::compute_cells_with_settings`] but
822    /// omit cells whose indices are not selected by `cell_mask`.
823    #[cfg(feature = "kzg")]
824    pub fn compute_matching_cells_with_settings(
825        &self,
826        cell_mask: BlobCellMask,
827        settings: &c_kzg::KzgSettings,
828    ) -> Result<Vec<crate::eip7594::Cell>, c_kzg::Error> {
829        let cells = self.compute_cells_with_settings(settings)?;
830        Ok(cell_mask
831            .matching_cells_from_computed_cells(&cells)
832            .expect("computed cells must contain full extended blob cell chunks"))
833    }
834
835    /// Verifies that the versioned hashes are valid for this sidecar's blob data, commitments, and
836    /// proofs.
837    ///
838    /// Takes as input the [KzgSettings](c_kzg::KzgSettings), which should contain the parameters
839    /// derived from the KZG trusted setup.
840    ///
841    /// This ensures that the blob transaction payload has the expected number of blob data
842    /// elements, commitments, and proofs. The cells are constructed from each blob and verified
843    /// against the commitments and proofs.
844    ///
845    /// Returns [BlobTransactionValidationError::InvalidProof] if any blob KZG proof in the response
846    /// fails to verify, or if the versioned hashes in the transaction do not match the actual
847    /// commitment versioned hashes.
848    #[cfg(feature = "kzg")]
849    pub fn validate(
850        &self,
851        blob_versioned_hashes: &[B256],
852        proof_settings: &c_kzg::KzgSettings,
853    ) -> Result<(), BlobTransactionValidationError> {
854        // Ensure the versioned hashes and commitments have the same length.
855        if blob_versioned_hashes.len() != self.commitments.len() {
856            return Err(c_kzg::Error::MismatchLength(format!(
857                "There are {} versioned commitment hashes and {} commitments",
858                blob_versioned_hashes.len(),
859                self.commitments.len()
860            ))
861            .into());
862        }
863
864        let blobs_len = self.blobs.len();
865        let expected_cell_proofs_len = blobs_len * CELLS_PER_EXT_BLOB;
866        if self.cell_proofs.len() != expected_cell_proofs_len {
867            return Err(c_kzg::Error::MismatchLength(format!(
868                "There are {} cell proofs and {} blobs. Expected {} cell proofs.",
869                self.cell_proofs.len(),
870                blobs_len,
871                expected_cell_proofs_len
872            ))
873            .into());
874        }
875
876        // calculate versioned hashes by zipping & iterating
877        for (versioned_hash, commitment) in
878            blob_versioned_hashes.iter().zip(self.commitments.iter())
879        {
880            // calculate & verify versioned hash
881            let calculated_versioned_hash =
882                crate::eip4844::kzg_to_versioned_hash(commitment.as_slice());
883            if *versioned_hash != calculated_versioned_hash {
884                return Err(BlobTransactionValidationError::WrongVersionedHash {
885                    have: *versioned_hash,
886                    expected: calculated_versioned_hash,
887                });
888            }
889        }
890
891        // Repeat cell ranges for each blob.
892        let cell_indices =
893            Vec::from_iter((0..blobs_len).flat_map(|_| 0..CELLS_PER_EXT_BLOB as u64));
894
895        // Repeat commitments for each cell.
896        let mut commitments = Vec::with_capacity(blobs_len * CELLS_PER_EXT_BLOB);
897        for commitment in &self.commitments {
898            commitments.extend(core::iter::repeat_n(*commitment, CELLS_PER_EXT_BLOB));
899        }
900
901        let cells = if let [blob] = self.blobs.as_slice() {
902            let cells: Box<[c_kzg::Cell]> = proof_settings.compute_cells(blob.as_ckzg())?;
903            cells.into()
904        } else {
905            let mut cells = Vec::with_capacity(blobs_len * CELLS_PER_EXT_BLOB);
906            for blob in &self.blobs {
907                let blob_cells = proof_settings.compute_cells(blob.as_ckzg())?;
908                cells.extend_from_slice(blob_cells.as_ref());
909            }
910            cells
911        };
912
913        let res = proof_settings.verify_cell_kzg_proof_batch(
914            Bytes48::slice_as_ckzg(&commitments),
915            &cell_indices,
916            &cells,
917            Bytes48::slice_as_ckzg(self.cell_proofs.as_slice()),
918        )?;
919
920        res.then_some(()).ok_or(BlobTransactionValidationError::InvalidProof)
921    }
922
923    /// Returns an iterator over the versioned hashes of the commitments.
924    pub fn versioned_hashes(&self) -> VersionedHashIter<'_> {
925        VersionedHashIter::new(&self.commitments)
926    }
927
928    /// Returns the index of the versioned hash in the commitments vector.
929    pub fn versioned_hash_index(&self, hash: &B256) -> Option<usize> {
930        self.commitments.iter().position(|commitment| {
931            crate::eip4844::kzg_to_versioned_hash(commitment.as_slice()) == *hash
932        })
933    }
934
935    /// Returns the blob corresponding to the versioned hash, if it exists.
936    pub fn blob_by_versioned_hash(&self, hash: &B256) -> Option<&Blob> {
937        self.versioned_hash_index(hash).and_then(|index| self.blobs.get(index))
938    }
939
940    /// Returns the requested cells and proofs for the blob at `blob_index`, if it exists.
941    ///
942    /// This uses the default KZG settings.
943    #[cfg(feature = "kzg")]
944    pub fn blob_cells_and_proofs(
945        &self,
946        blob_index: usize,
947        cell_mask: BlobCellMask,
948    ) -> Result<Option<crate::eip4844::BlobCellsAndProofsV1>, c_kzg::Error> {
949        use crate::eip4844::env_settings::EnvKzgSettings;
950
951        self.blob_cells_and_proofs_with_settings(
952            blob_index,
953            cell_mask,
954            EnvKzgSettings::Default.get(),
955        )
956    }
957
958    /// Returns the requested cells and proofs for the blob at `blob_index`, if it exists.
959    #[cfg(feature = "kzg")]
960    pub fn blob_cells_and_proofs_with_settings(
961        &self,
962        blob_index: usize,
963        cell_mask: BlobCellMask,
964        settings: &c_kzg::KzgSettings,
965    ) -> Result<Option<crate::eip4844::BlobCellsAndProofsV1>, c_kzg::Error> {
966        let Some(blob) = self.blobs.get(blob_index) else { return Ok(None) };
967
968        let proof_start = blob_index * CELLS_PER_EXT_BLOB;
969        let Some(proofs) = self.cell_proofs.get(proof_start..proof_start + CELLS_PER_EXT_BLOB)
970        else {
971            return Ok(None);
972        };
973
974        if cell_mask.count() == 0 {
975            return Ok(Some(crate::eip4844::BlobCellsAndProofsV1::default()));
976        }
977
978        let cells = settings.compute_cells(blob.as_ckzg())?;
979
980        Ok(Some(Self::blob_cells_and_proofs_from_computed_cells(cell_mask, cells.as_ref(), proofs)))
981    }
982
983    /// Returns the requested cells and proofs from precomputed cells.
984    #[cfg(feature = "kzg")]
985    fn blob_cells_and_proofs_from_computed_cells(
986        cell_mask: BlobCellMask,
987        cells: &[c_kzg::Cell],
988        proofs: &[Bytes48],
989    ) -> crate::eip4844::BlobCellsAndProofsV1 {
990        // The response needs two owned vectors, and `count()` exactly matches
991        // `selected_indices()`, so this avoids reallocations while staying simple.
992        let mut blob_cells = Vec::with_capacity(cell_mask.count());
993        let mut selected_proofs = Vec::with_capacity(cell_mask.count());
994        for cell_index in cell_mask.selected_indices() {
995            blob_cells
996                .push(cells.get(cell_index).map(|cell| crate::eip7594::Cell::new(cell.to_bytes())));
997            selected_proofs.push(proofs.get(cell_index).copied());
998        }
999
1000        crate::eip4844::BlobCellsAndProofsV1 { blob_cells, proofs: selected_proofs }
1001    }
1002
1003    /// Matches versioned hashes and returns an iterator of (index, [`BlobAndProofV2`]) pairs
1004    /// where index is the position in `versioned_hashes` that matched the versioned hash in the
1005    /// sidecar.
1006    ///
1007    /// This is used for the `engine_getBlobsV2` RPC endpoint of the engine API
1008    pub fn match_versioned_hashes<'a>(
1009        &'a self,
1010        versioned_hashes: &'a [B256],
1011    ) -> impl Iterator<Item = (usize, BlobAndProofV2)> + 'a {
1012        self.versioned_hashes().enumerate().flat_map(move |(i, blob_versioned_hash)| {
1013            versioned_hashes.iter().enumerate().filter_map(move |(j, target_hash)| {
1014                if blob_versioned_hash == *target_hash {
1015                    let maybe_blob = self.blobs.get(i);
1016                    let proof_range = i * CELLS_PER_EXT_BLOB..(i + 1) * CELLS_PER_EXT_BLOB;
1017                    let maybe_proofs = self
1018                        .cell_proofs
1019                        .get(proof_range)
1020                        .filter(|proofs| proofs.len() == CELLS_PER_EXT_BLOB);
1021                    if let Some((blob, proofs)) = maybe_blob.copied().zip(maybe_proofs) {
1022                        return Some((
1023                            j,
1024                            BlobAndProofV2 { blob: Box::new(blob), proofs: proofs.to_vec() },
1025                        ));
1026                    }
1027                }
1028                None
1029            })
1030        })
1031    }
1032
1033    /// Matches versioned hashes and returns (index, [`crate::eip4844::BlobCellsAndProofsV1`])
1034    /// pairs where index is the position in `versioned_hashes` that matched the versioned hash in
1035    /// the sidecar.
1036    ///
1037    /// This is used for the `engine_getBlobsV4` RPC endpoint of the engine API.
1038    ///
1039    /// This uses the default KZG settings.
1040    #[cfg(feature = "kzg")]
1041    pub fn match_versioned_hashes_cells<'a>(
1042        &'a self,
1043        versioned_hashes: &'a [B256],
1044        cell_mask: BlobCellMask,
1045    ) -> Result<
1046        impl Iterator<Item = (usize, crate::eip4844::BlobCellsAndProofsV1)> + 'a,
1047        c_kzg::Error,
1048    > {
1049        use crate::eip4844::env_settings::EnvKzgSettings;
1050
1051        self.match_versioned_hashes_cells_with_settings(
1052            versioned_hashes,
1053            cell_mask,
1054            EnvKzgSettings::Default.get(),
1055        )
1056    }
1057
1058    /// Matches versioned hashes and returns (index, [`crate::eip4844::BlobCellsAndProofsV1`])
1059    /// pairs where index is the position in `versioned_hashes` that matched the versioned hash in
1060    /// the sidecar.
1061    #[cfg(feature = "kzg")]
1062    pub fn match_versioned_hashes_cells_with_settings<'a>(
1063        &'a self,
1064        versioned_hashes: &'a [B256],
1065        cell_mask: BlobCellMask,
1066        settings: &c_kzg::KzgSettings,
1067    ) -> Result<
1068        impl Iterator<Item = (usize, crate::eip4844::BlobCellsAndProofsV1)> + 'a,
1069        c_kzg::Error,
1070    > {
1071        let mut matches = Vec::new();
1072        let mut cells_and_proofs_by_blob =
1073            Vec::<(usize, crate::eip4844::BlobCellsAndProofsV1)>::new();
1074
1075        for (blob_index, commitment) in self.commitments.iter().enumerate() {
1076            let blob_versioned_hash = crate::eip4844::kzg_to_versioned_hash(commitment.as_slice());
1077            for (matched_index, target_hash) in versioned_hashes.iter().enumerate() {
1078                if blob_versioned_hash != *target_hash {
1079                    continue;
1080                }
1081
1082                let Some(blob) = self.blobs.get(blob_index) else { continue };
1083                let proof_start = blob_index * CELLS_PER_EXT_BLOB;
1084                let Some(proofs) =
1085                    self.cell_proofs.get(proof_start..proof_start + CELLS_PER_EXT_BLOB)
1086                else {
1087                    continue;
1088                };
1089
1090                let cells_and_proofs = if cell_mask.count() == 0 {
1091                    crate::eip4844::BlobCellsAndProofsV1::default()
1092                } else if let Some((_, cells_and_proofs)) =
1093                    cells_and_proofs_by_blob.iter().find(|(index, _)| *index == blob_index)
1094                {
1095                    cells_and_proofs.clone()
1096                } else {
1097                    let cells = settings.compute_cells(blob.as_ckzg())?;
1098                    let cells_and_proofs = Self::blob_cells_and_proofs_from_computed_cells(
1099                        cell_mask,
1100                        cells.as_ref(),
1101                        proofs,
1102                    );
1103                    cells_and_proofs_by_blob.push((blob_index, cells_and_proofs.clone()));
1104                    cells_and_proofs
1105                };
1106
1107                matches.push((matched_index, cells_and_proofs));
1108            }
1109        }
1110
1111        Ok(matches.into_iter())
1112    }
1113
1114    /// Outputs the RLP length of [BlobTransactionSidecarEip7594] fields without a RLP header.
1115    #[doc(hidden)]
1116    pub fn rlp_encoded_fields_length(&self) -> usize {
1117        // wrapper version + blobs + commitments + cell proofs
1118        1 + self.blobs.length() + self.commitments.length() + self.cell_proofs.length()
1119    }
1120
1121    /// Encodes the inner [BlobTransactionSidecarEip7594] fields as RLP bytes, __without__ a
1122    /// RLP header.
1123    ///
1124    /// This encodes the fields in the following order:
1125    /// - `wrapper_version`
1126    /// - `blobs`
1127    /// - `commitments`
1128    /// - `cell_proofs`
1129    #[inline]
1130    #[doc(hidden)]
1131    pub fn rlp_encode_fields(&self, out: &mut dyn BufMut) {
1132        // Put version byte.
1133        out.put_u8(EIP_7594_WRAPPER_VERSION);
1134        // Encode the blobs, commitments, and cell proofs
1135        self.blobs.encode(out);
1136        self.commitments.encode(out);
1137        self.cell_proofs.encode(out);
1138    }
1139
1140    /// Creates an RLP header for the [BlobTransactionSidecarEip7594].
1141    fn rlp_header(&self) -> Header {
1142        Header { list: true, payload_length: self.rlp_encoded_fields_length() }
1143    }
1144
1145    /// Calculates the length of the [BlobTransactionSidecarEip7594] when encoded as
1146    /// RLP.
1147    pub fn rlp_encoded_length(&self) -> usize {
1148        self.rlp_header().length() + self.rlp_encoded_fields_length()
1149    }
1150
1151    /// Encodes the [BlobTransactionSidecarEip7594] as RLP bytes.
1152    pub fn rlp_encode(&self, out: &mut dyn BufMut) {
1153        self.rlp_header().encode(out);
1154        self.rlp_encode_fields(out);
1155    }
1156
1157    /// RLP decode the fields of a [BlobTransactionSidecarEip7594].
1158    #[doc(hidden)]
1159    pub fn rlp_decode_fields(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
1160        Ok(Self {
1161            blobs: Decodable::decode(buf)?,
1162            commitments: Decodable::decode(buf)?,
1163            cell_proofs: Decodable::decode(buf)?,
1164        })
1165    }
1166
1167    /// Decodes the [BlobTransactionSidecarEip7594] from RLP bytes.
1168    pub fn rlp_decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
1169        let header = Header::decode(buf)?;
1170        if !header.list {
1171            return Err(alloy_rlp::Error::UnexpectedString);
1172        }
1173        if buf.len() < header.payload_length {
1174            return Err(alloy_rlp::Error::InputTooShort);
1175        }
1176        let remaining = buf.len();
1177
1178        let this = Self::decode_7594(buf)?;
1179        if buf.len() + header.payload_length != remaining {
1180            return Err(alloy_rlp::Error::UnexpectedLength);
1181        }
1182
1183        Ok(this)
1184    }
1185}
1186
1187/// An error that can occur while recovering blobs from EIP-7594 cells.
1188#[cfg(feature = "kzg")]
1189#[derive(Debug)]
1190pub enum BlobCellRecoveryError {
1191    /// Fewer than half of the extended blob cells were selected.
1192    InsufficientCells {
1193        /// The number of cells supplied for each blob.
1194        provided: usize,
1195        /// The minimum number of cells required for recovery.
1196        required: usize,
1197    },
1198    /// The flattened cell slice does not match the commitments and cell mask.
1199    CellCountMismatch {
1200        /// The number of cells supplied.
1201        provided: usize,
1202        /// The number of cells implied by the commitments and cell mask.
1203        expected: usize,
1204    },
1205    /// The expected cell count cannot be represented as a `usize`.
1206    CellCountOverflow,
1207    /// A reconstructed blob does not match its supplied commitment.
1208    CommitmentMismatch {
1209        /// The index of the blob whose commitment did not match.
1210        blob_index: usize,
1211    },
1212    /// An error returned by [`c_kzg`].
1213    Kzg(c_kzg::Error),
1214}
1215
1216#[cfg(feature = "kzg")]
1217impl core::fmt::Display for BlobCellRecoveryError {
1218    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1219        match self {
1220            Self::InsufficientCells { provided, required } => {
1221                write!(f, "need at least {required} cells per blob for recovery, got {provided}")
1222            }
1223            Self::CellCountMismatch { provided, expected } => {
1224                write!(f, "expected {expected} cells, got {provided}")
1225            }
1226            Self::CellCountOverflow => f.write_str("the expected cell count overflows usize"),
1227            Self::CommitmentMismatch { blob_index } => {
1228                write!(f, "reconstructed blob {blob_index} does not match its commitment")
1229            }
1230            Self::Kzg(err) => write!(f, "KZG error: {err:?}"),
1231        }
1232    }
1233}
1234
1235#[cfg(feature = "kzg")]
1236impl core::error::Error for BlobCellRecoveryError {}
1237
1238#[cfg(feature = "kzg")]
1239impl From<c_kzg::Error> for BlobCellRecoveryError {
1240    fn from(source: c_kzg::Error) -> Self {
1241        Self::Kzg(source)
1242    }
1243}
1244
1245#[cfg(feature = "kzg")]
1246fn reconstruct_blob(recovered_cells: &[c_kzg::Cell; CELLS_PER_EXT_BLOB]) -> Blob {
1247    // `RecoverCells` returns cells in the canonical EIP-7594 order. The first half is the
1248    // original blob data; the remaining cells are the extension used for sampling and proofs.
1249    // The KZG backend performs the recovery, while this wrapper only materializes the original
1250    // blob cells.
1251    let mut blob = [0u8; BYTES_PER_BLOB];
1252    for (cell_index, cell) in recovered_cells.iter().take(CELLS_PER_EXT_BLOB / 2).enumerate() {
1253        let start = cell_index * crate::eip7594::BYTES_PER_CELL;
1254        let end = start + crate::eip7594::BYTES_PER_CELL;
1255        blob[start..end].copy_from_slice(cell.as_alloy().as_slice());
1256    }
1257    Blob::new(blob)
1258}
1259
1260impl Encodable for BlobTransactionSidecarEip7594 {
1261    /// Encodes the sidecar as an RLP list, including its outer header.
1262    fn encode(&self, out: &mut dyn BufMut) {
1263        self.rlp_encode(out);
1264    }
1265
1266    fn length(&self) -> usize {
1267        self.rlp_encoded_length()
1268    }
1269}
1270
1271impl Decodable for BlobTransactionSidecarEip7594 {
1272    /// Decodes an RLP list, including its outer header.
1273    fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
1274        Self::rlp_decode(buf)
1275    }
1276}
1277
1278impl Encodable7594 for BlobTransactionSidecarEip7594 {
1279    fn encode_7594_len(&self) -> usize {
1280        self.rlp_encoded_fields_length()
1281    }
1282
1283    fn encode_7594(&self, out: &mut dyn BufMut) {
1284        self.rlp_encode_fields(out);
1285    }
1286}
1287
1288impl Decodable7594 for BlobTransactionSidecarEip7594 {
1289    fn decode_7594(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
1290        let wrapper_version: u8 = Decodable::decode(buf)?;
1291        if wrapper_version != EIP_7594_WRAPPER_VERSION {
1292            return Err(alloy_rlp::Error::Custom("invalid wrapper version"));
1293        }
1294        Self::rlp_decode_fields(buf)
1295    }
1296}
1297
1298/// Cell indices requested by `engine_getBlobsV4`.
1299#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
1300pub struct BlobCellMask {
1301    value: u128,
1302}
1303
1304impl BlobCellMask {
1305    /// Creates a mask from the Engine API 16-byte, big-endian bitarray.
1306    #[inline]
1307    pub fn new(indices_bitarray: B128) -> Self {
1308        Self { value: u128::from(indices_bitarray) }
1309    }
1310
1311    /// Creates a mask from the raw bit representation.
1312    #[inline]
1313    pub const fn from_bits(value: u128) -> Self {
1314        Self { value }
1315    }
1316
1317    /// Returns the raw bit representation.
1318    #[inline]
1319    pub const fn bits(self) -> u128 {
1320        self.value
1321    }
1322
1323    /// Returns the number of selected cells.
1324    #[inline]
1325    pub const fn count(self) -> usize {
1326        self.value.count_ones() as usize
1327    }
1328
1329    /// Returns true if the given cell index is selected.
1330    #[inline]
1331    pub const fn contains(self, index: usize) -> bool {
1332        index < CELLS_PER_EXT_BLOB && self.value & (1u128 << index) != 0
1333    }
1334
1335    /// Iterates selected cell indices in ascending order.
1336    #[inline]
1337    pub fn selected_indices(self) -> impl Iterator<Item = usize> {
1338        let mut bits = self.value;
1339        core::iter::from_fn(move || {
1340            if bits == 0 {
1341                return None;
1342            }
1343
1344            let index = bits.trailing_zeros() as usize;
1345            bits &= bits - 1;
1346            Some(index)
1347        })
1348    }
1349
1350    /// Returns the selected cells from precomputed blob-major flattened cells.
1351    ///
1352    /// The `cells` slice must use the layout returned by `compute_cells`: each blob contributes
1353    /// one contiguous [`CELLS_PER_EXT_BLOB`]-cell chunk, so `cells.len()` must be evenly divisible
1354    /// by [`CELLS_PER_EXT_BLOB`] (128). This method returns `None` if `cells` ends with an
1355    /// incomplete chunk.
1356    ///
1357    /// The returned cells keep the same chunk order and include only the cell indices selected by
1358    /// this mask.
1359    pub fn matching_cells_from_computed_cells(
1360        self,
1361        cells: &[crate::eip7594::Cell],
1362    ) -> Option<Vec<crate::eip7594::Cell>> {
1363        let (chunks, remainder) = cells.as_chunks::<CELLS_PER_EXT_BLOB>();
1364        if !remainder.is_empty() {
1365            return None;
1366        }
1367
1368        let mut matching_cells = Vec::with_capacity(chunks.len() * self.count());
1369        for blob_cells in chunks {
1370            for cell_index in self.selected_indices() {
1371                let cell = blob_cells
1372                    .get(cell_index)
1373                    .expect("cell mask index must be within extended blob cells");
1374                matching_cells.push(*cell);
1375            }
1376        }
1377
1378        Some(matching_cells)
1379    }
1380}
1381
1382/// Bincode-compatible [`BlobTransactionSidecarVariant`] serde implementation.
1383#[cfg(all(feature = "serde", feature = "serde-bincode-compat"))]
1384pub mod serde_bincode_compat {
1385    use crate::eip4844::{Blob, Bytes48};
1386    use alloc::{borrow::Cow, vec::Vec};
1387    use serde::{Deserialize, Deserializer, Serialize, Serializer};
1388    use serde_with::{DeserializeAs, SerializeAs};
1389
1390    /// Bincode-compatible [`super::BlobTransactionSidecarVariant`] serde implementation.
1391    ///
1392    /// Intended to use with the [`serde_with::serde_as`] macro in the following way:
1393    /// ```rust
1394    /// use alloy_eips::eip7594::{serde_bincode_compat, BlobTransactionSidecarVariant};
1395    /// use serde::{Deserialize, Serialize};
1396    /// use serde_with::serde_as;
1397    ///
1398    /// #[serde_as]
1399    /// #[derive(Serialize, Deserialize)]
1400    /// struct Data {
1401    ///     #[serde_as(as = "serde_bincode_compat::BlobTransactionSidecarVariant")]
1402    ///     sidecar: BlobTransactionSidecarVariant,
1403    /// }
1404    /// ```
1405    #[derive(Debug, Serialize, Deserialize)]
1406    pub struct BlobTransactionSidecarVariant<'a> {
1407        /// The blob data (common to both variants).
1408        pub blobs: Cow<'a, Vec<Blob>>,
1409        /// The blob commitments (common to both variants).
1410        pub commitments: Cow<'a, Vec<Bytes48>>,
1411        /// The blob proofs (EIP-4844 only).
1412        pub proofs: Option<Cow<'a, Vec<Bytes48>>>,
1413        /// The cell proofs (EIP-7594 only).
1414        pub cell_proofs: Option<Cow<'a, Vec<Bytes48>>>,
1415    }
1416
1417    impl<'a> From<&'a super::BlobTransactionSidecarVariant> for BlobTransactionSidecarVariant<'a> {
1418        fn from(value: &'a super::BlobTransactionSidecarVariant) -> Self {
1419            match value {
1420                super::BlobTransactionSidecarVariant::Eip4844(sidecar) => Self {
1421                    blobs: Cow::Borrowed(&sidecar.blobs),
1422                    commitments: Cow::Borrowed(&sidecar.commitments),
1423                    proofs: Some(Cow::Borrowed(&sidecar.proofs)),
1424                    cell_proofs: None,
1425                },
1426                super::BlobTransactionSidecarVariant::Eip7594(sidecar) => Self {
1427                    blobs: Cow::Borrowed(&sidecar.blobs),
1428                    commitments: Cow::Borrowed(&sidecar.commitments),
1429                    proofs: None,
1430                    cell_proofs: Some(Cow::Borrowed(&sidecar.cell_proofs)),
1431                },
1432            }
1433        }
1434    }
1435
1436    impl<'a> BlobTransactionSidecarVariant<'a> {
1437        fn try_into_inner(self) -> Result<super::BlobTransactionSidecarVariant, &'static str> {
1438            match (self.proofs, self.cell_proofs) {
1439                (Some(proofs), None) => Ok(super::BlobTransactionSidecarVariant::Eip4844(
1440                    crate::eip4844::BlobTransactionSidecar {
1441                        blobs: self.blobs.into_owned(),
1442                        commitments: self.commitments.into_owned(),
1443                        proofs: proofs.into_owned(),
1444                    },
1445                )),
1446                (None, Some(cell_proofs)) => Ok(super::BlobTransactionSidecarVariant::Eip7594(
1447                    super::BlobTransactionSidecarEip7594 {
1448                        blobs: self.blobs.into_owned(),
1449                        commitments: self.commitments.into_owned(),
1450                        cell_proofs: cell_proofs.into_owned(),
1451                    },
1452                )),
1453                (None, None) => Err("Missing both 'proofs' and 'cell_proofs'"),
1454                (Some(_), Some(_)) => Err("Both 'proofs' and 'cell_proofs' cannot be present"),
1455            }
1456        }
1457    }
1458
1459    impl<'a> From<BlobTransactionSidecarVariant<'a>> for super::BlobTransactionSidecarVariant {
1460        fn from(value: BlobTransactionSidecarVariant<'a>) -> Self {
1461            value.try_into_inner().expect("Invalid BlobTransactionSidecarVariant")
1462        }
1463    }
1464
1465    impl SerializeAs<super::BlobTransactionSidecarVariant> for BlobTransactionSidecarVariant<'_> {
1466        fn serialize_as<S>(
1467            source: &super::BlobTransactionSidecarVariant,
1468            serializer: S,
1469        ) -> Result<S::Ok, S::Error>
1470        where
1471            S: Serializer,
1472        {
1473            BlobTransactionSidecarVariant::from(source).serialize(serializer)
1474        }
1475    }
1476
1477    impl<'de> DeserializeAs<'de, super::BlobTransactionSidecarVariant>
1478        for BlobTransactionSidecarVariant<'de>
1479    {
1480        fn deserialize_as<D>(
1481            deserializer: D,
1482        ) -> Result<super::BlobTransactionSidecarVariant, D::Error>
1483        where
1484            D: Deserializer<'de>,
1485        {
1486            let value = BlobTransactionSidecarVariant::deserialize(deserializer)?;
1487            value.try_into_inner().map_err(serde::de::Error::custom)
1488        }
1489    }
1490}
1491
1492#[cfg(test)]
1493mod tests {
1494    use super::*;
1495    #[cfg(feature = "kzg")]
1496    use crate::eip4844::{
1497        builder::{SidecarBuilder, SimpleCoder},
1498        env_settings::EnvKzgSettings,
1499    };
1500
1501    #[test]
1502    fn clear_eip7594_blobs_preserves_metadata() {
1503        let commitments = vec![Bytes48::repeat_byte(0x01)];
1504        let cell_proofs = vec![Bytes48::repeat_byte(0x02); CELLS_PER_EXT_BLOB];
1505        let sidecar = BlobTransactionSidecarEip7594::new(
1506            vec![Blob::repeat_byte(0x03)],
1507            commitments.clone(),
1508            cell_proofs.clone(),
1509        );
1510        let mut variant = BlobTransactionSidecarVariant::Eip7594(sidecar);
1511
1512        variant.clear_eip7594_blobs();
1513
1514        let sidecar = variant.as_eip7594().unwrap();
1515        assert!(sidecar.blobs.is_empty());
1516        assert_eq!(sidecar.commitments, commitments);
1517        assert_eq!(sidecar.cell_proofs, cell_proofs);
1518    }
1519
1520    #[test]
1521    fn clear_eip7594_blobs_ignores_eip4844_variant() {
1522        let sidecar = BlobTransactionSidecar::new(
1523            vec![Blob::repeat_byte(0x01)],
1524            vec![Bytes48::repeat_byte(0x02)],
1525            vec![Bytes48::repeat_byte(0x03)],
1526        );
1527        let mut variant = BlobTransactionSidecarVariant::Eip4844(sidecar.clone());
1528
1529        variant.clear_eip7594_blobs();
1530
1531        assert_eq!(variant, BlobTransactionSidecarVariant::Eip4844(sidecar));
1532    }
1533
1534    #[test]
1535    fn sidecar_variant_rlp_roundtrip() {
1536        let mut encoded = Vec::new();
1537
1538        // 4844
1539        let empty_sidecar_4844 =
1540            BlobTransactionSidecarVariant::Eip4844(BlobTransactionSidecar::default());
1541        empty_sidecar_4844.encode(&mut encoded);
1542        assert_eq!(
1543            empty_sidecar_4844,
1544            BlobTransactionSidecarVariant::decode(&mut &encoded[..]).unwrap()
1545        );
1546
1547        let sidecar_4844 = BlobTransactionSidecarVariant::Eip4844(BlobTransactionSidecar::new(
1548            vec![Blob::default()],
1549            vec![Bytes48::ZERO],
1550            vec![Bytes48::ZERO],
1551        ));
1552        encoded.clear();
1553        sidecar_4844.encode(&mut encoded);
1554        assert_eq!(sidecar_4844, BlobTransactionSidecarVariant::decode(&mut &encoded[..]).unwrap());
1555
1556        // 7594
1557        let empty_sidecar_7594 =
1558            BlobTransactionSidecarVariant::Eip7594(BlobTransactionSidecarEip7594::default());
1559        encoded.clear();
1560        empty_sidecar_7594.encode(&mut encoded);
1561        assert_eq!(
1562            empty_sidecar_7594,
1563            BlobTransactionSidecarVariant::decode(&mut &encoded[..]).unwrap()
1564        );
1565
1566        let sidecar_7594 =
1567            BlobTransactionSidecarVariant::Eip7594(BlobTransactionSidecarEip7594::new(
1568                vec![Blob::default()],
1569                vec![Bytes48::ZERO],
1570                core::iter::repeat_n(Bytes48::ZERO, CELLS_PER_EXT_BLOB).collect(),
1571            ));
1572        encoded.clear();
1573        sidecar_7594.encode(&mut encoded);
1574        assert_eq!(sidecar_7594, BlobTransactionSidecarVariant::decode(&mut &encoded[..]).unwrap());
1575    }
1576
1577    #[test]
1578    #[cfg(feature = "serde")]
1579    fn sidecar_variant_json_deserialize_sanity() {
1580        let mut eip4844 = BlobTransactionSidecar::default();
1581        eip4844.blobs.push(Blob::repeat_byte(0x2));
1582
1583        let json = serde_json::to_string(&eip4844).unwrap();
1584        let variant: BlobTransactionSidecarVariant = serde_json::from_str(&json).unwrap();
1585        assert!(variant.is_eip4844());
1586        let jsonvariant = serde_json::to_string(&variant).unwrap();
1587        assert_eq!(json, jsonvariant);
1588
1589        let mut eip7594 = BlobTransactionSidecarEip7594::default();
1590        eip7594.blobs.push(Blob::repeat_byte(0x4));
1591        let json = serde_json::to_string(&eip7594).unwrap();
1592        let variant: BlobTransactionSidecarVariant = serde_json::from_str(&json).unwrap();
1593        assert!(variant.is_eip7594());
1594        let jsonvariant = serde_json::to_string(&variant).unwrap();
1595        assert_eq!(json, jsonvariant);
1596    }
1597
1598    #[test]
1599    fn rlp_7594_roundtrip() {
1600        let mut encoded = Vec::new();
1601
1602        let sidecar_4844 = BlobTransactionSidecar::default();
1603        sidecar_4844.encode_7594(&mut encoded);
1604        assert_eq!(sidecar_4844, Decodable7594::decode_7594(&mut &encoded[..]).unwrap());
1605
1606        let sidecar_variant_4844 = BlobTransactionSidecarVariant::Eip4844(sidecar_4844);
1607        assert_eq!(sidecar_variant_4844, Decodable7594::decode_7594(&mut &encoded[..]).unwrap());
1608        encoded.clear();
1609        sidecar_variant_4844.encode_7594(&mut encoded);
1610        assert_eq!(sidecar_variant_4844, Decodable7594::decode_7594(&mut &encoded[..]).unwrap());
1611
1612        let sidecar_7594 = BlobTransactionSidecarEip7594::default();
1613        encoded.clear();
1614        sidecar_7594.encode_7594(&mut encoded);
1615        assert_eq!(sidecar_7594, Decodable7594::decode_7594(&mut &encoded[..]).unwrap());
1616
1617        let sidecar_variant_7594 = BlobTransactionSidecarVariant::Eip7594(sidecar_7594);
1618        assert_eq!(sidecar_variant_7594, Decodable7594::decode_7594(&mut &encoded[..]).unwrap());
1619        encoded.clear();
1620        sidecar_variant_7594.encode_7594(&mut encoded);
1621        assert_eq!(sidecar_variant_7594, Decodable7594::decode_7594(&mut &encoded[..]).unwrap());
1622    }
1623
1624    #[test]
1625    #[cfg(feature = "kzg")]
1626    fn validate_7594_sidecar() {
1627        let sidecar =
1628            SidecarBuilder::<SimpleCoder>::from_slice(b"Blobs are fun!").build_7594().unwrap();
1629        let versioned_hashes = sidecar.versioned_hashes().collect::<Vec<_>>();
1630
1631        sidecar.validate(&versioned_hashes, EnvKzgSettings::Default.get()).unwrap();
1632    }
1633
1634    #[test]
1635    #[cfg(feature = "kzg")]
1636    fn compute_cells_for_7594_sidecar() {
1637        let settings = EnvKzgSettings::Default.get();
1638        let sidecar = BlobTransactionSidecarEip7594::try_from_blobs_with_settings(
1639            vec![Blob::repeat_byte(0x01), Blob::repeat_byte(0x02)],
1640            settings,
1641        )
1642        .unwrap();
1643
1644        let cells = sidecar.compute_cells_with_settings(settings).unwrap();
1645        assert_eq!(cells.len(), sidecar.blobs.len() * CELLS_PER_EXT_BLOB);
1646        assert_eq!(sidecar.compute_cells().unwrap(), cells);
1647
1648        let cell_mask = BlobCellMask::from_bits((1u128 << 0) | (1u128 << 7));
1649        let matching_cells =
1650            sidecar.compute_matching_cells_with_settings(cell_mask, settings).unwrap();
1651        let expected_matching_cells = cells
1652            .as_chunks::<CELLS_PER_EXT_BLOB>()
1653            .0
1654            .iter()
1655            .flat_map(|blob_cells| [blob_cells[0], blob_cells[7]])
1656            .collect::<Vec<_>>();
1657        assert_eq!(
1658            cell_mask.matching_cells_from_computed_cells(&cells),
1659            Some(expected_matching_cells.clone())
1660        );
1661        assert_eq!(matching_cells, expected_matching_cells);
1662        assert_eq!(sidecar.compute_matching_cells(cell_mask).unwrap(), expected_matching_cells);
1663        assert!(sidecar.compute_matching_cells(BlobCellMask::default()).unwrap().is_empty());
1664
1665        for (blob_index, blob) in sidecar.blobs.iter().enumerate() {
1666            let expected_cells = settings.compute_cells(blob.as_ckzg()).unwrap();
1667            let start = blob_index * CELLS_PER_EXT_BLOB;
1668            let end = start + CELLS_PER_EXT_BLOB;
1669
1670            for (cell, expected_cell) in cells[start..end].iter().zip(expected_cells.iter()) {
1671                assert_eq!(*cell, crate::eip7594::Cell::new(expected_cell.to_bytes()));
1672            }
1673        }
1674    }
1675
1676    #[cfg(feature = "kzg")]
1677    fn sparse_cells_for_mask(
1678        sidecar: &BlobTransactionSidecarEip7594,
1679        cell_mask: BlobCellMask,
1680        settings: &c_kzg::KzgSettings,
1681    ) -> Vec<crate::eip7594::Cell> {
1682        let cells = sidecar.compute_cells_with_settings(settings).unwrap();
1683        cell_mask.matching_cells_from_computed_cells(&cells).unwrap()
1684    }
1685
1686    #[test]
1687    #[cfg(feature = "kzg")]
1688    fn recover_sidecar_from_complete_cells() {
1689        let settings = EnvKzgSettings::Default.get();
1690        assert_eq!(
1691            BlobTransactionSidecarEip7594::try_recover_from_cells_with_settings(
1692                Vec::new(),
1693                BlobCellMask::default(),
1694                &[],
1695                settings,
1696            )
1697            .unwrap(),
1698            BlobTransactionSidecarEip7594::default()
1699        );
1700
1701        let sidecar = BlobTransactionSidecarEip7594::try_from_blobs_with_settings(
1702            vec![Blob::repeat_byte(0x01), Blob::repeat_byte(0x02)],
1703            settings,
1704        )
1705        .unwrap();
1706        let cells = sidecar.compute_cells_with_settings(settings).unwrap();
1707        let cell_mask = BlobCellMask::from_bits(u128::MAX);
1708
1709        let recovered = BlobTransactionSidecarEip7594::try_recover_from_cells_with_settings(
1710            sidecar.commitments.clone(),
1711            cell_mask,
1712            &cells,
1713            settings,
1714        )
1715        .unwrap();
1716        assert_eq!(recovered, sidecar);
1717
1718        assert_eq!(
1719            BlobTransactionSidecarEip7594::try_recover_from_cells(
1720                recovered.commitments.clone(),
1721                cell_mask,
1722                &cells,
1723            )
1724            .unwrap(),
1725            recovered
1726        );
1727    }
1728
1729    /// Multiple blobs are recovered from a shared, non-contiguous set containing the minimum
1730    /// number of cells required for each blob.
1731    #[test]
1732    #[cfg(feature = "kzg")]
1733    fn recover_sparse_blobs_from_minimum_cells() {
1734        let settings = EnvKzgSettings::Default.get();
1735        let sidecar = BlobTransactionSidecarEip7594::try_from_blobs_with_settings(
1736            vec![Blob::repeat_byte(0x01), Blob::repeat_byte(0x02), Blob::repeat_byte(0x03)],
1737            settings,
1738        )
1739        .unwrap();
1740
1741        let cell_mask = BlobCellMask::from_bits(
1742            (0..CELLS_PER_EXT_BLOB).step_by(2).fold(0, |mask, index| mask | (1u128 << index)),
1743        );
1744        assert_eq!(cell_mask.count(), CELLS_PER_EXT_BLOB / 2);
1745        let sparse_cells = sparse_cells_for_mask(&sidecar, cell_mask, settings);
1746
1747        let recovered = BlobTransactionSidecarEip7594::try_recover_from_cells_with_settings(
1748            sidecar.commitments.clone(),
1749            cell_mask,
1750            &sparse_cells,
1751            settings,
1752        )
1753        .unwrap();
1754
1755        assert_eq!(recovered.blobs, sidecar.blobs);
1756        assert_eq!(recovered.commitments, sidecar.commitments);
1757        assert_eq!(recovered.cell_proofs, sidecar.cell_proofs);
1758    }
1759
1760    /// A sparse set above the minimum cell count follows the same recovery path.
1761    #[test]
1762    #[cfg(feature = "kzg")]
1763    fn recover_sparse_blobs_with_more_than_minimum_cells() {
1764        let settings = EnvKzgSettings::Default.get();
1765        let sidecar = BlobTransactionSidecarEip7594::try_from_blobs_with_settings(
1766            vec![Blob::repeat_byte(0x01), Blob::repeat_byte(0x02)],
1767            settings,
1768        )
1769        .unwrap();
1770
1771        let cell_mask = BlobCellMask::from_bits(
1772            ((1u128 << (CELLS_PER_EXT_BLOB / 2)) - 1) | (1u128 << (CELLS_PER_EXT_BLOB - 1)),
1773        );
1774        assert_eq!(cell_mask.count(), CELLS_PER_EXT_BLOB / 2 + 1);
1775        let sparse_cells = sparse_cells_for_mask(&sidecar, cell_mask, settings);
1776
1777        let recovered = BlobTransactionSidecarEip7594::try_recover_from_cells_with_settings(
1778            sidecar.commitments.clone(),
1779            cell_mask,
1780            &sparse_cells,
1781            settings,
1782        )
1783        .unwrap();
1784
1785        assert_eq!(recovered.blobs, sidecar.blobs);
1786        assert_eq!(recovered.cell_proofs, sidecar.cell_proofs);
1787    }
1788
1789    #[test]
1790    #[cfg(feature = "kzg")]
1791    fn recover_sparse_blobs_rejects_insufficient_cells() {
1792        let settings = EnvKzgSettings::Default.get();
1793        let cell_mask = BlobCellMask::from_bits((1u128 << (CELLS_PER_EXT_BLOB / 2 - 1)) - 1);
1794        let cells = vec![crate::eip7594::Cell::repeat_byte(0); cell_mask.count()];
1795
1796        let err = BlobTransactionSidecarEip7594::try_recover_from_cells_with_settings(
1797            vec![Bytes48::ZERO],
1798            cell_mask,
1799            &cells,
1800            settings,
1801        )
1802        .unwrap_err();
1803        assert!(matches!(
1804            err,
1805            BlobCellRecoveryError::InsufficientCells {
1806                provided,
1807                required,
1808            } if provided == CELLS_PER_EXT_BLOB / 2 - 1
1809                && required == CELLS_PER_EXT_BLOB / 2
1810        ));
1811    }
1812
1813    #[test]
1814    #[cfg(feature = "kzg")]
1815    fn recover_sparse_blobs_rejects_mismatched_cell_count() {
1816        let settings = EnvKzgSettings::Default.get();
1817        let cell_mask = BlobCellMask::from_bits((1u128 << (CELLS_PER_EXT_BLOB / 2)) - 1);
1818        let cells = vec![crate::eip7594::Cell::repeat_byte(0); cell_mask.count() - 1];
1819
1820        let err = BlobTransactionSidecarEip7594::try_recover_from_cells_with_settings(
1821            vec![Bytes48::ZERO, Bytes48::ZERO],
1822            cell_mask,
1823            &cells,
1824            settings,
1825        )
1826        .unwrap_err();
1827        assert!(matches!(
1828            err,
1829            BlobCellRecoveryError::CellCountMismatch {
1830                provided,
1831                expected,
1832            } if provided == CELLS_PER_EXT_BLOB / 2 - 1 && expected == CELLS_PER_EXT_BLOB
1833        ));
1834    }
1835
1836    #[test]
1837    #[cfg(feature = "kzg")]
1838    fn recover_sparse_blobs_rejects_commitment_mismatch() {
1839        let settings = EnvKzgSettings::Default.get();
1840        let sidecar = BlobTransactionSidecarEip7594::try_from_blobs_with_settings(
1841            vec![Blob::repeat_byte(0x01)],
1842            settings,
1843        )
1844        .unwrap();
1845        let cell_mask = BlobCellMask::from_bits((1u128 << (CELLS_PER_EXT_BLOB / 2)) - 1);
1846        let sparse_cells = sparse_cells_for_mask(&sidecar, cell_mask, settings);
1847
1848        let err = BlobTransactionSidecarEip7594::try_recover_from_cells_with_settings(
1849            vec![Bytes48::ZERO],
1850            cell_mask,
1851            &sparse_cells,
1852            settings,
1853        )
1854        .unwrap_err();
1855        assert!(matches!(err, BlobCellRecoveryError::CommitmentMismatch { blob_index: 0 }));
1856    }
1857
1858    /// A cell that no longer matches the commitment must not produce a sidecar.
1859    #[test]
1860    #[cfg(feature = "kzg")]
1861    fn recover_sparse_blobs_rejects_corrupted_cells() {
1862        let settings = EnvKzgSettings::Default.get();
1863        let sidecar = BlobTransactionSidecarEip7594::try_from_blobs_with_settings(
1864            vec![Blob::repeat_byte(0x01), Blob::repeat_byte(0x02), Blob::repeat_byte(0x03)],
1865            settings,
1866        )
1867        .unwrap();
1868        let cell_mask = BlobCellMask::from_bits((1u128 << (CELLS_PER_EXT_BLOB / 2)) - 1);
1869        let mut sparse_cells = sparse_cells_for_mask(&sidecar, cell_mask, settings);
1870        sparse_cells[0][0] ^= 0xff;
1871
1872        assert!(BlobTransactionSidecarEip7594::try_recover_from_cells_with_settings(
1873            sidecar.commitments,
1874            cell_mask,
1875            &sparse_cells,
1876            settings,
1877        )
1878        .is_err());
1879    }
1880
1881    #[test]
1882    fn blob_cell_mask_selects_indices() {
1883        let selected = (1u128 << 0) | (1u128 << 7);
1884        let mask = BlobCellMask::new(B128::from(selected));
1885
1886        assert_eq!(mask.bits(), selected);
1887        assert_eq!(mask.count(), 2);
1888        assert!(mask.contains(0));
1889        assert!(mask.contains(7));
1890        assert!(!mask.contains(1));
1891        assert_eq!(mask.selected_indices().collect::<Vec<_>>(), vec![0, 7]);
1892
1893        let cells = (0..CELLS_PER_EXT_BLOB * 2)
1894            .map(|i| crate::eip7594::Cell::repeat_byte(i as u8))
1895            .collect::<Vec<_>>();
1896        assert_eq!(
1897            mask.matching_cells_from_computed_cells(&cells),
1898            Some(vec![
1899                cells[0],
1900                cells[7],
1901                cells[CELLS_PER_EXT_BLOB],
1902                cells[CELLS_PER_EXT_BLOB + 7]
1903            ])
1904        );
1905        assert_eq!(mask.matching_cells_from_computed_cells(&cells[..cells.len() - 1]), None);
1906    }
1907
1908    #[test]
1909    fn match_versioned_hashes_skips_incomplete_proof_chunks() {
1910        let sidecar = BlobTransactionSidecarEip7594::new(
1911            vec![Blob::repeat_byte(0x01)],
1912            vec![Bytes48::repeat_byte(0x02)],
1913            vec![Bytes48::repeat_byte(0x03)],
1914        );
1915        let versioned_hash = sidecar.versioned_hashes().next().unwrap();
1916
1917        let matches = sidecar.match_versioned_hashes(&[versioned_hash]).collect::<Vec<_>>();
1918        assert!(matches.is_empty());
1919    }
1920
1921    #[test]
1922    #[cfg(feature = "kzg")]
1923    fn match_versioned_hashes_cells_for_7594_sidecar() {
1924        let settings = EnvKzgSettings::Default.get();
1925        let sidecar = BlobTransactionSidecarEip7594::try_from_blobs_with_settings(
1926            vec![Blob::repeat_byte(0x01), Blob::repeat_byte(0x02)],
1927            settings,
1928        )
1929        .unwrap();
1930        let versioned_hashes = sidecar.versioned_hashes().collect::<Vec<_>>();
1931        let cell_mask = BlobCellMask::from_bits((1u128 << 0) | (1u128 << 7));
1932
1933        let cells_and_proofs =
1934            sidecar.blob_cells_and_proofs_with_settings(0, cell_mask, settings).unwrap().unwrap();
1935        assert_eq!(cells_and_proofs.blob_cells.len(), 2);
1936        assert_eq!(cells_and_proofs.proofs.len(), 2);
1937        assert_eq!(
1938            cells_and_proofs.proofs,
1939            vec![Some(sidecar.cell_proofs[0]), Some(sidecar.cell_proofs[7])]
1940        );
1941
1942        let expected_cells = settings.compute_cells(sidecar.blobs[0].as_ckzg()).unwrap();
1943        assert_eq!(
1944            cells_and_proofs.blob_cells,
1945            vec![
1946                Some(crate::eip7594::Cell::new(expected_cells[0].to_bytes())),
1947                Some(crate::eip7594::Cell::new(expected_cells[7].to_bytes()))
1948            ]
1949        );
1950
1951        let request = vec![versioned_hashes[0], B256::ZERO, versioned_hashes[0]];
1952        let matches = sidecar
1953            .match_versioned_hashes_cells_with_settings(&request, cell_mask, settings)
1954            .unwrap()
1955            .collect::<Vec<_>>();
1956        assert_eq!(matches.len(), 2);
1957        assert_eq!(matches[0], (0, cells_and_proofs.clone()));
1958        assert_eq!(matches[1], (2, cells_and_proofs.clone()));
1959
1960        let default_matches = sidecar
1961            .match_versioned_hashes_cells(&[versioned_hashes[0]], cell_mask)
1962            .unwrap()
1963            .collect::<Vec<_>>();
1964        assert_eq!(default_matches, vec![(0, cells_and_proofs)]);
1965    }
1966
1967    #[test]
1968    #[cfg(feature = "kzg")]
1969    fn match_versioned_hashes_cells_only_computes_matched_blobs() {
1970        let settings = EnvKzgSettings::Default.get();
1971        let mut sidecar = BlobTransactionSidecarEip7594::try_from_blobs_with_settings(
1972            vec![Blob::repeat_byte(0x01)],
1973            settings,
1974        )
1975        .unwrap();
1976        let versioned_hash = sidecar.versioned_hashes().next().unwrap();
1977        let cell_mask = BlobCellMask::from_bits(1);
1978
1979        let invalid_blob = Blob::repeat_byte(0xff);
1980        assert!(settings.compute_cells(invalid_blob.as_ckzg()).is_err());
1981
1982        sidecar.blobs.push(invalid_blob);
1983        sidecar.commitments.push(Bytes48::ZERO);
1984        sidecar.cell_proofs.extend(core::iter::repeat_n(Bytes48::ZERO, CELLS_PER_EXT_BLOB));
1985
1986        let cells_and_proofs =
1987            sidecar.blob_cells_and_proofs_with_settings(0, cell_mask, settings).unwrap().unwrap();
1988        let matches = sidecar
1989            .match_versioned_hashes_cells_with_settings(&[versioned_hash], cell_mask, settings)
1990            .unwrap()
1991            .collect::<Vec<_>>();
1992        assert_eq!(matches, vec![(0, cells_and_proofs)]);
1993    }
1994}