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