Skip to main content

ethrex_common/types/
blobs_bundle.rs

1use std::ops::AddAssign;
2
3use crate::serde_utils;
4#[cfg(feature = "c-kzg")]
5use crate::types::Fork;
6use crate::types::constants::VERSIONED_HASH_VERSION_KZG;
7use crate::{Bytes, H256};
8
9use ethrex_rlp::{
10    decode::RLPDecode,
11    encode::RLPEncode,
12    error::RLPDecodeError,
13    structs::{Decoder, Encoder},
14};
15use serde::{Deserialize, Serialize};
16
17use super::{BYTES_PER_BLOB, CELLS_PER_EXT_BLOB, SAFE_BYTES_PER_BLOB};
18
19pub type Bytes48 = [u8; 48];
20pub type Blob = [u8; BYTES_PER_BLOB];
21pub type Commitment = Bytes48;
22pub type Proof = Bytes48;
23pub type BlobTuple = (Box<Blob>, Commitment, Vec<Proof>);
24
25#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
26#[serde(rename_all = "camelCase")]
27/// Struct containing all the blobs for a blob transaction, along with the corresponding commitments and proofs
28pub struct BlobsBundle {
29    #[serde(with = "serde_utils::blob::vec")]
30    pub blobs: Vec<Blob>,
31    #[serde(with = "serde_utils::bytes48::vec")]
32    pub commitments: Vec<Commitment>,
33    #[serde(with = "serde_utils::bytes48::vec")]
34    pub proofs: Vec<Proof>,
35    #[serde(skip, default)]
36    pub version: u8,
37}
38
39pub fn blob_from_bytes(bytes: Bytes) -> Result<Blob, BlobsBundleError> {
40    // This functions moved from `l2/utils/eth_client/transaction.rs`
41    // We set the first byte of every 32-bytes chunk to 0x00
42    // so it's always under the field module.
43    if bytes.len() > SAFE_BYTES_PER_BLOB {
44        return Err(BlobsBundleError::BlobDataInvalidBytesLength);
45    }
46
47    let mut buf = [0u8; BYTES_PER_BLOB];
48    buf[..(bytes.len() * 32).div_ceil(31)].copy_from_slice(
49        &bytes
50            .chunks(31)
51            .map(|x| [&[0x00], x].concat())
52            .collect::<Vec<_>>()
53            .concat(),
54    );
55
56    Ok(buf)
57}
58
59pub fn bytes_from_blob(blob: Bytes) -> [u8; SAFE_BYTES_PER_BLOB] {
60    let mut buf = [0u8; SAFE_BYTES_PER_BLOB];
61    buf.copy_from_slice(
62        &blob
63            .chunks(32)
64            .map(|x| x[1..].to_vec())
65            .collect::<Vec<_>>()
66            .concat(),
67    );
68
69    buf
70}
71
72pub fn kzg_commitment_to_versioned_hash(data: &Commitment) -> H256 {
73    use sha2::{Digest, Sha256};
74    let mut versioned_hash: [u8; 32] = Sha256::digest(data).into();
75    versioned_hash[0] = VERSIONED_HASH_VERSION_KZG;
76    versioned_hash.into()
77}
78
79impl BlobsBundle {
80    pub fn empty() -> Self {
81        Self::default()
82    }
83
84    pub fn is_empty(&self) -> bool {
85        self.blobs.is_empty() && self.commitments.is_empty() && self.proofs.is_empty()
86    }
87
88    // In the future we might want to provide a new method that calculates the commitments and proofs using the following.
89    #[cfg(feature = "c-kzg")]
90    pub fn create_from_blobs(
91        blobs: &Vec<Blob>,
92        wrapper_version: Option<u8>,
93    ) -> Result<Self, BlobsBundleError> {
94        use ethrex_crypto::kzg::{
95            blob_to_commitment_and_cell_proofs, blob_to_kzg_commitment_and_proof,
96        };
97        let mut commitments = Vec::new();
98        let mut proofs = Vec::new();
99
100        // Populate the commitments and proofs
101        for blob in blobs {
102            if wrapper_version.unwrap_or(0) == 0 {
103                let (commitment, proof) = blob_to_kzg_commitment_and_proof(blob)?;
104                commitments.push(commitment);
105                proofs.push(proof);
106            } else {
107                let (commitment, cell_proofs) = blob_to_commitment_and_cell_proofs(blob)?;
108                commitments.push(commitment);
109                proofs.extend(cell_proofs);
110            }
111        }
112
113        Ok(Self {
114            blobs: blobs.clone(),
115            commitments,
116            proofs,
117            version: wrapper_version.unwrap_or(0),
118        })
119    }
120
121    pub fn generate_versioned_hashes(&self) -> Vec<H256> {
122        self.commitments
123            .iter()
124            .map(kzg_commitment_to_versioned_hash)
125            .collect()
126    }
127
128    /// Given an index returns all or nothing `BlobTuple` if either of the commitment, proof or
129    /// blob is not found then it will return None instead of Partial data.
130    pub fn get_blob_tuple_by_index(&self, index: usize) -> Option<BlobTuple> {
131        let blob = Box::new(*self.blobs.get(index)?);
132        let commitment = *self.commitments.get(index)?;
133        let proofs = if self.version == 0 {
134            vec![*self.proofs.get(index)?]
135        } else {
136            self.proofs.chunks(CELLS_PER_EXT_BLOB).nth(index)?.to_vec()
137        };
138        Some((blob, commitment, proofs))
139    }
140
141    /// Full blob bundle validation: structural checks + KZG cryptographic proof verification.
142    #[cfg(feature = "c-kzg")]
143    pub fn validate(
144        &self,
145        tx: &super::EIP4844Transaction,
146        fork: super::Fork,
147    ) -> Result<(), BlobsBundleError> {
148        self.validate_cheap(tx, fork)?;
149        self.verify_kzg_proofs()
150    }
151
152    /// Verifies KZG cryptographic proofs against the blobs and commitments.
153    /// Dispatches to cell-proof or standard verification based on bundle version.
154    #[cfg(feature = "c-kzg")]
155    fn verify_kzg_proofs(&self) -> Result<(), BlobsBundleError> {
156        let valid = if self.version != 0 {
157            ethrex_crypto::kzg::verify_cell_kzg_proof_batch(
158                &self.blobs,
159                &self.commitments,
160                &self.proofs,
161            )?
162        } else {
163            ethrex_crypto::kzg::verify_kzg_proof_batch(
164                &self.blobs,
165                &self.commitments,
166                &self.proofs,
167            )?
168        };
169        if !valid {
170            return Err(BlobsBundleError::BlobToCommitmentAndProofError);
171        }
172        Ok(())
173    }
174
175    /// Validates blob bundle structure without expensive KZG cryptographic verification.
176    /// Used in P2P validation where full KZG is deferred to mempool insertion
177    /// (after dedup check), avoiding redundant proof verification for the same
178    /// blob tx received from multiple peers.
179    #[cfg(feature = "c-kzg")]
180    pub fn validate_cheap(
181        &self,
182        tx: &super::EIP4844Transaction,
183        fork: super::Fork,
184    ) -> Result<(), BlobsBundleError> {
185        use super::CELLS_PER_EXT_BLOB;
186
187        let max_blobs = max_blobs_per_block(fork);
188        let blob_count = self.blobs.len();
189
190        if blob_count > max_blobs {
191            return Err(BlobsBundleError::MaxBlobsExceeded);
192        }
193
194        // EIP-7594: a single transaction may carry at most MAX_BLOB_COUNT (6) blobs,
195        // independent of the higher per-block limit.
196        if fork >= Fork::Osaka && blob_count > MAX_BLOB_COUNT {
197            return Err(BlobsBundleError::MaxBlobsExceeded);
198        }
199
200        if blob_count == 0 {
201            return Err(BlobsBundleError::BlobBundleEmptyError);
202        }
203
204        // The wrapper version is fork-specific: 0 (blob proofs) before Osaka, 1 (cell
205        // proofs, EIP-7594) on Osaka+. Any other value is invalid.
206        let expected_version = if fork >= Fork::Osaka { 1 } else { 0 };
207        if self.version != expected_version {
208            return Err(BlobsBundleError::InvalidBlobVersionForFork);
209        }
210
211        if blob_count != self.commitments.len()
212            || (self.version == 0 && blob_count != self.proofs.len())
213            || (self.version != 0 && blob_count * CELLS_PER_EXT_BLOB != self.proofs.len())
214            || blob_count != tx.blob_versioned_hashes.len()
215        {
216            return Err(BlobsBundleError::BlobsBundleWrongLen);
217        };
218
219        self.validate_blob_commitment_hashes(&tx.blob_versioned_hashes)?;
220
221        Ok(())
222    }
223
224    pub fn validate_blob_commitment_hashes(
225        &self,
226        blob_versioned_hashes: &[H256],
227    ) -> Result<(), BlobsBundleError> {
228        if self.commitments.len() != blob_versioned_hashes.len() {
229            return Err(BlobsBundleError::BlobVersionedHashesError);
230        }
231        for (commitment, blob_versioned_hash) in
232            self.commitments.iter().zip(blob_versioned_hashes.iter())
233        {
234            if *blob_versioned_hash != kzg_commitment_to_versioned_hash(commitment) {
235                return Err(BlobsBundleError::BlobVersionedHashesError);
236            }
237        }
238        Ok(())
239    }
240}
241
242impl RLPEncode for BlobsBundle {
243    fn encode(&self, buf: &mut dyn bytes::BufMut) {
244        let encoder = Encoder::new(buf);
245        encoder
246            .encode_field(&self.blobs)
247            .encode_field(&self.commitments)
248            .encode_field(&self.proofs)
249            .encode_optional_field(&(self.version != 0).then_some(self.version))
250            .finish();
251    }
252}
253
254impl RLPDecode for BlobsBundle {
255    fn decode_unfinished(rlp: &[u8]) -> Result<(Self, &[u8]), RLPDecodeError> {
256        let decoder = Decoder::new(rlp)?;
257        let (blobs, decoder) = decoder.decode_field("blobs")?;
258        let (commitments, decoder) = decoder.decode_field("commitments")?;
259        let (proofs, decoder) = decoder.decode_field("proofs")?;
260        let (version, decoder) = decoder.decode_optional_field();
261        Ok((
262            Self {
263                blobs,
264                commitments,
265                proofs,
266                version: version.unwrap_or_default(),
267            },
268            decoder.finish()?,
269        ))
270    }
271}
272
273impl AddAssign for BlobsBundle {
274    fn add_assign(&mut self, rhs: Self) {
275        self.blobs.extend_from_slice(&rhs.blobs);
276        self.commitments.extend_from_slice(&rhs.commitments);
277        self.proofs.extend_from_slice(&rhs.proofs);
278    }
279}
280
281#[cfg(feature = "c-kzg")]
282const MAX_BLOB_COUNT: usize = 6;
283#[cfg(feature = "c-kzg")]
284const MAX_BLOB_COUNT_ELECTRA: usize = 9;
285
286#[cfg(feature = "c-kzg")]
287fn max_blobs_per_block(fork: crate::types::Fork) -> usize {
288    if fork >= crate::types::Fork::Prague {
289        MAX_BLOB_COUNT_ELECTRA
290    } else {
291        MAX_BLOB_COUNT
292    }
293}
294
295#[derive(Debug, thiserror::Error)]
296pub enum BlobsBundleError {
297    #[error("Blob data has an invalid length")]
298    BlobDataInvalidBytesLength,
299    #[error("Blob bundle is empty")]
300    BlobBundleEmptyError,
301    #[error("Blob versioned hashes and blobs bundle content length mismatch")]
302    BlobsBundleWrongLen,
303    #[error("Blob versioned hashes are incorrect")]
304    BlobVersionedHashesError,
305    #[error("Blob to commitment and proof generation error")]
306    BlobToCommitmentAndProofError,
307    #[error("Max blobs per block exceeded")]
308    MaxBlobsExceeded,
309    #[error("Invalid blob version for the current fork")]
310    InvalidBlobVersionForFork,
311    #[cfg(feature = "c-kzg")]
312    #[error("KZG related error: {0}")]
313    Kzg(#[from] ethrex_crypto::kzg::KzgError),
314}
315
316#[cfg(test)]
317mod tests {
318    mod shared {
319        #[cfg(feature = "c-kzg")]
320        pub fn convert_str_to_bytes48(s: &str) -> [u8; 48] {
321            let bytes = hex::decode(s).expect("Invalid hex string");
322            let mut array = [0u8; 48];
323            array.copy_from_slice(&bytes[..48]);
324            array
325        }
326    }
327
328    #[test]
329    #[cfg(feature = "c-kzg")]
330    fn transaction_with_valid_blobs_should_pass() {
331        let blobs = vec!["Hello, world!".as_bytes(), "Goodbye, world!".as_bytes()]
332            .into_iter()
333            .map(|data| {
334                crate::types::blobs_bundle::blob_from_bytes(data.into())
335                    .expect("Failed to create blob")
336            })
337            .collect();
338
339        let blobs_bundle = crate::types::BlobsBundle::create_from_blobs(&blobs, None)
340            .expect("Failed to create blobs bundle");
341
342        let blob_versioned_hashes = blobs_bundle.generate_versioned_hashes();
343
344        let tx = crate::types::transaction::EIP4844Transaction {
345            nonce: 3,
346            max_priority_fee_per_gas: 0,
347            max_fee_per_gas: 0,
348            max_fee_per_blob_gas: 0.into(),
349            gas: 15_000_000,
350            to: crate::Address::from_low_u64_be(1), // Normal tx
351            value: crate::U256::zero(),             // Value zero
352            data: crate::Bytes::default(),          // No data
353            access_list: Default::default(),        // No access list
354            blob_versioned_hashes,
355            ..Default::default()
356        };
357
358        assert!(matches!(
359            blobs_bundle.validate(&tx, crate::types::Fork::Prague),
360            Ok(())
361        ));
362    }
363
364    #[test]
365    #[cfg(feature = "c-kzg")]
366    fn transaction_with_valid_blobs_should_pass_on_osaka() {
367        let blobs = vec!["Hello, world!".as_bytes(), "Goodbye, world!".as_bytes()]
368            .into_iter()
369            .map(|data| {
370                crate::types::blobs_bundle::blob_from_bytes(data.into())
371                    .expect("Failed to create blob")
372            })
373            .collect();
374
375        let blobs_bundle = crate::types::BlobsBundle::create_from_blobs(&blobs, Some(1))
376            .expect("Failed to create blobs bundle");
377
378        let blob_versioned_hashes = blobs_bundle.generate_versioned_hashes();
379
380        let tx = crate::types::transaction::EIP4844Transaction {
381            nonce: 3,
382            max_priority_fee_per_gas: 0,
383            max_fee_per_gas: 0,
384            max_fee_per_blob_gas: 0.into(),
385            gas: 15_000_000,
386            to: crate::Address::from_low_u64_be(1), // Normal tx
387            value: crate::U256::zero(),             // Value zero
388            data: crate::Bytes::default(),          // No data
389            access_list: Default::default(),        // No access list
390            blob_versioned_hashes,
391            ..Default::default()
392        };
393
394        assert!(matches!(
395            blobs_bundle.validate(&tx, crate::types::Fork::Osaka),
396            Ok(())
397        ));
398    }
399
400    #[test]
401    #[cfg(feature = "c-kzg")]
402    fn transaction_with_invalid_fork_should_fail() {
403        let blobs = vec!["Hello, world!".as_bytes(), "Goodbye, world!".as_bytes()]
404            .into_iter()
405            .map(|data| {
406                crate::types::blobs_bundle::blob_from_bytes(data.into())
407                    .expect("Failed to create blob")
408            })
409            .collect();
410
411        let blobs_bundle = crate::types::BlobsBundle::create_from_blobs(&blobs, Some(1))
412            .expect("Failed to create blobs bundle");
413
414        let blob_versioned_hashes = blobs_bundle.generate_versioned_hashes();
415
416        let tx = crate::types::transaction::EIP4844Transaction {
417            nonce: 3,
418            max_priority_fee_per_gas: 0,
419            max_fee_per_gas: 0,
420            max_fee_per_blob_gas: 0.into(),
421            gas: 15_000_000,
422            to: crate::Address::from_low_u64_be(1), // Normal tx
423            value: crate::U256::zero(),             // Value zero
424            data: crate::Bytes::default(),          // No data
425            access_list: Default::default(),        // No access list
426            blob_versioned_hashes,
427            ..Default::default()
428        };
429
430        assert!(!matches!(
431            blobs_bundle.validate(&tx, crate::types::Fork::Prague),
432            Ok(())
433        ));
434    }
435
436    #[test]
437    #[cfg(feature = "c-kzg")]
438    fn transaction_with_invalid_proofs_should_fail() {
439        // blob data taken from: https://etherscan.io/tx/0x02a623925c05c540a7633ffa4eb78474df826497faa81035c4168695656801a2#blobs, but with 0 size blobs
440        let blobs_bundle = crate::types::BlobsBundle {
441            blobs: vec![[0; crate::types::BYTES_PER_BLOB], [0; crate::types::BYTES_PER_BLOB]],
442            commitments: vec!["b90289aabe0fcfb8db20a76b863ba90912d1d4d040cb7a156427d1c8cd5825b4d95eaeb221124782cc216960a3d01ec5",
443                              "91189a03ce1fe1225fc5de41d502c3911c2b19596f9011ea5fca4bf311424e5f853c9c46fe026038036c766197af96a0"]
444                              .into_iter()
445                              .map(|s| {
446                                  shared::convert_str_to_bytes48(s)
447                              })
448                              .collect(),
449            proofs: vec!["b502263fc5e75b3587f4fb418e61c5d0f0c18980b4e00179326a65d082539a50c063507a0b028e2db10c55814acbe4e9",
450                         "a29c43f6d05b7f15ab6f3e5004bd5f6b190165dc17e3d51fd06179b1e42c7aef50c145750d7c1cd1cd28357593bc7658"]
451                            .into_iter()
452                            .map(|s| {
453                                shared::convert_str_to_bytes48(s)
454                            })
455                            .collect(),
456                            version: 0,
457        };
458
459        let tx = crate::types::transaction::EIP4844Transaction {
460            nonce: 3,
461            max_priority_fee_per_gas: 0,
462            max_fee_per_gas: 0,
463            max_fee_per_blob_gas: 0.into(),
464            gas: 15_000_000,
465            to: crate::Address::from_low_u64_be(1), // Normal tx
466            value: crate::U256::zero(),             // Value zero
467            data: crate::Bytes::default(),          // No data
468            access_list: Default::default(),        // No access list
469            blob_versioned_hashes: vec![
470                "01ec8054d05bfec80f49231c6e90528bbb826ccd1464c255f38004099c8918d9",
471                "0180cb2dee9e6e016fabb5da4fb208555f5145c32895ccd13b26266d558cd77d",
472            ]
473            .into_iter()
474            .map(|b| {
475                let bytes = hex::decode(b).expect("Invalid hex string");
476                crate::H256::from_slice(&bytes)
477            })
478            .collect::<Vec<crate::H256>>(),
479            ..Default::default()
480        };
481
482        assert!(matches!(
483            blobs_bundle.validate(&tx, crate::types::Fork::Prague),
484            Err(crate::types::BlobsBundleError::BlobToCommitmentAndProofError)
485        ));
486    }
487
488    #[test]
489    #[cfg(feature = "c-kzg")]
490    fn transaction_with_incorrect_blobs_should_fail() {
491        // blob data taken from: https://etherscan.io/tx/0x02a623925c05c540a7633ffa4eb78474df826497faa81035c4168695656801a2#blobs
492        let blobs_bundle = crate::types::BlobsBundle {
493            blobs: vec![[0; crate::types::BYTES_PER_BLOB], [0; crate::types::BYTES_PER_BLOB]],
494            commitments: vec!["dead89aabe0fcfb8db20a76b863ba90912d1d4d040cb7a156427d1c8cd5825b4d95eaeb221124782cc216960a3d01ec5",
495                              "91189a03ce1fe1225fc5de41d502c3911c2b19596f9011ea5fca4bf311424e5f853c9c46fe026038036c766197af96a0"]
496                              .into_iter()
497                              .map(|s| {
498                                shared::convert_str_to_bytes48(s)
499                              })
500                              .collect(),
501            proofs: vec!["b502263fc5e75b3587f4fb418e61c5d0f0c18980b4e00179326a65d082539a50c063507a0b028e2db10c55814acbe4e9",
502                         "a29c43f6d05b7f15ab6f3e5004bd5f6b190165dc17e3d51fd06179b1e42c7aef50c145750d7c1cd1cd28357593bc7658"]
503                         .into_iter()
504                              .map(|s| {
505                                shared::convert_str_to_bytes48(s)
506                              })
507                              .collect(),
508                              version: 0,
509        };
510
511        let tx = crate::types::transaction::EIP4844Transaction {
512            nonce: 3,
513            max_priority_fee_per_gas: 0,
514            max_fee_per_gas: 0,
515            max_fee_per_blob_gas: 0.into(),
516            gas: 15_000_000,
517            to: crate::Address::from_low_u64_be(1), // Normal tx
518            value: crate::U256::zero(),             // Value zero
519            data: crate::Bytes::default(),          // No data
520            access_list: Default::default(),        // No access list
521            blob_versioned_hashes: vec![
522                "01ec8054d05bfec80f49231c6e90528bbb826ccd1464c255f38004099c8918d9",
523                "0180cb2dee9e6e016fabb5da4fb208555f5145c32895ccd13b26266d558cd77d",
524            ]
525            .into_iter()
526            .map(|b| {
527                let bytes = hex::decode(b).expect("Invalid hex string");
528                crate::H256::from_slice(&bytes)
529            })
530            .collect::<Vec<crate::H256>>(),
531            ..Default::default()
532        };
533
534        assert!(matches!(
535            blobs_bundle.validate(&tx, crate::types::Fork::Prague),
536            Err(crate::types::BlobsBundleError::BlobVersionedHashesError)
537        ));
538    }
539
540    #[test]
541    #[cfg(feature = "c-kzg")]
542    fn transaction_with_too_many_blobs_should_fail() {
543        let blob = crate::types::blobs_bundle::blob_from_bytes("Im a Blob".as_bytes().into())
544            .expect("Failed to create blob");
545        let blobs =
546            std::iter::repeat_n(blob, super::MAX_BLOB_COUNT_ELECTRA + 1).collect::<Vec<_>>();
547
548        let blobs_bundle = crate::types::BlobsBundle::create_from_blobs(&blobs, None)
549            .expect("Failed to create blobs bundle");
550
551        let blob_versioned_hashes = blobs_bundle.generate_versioned_hashes();
552
553        let tx = crate::types::transaction::EIP4844Transaction {
554            nonce: 3,
555            max_priority_fee_per_gas: 0,
556            max_fee_per_gas: 0,
557            max_fee_per_blob_gas: 0.into(),
558            gas: 15_000_000,
559            to: crate::Address::from_low_u64_be(1), // Normal tx
560            value: crate::U256::zero(),             // Value zero
561            data: crate::Bytes::default(),          // No data
562            access_list: Default::default(),        // No access list
563            blob_versioned_hashes,
564            ..Default::default()
565        };
566
567        assert!(matches!(
568            blobs_bundle.validate(&tx, crate::types::Fork::Prague),
569            Err(crate::types::BlobsBundleError::MaxBlobsExceeded)
570        ));
571    }
572
573    #[test]
574    #[cfg(feature = "c-kzg")]
575    fn transaction_with_version_0_blobs_should_fail_on_amsterdam() {
576        // Version 0 blobs should be invalid on Amsterdam fork (which comes after Osaka)
577        // The validation requires version 0 only on Osaka; Amsterdam >= Osaka so version 0 is rejected
578        let blobs = vec!["Hello, world!".as_bytes(), "Goodbye, world!".as_bytes()]
579            .into_iter()
580            .map(|data| {
581                crate::types::blobs_bundle::blob_from_bytes(data.into())
582                    .expect("Failed to create blob")
583            })
584            .collect();
585
586        let blobs_bundle = crate::types::BlobsBundle::create_from_blobs(&blobs, None)
587            .expect("Failed to create blobs bundle");
588
589        let blob_versioned_hashes = blobs_bundle.generate_versioned_hashes();
590
591        let tx = crate::types::transaction::EIP4844Transaction {
592            nonce: 3,
593            max_priority_fee_per_gas: 0,
594            max_fee_per_gas: 0,
595            max_fee_per_blob_gas: 0.into(),
596            gas: 15_000_000,
597            to: crate::Address::from_low_u64_be(1), // Normal tx
598            value: crate::U256::zero(),             // Value zero
599            data: crate::Bytes::default(),          // No data
600            access_list: Default::default(),        // No access list
601            blob_versioned_hashes,
602            ..Default::default()
603        };
604
605        assert!(matches!(
606            blobs_bundle.validate(&tx, crate::types::Fork::Amsterdam),
607            Err(crate::types::BlobsBundleError::InvalidBlobVersionForFork)
608        ));
609    }
610}