Skip to main content

alloy_eips/eip4844/
engine.rs

1//! Misc types related to the 4844
2
3#[cfg(feature = "ssz")]
4use crate::eip7594::CELLS_PER_EXT_BLOB;
5use crate::{
6    eip4844::{Blob, Bytes48},
7    eip7594::Cell,
8};
9use alloc::{boxed::Box, vec::Vec};
10
11/// Blob type returned in responses to `engine_getBlobsV1`: <https://github.com/ethereum/execution-apis/pull/559>
12#[derive(Debug, Clone, PartialEq, Eq)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14pub struct BlobAndProofV1 {
15    /// The blob data.
16    pub blob: Box<Blob>,
17    /// The KZG proof for the blob.
18    pub proof: Bytes48,
19}
20
21#[cfg(feature = "ssz")]
22impl ssz::Encode for BlobAndProofV1 {
23    fn is_ssz_fixed_len() -> bool {
24        true
25    }
26
27    fn ssz_fixed_len() -> usize {
28        <Blob as ssz::Encode>::ssz_fixed_len() + <Bytes48 as ssz::Encode>::ssz_fixed_len()
29    }
30
31    fn ssz_bytes_len(&self) -> usize {
32        Self::ssz_fixed_len()
33    }
34
35    fn ssz_append(&self, buf: &mut Vec<u8>) {
36        ssz::Encode::ssz_append(self.blob.as_ref(), buf);
37        ssz::Encode::ssz_append(&self.proof, buf);
38    }
39}
40
41#[cfg(feature = "ssz")]
42impl ssz::Decode for BlobAndProofV1 {
43    fn is_ssz_fixed_len() -> bool {
44        true
45    }
46
47    fn ssz_fixed_len() -> usize {
48        <Blob as ssz::Decode>::ssz_fixed_len() + <Bytes48 as ssz::Decode>::ssz_fixed_len()
49    }
50
51    fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, ssz::DecodeError> {
52        let mut builder = ssz::SszDecoderBuilder::new(bytes);
53        builder.register_type::<Blob>()?;
54        builder.register_type::<Bytes48>()?;
55
56        let mut decoder = builder.build()?;
57        Ok(Self { blob: Box::new(decoder.decode_next()?), proof: decoder.decode_next()? })
58    }
59}
60
61/// Blob type returned in responses to `engine_getBlobsV2`: <https://github.com/ethereum/execution-apis/pull/630>
62#[derive(Debug, Clone, PartialEq, Eq)]
63#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
64#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
65pub struct BlobAndProofV2 {
66    /// The blob data.
67    pub blob: Box<Blob>,
68    /// The cell proofs for the blob.
69    pub proofs: Vec<Bytes48>,
70}
71
72#[cfg(feature = "ssz")]
73impl ssz::Encode for BlobAndProofV2 {
74    fn is_ssz_fixed_len() -> bool {
75        false
76    }
77
78    fn ssz_bytes_len(&self) -> usize {
79        <Blob as ssz::Encode>::ssz_fixed_len()
80            + <Vec<Bytes48> as ssz::Encode>::ssz_fixed_len()
81            + ssz::Encode::ssz_bytes_len(&self.proofs)
82    }
83
84    fn ssz_append(&self, buf: &mut Vec<u8>) {
85        let offset =
86            <Blob as ssz::Encode>::ssz_fixed_len() + <Vec<Bytes48> as ssz::Encode>::ssz_fixed_len();
87        let mut encoder = ssz::SszEncoder::container(buf, offset);
88        encoder.append(self.blob.as_ref());
89        encoder.append(&self.proofs);
90        encoder.finalize();
91    }
92}
93
94#[cfg(feature = "ssz")]
95impl ssz::Decode for BlobAndProofV2 {
96    fn is_ssz_fixed_len() -> bool {
97        false
98    }
99
100    fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, ssz::DecodeError> {
101        let mut builder = ssz::SszDecoderBuilder::new(bytes);
102        builder.register_type::<Blob>()?;
103        builder.register_type::<Vec<Bytes48>>()?;
104
105        let mut decoder = builder.build()?;
106        let blob = decoder.decode_next()?;
107        let proofs: Vec<Bytes48> = decoder.decode_next()?;
108        if proofs.len() > crate::eip7594::CELLS_PER_EXT_BLOB {
109            return Err(ssz::DecodeError::BytesInvalid(format!(
110                "Invalid BlobAndProofV2: expected at most {} proofs, got {}",
111                crate::eip7594::CELLS_PER_EXT_BLOB,
112                proofs.len()
113            )));
114        }
115
116        Ok(Self { blob: Box::new(blob), proofs })
117    }
118}
119
120/// Blob cells type returned in responses to `engine_getBlobsV4`:
121/// <https://github.com/ethereum/execution-apis/pull/774>
122#[derive(Debug, Clone, Default, PartialEq, Eq)]
123#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
124pub struct BlobCellsAndProofsV1 {
125    /// The requested blob cells.
126    pub blob_cells: Vec<Option<Cell>>,
127    /// The KZG proofs for the requested blob cells.
128    pub proofs: Vec<Option<Bytes48>>,
129}
130
131#[cfg(feature = "ssz")]
132impl ssz::Encode for BlobCellsAndProofsV1 {
133    fn is_ssz_fixed_len() -> bool {
134        false
135    }
136
137    fn ssz_bytes_len(&self) -> usize {
138        <Vec<Option<Cell>> as ssz::Encode>::ssz_fixed_len()
139            + <Vec<Option<Bytes48>> as ssz::Encode>::ssz_fixed_len()
140            + ssz::Encode::ssz_bytes_len(&self.blob_cells)
141            + ssz::Encode::ssz_bytes_len(&self.proofs)
142    }
143
144    fn ssz_append(&self, buf: &mut Vec<u8>) {
145        let offset = <Vec<Option<Cell>> as ssz::Encode>::ssz_fixed_len()
146            + <Vec<Option<Bytes48>> as ssz::Encode>::ssz_fixed_len();
147        let mut encoder = ssz::SszEncoder::container(buf, offset);
148        encoder.append(&self.blob_cells);
149        encoder.append(&self.proofs);
150        encoder.finalize();
151    }
152}
153
154#[cfg(feature = "ssz")]
155impl ssz::Decode for BlobCellsAndProofsV1 {
156    fn is_ssz_fixed_len() -> bool {
157        false
158    }
159
160    fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, ssz::DecodeError> {
161        let mut builder = ssz::SszDecoderBuilder::new(bytes);
162        builder.register_type::<Vec<Option<Cell>>>()?;
163        builder.register_type::<Vec<Option<Bytes48>>>()?;
164
165        let mut decoder = builder.build()?;
166        let blob_cells: Vec<Option<Cell>> = decoder.decode_next()?;
167        let proofs: Vec<Option<Bytes48>> = decoder.decode_next()?;
168
169        if blob_cells.len() > CELLS_PER_EXT_BLOB {
170            return Err(ssz::DecodeError::BytesInvalid(format!(
171                "Invalid BlobCellsAndProofsV1: expected at most {} blob cells, got {}",
172                CELLS_PER_EXT_BLOB,
173                blob_cells.len()
174            )));
175        }
176
177        if blob_cells.len() != proofs.len() {
178            return Err(ssz::DecodeError::BytesInvalid(format!(
179                "Invalid BlobCellsAndProofsV1: blob_cells length {} does not match proofs length {}",
180                blob_cells.len(),
181                proofs.len()
182            )));
183        }
184
185        if blob_cells.iter().zip(&proofs).any(|(cell, proof)| cell.is_some() != proof.is_some()) {
186            return Err(ssz::DecodeError::BytesInvalid(
187                "Invalid BlobCellsAndProofsV1: blob_cells and proofs must have matching null positions".into(),
188            ));
189        }
190
191        Ok(Self { blob_cells, proofs })
192    }
193}
194
195#[cfg(all(test, feature = "ssz"))]
196mod tests {
197    use super::*;
198    use crate::eip4844::BYTES_PER_BLOB;
199
200    #[test]
201    fn ssz_blob_and_proof_v1_roundtrip() {
202        let blob_and_proof = BlobAndProofV1 {
203            blob: Box::new(Blob::repeat_byte(0x42)),
204            proof: Bytes48::repeat_byte(0x24),
205        };
206
207        let encoded = ssz::Encode::as_ssz_bytes(&blob_and_proof);
208        assert_eq!(encoded.len(), BYTES_PER_BLOB + 48);
209        assert_eq!(&encoded[..BYTES_PER_BLOB], blob_and_proof.blob.as_slice());
210        assert_eq!(&encoded[BYTES_PER_BLOB..], blob_and_proof.proof.as_slice());
211
212        let decoded = <BlobAndProofV1 as ssz::Decode>::from_ssz_bytes(&encoded).unwrap();
213        assert_eq!(decoded, blob_and_proof);
214    }
215
216    #[test]
217    fn ssz_blob_and_proof_v2_roundtrip() {
218        let proofs =
219            (0..CELLS_PER_EXT_BLOB).map(|i| Bytes48::repeat_byte(i as u8)).collect::<Vec<_>>();
220        let blob_and_proof = BlobAndProofV2 { blob: Box::new(Blob::repeat_byte(0x42)), proofs };
221
222        let encoded = ssz::Encode::as_ssz_bytes(&blob_and_proof);
223        let expected_offset = BYTES_PER_BLOB + ssz::BYTES_PER_LENGTH_OFFSET;
224        assert_eq!(encoded.len(), expected_offset + CELLS_PER_EXT_BLOB * 48);
225        assert_eq!(
226            u32::from_le_bytes(encoded[BYTES_PER_BLOB..expected_offset].try_into().unwrap())
227                as usize,
228            expected_offset
229        );
230        assert_eq!(&encoded[..BYTES_PER_BLOB], blob_and_proof.blob.as_slice());
231
232        let mut proof_chunks = encoded[expected_offset..].chunks_exact(48);
233        for (proof, chunk) in blob_and_proof.proofs.iter().zip(&mut proof_chunks) {
234            assert_eq!(chunk, proof.as_slice());
235        }
236        assert!(proof_chunks.remainder().is_empty());
237
238        let decoded = <BlobAndProofV2 as ssz::Decode>::from_ssz_bytes(&encoded).unwrap();
239        assert_eq!(decoded, blob_and_proof);
240    }
241
242    #[test]
243    fn ssz_blob_and_proof_v2_rejects_too_many_proofs() {
244        let blob_and_proof = BlobAndProofV2 {
245            blob: Box::new(Blob::repeat_byte(0x42)),
246            proofs: vec![Bytes48::ZERO; CELLS_PER_EXT_BLOB + 1],
247        };
248        let encoded = ssz::Encode::as_ssz_bytes(&blob_and_proof);
249
250        let err = <BlobAndProofV2 as ssz::Decode>::from_ssz_bytes(&encoded).unwrap_err();
251        assert!(
252            matches!(err, ssz::DecodeError::BytesInvalid(message) if message.contains("BlobAndProofV2"))
253        );
254    }
255
256    #[test]
257    fn ssz_blob_cells_and_proofs_v1_roundtrip() {
258        let blob_cells = vec![
259            Some(Cell::repeat_byte(0x01)),
260            None,
261            Some(Cell::repeat_byte(0x03)),
262            Some(Cell::repeat_byte(0x04)),
263        ];
264        let proofs = vec![
265            Some(Bytes48::repeat_byte(0x11)),
266            None,
267            Some(Bytes48::repeat_byte(0x33)),
268            Some(Bytes48::repeat_byte(0x44)),
269        ];
270        let blob_cells_and_proofs = BlobCellsAndProofsV1 { blob_cells, proofs };
271
272        let encoded = ssz::Encode::as_ssz_bytes(&blob_cells_and_proofs);
273        let decoded = <BlobCellsAndProofsV1 as ssz::Decode>::from_ssz_bytes(&encoded).unwrap();
274        assert_eq!(decoded, blob_cells_and_proofs);
275    }
276
277    #[test]
278    fn ssz_blob_cells_and_proofs_v1_rejects_too_many_cells() {
279        let blob_cells_and_proofs = BlobCellsAndProofsV1 {
280            blob_cells: vec![Some(Cell::ZERO); CELLS_PER_EXT_BLOB + 1],
281            proofs: vec![Some(Bytes48::ZERO); CELLS_PER_EXT_BLOB + 1],
282        };
283        let encoded = ssz::Encode::as_ssz_bytes(&blob_cells_and_proofs);
284
285        let err = <BlobCellsAndProofsV1 as ssz::Decode>::from_ssz_bytes(&encoded).unwrap_err();
286        assert!(
287            matches!(err, ssz::DecodeError::BytesInvalid(message) if message.contains("expected at most"))
288        );
289    }
290
291    #[test]
292    fn ssz_blob_cells_and_proofs_v1_rejects_mismatched_lengths() {
293        let blob_cells_and_proofs = BlobCellsAndProofsV1 {
294            blob_cells: vec![Some(Cell::ZERO)],
295            proofs: vec![Some(Bytes48::ZERO), Some(Bytes48::ZERO)],
296        };
297        let encoded = ssz::Encode::as_ssz_bytes(&blob_cells_and_proofs);
298
299        let err = <BlobCellsAndProofsV1 as ssz::Decode>::from_ssz_bytes(&encoded).unwrap_err();
300        assert!(
301            matches!(err, ssz::DecodeError::BytesInvalid(message) if message.contains("does not match"))
302        );
303    }
304
305    #[test]
306    fn ssz_blob_cells_and_proofs_v1_rejects_mismatched_null_positions() {
307        let blob_cells_and_proofs = BlobCellsAndProofsV1 {
308            blob_cells: vec![Some(Cell::ZERO), None],
309            proofs: vec![None, Some(Bytes48::ZERO)],
310        };
311        let encoded = ssz::Encode::as_ssz_bytes(&blob_cells_and_proofs);
312
313        let err = <BlobCellsAndProofsV1 as ssz::Decode>::from_ssz_bytes(&encoded).unwrap_err();
314        assert!(
315            matches!(err, ssz::DecodeError::BytesInvalid(message) if message.contains("matching null positions"))
316        );
317    }
318}
319
320#[cfg(all(test, feature = "serde"))]
321mod serde_tests {
322    use super::*;
323
324    #[test]
325    fn blob_cells_and_proofs_v1_uses_spec_field_name() {
326        let blob_cells_and_proofs = BlobCellsAndProofsV1 {
327            blob_cells: vec![Some(Cell::ZERO), None],
328            proofs: vec![Some(Bytes48::ZERO), None],
329        };
330
331        let json = serde_json::to_string(&blob_cells_and_proofs).unwrap();
332        assert!(json.contains("\"blob_cells\""));
333        assert!(!json.contains("\"blobCells\""));
334    }
335}