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