1use crate::{
4 eip4844::{
5 kzg_to_versioned_hash, Blob, BlobAndProofV1, Bytes48, BYTES_PER_BLOB, BYTES_PER_COMMITMENT,
6 BYTES_PER_PROOF,
7 },
8 eip7594::{Decodable7594, Encodable7594},
9};
10use alloc::{boxed::Box, vec::Vec};
11use alloy_primitives::{bytes::BufMut, B256};
12use alloy_rlp::{Decodable, Encodable, Header};
13
14#[cfg(any(test, feature = "arbitrary"))]
15use crate::eip4844::MAX_BLOBS_PER_BLOCK_DENCUN;
16#[cfg(feature = "kzg")]
17use crate::eip4844::{AsAlloy, AsCkzg};
18
19#[cfg(feature = "kzg")]
21pub(crate) const VERSIONED_HASH_VERSION_KZG: u8 = 0x01;
22
23#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
25#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
26pub struct IndexedBlobHash {
27 pub index: u64,
29 pub hash: B256,
31}
32
33#[derive(Clone, Default, PartialEq, Eq, Hash)]
37#[repr(C)]
38#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
39#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))]
40#[doc(alias = "BlobTxSidecar")]
41pub struct BlobTransactionSidecar {
42 #[cfg_attr(feature = "serde", serde(deserialize_with = "crate::eip4844::deserialize_blobs"))]
44 pub blobs: Vec<Blob>,
45 pub commitments: Vec<Bytes48>,
47 pub proofs: Vec<Bytes48>,
49}
50
51impl core::fmt::Debug for BlobTransactionSidecar {
52 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
53 f.debug_struct("BlobTransactionSidecar")
54 .field("blobs", &self.blobs.len())
55 .field("commitments", &self.commitments)
56 .field("proofs", &self.proofs)
57 .finish()
58 }
59}
60
61impl BlobTransactionSidecar {
62 pub fn match_versioned_hashes<'a>(
68 &'a self,
69 versioned_hashes: &'a [B256],
70 ) -> impl Iterator<Item = (usize, BlobAndProofV1)> + 'a {
71 self.versioned_hashes().enumerate().flat_map(move |(i, blob_versioned_hash)| {
72 versioned_hashes.iter().enumerate().filter_map(move |(j, target_hash)| {
73 if blob_versioned_hash == *target_hash {
74 if let Some((blob, proof)) =
75 self.blobs.get(i).copied().zip(self.proofs.get(i).copied())
76 {
77 return Some((j, BlobAndProofV1 { blob: Box::new(blob), proof }));
78 }
79 }
80 None
81 })
82 })
83 }
84
85 #[cfg(feature = "kzg")]
90 pub fn try_into_7594(
91 self,
92 settings: &c_kzg::KzgSettings,
93 ) -> Result<crate::eip7594::BlobTransactionSidecarEip7594, c_kzg::Error> {
94 use crate::eip7594::CELLS_PER_EXT_BLOB;
95
96 if let [blob] = self.blobs.as_slice() {
97 let (_cells, kzg_proofs) = settings.compute_cells_and_kzg_proofs(blob.as_ckzg())?;
98 let cell_proofs = c_kzg::KzgProof::boxed_slice_as_alloy(kzg_proofs).into();
99 return Ok(crate::eip7594::BlobTransactionSidecarEip7594::new(
100 self.blobs,
101 self.commitments,
102 cell_proofs,
103 ));
104 }
105
106 let mut cell_proofs = Vec::with_capacity(self.blobs.len() * CELLS_PER_EXT_BLOB);
107
108 for blob in self.blobs.iter() {
109 let (_cells, kzg_proofs) = settings.compute_cells_and_kzg_proofs(blob.as_ckzg())?;
111 cell_proofs.extend_from_slice(c_kzg::KzgProof::slice_as_alloy(kzg_proofs.as_ref()));
112 }
113
114 Ok(crate::eip7594::BlobTransactionSidecarEip7594::new(
115 self.blobs,
116 self.commitments,
117 cell_proofs,
118 ))
119 }
120}
121
122impl IntoIterator for BlobTransactionSidecar {
123 type Item = BlobTransactionSidecarItem;
124 type IntoIter = alloc::vec::IntoIter<BlobTransactionSidecarItem>;
125
126 fn into_iter(self) -> Self::IntoIter {
127 self.blobs
128 .into_iter()
129 .zip(self.commitments)
130 .zip(self.proofs)
131 .enumerate()
132 .map(|(index, ((blob, commitment), proof))| BlobTransactionSidecarItem {
133 index: index as u64,
134 blob: Box::new(blob),
135 kzg_commitment: commitment,
136 kzg_proof: proof,
137 })
138 .collect::<Vec<_>>()
139 .into_iter()
140 }
141}
142
143#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
145#[repr(C)]
146#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
147pub struct BlobTransactionSidecarItem {
148 #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
150 pub index: u64,
151 #[cfg_attr(feature = "serde", serde(deserialize_with = "super::deserialize_blob"))]
153 pub blob: Box<Blob>,
154 pub kzg_commitment: Bytes48,
156 pub kzg_proof: Bytes48,
158}
159
160#[cfg(feature = "kzg")]
161impl BlobTransactionSidecarItem {
162 pub fn to_kzg_versioned_hash(&self) -> [u8; 32] {
164 use sha2::Digest;
165 let commitment = self.kzg_commitment.as_slice();
166 let mut hash: [u8; 32] = sha2::Sha256::digest(commitment).into();
167 hash[0] = VERSIONED_HASH_VERSION_KZG;
168 hash
169 }
170
171 pub fn verify_blob_kzg_proof(&self) -> Result<(), BlobTransactionValidationError> {
173 let binding = crate::eip4844::env_settings::EnvKzgSettings::Default;
174 let settings = binding.get();
175
176 let blob = c_kzg::Blob::from_bytes(self.blob.as_slice())
177 .map_err(BlobTransactionValidationError::KZGError)?;
178
179 let commitment = c_kzg::Bytes48::from_bytes(self.kzg_commitment.as_slice())
180 .map_err(BlobTransactionValidationError::KZGError)?;
181
182 let proof = c_kzg::Bytes48::from_bytes(self.kzg_proof.as_slice())
183 .map_err(BlobTransactionValidationError::KZGError)?;
184
185 let result = settings
186 .verify_blob_kzg_proof(&blob, &commitment, &proof)
187 .map_err(BlobTransactionValidationError::KZGError)?;
188
189 result.then_some(()).ok_or(BlobTransactionValidationError::InvalidProof)
190 }
191
192 pub fn verify_blob(
194 &self,
195 hash: &IndexedBlobHash,
196 ) -> Result<(), BlobTransactionValidationError> {
197 if self.index != hash.index {
198 let blob_hash_part = B256::from_slice(&self.blob[0..32]);
199 return Err(BlobTransactionValidationError::WrongVersionedHash {
200 have: blob_hash_part,
201 expected: hash.hash,
202 });
203 }
204
205 let computed_hash = self.to_kzg_versioned_hash();
206 if computed_hash != hash.hash {
207 return Err(BlobTransactionValidationError::WrongVersionedHash {
208 have: computed_hash.into(),
209 expected: hash.hash,
210 });
211 }
212
213 self.verify_blob_kzg_proof()
214 }
215}
216
217#[cfg(any(test, feature = "arbitrary"))]
218impl<'a> arbitrary::Arbitrary<'a> for BlobTransactionSidecar {
219 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
220 let num_blobs = u.int_in_range(1..=MAX_BLOBS_PER_BLOCK_DENCUN)?;
221 let mut blobs = Vec::with_capacity(num_blobs);
222 for _ in 0..num_blobs {
223 blobs.push(Blob::arbitrary(u)?);
224 }
225
226 let mut commitments = Vec::with_capacity(num_blobs);
227 let mut proofs = Vec::with_capacity(num_blobs);
228 for _ in 0..num_blobs {
229 commitments.push(Bytes48::arbitrary(u)?);
230 proofs.push(Bytes48::arbitrary(u)?);
231 }
232
233 Ok(Self { blobs, commitments, proofs })
234 }
235}
236
237impl BlobTransactionSidecar {
238 pub const fn new(blobs: Vec<Blob>, commitments: Vec<Bytes48>, proofs: Vec<Bytes48>) -> Self {
240 Self { blobs, commitments, proofs }
241 }
242
243 #[inline]
245 pub fn shrink_to_fit(&mut self) {
246 self.blobs.shrink_to_fit();
247 self.commitments.shrink_to_fit();
248 self.proofs.shrink_to_fit();
249 }
250
251 #[cfg(feature = "kzg")]
253 pub fn from_kzg(
254 blobs: Vec<c_kzg::Blob>,
255 commitments: Vec<c_kzg::Bytes48>,
256 proofs: Vec<c_kzg::Bytes48>,
257 ) -> Self {
258 let blobs = Blob::vec_from_ckzg(blobs);
259 let commitments = Bytes48::vec_from_ckzg(commitments);
260 let proofs = Bytes48::vec_from_ckzg(proofs);
261 Self { blobs, commitments, proofs }
262 }
263
264 #[cfg(feature = "kzg")]
278 pub fn validate(
279 &self,
280 blob_versioned_hashes: &[B256],
281 proof_settings: &c_kzg::KzgSettings,
282 ) -> Result<(), BlobTransactionValidationError> {
283 if blob_versioned_hashes.len() != self.commitments.len() {
285 return Err(c_kzg::Error::MismatchLength(format!(
286 "There are {} versioned commitment hashes and {} commitments",
287 blob_versioned_hashes.len(),
288 self.commitments.len()
289 ))
290 .into());
291 }
292
293 for (versioned_hash, commitment) in
295 blob_versioned_hashes.iter().zip(self.commitments.iter())
296 {
297 let calculated_versioned_hash = kzg_to_versioned_hash(commitment.as_slice());
299 if *versioned_hash != calculated_versioned_hash {
300 return Err(BlobTransactionValidationError::WrongVersionedHash {
301 have: *versioned_hash,
302 expected: calculated_versioned_hash,
303 });
304 }
305 }
306
307 let res = proof_settings
308 .verify_blob_kzg_proof_batch(
309 Blob::slice_as_ckzg(self.blobs.as_slice()),
310 Bytes48::slice_as_ckzg(self.commitments.as_slice()),
311 Bytes48::slice_as_ckzg(self.proofs.as_slice()),
312 )
313 .map_err(BlobTransactionValidationError::KZGError)?;
314
315 res.then_some(()).ok_or(BlobTransactionValidationError::InvalidProof)
316 }
317
318 pub fn versioned_hashes(&self) -> VersionedHashIter<'_> {
320 VersionedHashIter::new(&self.commitments)
321 }
322
323 pub fn versioned_hash_for_blob(&self, blob_index: usize) -> Option<B256> {
326 self.commitments.get(blob_index).map(|c| kzg_to_versioned_hash(c.as_slice()))
327 }
328
329 pub fn versioned_hash_index(&self, hash: &B256) -> Option<usize> {
331 self.commitments
332 .iter()
333 .position(|commitment| kzg_to_versioned_hash(commitment.as_slice()) == *hash)
334 }
335
336 pub fn blob_by_versioned_hash(&self, hash: &B256) -> Option<&Blob> {
338 self.versioned_hash_index(hash).and_then(|index| self.blobs.get(index))
339 }
340
341 #[inline]
343 pub const fn size(&self) -> usize {
344 self.blobs.len() * BYTES_PER_BLOB + self.commitments.len() * BYTES_PER_COMMITMENT + self.proofs.len() * BYTES_PER_PROOF }
348
349 #[cfg(all(feature = "kzg", any(test, feature = "arbitrary")))]
353 pub fn try_from_blobs_hex<I, B>(blobs: I) -> Result<Self, c_kzg::Error>
354 where
355 I: IntoIterator<Item = B>,
356 B: AsRef<str>,
357 {
358 let mut converted = Vec::new();
359 for blob in blobs {
360 converted.push(crate::eip4844::utils::hex_to_blob(blob)?);
361 }
362 Self::try_from_blobs(converted)
363 }
364
365 #[cfg(all(feature = "kzg", any(test, feature = "arbitrary")))]
370 pub fn try_from_blobs_bytes<I, B>(blobs: I) -> Result<Self, c_kzg::Error>
371 where
372 I: IntoIterator<Item = B>,
373 B: AsRef<[u8]>,
374 {
375 let mut converted = Vec::new();
376 for blob in blobs {
377 converted.push(crate::eip4844::utils::bytes_to_blob(blob)?);
378 }
379 Self::try_from_blobs(converted)
380 }
381
382 #[cfg(feature = "kzg")]
385 pub fn try_from_blobs_with_settings(
386 blobs: Vec<Blob>,
387 settings: &c_kzg::KzgSettings,
388 ) -> Result<Self, c_kzg::Error> {
389 let mut commitments = Vec::with_capacity(blobs.len());
390 let mut proofs = Vec::with_capacity(blobs.len());
391 for blob in &blobs {
392 let blob = blob.as_ckzg();
393 let commitment = settings.blob_to_kzg_commitment(blob)?;
394 let proof = settings.compute_blob_kzg_proof(blob, &commitment.to_bytes())?;
395
396 commitments.push(Bytes48::from_ckzg(commitment.to_bytes()));
397 proofs.push(Bytes48::from_ckzg(proof.to_bytes()));
398 }
399
400 Ok(Self::new(blobs, commitments, proofs))
401 }
402
403 #[cfg(all(feature = "kzg", any(test, feature = "arbitrary")))]
408 pub fn try_from_blobs(blobs: Vec<Blob>) -> Result<Self, c_kzg::Error> {
409 use crate::eip4844::env_settings::EnvKzgSettings;
410
411 Self::try_from_blobs_with_settings(blobs, EnvKzgSettings::Default.get())
412 }
413
414 #[doc(hidden)]
417 pub fn rlp_encoded_fields_length(&self) -> usize {
418 self.blobs.length() + self.commitments.length() + self.proofs.length()
419 }
420
421 #[inline]
428 #[doc(hidden)]
429 pub fn rlp_encode_fields(&self, out: &mut dyn BufMut) {
430 self.blobs.encode(out);
432 self.commitments.encode(out);
433 self.proofs.encode(out);
434 }
435
436 fn rlp_header(&self) -> Header {
438 Header { list: true, payload_length: self.rlp_encoded_fields_length() }
439 }
440
441 pub fn rlp_encoded_length(&self) -> usize {
444 self.rlp_header().length() + self.rlp_encoded_fields_length()
445 }
446
447 pub fn rlp_encode(&self, out: &mut dyn BufMut) {
449 self.rlp_header().encode(out);
450 self.rlp_encode_fields(out);
451 }
452
453 #[doc(hidden)]
455 pub fn rlp_decode_fields(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
456 Ok(Self {
457 blobs: Decodable::decode(buf)?,
458 commitments: Decodable::decode(buf)?,
459 proofs: Decodable::decode(buf)?,
460 })
461 }
462
463 pub fn rlp_decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
465 let header = Header::decode(buf)?;
466 if !header.list {
467 return Err(alloy_rlp::Error::UnexpectedString);
468 }
469 if buf.len() < header.payload_length {
470 return Err(alloy_rlp::Error::InputTooShort);
471 }
472 let remaining = buf.len();
473 let this = Self::rlp_decode_fields(buf)?;
474
475 if buf.len() + header.payload_length != remaining {
476 return Err(alloy_rlp::Error::UnexpectedLength);
477 }
478
479 Ok(this)
480 }
481}
482
483impl Encodable for BlobTransactionSidecar {
484 fn encode(&self, out: &mut dyn BufMut) {
486 self.rlp_encode(out);
487 }
488
489 fn length(&self) -> usize {
490 self.rlp_encoded_length()
491 }
492}
493
494impl Decodable for BlobTransactionSidecar {
495 fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
497 Self::rlp_decode(buf)
498 }
499}
500
501impl Encodable7594 for BlobTransactionSidecar {
502 fn encode_7594_len(&self) -> usize {
503 self.rlp_encoded_fields_length()
504 }
505
506 fn encode_7594(&self, out: &mut dyn BufMut) {
507 self.rlp_encode_fields(out);
508 }
509}
510
511impl Decodable7594 for BlobTransactionSidecar {
512 fn decode_7594(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
513 Self::rlp_decode_fields(buf)
514 }
515}
516
517#[cfg(all(debug_assertions, feature = "serde"))]
521pub(crate) fn deserialize_blobs_map<'de, M: serde::de::MapAccess<'de>>(
522 map_access: &mut M,
523) -> Result<Vec<Blob>, M::Error> {
524 let raw_blobs: Vec<alloy_primitives::Bytes> = map_access.next_value()?;
525 let mut blobs = Vec::with_capacity(raw_blobs.len());
526 for blob in raw_blobs {
527 blobs.push(Blob::try_from(blob.as_ref()).map_err(serde::de::Error::custom)?);
528 }
529 Ok(blobs)
530}
531
532#[cfg(all(not(debug_assertions), feature = "serde"))]
533#[inline(always)]
534pub(crate) fn deserialize_blobs_map<'de, M: serde::de::MapAccess<'de>>(
535 map_access: &mut M,
536) -> Result<Vec<Blob>, M::Error> {
537 map_access.next_value()
538}
539
540#[derive(Debug)]
542#[cfg(feature = "kzg")]
543pub enum BlobTransactionValidationError {
544 InvalidProof,
546 KZGError(c_kzg::Error),
548 NotBlobTransaction(u8),
550 MissingSidecar,
552 WrongVersionedHash {
554 have: B256,
556 expected: B256,
558 },
559}
560
561#[cfg(feature = "kzg")]
562impl core::error::Error for BlobTransactionValidationError {}
563
564#[cfg(feature = "kzg")]
565impl core::fmt::Display for BlobTransactionValidationError {
566 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
567 match self {
568 Self::InvalidProof => f.write_str("invalid KZG proof"),
569 Self::KZGError(err) => {
570 write!(f, "KZG error: {err:?}")
571 }
572 Self::NotBlobTransaction(err) => {
573 write!(f, "unable to verify proof for non blob transaction: {err}")
574 }
575 Self::MissingSidecar => {
576 f.write_str("eip4844 tx variant without sidecar being used for verification.")
577 }
578 Self::WrongVersionedHash { have, expected } => {
579 write!(f, "wrong versioned hash: have {have}, expected {expected}")
580 }
581 }
582 }
583}
584
585#[cfg(feature = "kzg")]
586impl From<c_kzg::Error> for BlobTransactionValidationError {
587 fn from(source: c_kzg::Error) -> Self {
588 Self::KZGError(source)
589 }
590}
591
592#[derive(Debug, Clone)]
594pub struct VersionedHashIter<'a> {
595 commitments: core::slice::Iter<'a, Bytes48>,
597}
598
599impl<'a> Iterator for VersionedHashIter<'a> {
600 type Item = B256;
601
602 fn next(&mut self) -> Option<Self::Item> {
603 self.commitments.next().map(|c| kzg_to_versioned_hash(c.as_slice()))
604 }
605}
606
607impl<'a> VersionedHashIter<'a> {
609 pub fn new(commitments: &'a [Bytes48]) -> Self {
611 Self { commitments: commitments.iter() }
612 }
613}
614
615#[cfg(test)]
616mod tests {
617 use super::*;
618 use arbitrary::Arbitrary;
619
620 #[test]
621 #[cfg(feature = "serde")]
622 fn deserialize_blob() {
623 let blob = BlobTransactionSidecar {
624 blobs: vec![Blob::default(), Blob::default(), Blob::default(), Blob::default()],
625 commitments: vec![
626 Bytes48::default(),
627 Bytes48::default(),
628 Bytes48::default(),
629 Bytes48::default(),
630 ],
631 proofs: vec![
632 Bytes48::default(),
633 Bytes48::default(),
634 Bytes48::default(),
635 Bytes48::default(),
636 ],
637 };
638
639 let s = serde_json::to_string(&blob).unwrap();
640 let deserialized: BlobTransactionSidecar = serde_json::from_str(&s).unwrap();
641 assert_eq!(blob, deserialized);
642 }
643
644 #[test]
645 fn test_arbitrary_blob() {
646 let mut unstructured = arbitrary::Unstructured::new(b"unstructured blob");
647 let _blob = BlobTransactionSidecar::arbitrary(&mut unstructured).unwrap();
648 }
649
650 #[test]
651 #[cfg(feature = "serde")]
652 fn test_blob_item_serde_roundtrip() {
653 let blob_item = BlobTransactionSidecarItem {
654 index: 0,
655 blob: Box::new(Blob::default()),
656 kzg_commitment: Bytes48::default(),
657 kzg_proof: Bytes48::default(),
658 };
659
660 let s = serde_json::to_string(&blob_item).unwrap();
661 let deserialized: BlobTransactionSidecarItem = serde_json::from_str(&s).unwrap();
662 assert_eq!(blob_item, deserialized);
663 }
664}