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