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")]
31pub 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 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 #[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 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 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 #[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 #[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 #[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 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 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 #[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 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 #[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 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), value: crate::U256::zero(), data: crate::Bytes::default(), access_list: Default::default(), 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), value: crate::U256::zero(), data: crate::Bytes::default(), access_list: Default::default(), 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 #[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), value: crate::U256::zero(), data: crate::Bytes::default(), access_list: Default::default(), 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), value: crate::U256::zero(), data: crate::Bytes::default(), access_list: Default::default(), 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 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), value: crate::U256::zero(), data: crate::Bytes::default(), access_list: Default::default(), 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 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), value: crate::U256::zero(), data: crate::Bytes::default(), access_list: Default::default(), 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), value: crate::U256::zero(), data: crate::Bytes::default(), access_list: Default::default(), 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 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), value: crate::U256::zero(), data: crate::Bytes::default(), access_list: Default::default(), 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}