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")]
27pub 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 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 #[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 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 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 #[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 #[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 #[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 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 let version_ok = if fork >= Fork::Osaka {
215 self.version == 1
216 } else {
217 self.version <= 1
218 };
219 if !version_ok {
220 return Err(BlobsBundleError::InvalidBlobVersionForFork);
221 }
222
223 if blob_count != self.commitments.len()
224 || (self.version == 0 && blob_count != self.proofs.len())
225 || (self.version != 0 && blob_count * CELLS_PER_EXT_BLOB != self.proofs.len())
226 || blob_count != tx.blob_versioned_hashes.len()
227 {
228 return Err(BlobsBundleError::BlobsBundleWrongLen);
229 };
230
231 self.validate_blob_commitment_hashes(&tx.blob_versioned_hashes)?;
232
233 Ok(())
234 }
235
236 pub fn validate_blob_commitment_hashes(
237 &self,
238 blob_versioned_hashes: &[H256],
239 ) -> Result<(), BlobsBundleError> {
240 if self.commitments.len() != blob_versioned_hashes.len() {
241 return Err(BlobsBundleError::BlobVersionedHashesError);
242 }
243 for (commitment, blob_versioned_hash) in
244 self.commitments.iter().zip(blob_versioned_hashes.iter())
245 {
246 if *blob_versioned_hash != kzg_commitment_to_versioned_hash(commitment) {
247 return Err(BlobsBundleError::BlobVersionedHashesError);
248 }
249 }
250 Ok(())
251 }
252}
253
254impl RLPEncode for BlobsBundle {
255 fn encode(&self, buf: &mut dyn bytes::BufMut) {
256 let encoder = Encoder::new(buf);
257 encoder
258 .encode_field(&self.blobs)
259 .encode_field(&self.commitments)
260 .encode_field(&self.proofs)
261 .encode_optional_field(&(self.version != 0).then_some(self.version))
262 .finish();
263 }
264}
265
266impl RLPDecode for BlobsBundle {
267 fn decode_unfinished(rlp: &[u8]) -> Result<(Self, &[u8]), RLPDecodeError> {
268 let decoder = Decoder::new(rlp)?;
269 let (blobs, decoder) = decoder.decode_field("blobs")?;
270 let (commitments, decoder) = decoder.decode_field("commitments")?;
271 let (proofs, decoder) = decoder.decode_field("proofs")?;
272 let (version, decoder) = decoder.decode_optional_field();
273 Ok((
274 Self {
275 blobs,
276 commitments,
277 proofs,
278 version: version.unwrap_or_default(),
279 },
280 decoder.finish()?,
281 ))
282 }
283}
284
285impl AddAssign for BlobsBundle {
286 fn add_assign(&mut self, rhs: Self) {
287 self.blobs.extend_from_slice(&rhs.blobs);
288 self.commitments.extend_from_slice(&rhs.commitments);
289 self.proofs.extend_from_slice(&rhs.proofs);
290 }
291}
292
293#[cfg(feature = "c-kzg")]
294const MAX_BLOB_COUNT: usize = 6;
295#[cfg(feature = "c-kzg")]
296const MAX_BLOB_COUNT_ELECTRA: usize = 9;
297
298#[cfg(feature = "c-kzg")]
299fn max_blobs_per_block(fork: crate::types::Fork) -> usize {
300 if fork >= crate::types::Fork::Prague {
301 MAX_BLOB_COUNT_ELECTRA
302 } else {
303 MAX_BLOB_COUNT
304 }
305}
306
307#[derive(Debug, thiserror::Error)]
308pub enum BlobsBundleError {
309 #[error("Blob data has an invalid length")]
310 BlobDataInvalidBytesLength,
311 #[error("Blob bundle is empty")]
312 BlobBundleEmptyError,
313 #[error("Blob versioned hashes and blobs bundle content length mismatch")]
314 BlobsBundleWrongLen,
315 #[error("Blob versioned hashes are incorrect")]
316 BlobVersionedHashesError,
317 #[error("Blob to commitment and proof generation error")]
318 BlobToCommitmentAndProofError,
319 #[error("Max blobs per block exceeded")]
320 MaxBlobsExceeded,
321 #[error("Invalid blob version for the current fork")]
322 InvalidBlobVersionForFork,
323 #[cfg(feature = "c-kzg")]
324 #[error("KZG related error: {0}")]
325 Kzg(#[from] ethrex_crypto::kzg::KzgError),
326}
327
328#[cfg(test)]
329mod tests {
330 mod shared {
331 #[cfg(feature = "c-kzg")]
332 pub fn convert_str_to_bytes48(s: &str) -> [u8; 48] {
333 let bytes = hex::decode(s).expect("Invalid hex string");
334 let mut array = [0u8; 48];
335 array.copy_from_slice(&bytes[..48]);
336 array
337 }
338 }
339
340 #[test]
341 #[cfg(feature = "c-kzg")]
342 fn transaction_with_valid_blobs_should_pass() {
343 let blobs = vec!["Hello, world!".as_bytes(), "Goodbye, world!".as_bytes()]
344 .into_iter()
345 .map(|data| {
346 crate::types::blobs_bundle::blob_from_bytes(data.into())
347 .expect("Failed to create blob")
348 })
349 .collect();
350
351 let blobs_bundle = crate::types::BlobsBundle::create_from_blobs(&blobs, None)
352 .expect("Failed to create blobs bundle");
353
354 let blob_versioned_hashes = blobs_bundle.generate_versioned_hashes();
355
356 let tx = crate::types::transaction::EIP4844Transaction {
357 nonce: 3,
358 max_priority_fee_per_gas: 0,
359 max_fee_per_gas: 0,
360 max_fee_per_blob_gas: 0.into(),
361 gas: 15_000_000,
362 to: crate::Address::from_low_u64_be(1), value: crate::U256::zero(), data: crate::Bytes::default(), access_list: Default::default(), blob_versioned_hashes,
367 ..Default::default()
368 };
369
370 assert!(matches!(
371 blobs_bundle.validate(&tx, crate::types::Fork::Prague),
372 Ok(())
373 ));
374 }
375
376 #[test]
377 #[cfg(feature = "c-kzg")]
378 fn transaction_with_valid_blobs_should_pass_on_osaka() {
379 let blobs = vec!["Hello, world!".as_bytes(), "Goodbye, world!".as_bytes()]
380 .into_iter()
381 .map(|data| {
382 crate::types::blobs_bundle::blob_from_bytes(data.into())
383 .expect("Failed to create blob")
384 })
385 .collect();
386
387 let blobs_bundle = crate::types::BlobsBundle::create_from_blobs(&blobs, Some(1))
388 .expect("Failed to create blobs bundle");
389
390 let blob_versioned_hashes = blobs_bundle.generate_versioned_hashes();
391
392 let tx = crate::types::transaction::EIP4844Transaction {
393 nonce: 3,
394 max_priority_fee_per_gas: 0,
395 max_fee_per_gas: 0,
396 max_fee_per_blob_gas: 0.into(),
397 gas: 15_000_000,
398 to: crate::Address::from_low_u64_be(1), value: crate::U256::zero(), data: crate::Bytes::default(), access_list: Default::default(), blob_versioned_hashes,
403 ..Default::default()
404 };
405
406 assert!(matches!(
407 blobs_bundle.validate(&tx, crate::types::Fork::Osaka),
408 Ok(())
409 ));
410 }
411
412 #[test]
416 #[cfg(feature = "c-kzg")]
417 fn v1_sidecar_is_accepted_pre_osaka() {
418 let blobs = vec!["Hello, world!".as_bytes(), "Goodbye, world!".as_bytes()]
419 .into_iter()
420 .map(|data| {
421 crate::types::blobs_bundle::blob_from_bytes(data.into())
422 .expect("Failed to create blob")
423 })
424 .collect();
425
426 let blobs_bundle = crate::types::BlobsBundle::create_from_blobs(&blobs, Some(1))
427 .expect("Failed to create blobs bundle");
428
429 let blob_versioned_hashes = blobs_bundle.generate_versioned_hashes();
430
431 let tx = crate::types::transaction::EIP4844Transaction {
432 nonce: 3,
433 max_priority_fee_per_gas: 0,
434 max_fee_per_gas: 0,
435 max_fee_per_blob_gas: 0.into(),
436 gas: 15_000_000,
437 to: crate::Address::from_low_u64_be(1), value: crate::U256::zero(), data: crate::Bytes::default(), access_list: Default::default(), blob_versioned_hashes,
442 ..Default::default()
443 };
444
445 assert!(matches!(
446 blobs_bundle.validate(&tx, crate::types::Fork::Prague),
447 Ok(())
448 ));
449 }
450
451 #[test]
452 #[cfg(feature = "c-kzg")]
453 fn v0_sidecar_is_rejected_on_osaka() {
454 let blobs = vec!["Hello, world!".as_bytes(), "Goodbye, world!".as_bytes()]
455 .into_iter()
456 .map(|data| {
457 crate::types::blobs_bundle::blob_from_bytes(data.into())
458 .expect("Failed to create blob")
459 })
460 .collect();
461
462 let blobs_bundle = crate::types::BlobsBundle::create_from_blobs(&blobs, Some(0))
463 .expect("Failed to create blobs bundle");
464
465 let blob_versioned_hashes = blobs_bundle.generate_versioned_hashes();
466
467 let tx = crate::types::transaction::EIP4844Transaction {
468 nonce: 3,
469 max_priority_fee_per_gas: 0,
470 max_fee_per_gas: 0,
471 max_fee_per_blob_gas: 0.into(),
472 gas: 15_000_000,
473 to: crate::Address::from_low_u64_be(1), value: crate::U256::zero(), data: crate::Bytes::default(), access_list: Default::default(), blob_versioned_hashes,
478 ..Default::default()
479 };
480
481 assert!(!matches!(
482 blobs_bundle.validate(&tx, crate::types::Fork::Osaka),
483 Ok(())
484 ));
485 }
486
487 #[test]
488 #[cfg(feature = "c-kzg")]
489 fn transaction_with_invalid_proofs_should_fail() {
490 let blobs_bundle = crate::types::BlobsBundle {
492 blobs: vec![[0; crate::types::BYTES_PER_BLOB], [0; crate::types::BYTES_PER_BLOB]],
493 commitments: vec!["b90289aabe0fcfb8db20a76b863ba90912d1d4d040cb7a156427d1c8cd5825b4d95eaeb221124782cc216960a3d01ec5",
494 "91189a03ce1fe1225fc5de41d502c3911c2b19596f9011ea5fca4bf311424e5f853c9c46fe026038036c766197af96a0"]
495 .into_iter()
496 .map(|s| {
497 shared::convert_str_to_bytes48(s)
498 })
499 .collect(),
500 proofs: vec!["b502263fc5e75b3587f4fb418e61c5d0f0c18980b4e00179326a65d082539a50c063507a0b028e2db10c55814acbe4e9",
501 "a29c43f6d05b7f15ab6f3e5004bd5f6b190165dc17e3d51fd06179b1e42c7aef50c145750d7c1cd1cd28357593bc7658"]
502 .into_iter()
503 .map(|s| {
504 shared::convert_str_to_bytes48(s)
505 })
506 .collect(),
507 version: 0,
508 };
509
510 let tx = crate::types::transaction::EIP4844Transaction {
511 nonce: 3,
512 max_priority_fee_per_gas: 0,
513 max_fee_per_gas: 0,
514 max_fee_per_blob_gas: 0.into(),
515 gas: 15_000_000,
516 to: crate::Address::from_low_u64_be(1), value: crate::U256::zero(), data: crate::Bytes::default(), access_list: Default::default(), blob_versioned_hashes: vec![
521 "01ec8054d05bfec80f49231c6e90528bbb826ccd1464c255f38004099c8918d9",
522 "0180cb2dee9e6e016fabb5da4fb208555f5145c32895ccd13b26266d558cd77d",
523 ]
524 .into_iter()
525 .map(|b| {
526 let bytes = hex::decode(b).expect("Invalid hex string");
527 crate::H256::from_slice(&bytes)
528 })
529 .collect::<Vec<crate::H256>>(),
530 ..Default::default()
531 };
532
533 assert!(matches!(
534 blobs_bundle.validate(&tx, crate::types::Fork::Prague),
535 Err(crate::types::BlobsBundleError::BlobToCommitmentAndProofError)
536 ));
537 }
538
539 #[test]
540 #[cfg(feature = "c-kzg")]
541 fn transaction_with_incorrect_blobs_should_fail() {
542 let blobs_bundle = crate::types::BlobsBundle {
544 blobs: vec![[0; crate::types::BYTES_PER_BLOB], [0; crate::types::BYTES_PER_BLOB]],
545 commitments: vec!["dead89aabe0fcfb8db20a76b863ba90912d1d4d040cb7a156427d1c8cd5825b4d95eaeb221124782cc216960a3d01ec5",
546 "91189a03ce1fe1225fc5de41d502c3911c2b19596f9011ea5fca4bf311424e5f853c9c46fe026038036c766197af96a0"]
547 .into_iter()
548 .map(|s| {
549 shared::convert_str_to_bytes48(s)
550 })
551 .collect(),
552 proofs: vec!["b502263fc5e75b3587f4fb418e61c5d0f0c18980b4e00179326a65d082539a50c063507a0b028e2db10c55814acbe4e9",
553 "a29c43f6d05b7f15ab6f3e5004bd5f6b190165dc17e3d51fd06179b1e42c7aef50c145750d7c1cd1cd28357593bc7658"]
554 .into_iter()
555 .map(|s| {
556 shared::convert_str_to_bytes48(s)
557 })
558 .collect(),
559 version: 0,
560 };
561
562 let tx = crate::types::transaction::EIP4844Transaction {
563 nonce: 3,
564 max_priority_fee_per_gas: 0,
565 max_fee_per_gas: 0,
566 max_fee_per_blob_gas: 0.into(),
567 gas: 15_000_000,
568 to: crate::Address::from_low_u64_be(1), value: crate::U256::zero(), data: crate::Bytes::default(), access_list: Default::default(), blob_versioned_hashes: vec![
573 "01ec8054d05bfec80f49231c6e90528bbb826ccd1464c255f38004099c8918d9",
574 "0180cb2dee9e6e016fabb5da4fb208555f5145c32895ccd13b26266d558cd77d",
575 ]
576 .into_iter()
577 .map(|b| {
578 let bytes = hex::decode(b).expect("Invalid hex string");
579 crate::H256::from_slice(&bytes)
580 })
581 .collect::<Vec<crate::H256>>(),
582 ..Default::default()
583 };
584
585 assert!(matches!(
586 blobs_bundle.validate(&tx, crate::types::Fork::Prague),
587 Err(crate::types::BlobsBundleError::BlobVersionedHashesError)
588 ));
589 }
590
591 #[test]
592 #[cfg(feature = "c-kzg")]
593 fn transaction_with_too_many_blobs_should_fail() {
594 let blob = crate::types::blobs_bundle::blob_from_bytes("Im a Blob".as_bytes().into())
595 .expect("Failed to create blob");
596 let blobs =
597 std::iter::repeat_n(blob, super::MAX_BLOB_COUNT_ELECTRA + 1).collect::<Vec<_>>();
598
599 let blobs_bundle = crate::types::BlobsBundle::create_from_blobs(&blobs, None)
600 .expect("Failed to create blobs bundle");
601
602 let blob_versioned_hashes = blobs_bundle.generate_versioned_hashes();
603
604 let tx = crate::types::transaction::EIP4844Transaction {
605 nonce: 3,
606 max_priority_fee_per_gas: 0,
607 max_fee_per_gas: 0,
608 max_fee_per_blob_gas: 0.into(),
609 gas: 15_000_000,
610 to: crate::Address::from_low_u64_be(1), value: crate::U256::zero(), data: crate::Bytes::default(), access_list: Default::default(), blob_versioned_hashes,
615 ..Default::default()
616 };
617
618 assert!(matches!(
619 blobs_bundle.validate(&tx, crate::types::Fork::Prague),
620 Err(crate::types::BlobsBundleError::MaxBlobsExceeded)
621 ));
622 }
623
624 #[test]
625 #[cfg(feature = "c-kzg")]
626 fn transaction_with_version_0_blobs_should_fail_on_amsterdam() {
627 let blobs = vec!["Hello, world!".as_bytes(), "Goodbye, world!".as_bytes()]
630 .into_iter()
631 .map(|data| {
632 crate::types::blobs_bundle::blob_from_bytes(data.into())
633 .expect("Failed to create blob")
634 })
635 .collect();
636
637 let blobs_bundle = crate::types::BlobsBundle::create_from_blobs(&blobs, None)
638 .expect("Failed to create blobs bundle");
639
640 let blob_versioned_hashes = blobs_bundle.generate_versioned_hashes();
641
642 let tx = crate::types::transaction::EIP4844Transaction {
643 nonce: 3,
644 max_priority_fee_per_gas: 0,
645 max_fee_per_gas: 0,
646 max_fee_per_blob_gas: 0.into(),
647 gas: 15_000_000,
648 to: crate::Address::from_low_u64_be(1), value: crate::U256::zero(), data: crate::Bytes::default(), access_list: Default::default(), blob_versioned_hashes,
653 ..Default::default()
654 };
655
656 assert!(matches!(
657 blobs_bundle.validate(&tx, crate::types::Fork::Amsterdam),
658 Err(crate::types::BlobsBundleError::InvalidBlobVersionForFork)
659 ));
660 }
661}