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