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