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