Skip to main content

alloy_eips/eip4844/
sidecar.rs

1//! EIP-4844 sidecar type
2
3use crate::{
4    eip4844::{
5        kzg_to_versioned_hash, Blob, BlobAndProofV1, Bytes48, BYTES_PER_BLOB, BYTES_PER_COMMITMENT,
6        BYTES_PER_PROOF,
7    },
8    eip7594::{Decodable7594, Encodable7594},
9};
10use alloc::{boxed::Box, vec::Vec};
11use alloy_primitives::{bytes::BufMut, B256};
12use alloy_rlp::{Decodable, Encodable, Header};
13
14#[cfg(any(test, feature = "arbitrary"))]
15use crate::eip4844::MAX_BLOBS_PER_BLOCK_DENCUN;
16#[cfg(feature = "kzg")]
17use crate::eip4844::{AsAlloy, AsCkzg};
18
19/// The versioned hash version for KZG.
20#[cfg(feature = "kzg")]
21pub(crate) const VERSIONED_HASH_VERSION_KZG: u8 = 0x01;
22
23/// A Blob hash
24#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
25#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
26pub struct IndexedBlobHash {
27    /// The index of the blob
28    pub index: u64,
29    /// The hash of the blob
30    pub hash: B256,
31}
32
33/// This represents a set of blobs, and its corresponding commitments and proofs.
34///
35/// For a well-formed sidecar, all three vectors have equal lengths and describe the same blob at
36/// each index. Public fields and [`Self::new`] do not enforce this invariant. With the `kzg`
37/// feature, prefer `try_from_blobs_with_settings` or call `validate` before use. Consuming
38/// iteration uses `zip`, so malformed unequal vectors are truncated to the shortest vector.
39///
40/// Its [`Encodable`] and [`Decodable`] implementations include an outer RLP list header. The
41/// field-level [`Encodable7594`] and [`Decodable7594`] codecs omit that header.
42#[derive(Clone, Default, PartialEq, Eq, Hash)]
43#[repr(C)]
44#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
45#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))]
46#[doc(alias = "BlobTxSidecar")]
47pub struct BlobTransactionSidecar {
48    /// The blob data.
49    #[cfg_attr(feature = "serde", serde(deserialize_with = "crate::eip4844::deserialize_blobs"))]
50    pub blobs: Vec<Blob>,
51    /// The blob commitments.
52    pub commitments: Vec<Bytes48>,
53    /// The blob proofs.
54    pub proofs: Vec<Bytes48>,
55}
56
57impl core::fmt::Debug for BlobTransactionSidecar {
58    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
59        f.debug_struct("BlobTransactionSidecar")
60            .field("blobs", &self.blobs.len())
61            .field("commitments", &self.commitments)
62            .field("proofs", &self.proofs)
63            .finish()
64    }
65}
66
67impl BlobTransactionSidecar {
68    /// Matches versioned hashes and returns an iterator of (index, [`BlobAndProofV1`]) pairs
69    /// where index is the position in `versioned_hashes` that matched the versioned hash in the
70    /// sidecar.
71    ///
72    /// This is used for the `engine_getBlobsV1` RPC endpoint of the engine API
73    pub fn match_versioned_hashes<'a>(
74        &'a self,
75        versioned_hashes: &'a [B256],
76    ) -> impl Iterator<Item = (usize, BlobAndProofV1)> + 'a {
77        self.versioned_hashes().enumerate().flat_map(move |(i, blob_versioned_hash)| {
78            versioned_hashes.iter().enumerate().filter_map(move |(j, target_hash)| {
79                if blob_versioned_hash == *target_hash {
80                    if let Some((blob, proof)) =
81                        self.blobs.get(i).copied().zip(self.proofs.get(i).copied())
82                    {
83                        return Some((j, BlobAndProofV1 { blob: Box::new(blob), proof }));
84                    }
85                }
86                None
87            })
88        })
89    }
90
91    /// Converts this EIP-4844 sidecar into an EIP-7594 sidecar.
92    ///
93    /// This requires computing cell KZG proofs from the blob data using the KZG trusted setup.
94    /// Each blob produces `CELLS_PER_EXT_BLOB` cell proofs.
95    #[cfg(feature = "kzg")]
96    pub fn try_into_7594(
97        self,
98        settings: &c_kzg::KzgSettings,
99    ) -> Result<crate::eip7594::BlobTransactionSidecarEip7594, c_kzg::Error> {
100        use crate::eip7594::CELLS_PER_EXT_BLOB;
101
102        if let [blob] = self.blobs.as_slice() {
103            let (_cells, kzg_proofs) = settings.compute_cells_and_kzg_proofs(blob.as_ckzg())?;
104            let cell_proofs = c_kzg::KzgProof::boxed_slice_as_alloy(kzg_proofs).into();
105            return Ok(crate::eip7594::BlobTransactionSidecarEip7594::new(
106                self.blobs,
107                self.commitments,
108                cell_proofs,
109            ));
110        }
111
112        let mut cell_proofs = Vec::with_capacity(self.blobs.len() * CELLS_PER_EXT_BLOB);
113
114        for blob in self.blobs.iter() {
115            // Compute cells and their KZG proofs for this blob
116            let (_cells, kzg_proofs) = settings.compute_cells_and_kzg_proofs(blob.as_ckzg())?;
117            cell_proofs.extend_from_slice(c_kzg::KzgProof::slice_as_alloy(kzg_proofs.as_ref()));
118        }
119
120        Ok(crate::eip7594::BlobTransactionSidecarEip7594::new(
121            self.blobs,
122            self.commitments,
123            cell_proofs,
124        ))
125    }
126}
127
128impl IntoIterator for BlobTransactionSidecar {
129    type Item = BlobTransactionSidecarItem;
130    type IntoIter = alloc::vec::IntoIter<BlobTransactionSidecarItem>;
131
132    fn into_iter(self) -> Self::IntoIter {
133        self.blobs
134            .into_iter()
135            .zip(self.commitments)
136            .zip(self.proofs)
137            .enumerate()
138            .map(|(index, ((blob, commitment), proof))| BlobTransactionSidecarItem {
139                index: index as u64,
140                blob: Box::new(blob),
141                kzg_commitment: commitment,
142                kzg_proof: proof,
143            })
144            .collect::<Vec<_>>()
145            .into_iter()
146    }
147}
148
149/// A single blob sidecar.
150#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
151#[repr(C)]
152#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
153pub struct BlobTransactionSidecarItem {
154    /// The index of this item within the [BlobTransactionSidecar].
155    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
156    pub index: u64,
157    /// The blob in this sidecar item.
158    #[cfg_attr(feature = "serde", serde(deserialize_with = "super::deserialize_blob"))]
159    pub blob: Box<Blob>,
160    /// The KZG commitment.
161    pub kzg_commitment: Bytes48,
162    /// The KZG proof.
163    pub kzg_proof: Bytes48,
164}
165
166#[cfg(feature = "kzg")]
167impl BlobTransactionSidecarItem {
168    /// `VERSIONED_HASH_VERSION_KZG ++ sha256(commitment)[1..]`
169    pub fn to_kzg_versioned_hash(&self) -> [u8; 32] {
170        use sha2::Digest;
171        let commitment = self.kzg_commitment.as_slice();
172        let mut hash: [u8; 32] = sha2::Sha256::digest(commitment).into();
173        hash[0] = VERSIONED_HASH_VERSION_KZG;
174        hash
175    }
176
177    /// Verifies the KZG proof of a blob to ensure its integrity and correctness.
178    pub fn verify_blob_kzg_proof(&self) -> Result<(), BlobTransactionValidationError> {
179        let binding = crate::eip4844::env_settings::EnvKzgSettings::Default;
180        let settings = binding.get();
181
182        let blob = c_kzg::Blob::from_bytes(self.blob.as_slice())
183            .map_err(BlobTransactionValidationError::KZGError)?;
184
185        let commitment = c_kzg::Bytes48::from_bytes(self.kzg_commitment.as_slice())
186            .map_err(BlobTransactionValidationError::KZGError)?;
187
188        let proof = c_kzg::Bytes48::from_bytes(self.kzg_proof.as_slice())
189            .map_err(BlobTransactionValidationError::KZGError)?;
190
191        let result = settings
192            .verify_blob_kzg_proof(&blob, &commitment, &proof)
193            .map_err(BlobTransactionValidationError::KZGError)?;
194
195        result.then_some(()).ok_or(BlobTransactionValidationError::InvalidProof)
196    }
197
198    /// Verify the blob sidecar against its [IndexedBlobHash].
199    pub fn verify_blob(
200        &self,
201        hash: &IndexedBlobHash,
202    ) -> Result<(), BlobTransactionValidationError> {
203        if self.index != hash.index {
204            let blob_hash_part = B256::from_slice(&self.blob[0..32]);
205            return Err(BlobTransactionValidationError::WrongVersionedHash {
206                have: blob_hash_part,
207                expected: hash.hash,
208            });
209        }
210
211        let computed_hash = self.to_kzg_versioned_hash();
212        if computed_hash != hash.hash {
213            return Err(BlobTransactionValidationError::WrongVersionedHash {
214                have: computed_hash.into(),
215                expected: hash.hash,
216            });
217        }
218
219        self.verify_blob_kzg_proof()
220    }
221}
222
223#[cfg(any(test, feature = "arbitrary"))]
224impl<'a> arbitrary::Arbitrary<'a> for BlobTransactionSidecar {
225    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
226        let num_blobs = u.int_in_range(1..=MAX_BLOBS_PER_BLOCK_DENCUN)?;
227        let mut blobs = Vec::with_capacity(num_blobs);
228        for _ in 0..num_blobs {
229            blobs.push(Blob::arbitrary(u)?);
230        }
231
232        let mut commitments = Vec::with_capacity(num_blobs);
233        let mut proofs = Vec::with_capacity(num_blobs);
234        for _ in 0..num_blobs {
235            commitments.push(Bytes48::arbitrary(u)?);
236            proofs.push(Bytes48::arbitrary(u)?);
237        }
238
239        Ok(Self { blobs, commitments, proofs })
240    }
241}
242
243impl BlobTransactionSidecar {
244    /// Constructs a sidecar without validating vector lengths, commitments, or proofs.
245    pub const fn new(blobs: Vec<Blob>, commitments: Vec<Bytes48>, proofs: Vec<Bytes48>) -> Self {
246        Self { blobs, commitments, proofs }
247    }
248
249    /// Shrinks the sidecar vectors to fit their current contents.
250    #[inline]
251    pub fn shrink_to_fit(&mut self) {
252        self.blobs.shrink_to_fit();
253        self.commitments.shrink_to_fit();
254        self.proofs.shrink_to_fit();
255    }
256
257    /// Creates a new instance from the given KZG types.
258    #[cfg(feature = "kzg")]
259    pub fn from_kzg(
260        blobs: Vec<c_kzg::Blob>,
261        commitments: Vec<c_kzg::Bytes48>,
262        proofs: Vec<c_kzg::Bytes48>,
263    ) -> Self {
264        let blobs = Blob::vec_from_ckzg(blobs);
265        let commitments = Bytes48::vec_from_ckzg(commitments);
266        let proofs = Bytes48::vec_from_ckzg(proofs);
267        Self { blobs, commitments, proofs }
268    }
269
270    /// Verifies that the versioned hashes are valid for this sidecar's blob data, commitments, and
271    /// proofs.
272    ///
273    /// Takes as input the [KzgSettings](c_kzg::KzgSettings), which should contain the parameters
274    /// derived from the KZG trusted setup.
275    ///
276    /// This ensures that the blob transaction payload has the same number of blob data elements,
277    /// commitments, and proofs. Each blob data element is verified against its commitment and
278    /// proof.
279    ///
280    /// Returns [BlobTransactionValidationError::InvalidProof] if any blob KZG proof in the response
281    /// fails to verify, or if the versioned hashes in the transaction do not match the actual
282    /// commitment versioned hashes.
283    #[cfg(feature = "kzg")]
284    pub fn validate(
285        &self,
286        blob_versioned_hashes: &[B256],
287        proof_settings: &c_kzg::KzgSettings,
288    ) -> Result<(), BlobTransactionValidationError> {
289        // Ensure the versioned hashes and commitments have the same length.
290        if blob_versioned_hashes.len() != self.commitments.len() {
291            return Err(c_kzg::Error::MismatchLength(format!(
292                "There are {} versioned commitment hashes and {} commitments",
293                blob_versioned_hashes.len(),
294                self.commitments.len()
295            ))
296            .into());
297        }
298
299        // calculate versioned hashes by zipping & iterating
300        for (versioned_hash, commitment) in
301            blob_versioned_hashes.iter().zip(self.commitments.iter())
302        {
303            // calculate & verify versioned hash
304            let calculated_versioned_hash = kzg_to_versioned_hash(commitment.as_slice());
305            if *versioned_hash != calculated_versioned_hash {
306                return Err(BlobTransactionValidationError::WrongVersionedHash {
307                    have: *versioned_hash,
308                    expected: calculated_versioned_hash,
309                });
310            }
311        }
312
313        let res = proof_settings
314            .verify_blob_kzg_proof_batch(
315                Blob::slice_as_ckzg(self.blobs.as_slice()),
316                Bytes48::slice_as_ckzg(self.commitments.as_slice()),
317                Bytes48::slice_as_ckzg(self.proofs.as_slice()),
318            )
319            .map_err(BlobTransactionValidationError::KZGError)?;
320
321        res.then_some(()).ok_or(BlobTransactionValidationError::InvalidProof)
322    }
323
324    /// Returns an iterator over the versioned hashes of the commitments.
325    pub fn versioned_hashes(&self) -> VersionedHashIter<'_> {
326        VersionedHashIter::new(&self.commitments)
327    }
328
329    /// Returns the versioned hash for the blob at the given index, if it
330    /// exists.
331    pub fn versioned_hash_for_blob(&self, blob_index: usize) -> Option<B256> {
332        self.commitments.get(blob_index).map(|c| kzg_to_versioned_hash(c.as_slice()))
333    }
334
335    /// Returns the index of the versioned hash in the commitments vector.
336    pub fn versioned_hash_index(&self, hash: &B256) -> Option<usize> {
337        self.commitments
338            .iter()
339            .position(|commitment| kzg_to_versioned_hash(commitment.as_slice()) == *hash)
340    }
341
342    /// Returns the blob corresponding to the versioned hash, if it exists.
343    pub fn blob_by_versioned_hash(&self, hash: &B256) -> Option<&Blob> {
344        self.versioned_hash_index(hash).and_then(|index| self.blobs.get(index))
345    }
346
347    /// Calculates a size heuristic for the in-memory size of the [BlobTransactionSidecar].
348    #[inline]
349    pub const fn size(&self) -> usize {
350        self.blobs.len() * BYTES_PER_BLOB + // blobs
351            self.commitments.len() * BYTES_PER_COMMITMENT + // commitments
352            self.proofs.len() * BYTES_PER_PROOF // proofs
353    }
354
355    /// Tries to create a new [`BlobTransactionSidecar`] from the hex encoded blob str.
356    ///
357    /// See also [`Blob::from_hex`](c_kzg::Blob::from_hex)
358    #[cfg(all(feature = "kzg", any(test, feature = "arbitrary")))]
359    pub fn try_from_blobs_hex<I, B>(blobs: I) -> Result<Self, c_kzg::Error>
360    where
361        I: IntoIterator<Item = B>,
362        B: AsRef<str>,
363    {
364        let mut converted = Vec::new();
365        for blob in blobs {
366            converted.push(crate::eip4844::utils::hex_to_blob(blob)?);
367        }
368        Self::try_from_blobs(converted)
369    }
370
371    /// Tries to create a new [`BlobTransactionSidecar`] from the given blob
372    /// bytes.
373    ///
374    /// See also [`Blob::from_bytes`](c_kzg::Blob::from_bytes)
375    #[cfg(all(feature = "kzg", any(test, feature = "arbitrary")))]
376    pub fn try_from_blobs_bytes<I, B>(blobs: I) -> Result<Self, c_kzg::Error>
377    where
378        I: IntoIterator<Item = B>,
379        B: AsRef<[u8]>,
380    {
381        let mut converted = Vec::new();
382        for blob in blobs {
383            converted.push(crate::eip4844::utils::bytes_to_blob(blob)?);
384        }
385        Self::try_from_blobs(converted)
386    }
387
388    /// Tries to create a new [`BlobTransactionSidecar`] from the given blobs
389    /// and KZG settings.
390    #[cfg(feature = "kzg")]
391    pub fn try_from_blobs_with_settings(
392        blobs: Vec<Blob>,
393        settings: &c_kzg::KzgSettings,
394    ) -> Result<Self, c_kzg::Error> {
395        let mut commitments = Vec::with_capacity(blobs.len());
396        let mut proofs = Vec::with_capacity(blobs.len());
397        for blob in &blobs {
398            let blob = blob.as_ckzg();
399            let commitment = settings.blob_to_kzg_commitment(blob)?;
400            let proof = settings.compute_blob_kzg_proof(blob, &commitment.to_bytes())?;
401
402            commitments.push(Bytes48::from_ckzg(commitment.to_bytes()));
403            proofs.push(Bytes48::from_ckzg(proof.to_bytes()));
404        }
405
406        Ok(Self::new(blobs, commitments, proofs))
407    }
408
409    /// Tries to create a new [`BlobTransactionSidecar`] from the given blobs.
410    ///
411    /// This uses the global/default KZG settings, see also
412    /// [`EnvKzgSettings::Default`](crate::eip4844::env_settings::EnvKzgSettings).
413    #[cfg(all(feature = "kzg", any(test, feature = "arbitrary")))]
414    pub fn try_from_blobs(blobs: Vec<Blob>) -> Result<Self, c_kzg::Error> {
415        use crate::eip4844::env_settings::EnvKzgSettings;
416
417        Self::try_from_blobs_with_settings(blobs, EnvKzgSettings::Default.get())
418    }
419
420    /// Outputs the RLP length of the [BlobTransactionSidecar] fields, without
421    /// a RLP header.
422    #[doc(hidden)]
423    pub fn rlp_encoded_fields_length(&self) -> usize {
424        self.blobs.length() + self.commitments.length() + self.proofs.length()
425    }
426
427    /// Encodes the inner [BlobTransactionSidecar] fields as RLP bytes, __without__ a RLP header.
428    ///
429    /// This encodes the fields in the following order:
430    /// - `blobs`
431    /// - `commitments`
432    /// - `proofs`
433    #[inline]
434    #[doc(hidden)]
435    pub fn rlp_encode_fields(&self, out: &mut dyn BufMut) {
436        // Encode the blobs, commitments, and proofs
437        self.blobs.encode(out);
438        self.commitments.encode(out);
439        self.proofs.encode(out);
440    }
441
442    /// Creates an RLP header for the [BlobTransactionSidecar].
443    fn rlp_header(&self) -> Header {
444        Header { list: true, payload_length: self.rlp_encoded_fields_length() }
445    }
446
447    /// Calculates the length of the [BlobTransactionSidecar] when encoded as
448    /// RLP.
449    pub fn rlp_encoded_length(&self) -> usize {
450        self.rlp_header().length() + self.rlp_encoded_fields_length()
451    }
452
453    /// Encodes the [BlobTransactionSidecar] as RLP bytes.
454    pub fn rlp_encode(&self, out: &mut dyn BufMut) {
455        self.rlp_header().encode(out);
456        self.rlp_encode_fields(out);
457    }
458
459    /// RLP decode the fields of a [BlobTransactionSidecar].
460    #[doc(hidden)]
461    pub fn rlp_decode_fields(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
462        Ok(Self {
463            blobs: Decodable::decode(buf)?,
464            commitments: Decodable::decode(buf)?,
465            proofs: Decodable::decode(buf)?,
466        })
467    }
468
469    /// Decodes the [BlobTransactionSidecar] from RLP bytes.
470    pub fn rlp_decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
471        let header = Header::decode(buf)?;
472        if !header.list {
473            return Err(alloy_rlp::Error::UnexpectedString);
474        }
475        if buf.len() < header.payload_length {
476            return Err(alloy_rlp::Error::InputTooShort);
477        }
478        let remaining = buf.len();
479        let this = Self::rlp_decode_fields(buf)?;
480
481        if buf.len() + header.payload_length != remaining {
482            return Err(alloy_rlp::Error::UnexpectedLength);
483        }
484
485        Ok(this)
486    }
487}
488
489impl Encodable for BlobTransactionSidecar {
490    /// Encodes the sidecar as an RLP list, including its outer header.
491    fn encode(&self, out: &mut dyn BufMut) {
492        self.rlp_encode(out);
493    }
494
495    fn length(&self) -> usize {
496        self.rlp_encoded_length()
497    }
498}
499
500impl Decodable for BlobTransactionSidecar {
501    /// Decodes an RLP list, including its outer header.
502    fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
503        Self::rlp_decode(buf)
504    }
505}
506
507impl Encodable7594 for BlobTransactionSidecar {
508    fn encode_7594_len(&self) -> usize {
509        self.rlp_encoded_fields_length()
510    }
511
512    fn encode_7594(&self, out: &mut dyn BufMut) {
513        self.rlp_encode_fields(out);
514    }
515}
516
517impl Decodable7594 for BlobTransactionSidecar {
518    fn decode_7594(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
519        Self::rlp_decode_fields(buf)
520    }
521}
522
523/// Helper function to deserialize boxed blobs from an existing [`MapAccess`]
524///
525/// [`MapAccess`]: serde::de::MapAccess
526#[cfg(all(debug_assertions, feature = "serde"))]
527pub(crate) fn deserialize_blobs_map<'de, M: serde::de::MapAccess<'de>>(
528    map_access: &mut M,
529) -> Result<Vec<Blob>, M::Error> {
530    let raw_blobs: Vec<alloy_primitives::Bytes> = map_access.next_value()?;
531    let mut blobs = Vec::with_capacity(raw_blobs.len());
532    for blob in raw_blobs {
533        blobs.push(Blob::try_from(blob.as_ref()).map_err(serde::de::Error::custom)?);
534    }
535    Ok(blobs)
536}
537
538#[cfg(all(not(debug_assertions), feature = "serde"))]
539#[inline(always)]
540pub(crate) fn deserialize_blobs_map<'de, M: serde::de::MapAccess<'de>>(
541    map_access: &mut M,
542) -> Result<Vec<Blob>, M::Error> {
543    map_access.next_value()
544}
545
546/// An error that can occur when validating a [BlobTransactionSidecar::validate].
547#[derive(Debug)]
548#[cfg(feature = "kzg")]
549pub enum BlobTransactionValidationError {
550    /// Proof validation failed.
551    InvalidProof,
552    /// An error returned by [`c_kzg`].
553    KZGError(c_kzg::Error),
554    /// The inner transaction is not a blob transaction.
555    NotBlobTransaction(u8),
556    /// Error variant for thrown by EIP-4844 tx variants without a sidecar.
557    MissingSidecar,
558    /// The versioned hash is incorrect.
559    WrongVersionedHash {
560        /// The versioned hash we got
561        have: B256,
562        /// The versioned hash we expected
563        expected: B256,
564    },
565}
566
567#[cfg(feature = "kzg")]
568impl core::error::Error for BlobTransactionValidationError {}
569
570#[cfg(feature = "kzg")]
571impl core::fmt::Display for BlobTransactionValidationError {
572    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
573        match self {
574            Self::InvalidProof => f.write_str("invalid KZG proof"),
575            Self::KZGError(err) => {
576                write!(f, "KZG error: {err:?}")
577            }
578            Self::NotBlobTransaction(err) => {
579                write!(f, "unable to verify proof for non blob transaction: {err}")
580            }
581            Self::MissingSidecar => {
582                f.write_str("eip4844 tx variant without sidecar being used for verification.")
583            }
584            Self::WrongVersionedHash { have, expected } => {
585                write!(f, "wrong versioned hash: have {have}, expected {expected}")
586            }
587        }
588    }
589}
590
591#[cfg(feature = "kzg")]
592impl From<c_kzg::Error> for BlobTransactionValidationError {
593    fn from(source: c_kzg::Error) -> Self {
594        Self::KZGError(source)
595    }
596}
597
598/// Iterator that returns versioned hashes from commitments.
599#[derive(Debug, Clone)]
600pub struct VersionedHashIter<'a> {
601    /// The iterator over KZG commitments from which versioned hashes are generated.
602    commitments: core::slice::Iter<'a, Bytes48>,
603}
604
605impl<'a> Iterator for VersionedHashIter<'a> {
606    type Item = B256;
607
608    fn next(&mut self) -> Option<Self::Item> {
609        self.commitments.next().map(|c| kzg_to_versioned_hash(c.as_slice()))
610    }
611}
612
613// Constructor method for VersionedHashIter
614impl<'a> VersionedHashIter<'a> {
615    /// Creates a new iterator over commitments to generate versioned hashes.
616    pub fn new(commitments: &'a [Bytes48]) -> Self {
617        Self { commitments: commitments.iter() }
618    }
619}
620
621#[cfg(test)]
622mod tests {
623    use super::*;
624    use arbitrary::Arbitrary;
625
626    #[test]
627    #[cfg(feature = "serde")]
628    fn deserialize_blob() {
629        let blob = BlobTransactionSidecar {
630            blobs: vec![Blob::default(), Blob::default(), Blob::default(), Blob::default()],
631            commitments: vec![
632                Bytes48::default(),
633                Bytes48::default(),
634                Bytes48::default(),
635                Bytes48::default(),
636            ],
637            proofs: vec![
638                Bytes48::default(),
639                Bytes48::default(),
640                Bytes48::default(),
641                Bytes48::default(),
642            ],
643        };
644
645        let s = serde_json::to_string(&blob).unwrap();
646        let deserialized: BlobTransactionSidecar = serde_json::from_str(&s).unwrap();
647        assert_eq!(blob, deserialized);
648    }
649
650    #[test]
651    fn test_arbitrary_blob() {
652        let mut unstructured = arbitrary::Unstructured::new(b"unstructured blob");
653        let _blob = BlobTransactionSidecar::arbitrary(&mut unstructured).unwrap();
654    }
655
656    #[test]
657    #[cfg(feature = "serde")]
658    fn test_blob_item_serde_roundtrip() {
659        let blob_item = BlobTransactionSidecarItem {
660            index: 0,
661            blob: Box::new(Blob::default()),
662            kzg_commitment: Bytes48::default(),
663            kzg_proof: Bytes48::default(),
664        };
665
666        let s = serde_json::to_string(&blob_item).unwrap();
667        let deserialized: BlobTransactionSidecarItem = serde_json::from_str(&s).unwrap();
668        assert_eq!(blob_item, deserialized);
669    }
670}