alloy_eips/eip7594/
sidecar.rs

1use crate::{
2    eip4844::{
3        kzg_to_versioned_hash, Blob, BlobAndProofV2, BlobTransactionSidecar, Bytes48,
4        BYTES_PER_BLOB, BYTES_PER_COMMITMENT, BYTES_PER_PROOF,
5    },
6    eip7594::{CELLS_PER_EXT_BLOB, EIP_7594_WRAPPER_VERSION},
7};
8use alloc::{boxed::Box, vec::Vec};
9use alloy_primitives::B256;
10use alloy_rlp::{BufMut, Decodable, Encodable, Header};
11
12#[cfg(feature = "kzg")]
13use crate::eip4844::BlobTransactionValidationError;
14
15use super::{Decodable7594, Encodable7594};
16
17/// This represents a set of blobs, and its corresponding commitments and proofs.
18/// Proof type depends on the sidecar variant.
19///
20/// This type encodes and decodes the fields without an rlp header.
21#[derive(Clone, PartialEq, Eq, Hash, Debug, derive_more::From)]
22#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
23#[cfg_attr(feature = "serde", serde(untagged))]
24#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
25pub enum BlobTransactionSidecarVariant {
26    /// EIP-4844 style blob transaction sidecar.
27    Eip4844(BlobTransactionSidecar),
28    /// EIP-7594 style blob transaction sidecar with cell proofs.
29    Eip7594(BlobTransactionSidecarEip7594),
30}
31
32impl BlobTransactionSidecarVariant {
33    /// Returns the EIP-4844 sidecar if it is [`Self::Eip4844`].
34    pub const fn as_eip4844(&self) -> Option<&BlobTransactionSidecar> {
35        match self {
36            Self::Eip4844(sidecar) => Some(sidecar),
37            _ => None,
38        }
39    }
40
41    /// Returns the EIP-4844 sidecar if it is [`Self::Eip7594`].
42    pub const fn as_eip7594(&self) -> Option<&BlobTransactionSidecarEip7594> {
43        match self {
44            Self::Eip7594(sidecar) => Some(sidecar),
45            _ => None,
46        }
47    }
48
49    /// Calculates a size heuristic for the in-memory size of the [BlobTransactionSidecarVariant].
50    #[inline]
51    pub fn size(&self) -> usize {
52        match self {
53            Self::Eip4844(sidecar) => sidecar.size(),
54            Self::Eip7594(sidecar) => sidecar.size(),
55        }
56    }
57
58    /// Verifies that the sidecar is valid. See relevant methods for each variant for more info.
59    #[cfg(feature = "kzg")]
60    pub fn validate(
61        &self,
62        blob_versioned_hashes: &[B256],
63        proof_settings: &c_kzg::KzgSettings,
64    ) -> Result<(), BlobTransactionValidationError> {
65        match self {
66            Self::Eip4844(sidecar) => sidecar.validate(blob_versioned_hashes, proof_settings),
67            Self::Eip7594(sidecar) => sidecar.validate(blob_versioned_hashes, proof_settings),
68        }
69    }
70
71    /// Returns an iterator over the versioned hashes of the commitments.
72    pub fn versioned_hashes(&self) -> Vec<B256> {
73        match self {
74            Self::Eip4844(sidecar) => sidecar.versioned_hashes().collect(),
75            Self::Eip7594(sidecar) => sidecar.versioned_hashes().collect(),
76        }
77    }
78
79    /// Outputs the RLP length of the [BlobTransactionSidecarVariant] fields, without a RLP header.
80    #[doc(hidden)]
81    pub fn rlp_encoded_fields_length(&self) -> usize {
82        match self {
83            Self::Eip4844(sidecar) => sidecar.rlp_encoded_fields_length(),
84            Self::Eip7594(sidecar) => sidecar.rlp_encoded_fields_length(),
85        }
86    }
87
88    /// Encodes the inner [BlobTransactionSidecarVariant] fields as RLP bytes, __without__ a RLP
89    /// header.
90    #[inline]
91    #[doc(hidden)]
92    pub fn rlp_encode_fields(&self, out: &mut dyn BufMut) {
93        match self {
94            Self::Eip4844(sidecar) => sidecar.rlp_encode_fields(out),
95            Self::Eip7594(sidecar) => sidecar.rlp_encode_fields(out),
96        }
97    }
98
99    /// RLP decode the fields of a [BlobTransactionSidecarVariant] based on the wrapper version.
100    #[doc(hidden)]
101    pub fn rlp_decode_fields(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
102        Self::decode_7594(buf)
103    }
104}
105
106impl Encodable for BlobTransactionSidecarVariant {
107    /// Encodes the [BlobTransactionSidecar] fields as RLP bytes, without a RLP header.
108    fn encode(&self, out: &mut dyn BufMut) {
109        match self {
110            Self::Eip4844(sidecar) => sidecar.encode(out),
111            Self::Eip7594(sidecar) => sidecar.encode(out),
112        }
113    }
114
115    fn length(&self) -> usize {
116        match self {
117            Self::Eip4844(sidecar) => sidecar.rlp_encoded_length(),
118            Self::Eip7594(sidecar) => sidecar.rlp_encoded_length(),
119        }
120    }
121}
122
123impl Decodable for BlobTransactionSidecarVariant {
124    /// Decodes the inner [BlobTransactionSidecar] fields from RLP bytes, without a RLP header.
125    fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
126        let header = Header::decode(buf)?;
127        if !header.list {
128            return Err(alloy_rlp::Error::UnexpectedString);
129        }
130        if buf.len() < header.payload_length {
131            return Err(alloy_rlp::Error::InputTooShort);
132        }
133        let remaining = buf.len();
134        let this = Self::rlp_decode_fields(buf)?;
135        if buf.len() + header.payload_length != remaining {
136            return Err(alloy_rlp::Error::UnexpectedLength);
137        }
138
139        Ok(this)
140    }
141}
142
143impl Encodable7594 for BlobTransactionSidecarVariant {
144    fn encode_7594_len(&self) -> usize {
145        self.rlp_encoded_fields_length()
146    }
147
148    fn encode_7594(&self, out: &mut dyn BufMut) {
149        self.rlp_encode_fields(out);
150    }
151}
152
153impl Decodable7594 for BlobTransactionSidecarVariant {
154    fn decode_7594(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
155        if buf.first() == Some(&EIP_7594_WRAPPER_VERSION) {
156            Ok(Self::Eip7594(Decodable7594::decode_7594(buf)?))
157        } else {
158            Ok(Self::Eip4844(Decodable7594::decode_7594(buf)?))
159        }
160    }
161}
162
163/// This represents a set of blobs, and its corresponding commitments and cell proofs.
164///
165/// This type encodes and decodes the fields without an rlp header.
166#[derive(Clone, Default, PartialEq, Eq, Hash)]
167#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
168#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
169pub struct BlobTransactionSidecarEip7594 {
170    /// The blob data.
171    #[cfg_attr(
172        all(debug_assertions, feature = "serde"),
173        serde(deserialize_with = "crate::eip4844::deserialize_blobs")
174    )]
175    pub blobs: Vec<Blob>,
176    /// The blob commitments.
177    pub commitments: Vec<Bytes48>,
178    /// List of cell proofs for all blobs in the sidecar, including the proofs for the extension
179    /// indices, for a total of `CELLS_PER_EXT_BLOB` proofs per blob (`CELLS_PER_EXT_BLOB` is the
180    /// number of cells for an extended blob, defined in
181    /// [the consensus specs](https://github.com/ethereum/consensus-specs/tree/9d377fd53d029536e57cfda1a4d2c700c59f86bf/specs/fulu/polynomial-commitments-sampling.md#cells))
182    pub cell_proofs: Vec<Bytes48>,
183}
184
185impl core::fmt::Debug for BlobTransactionSidecarEip7594 {
186    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
187        f.debug_struct("BlobTransactionSidecarEip7594")
188            .field("blobs", &self.blobs.len())
189            .field("commitments", &self.commitments)
190            .field("cell_proofs", &self.cell_proofs)
191            .finish()
192    }
193}
194
195impl BlobTransactionSidecarEip7594 {
196    /// Constructs a new [BlobTransactionSidecarEip7594] from a set of blobs, commitments, and
197    /// cell proofs.
198    pub const fn new(
199        blobs: Vec<Blob>,
200        commitments: Vec<Bytes48>,
201        cell_proofs: Vec<Bytes48>,
202    ) -> Self {
203        Self { blobs, commitments, cell_proofs }
204    }
205
206    /// Calculates a size heuristic for the in-memory size of the [BlobTransactionSidecarEip7594].
207    #[inline]
208    pub fn size(&self) -> usize {
209        self.blobs.len() * BYTES_PER_BLOB + // blobs
210               self.commitments.len() * BYTES_PER_COMMITMENT + // commitments
211               self.cell_proofs.len() * BYTES_PER_PROOF // proofs
212    }
213
214    /// Verifies that the versioned hashes are valid for this sidecar's blob data, commitments, and
215    /// proofs.
216    ///
217    /// Takes as input the [KzgSettings](c_kzg::KzgSettings), which should contain the parameters
218    /// derived from the KZG trusted setup.
219    ///
220    /// This ensures that the blob transaction payload has the expected number of blob data
221    /// elements, commitments, and proofs. The cells are constructed from each blob and verified
222    /// against the commitments and proofs.
223    ///
224    /// Returns [BlobTransactionValidationError::InvalidProof] if any blob KZG proof in the response
225    /// fails to verify, or if the versioned hashes in the transaction do not match the actual
226    /// commitment versioned hashes.
227    #[cfg(feature = "kzg")]
228    pub fn validate(
229        &self,
230        blob_versioned_hashes: &[B256],
231        proof_settings: &c_kzg::KzgSettings,
232    ) -> Result<(), BlobTransactionValidationError> {
233        // Ensure the versioned hashes and commitments have the same length.
234        if blob_versioned_hashes.len() != self.commitments.len() {
235            return Err(c_kzg::Error::MismatchLength(format!(
236                "There are {} versioned commitment hashes and {} commitments",
237                blob_versioned_hashes.len(),
238                self.commitments.len()
239            ))
240            .into());
241        }
242
243        let blobs_len = self.blobs.len();
244        let expected_cell_proofs_len = blobs_len * CELLS_PER_EXT_BLOB;
245        if self.cell_proofs.len() != expected_cell_proofs_len {
246            return Err(c_kzg::Error::MismatchLength(format!(
247                "There are {} cell proofs and {} blobs. Expected {} cell proofs.",
248                self.cell_proofs.len(),
249                blobs_len,
250                expected_cell_proofs_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            let commitment = c_kzg::KzgCommitment::from(commitment.0);
260
261            // calculate & verify versioned hash
262            let calculated_versioned_hash = kzg_to_versioned_hash(commitment.as_slice());
263            if *versioned_hash != calculated_versioned_hash {
264                return Err(BlobTransactionValidationError::WrongVersionedHash {
265                    have: *versioned_hash,
266                    expected: calculated_versioned_hash,
267                });
268            }
269        }
270
271        // Repeat cell ranges for each blob.
272        let cell_indices =
273            Vec::from_iter((0..blobs_len).flat_map(|_| 0..CELLS_PER_EXT_BLOB as u64));
274
275        // Repeat commitments for each cell.
276        let mut commitments = Vec::with_capacity(blobs_len * CELLS_PER_EXT_BLOB);
277        for commitment in &self.commitments {
278            commitments.extend(core::iter::repeat_n(*commitment, CELLS_PER_EXT_BLOB));
279        }
280
281        // SAFETY: ALL types have the same size
282        let res = unsafe {
283            let mut cells = Vec::with_capacity(blobs_len * CELLS_PER_EXT_BLOB);
284            for blob in &self.blobs {
285                let blob = core::mem::transmute::<&Blob, &c_kzg::Blob>(blob);
286                cells.extend(proof_settings.compute_cells(blob)?.into_iter());
287            }
288
289            proof_settings.verify_cell_kzg_proof_batch(
290                // commitments
291                core::mem::transmute::<&[Bytes48], &[c_kzg::Bytes48]>(&commitments),
292                // cell indices
293                &cell_indices,
294                // cells
295                &cells,
296                // proofs
297                core::mem::transmute::<&[Bytes48], &[c_kzg::Bytes48]>(self.cell_proofs.as_slice()),
298            )?
299        };
300
301        res.then_some(()).ok_or(BlobTransactionValidationError::InvalidProof)
302    }
303
304    /// Returns an iterator over the versioned hashes of the commitments.
305    pub fn versioned_hashes(&self) -> impl Iterator<Item = B256> + '_ {
306        self.commitments.iter().map(|c| kzg_to_versioned_hash(c.as_slice()))
307    }
308
309    /// Matches versioned hashes and returns an iterator of (index, [`BlobAndProofV2`]) pairs
310    /// where index is the position in `versioned_hashes` that matched the versioned hash in the
311    /// sidecar.
312    ///
313    /// This is used for the `engine_getBlobsV2` RPC endpoint of the engine API
314    pub fn match_versioned_hashes<'a>(
315        &'a self,
316        versioned_hashes: &'a [B256],
317    ) -> impl Iterator<Item = (usize, BlobAndProofV2)> + 'a {
318        self.versioned_hashes().enumerate().flat_map(move |(i, blob_versioned_hash)| {
319            versioned_hashes.iter().enumerate().filter_map(move |(j, target_hash)| {
320                if blob_versioned_hash == *target_hash {
321                    let maybe_blob = self.blobs.get(i);
322                    let proof_range = i * CELLS_PER_EXT_BLOB..(i + 1) * CELLS_PER_EXT_BLOB;
323                    let maybe_proofs = Some(&self.cell_proofs[proof_range])
324                        .filter(|proofs| proofs.len() == CELLS_PER_EXT_BLOB);
325                    if let Some((blob, proofs)) = maybe_blob.copied().zip(maybe_proofs) {
326                        return Some((
327                            j,
328                            BlobAndProofV2 { blob: Box::new(blob), proofs: proofs.to_vec() },
329                        ));
330                    }
331                }
332                None
333            })
334        })
335    }
336
337    /// Outputs the RLP length of [BlobTransactionSidecarEip7594] fields without a RLP header.
338    #[doc(hidden)]
339    pub fn rlp_encoded_fields_length(&self) -> usize {
340        // wrapper version + blobs + commitments + cell proofs
341        1 + self.blobs.length() + self.commitments.length() + self.cell_proofs.length()
342    }
343
344    /// Encodes the inner [BlobTransactionSidecarEip7594] fields as RLP bytes, __without__ a
345    /// RLP header.
346    ///
347    /// This encodes the fields in the following order:
348    /// - `wrapper_version`
349    /// - `blobs`
350    /// - `commitments`
351    /// - `cell_proofs`
352    #[inline]
353    #[doc(hidden)]
354    pub fn rlp_encode_fields(&self, out: &mut dyn BufMut) {
355        // Put version byte.
356        out.put_u8(EIP_7594_WRAPPER_VERSION);
357        // Encode the blobs, commitments, and cell proofs
358        self.blobs.encode(out);
359        self.commitments.encode(out);
360        self.cell_proofs.encode(out);
361    }
362
363    /// Creates an RLP header for the [BlobTransactionSidecarEip7594].
364    fn rlp_header(&self) -> Header {
365        Header { list: true, payload_length: self.rlp_encoded_fields_length() }
366    }
367
368    /// Calculates the length of the [BlobTransactionSidecarEip7594] when encoded as
369    /// RLP.
370    pub fn rlp_encoded_length(&self) -> usize {
371        self.rlp_header().length() + self.rlp_encoded_fields_length()
372    }
373
374    /// Encodes the [BlobTransactionSidecarEip7594] as RLP bytes.
375    pub fn rlp_encode(&self, out: &mut dyn BufMut) {
376        self.rlp_header().encode(out);
377        self.rlp_encode_fields(out);
378    }
379
380    /// RLP decode the fields of a [BlobTransactionSidecarEip7594].
381    #[doc(hidden)]
382    pub fn rlp_decode_fields(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
383        Ok(Self {
384            blobs: Decodable::decode(buf)?,
385            commitments: Decodable::decode(buf)?,
386            cell_proofs: Decodable::decode(buf)?,
387        })
388    }
389
390    /// Decodes the [BlobTransactionSidecarEip7594] from RLP bytes.
391    pub fn rlp_decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
392        let header = Header::decode(buf)?;
393        if !header.list {
394            return Err(alloy_rlp::Error::UnexpectedString);
395        }
396        if buf.len() < header.payload_length {
397            return Err(alloy_rlp::Error::InputTooShort);
398        }
399        let remaining = buf.len();
400
401        let this = Self::decode_7594(buf)?;
402        if buf.len() + header.payload_length != remaining {
403            return Err(alloy_rlp::Error::UnexpectedLength);
404        }
405
406        Ok(this)
407    }
408}
409
410impl Encodable for BlobTransactionSidecarEip7594 {
411    /// Encodes the inner [BlobTransactionSidecarEip7594] fields as RLP bytes, without a RLP header.
412    fn encode(&self, out: &mut dyn BufMut) {
413        self.rlp_encode(out);
414    }
415
416    fn length(&self) -> usize {
417        self.rlp_encoded_length()
418    }
419}
420
421impl Decodable for BlobTransactionSidecarEip7594 {
422    /// Decodes the inner [BlobTransactionSidecarEip7594] fields from RLP bytes, without a RLP
423    /// header.
424    fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
425        Self::rlp_decode(buf)
426    }
427}
428
429impl Encodable7594 for BlobTransactionSidecarEip7594 {
430    fn encode_7594_len(&self) -> usize {
431        self.rlp_encoded_fields_length()
432    }
433
434    fn encode_7594(&self, out: &mut dyn BufMut) {
435        self.rlp_encode_fields(out);
436    }
437}
438
439impl Decodable7594 for BlobTransactionSidecarEip7594 {
440    fn decode_7594(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
441        let wrapper_version: u8 = Decodable::decode(buf)?;
442        if wrapper_version != EIP_7594_WRAPPER_VERSION {
443            return Err(alloy_rlp::Error::Custom("invalid wrapper version"));
444        }
445        Self::rlp_decode_fields(buf)
446    }
447}
448
449#[cfg(test)]
450mod tests {
451    use super::*;
452
453    #[test]
454    fn sidecar_variant_rlp_roundtrip() {
455        let mut encoded = Vec::new();
456
457        // 4844
458        let empty_sidecar_4844 =
459            BlobTransactionSidecarVariant::Eip4844(BlobTransactionSidecar::default());
460        empty_sidecar_4844.encode(&mut encoded);
461        assert_eq!(
462            empty_sidecar_4844,
463            BlobTransactionSidecarVariant::decode(&mut &encoded[..]).unwrap()
464        );
465
466        let sidecar_4844 = BlobTransactionSidecarVariant::Eip4844(BlobTransactionSidecar::new(
467            vec![Blob::default()],
468            vec![Bytes48::ZERO],
469            vec![Bytes48::ZERO],
470        ));
471        encoded.clear();
472        sidecar_4844.encode(&mut encoded);
473        assert_eq!(sidecar_4844, BlobTransactionSidecarVariant::decode(&mut &encoded[..]).unwrap());
474
475        // 7594
476        let empty_sidecar_7594 =
477            BlobTransactionSidecarVariant::Eip7594(BlobTransactionSidecarEip7594::default());
478        encoded.clear();
479        empty_sidecar_7594.encode(&mut encoded);
480        assert_eq!(
481            empty_sidecar_7594,
482            BlobTransactionSidecarVariant::decode(&mut &encoded[..]).unwrap()
483        );
484
485        let sidecar_7594 =
486            BlobTransactionSidecarVariant::Eip7594(BlobTransactionSidecarEip7594::new(
487                vec![Blob::default()],
488                vec![Bytes48::ZERO],
489                core::iter::repeat_n(Bytes48::ZERO, CELLS_PER_EXT_BLOB).collect(),
490            ));
491        encoded.clear();
492        sidecar_7594.encode(&mut encoded);
493        assert_eq!(sidecar_7594, BlobTransactionSidecarVariant::decode(&mut &encoded[..]).unwrap());
494    }
495
496    #[test]
497    fn rlp_7594_roundtrip() {
498        let mut encoded = Vec::new();
499
500        let sidecar_4844 = BlobTransactionSidecar::default();
501        sidecar_4844.encode_7594(&mut encoded);
502        assert_eq!(sidecar_4844, Decodable7594::decode_7594(&mut &encoded[..]).unwrap());
503
504        let sidecar_variant_4844 = BlobTransactionSidecarVariant::Eip4844(sidecar_4844);
505        assert_eq!(sidecar_variant_4844, Decodable7594::decode_7594(&mut &encoded[..]).unwrap());
506        encoded.clear();
507        sidecar_variant_4844.encode_7594(&mut encoded);
508        assert_eq!(sidecar_variant_4844, Decodable7594::decode_7594(&mut &encoded[..]).unwrap());
509
510        let sidecar_7594 = BlobTransactionSidecarEip7594::default();
511        encoded.clear();
512        sidecar_7594.encode_7594(&mut encoded);
513        assert_eq!(sidecar_7594, Decodable7594::decode_7594(&mut &encoded[..]).unwrap());
514
515        let sidecar_variant_7594 = BlobTransactionSidecarVariant::Eip7594(sidecar_7594);
516        assert_eq!(sidecar_variant_7594, Decodable7594::decode_7594(&mut &encoded[..]).unwrap());
517        encoded.clear();
518        sidecar_variant_7594.encode_7594(&mut encoded);
519        assert_eq!(sidecar_variant_7594, Decodable7594::decode_7594(&mut &encoded[..]).unwrap());
520    }
521}