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