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 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), value: crate::U256::zero(), data: crate::Bytes::default(), access_list: Default::default(), 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), value: crate::U256::zero(), data: crate::Bytes::default(), access_list: Default::default(), 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), value: crate::U256::zero(), data: crate::Bytes::default(), access_list: Default::default(), 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 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), value: crate::U256::zero(), data: crate::Bytes::default(), access_list: Default::default(), 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 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), value: crate::U256::zero(), data: crate::Bytes::default(), access_list: Default::default(), 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), value: crate::U256::zero(), data: crate::Bytes::default(), access_list: Default::default(), 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 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), value: crate::U256::zero(), data: crate::Bytes::default(), access_list: Default::default(), 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}