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
17#[cfg(feature = "kzg")]
19pub(crate) const VERSIONED_HASH_VERSION_KZG: u8 = 0x01;
20
21#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
23#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
24pub struct IndexedBlobHash {
25 pub index: u64,
27 pub hash: B256,
29}
30
31#[derive(Clone, Default, PartialEq, Eq, Hash)]
35#[repr(C)]
36#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
37#[doc(alias = "BlobTxSidecar")]
38pub struct BlobTransactionSidecar {
39 #[cfg_attr(feature = "serde", serde(deserialize_with = "deserialize_blobs"))]
41 pub blobs: Vec<Blob>,
42 pub commitments: Vec<Bytes48>,
44 pub proofs: Vec<Bytes48>,
46}
47
48impl core::fmt::Debug for BlobTransactionSidecar {
49 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
50 f.debug_struct("BlobTransactionSidecar")
51 .field("blobs", &self.blobs.len())
52 .field("commitments", &self.commitments)
53 .field("proofs", &self.proofs)
54 .finish()
55 }
56}
57
58impl BlobTransactionSidecar {
59 pub fn match_versioned_hashes<'a>(
65 &'a self,
66 versioned_hashes: &'a [B256],
67 ) -> impl Iterator<Item = (usize, BlobAndProofV1)> + 'a {
68 self.versioned_hashes().enumerate().flat_map(move |(i, blob_versioned_hash)| {
69 versioned_hashes.iter().enumerate().filter_map(move |(j, target_hash)| {
70 if blob_versioned_hash == *target_hash {
71 if let Some((blob, proof)) =
72 self.blobs.get(i).copied().zip(self.proofs.get(i).copied())
73 {
74 return Some((j, BlobAndProofV1 { blob: Box::new(blob), proof }));
75 }
76 }
77 None
78 })
79 })
80 }
81}
82
83impl IntoIterator for BlobTransactionSidecar {
84 type Item = BlobTransactionSidecarItem;
85 type IntoIter = alloc::vec::IntoIter<BlobTransactionSidecarItem>;
86
87 fn into_iter(self) -> Self::IntoIter {
88 self.blobs
89 .into_iter()
90 .zip(self.commitments)
91 .zip(self.proofs)
92 .enumerate()
93 .map(|(index, ((blob, commitment), proof))| BlobTransactionSidecarItem {
94 index: index as u64,
95 blob: Box::new(blob),
96 kzg_commitment: commitment,
97 kzg_proof: proof,
98 })
99 .collect::<Vec<_>>()
100 .into_iter()
101 }
102}
103
104#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
106#[repr(C)]
107#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
108pub struct BlobTransactionSidecarItem {
109 #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
111 pub index: u64,
112 #[cfg_attr(feature = "serde", serde(deserialize_with = "super::deserialize_blob"))]
114 pub blob: Box<Blob>,
115 pub kzg_commitment: Bytes48,
117 pub kzg_proof: Bytes48,
119}
120
121#[cfg(feature = "kzg")]
122impl BlobTransactionSidecarItem {
123 pub fn to_kzg_versioned_hash(&self) -> [u8; 32] {
125 use sha2::Digest;
126 let commitment = self.kzg_commitment.as_slice();
127 let mut hash: [u8; 32] = sha2::Sha256::digest(commitment).into();
128 hash[0] = VERSIONED_HASH_VERSION_KZG;
129 hash
130 }
131
132 pub fn verify_blob_kzg_proof(&self) -> Result<(), BlobTransactionValidationError> {
134 let binding = crate::eip4844::env_settings::EnvKzgSettings::Default;
135 let settings = binding.get();
136
137 let blob = c_kzg::Blob::from_bytes(self.blob.as_slice())
138 .map_err(BlobTransactionValidationError::KZGError)?;
139
140 let commitment = c_kzg::Bytes48::from_bytes(self.kzg_commitment.as_slice())
141 .map_err(BlobTransactionValidationError::KZGError)?;
142
143 let proof = c_kzg::Bytes48::from_bytes(self.kzg_proof.as_slice())
144 .map_err(BlobTransactionValidationError::KZGError)?;
145
146 let result = settings
147 .verify_blob_kzg_proof(&blob, &commitment, &proof)
148 .map_err(BlobTransactionValidationError::KZGError)?;
149
150 result.then_some(()).ok_or(BlobTransactionValidationError::InvalidProof)
151 }
152
153 pub fn verify_blob(
155 &self,
156 hash: &IndexedBlobHash,
157 ) -> Result<(), BlobTransactionValidationError> {
158 if self.index != hash.index {
159 let blob_hash_part = B256::from_slice(&self.blob[0..32]);
160 return Err(BlobTransactionValidationError::WrongVersionedHash {
161 have: blob_hash_part,
162 expected: hash.hash,
163 });
164 }
165
166 let computed_hash = self.to_kzg_versioned_hash();
167 if computed_hash != hash.hash {
168 return Err(BlobTransactionValidationError::WrongVersionedHash {
169 have: computed_hash.into(),
170 expected: hash.hash,
171 });
172 }
173
174 self.verify_blob_kzg_proof()
175 }
176}
177
178#[cfg(any(test, feature = "arbitrary"))]
179impl<'a> arbitrary::Arbitrary<'a> for BlobTransactionSidecar {
180 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
181 let num_blobs = u.int_in_range(1..=MAX_BLOBS_PER_BLOCK_DENCUN)?;
182 let mut blobs = Vec::with_capacity(num_blobs);
183 for _ in 0..num_blobs {
184 blobs.push(Blob::arbitrary(u)?);
185 }
186
187 let mut commitments = Vec::with_capacity(num_blobs);
188 let mut proofs = Vec::with_capacity(num_blobs);
189 for _ in 0..num_blobs {
190 commitments.push(Bytes48::arbitrary(u)?);
191 proofs.push(Bytes48::arbitrary(u)?);
192 }
193
194 Ok(Self { blobs, commitments, proofs })
195 }
196}
197
198impl BlobTransactionSidecar {
199 pub const fn new(blobs: Vec<Blob>, commitments: Vec<Bytes48>, proofs: Vec<Bytes48>) -> Self {
201 Self { blobs, commitments, proofs }
202 }
203
204 #[cfg(feature = "kzg")]
206 pub fn from_kzg(
207 blobs: Vec<c_kzg::Blob>,
208 commitments: Vec<c_kzg::Bytes48>,
209 proofs: Vec<c_kzg::Bytes48>,
210 ) -> Self {
211 unsafe fn transmute_vec<U, T>(input: Vec<T>) -> Vec<U> {
213 let mut v = core::mem::ManuallyDrop::new(input);
214 Vec::from_raw_parts(v.as_mut_ptr() as *mut U, v.len(), v.capacity())
215 }
216
217 unsafe {
219 let blobs = transmute_vec::<Blob, c_kzg::Blob>(blobs);
220 let commitments = transmute_vec::<Bytes48, c_kzg::Bytes48>(commitments);
221 let proofs = transmute_vec::<Bytes48, c_kzg::Bytes48>(proofs);
222 Self { blobs, commitments, proofs }
223 }
224 }
225
226 #[cfg(feature = "kzg")]
240 pub fn validate(
241 &self,
242 blob_versioned_hashes: &[B256],
243 proof_settings: &c_kzg::KzgSettings,
244 ) -> Result<(), BlobTransactionValidationError> {
245 if blob_versioned_hashes.len() != self.commitments.len() {
247 return Err(c_kzg::Error::MismatchLength(format!(
248 "There are {} versioned commitment hashes and {} commitments",
249 blob_versioned_hashes.len(),
250 self.commitments.len()
251 ))
252 .into());
253 }
254
255 for (versioned_hash, commitment) in
257 blob_versioned_hashes.iter().zip(self.commitments.iter())
258 {
259 let calculated_versioned_hash = kzg_to_versioned_hash(commitment.as_slice());
261 if *versioned_hash != calculated_versioned_hash {
262 return Err(BlobTransactionValidationError::WrongVersionedHash {
263 have: *versioned_hash,
264 expected: calculated_versioned_hash,
265 });
266 }
267 }
268
269 let res = unsafe {
271 proof_settings.verify_blob_kzg_proof_batch(
272 core::mem::transmute::<&[Blob], &[c_kzg::Blob]>(self.blobs.as_slice()),
274 core::mem::transmute::<&[Bytes48], &[c_kzg::Bytes48]>(self.commitments.as_slice()),
276 core::mem::transmute::<&[Bytes48], &[c_kzg::Bytes48]>(self.proofs.as_slice()),
278 )
279 }
280 .map_err(BlobTransactionValidationError::KZGError)?;
281
282 res.then_some(()).ok_or(BlobTransactionValidationError::InvalidProof)
283 }
284
285 pub fn versioned_hashes(&self) -> VersionedHashIter<'_> {
287 VersionedHashIter::new(&self.commitments)
288 }
289
290 pub fn versioned_hash_for_blob(&self, blob_index: usize) -> Option<B256> {
293 self.commitments.get(blob_index).map(|c| kzg_to_versioned_hash(c.as_slice()))
294 }
295
296 pub fn versioned_hash_index(&self, hash: &B256) -> Option<usize> {
298 self.commitments
299 .iter()
300 .position(|commitment| kzg_to_versioned_hash(commitment.as_slice()) == *hash)
301 }
302
303 pub fn blob_by_versioned_hash(&self, hash: &B256) -> Option<&Blob> {
305 self.versioned_hash_index(hash).and_then(|index| self.blobs.get(index))
306 }
307
308 #[inline]
310 pub fn size(&self) -> usize {
311 self.blobs.len() * BYTES_PER_BLOB + self.commitments.len() * BYTES_PER_COMMITMENT + self.proofs.len() * BYTES_PER_PROOF }
315
316 #[cfg(all(feature = "kzg", any(test, feature = "arbitrary")))]
320 pub fn try_from_blobs_hex<I, B>(blobs: I) -> Result<Self, c_kzg::Error>
321 where
322 I: IntoIterator<Item = B>,
323 B: AsRef<str>,
324 {
325 let mut b = Vec::new();
326 for blob in blobs {
327 b.push(c_kzg::Blob::from_hex(blob.as_ref())?)
328 }
329 Self::try_from_blobs(b)
330 }
331
332 #[cfg(all(feature = "kzg", any(test, feature = "arbitrary")))]
336 pub fn try_from_blobs_bytes<I, B>(blobs: I) -> Result<Self, c_kzg::Error>
337 where
338 I: IntoIterator<Item = B>,
339 B: AsRef<[u8]>,
340 {
341 let mut b = Vec::new();
342 for blob in blobs {
343 b.push(c_kzg::Blob::from_bytes(blob.as_ref())?)
344 }
345 Self::try_from_blobs(b)
346 }
347
348 #[cfg(all(feature = "kzg", any(test, feature = "arbitrary")))]
353 pub fn try_from_blobs(blobs: Vec<c_kzg::Blob>) -> Result<Self, c_kzg::Error> {
354 use crate::eip4844::env_settings::EnvKzgSettings;
355
356 let kzg_settings = EnvKzgSettings::Default;
357
358 let commitments = blobs
359 .iter()
360 .map(|blob| {
361 kzg_settings.get().blob_to_kzg_commitment(&blob.clone()).map(|blob| blob.to_bytes())
362 })
363 .collect::<Result<Vec<_>, _>>()?;
364
365 let proofs = blobs
366 .iter()
367 .zip(commitments.iter())
368 .map(|(blob, commitment)| {
369 kzg_settings
370 .get()
371 .compute_blob_kzg_proof(blob, commitment)
372 .map(|blob| blob.to_bytes())
373 })
374 .collect::<Result<Vec<_>, _>>()?;
375
376 Ok(Self::from_kzg(blobs, commitments, proofs))
377 }
378
379 #[doc(hidden)]
382 pub fn rlp_encoded_fields_length(&self) -> usize {
383 self.blobs.length() + self.commitments.length() + self.proofs.length()
384 }
385
386 #[inline]
393 #[doc(hidden)]
394 pub fn rlp_encode_fields(&self, out: &mut dyn BufMut) {
395 self.blobs.encode(out);
397 self.commitments.encode(out);
398 self.proofs.encode(out);
399 }
400
401 fn rlp_header(&self) -> Header {
403 Header { list: true, payload_length: self.rlp_encoded_fields_length() }
404 }
405
406 pub fn rlp_encoded_length(&self) -> usize {
409 self.rlp_header().length() + self.rlp_encoded_fields_length()
410 }
411
412 pub fn rlp_encode(&self, out: &mut dyn BufMut) {
414 self.rlp_header().encode(out);
415 self.rlp_encode_fields(out);
416 }
417
418 #[doc(hidden)]
420 pub fn rlp_decode_fields(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
421 Ok(Self {
422 blobs: Decodable::decode(buf)?,
423 commitments: Decodable::decode(buf)?,
424 proofs: Decodable::decode(buf)?,
425 })
426 }
427
428 pub fn rlp_decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
430 let header = Header::decode(buf)?;
431 if !header.list {
432 return Err(alloy_rlp::Error::UnexpectedString);
433 }
434 if buf.len() < header.payload_length {
435 return Err(alloy_rlp::Error::InputTooShort);
436 }
437 let remaining = buf.len();
438 let this = Self::rlp_decode_fields(buf)?;
439
440 if buf.len() + header.payload_length != remaining {
441 return Err(alloy_rlp::Error::UnexpectedLength);
442 }
443
444 Ok(this)
445 }
446}
447
448impl Encodable for BlobTransactionSidecar {
449 fn encode(&self, out: &mut dyn BufMut) {
451 self.rlp_encode(out);
452 }
453
454 fn length(&self) -> usize {
455 self.rlp_encoded_length()
456 }
457}
458
459impl Decodable for BlobTransactionSidecar {
460 fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
462 Self::rlp_decode(buf)
463 }
464}
465
466impl Encodable7594 for BlobTransactionSidecar {
467 fn encode_7594_len(&self) -> usize {
468 self.rlp_encoded_fields_length()
469 }
470
471 fn encode_7594(&self, out: &mut dyn BufMut) {
472 self.rlp_encode_fields(out);
473 }
474}
475
476impl Decodable7594 for BlobTransactionSidecar {
477 fn decode_7594(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
478 Self::rlp_decode_fields(buf)
479 }
480}
481
482#[cfg(all(debug_assertions, feature = "serde"))]
484pub(crate) fn deserialize_blobs<'de, D>(deserializer: D) -> Result<Vec<Blob>, D::Error>
485where
486 D: serde::de::Deserializer<'de>,
487{
488 use serde::Deserialize;
489
490 let raw_blobs = Vec::<alloy_primitives::Bytes>::deserialize(deserializer)?;
491 let mut blobs = Vec::with_capacity(raw_blobs.len());
492 for blob in raw_blobs {
493 blobs.push(Blob::try_from(blob.as_ref()).map_err(serde::de::Error::custom)?);
494 }
495 Ok(blobs)
496}
497
498#[cfg(all(not(debug_assertions), feature = "serde"))]
499#[inline(always)]
500pub(crate) fn deserialize_blobs<'de, D>(deserializer: D) -> Result<Vec<Blob>, D::Error>
501where
502 D: serde::de::Deserializer<'de>,
503{
504 use serde::Deserialize;
505 Vec::<Blob>::deserialize(deserializer)
506}
507
508#[cfg(all(debug_assertions, feature = "serde"))]
512pub(crate) fn deserialize_blobs_map<'de, M: serde::de::MapAccess<'de>>(
513 map_access: &mut M,
514) -> Result<Vec<Blob>, M::Error> {
515 let raw_blobs: Vec<alloy_primitives::Bytes> = map_access.next_value()?;
516 let mut blobs = Vec::with_capacity(raw_blobs.len());
517 for blob in raw_blobs {
518 blobs.push(Blob::try_from(blob.as_ref()).map_err(serde::de::Error::custom)?);
519 }
520 Ok(blobs)
521}
522
523#[cfg(all(not(debug_assertions), feature = "serde"))]
524#[inline(always)]
525pub(crate) fn deserialize_blobs_map<'de, M: serde::de::MapAccess<'de>>(
526 map_access: &mut M,
527) -> Result<Vec<Blob>, M::Error> {
528 use serde::de::MapAccess;
529 map_access.next_value()
530}
531
532#[derive(Debug)]
534#[cfg(feature = "kzg")]
535pub enum BlobTransactionValidationError {
536 InvalidProof,
538 KZGError(c_kzg::Error),
540 NotBlobTransaction(u8),
542 MissingSidecar,
544 WrongVersionedHash {
546 have: B256,
548 expected: B256,
550 },
551}
552
553#[cfg(feature = "kzg")]
554impl core::error::Error for BlobTransactionValidationError {}
555
556#[cfg(feature = "kzg")]
557impl core::fmt::Display for BlobTransactionValidationError {
558 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
559 match self {
560 Self::InvalidProof => f.write_str("invalid KZG proof"),
561 Self::KZGError(err) => {
562 write!(f, "KZG error: {err:?}")
563 }
564 Self::NotBlobTransaction(err) => {
565 write!(f, "unable to verify proof for non blob transaction: {err}")
566 }
567 Self::MissingSidecar => {
568 f.write_str("eip4844 tx variant without sidecar being used for verification.")
569 }
570 Self::WrongVersionedHash { have, expected } => {
571 write!(f, "wrong versioned hash: have {have}, expected {expected}")
572 }
573 }
574 }
575}
576
577#[cfg(feature = "kzg")]
578impl From<c_kzg::Error> for BlobTransactionValidationError {
579 fn from(source: c_kzg::Error) -> Self {
580 Self::KZGError(source)
581 }
582}
583
584#[derive(Debug, Clone)]
586pub struct VersionedHashIter<'a> {
587 commitments: core::slice::Iter<'a, Bytes48>,
589}
590
591impl<'a> Iterator for VersionedHashIter<'a> {
592 type Item = B256;
593
594 fn next(&mut self) -> Option<Self::Item> {
595 self.commitments.next().map(|c| kzg_to_versioned_hash(c.as_slice()))
596 }
597}
598
599impl<'a> VersionedHashIter<'a> {
601 pub fn new(commitments: &'a [Bytes48]) -> Self {
603 Self { commitments: commitments.iter() }
604 }
605}
606
607#[cfg(test)]
608mod tests {
609 use super::*;
610 use arbitrary::Arbitrary;
611
612 #[test]
613 #[cfg(feature = "serde")]
614 fn deserialize_blob() {
615 let blob = BlobTransactionSidecar {
616 blobs: vec![Blob::default(), Blob::default(), Blob::default(), Blob::default()],
617 commitments: vec![
618 Bytes48::default(),
619 Bytes48::default(),
620 Bytes48::default(),
621 Bytes48::default(),
622 ],
623 proofs: vec![
624 Bytes48::default(),
625 Bytes48::default(),
626 Bytes48::default(),
627 Bytes48::default(),
628 ],
629 };
630
631 let s = serde_json::to_string(&blob).unwrap();
632 let deserialized: BlobTransactionSidecar = serde_json::from_str(&s).unwrap();
633 assert_eq!(blob, deserialized);
634 }
635
636 #[test]
637 fn test_arbitrary_blob() {
638 let mut unstructured = arbitrary::Unstructured::new(b"unstructured blob");
639 let _blob = BlobTransactionSidecar::arbitrary(&mut unstructured).unwrap();
640 }
641
642 #[test]
643 #[cfg(feature = "serde")]
644 fn test_blob_item_serde_roundtrip() {
645 let blob_item = BlobTransactionSidecarItem {
646 index: 0,
647 blob: Box::new(Blob::default()),
648 kzg_commitment: Bytes48::default(),
649 kzg_proof: Bytes48::default(),
650 };
651
652 let s = serde_json::to_string(&blob_item).unwrap();
653 let deserialized: BlobTransactionSidecarItem = serde_json::from_str(&s).unwrap();
654 assert_eq!(blob_item, deserialized);
655 }
656}