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(
41 all(debug_assertions, feature = "serde"),
42 serde(deserialize_with = "deserialize_blobs")
43 )]
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
86impl IntoIterator for BlobTransactionSidecar {
87 type Item = BlobTransactionSidecarItem;
88 type IntoIter = alloc::vec::IntoIter<BlobTransactionSidecarItem>;
89
90 fn into_iter(self) -> Self::IntoIter {
91 self.blobs
92 .into_iter()
93 .zip(self.commitments)
94 .zip(self.proofs)
95 .enumerate()
96 .map(|(index, ((blob, commitment), proof))| BlobTransactionSidecarItem {
97 index: index as u64,
98 blob: Box::new(blob),
99 kzg_commitment: commitment,
100 kzg_proof: proof,
101 })
102 .collect::<Vec<_>>()
103 .into_iter()
104 }
105}
106
107#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
109#[repr(C)]
110#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
111pub struct BlobTransactionSidecarItem {
112 #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
114 pub index: u64,
115 #[cfg_attr(feature = "serde", serde(deserialize_with = "super::deserialize_blob"))]
117 pub blob: Box<Blob>,
118 pub kzg_commitment: Bytes48,
120 pub kzg_proof: Bytes48,
122}
123
124#[cfg(feature = "kzg")]
125impl BlobTransactionSidecarItem {
126 pub fn to_kzg_versioned_hash(&self) -> [u8; 32] {
128 use sha2::Digest;
129 let commitment = self.kzg_commitment.as_slice();
130 let mut hash: [u8; 32] = sha2::Sha256::digest(commitment).into();
131 hash[0] = VERSIONED_HASH_VERSION_KZG;
132 hash
133 }
134
135 pub fn verify_blob_kzg_proof(&self) -> Result<(), BlobTransactionValidationError> {
137 let binding = crate::eip4844::env_settings::EnvKzgSettings::Default;
138 let settings = binding.get();
139
140 let blob = c_kzg::Blob::from_bytes(self.blob.as_slice())
141 .map_err(BlobTransactionValidationError::KZGError)?;
142
143 let commitment = c_kzg::Bytes48::from_bytes(self.kzg_commitment.as_slice())
144 .map_err(BlobTransactionValidationError::KZGError)?;
145
146 let proof = c_kzg::Bytes48::from_bytes(self.kzg_proof.as_slice())
147 .map_err(BlobTransactionValidationError::KZGError)?;
148
149 let result = settings
150 .verify_blob_kzg_proof(&blob, &commitment, &proof)
151 .map_err(BlobTransactionValidationError::KZGError)?;
152
153 result.then_some(()).ok_or(BlobTransactionValidationError::InvalidProof)
154 }
155
156 pub fn verify_blob(
158 &self,
159 hash: &IndexedBlobHash,
160 ) -> Result<(), BlobTransactionValidationError> {
161 if self.index != hash.index {
162 let blob_hash_part = B256::from_slice(&self.blob[0..32]);
163 return Err(BlobTransactionValidationError::WrongVersionedHash {
164 have: blob_hash_part,
165 expected: hash.hash,
166 });
167 }
168
169 let computed_hash = self.to_kzg_versioned_hash();
170 if computed_hash != hash.hash {
171 return Err(BlobTransactionValidationError::WrongVersionedHash {
172 have: computed_hash.into(),
173 expected: hash.hash,
174 });
175 }
176
177 self.verify_blob_kzg_proof()
178 }
179}
180
181#[cfg(any(test, feature = "arbitrary"))]
182impl<'a> arbitrary::Arbitrary<'a> for BlobTransactionSidecar {
183 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
184 let num_blobs = u.int_in_range(1..=MAX_BLOBS_PER_BLOCK_DENCUN)?;
185 let mut blobs = Vec::with_capacity(num_blobs);
186 for _ in 0..num_blobs {
187 blobs.push(Blob::arbitrary(u)?);
188 }
189
190 let mut commitments = Vec::with_capacity(num_blobs);
191 let mut proofs = Vec::with_capacity(num_blobs);
192 for _ in 0..num_blobs {
193 commitments.push(Bytes48::arbitrary(u)?);
194 proofs.push(Bytes48::arbitrary(u)?);
195 }
196
197 Ok(Self { blobs, commitments, proofs })
198 }
199}
200
201impl BlobTransactionSidecar {
202 pub const fn new(blobs: Vec<Blob>, commitments: Vec<Bytes48>, proofs: Vec<Bytes48>) -> Self {
204 Self { blobs, commitments, proofs }
205 }
206
207 #[cfg(feature = "kzg")]
209 pub fn from_kzg(
210 blobs: Vec<c_kzg::Blob>,
211 commitments: Vec<c_kzg::Bytes48>,
212 proofs: Vec<c_kzg::Bytes48>,
213 ) -> Self {
214 unsafe fn transmute_vec<U, T>(input: Vec<T>) -> Vec<U> {
216 let mut v = core::mem::ManuallyDrop::new(input);
217 Vec::from_raw_parts(v.as_mut_ptr() as *mut U, v.len(), v.capacity())
218 }
219
220 unsafe {
222 let blobs = transmute_vec::<Blob, c_kzg::Blob>(blobs);
223 let commitments = transmute_vec::<Bytes48, c_kzg::Bytes48>(commitments);
224 let proofs = transmute_vec::<Bytes48, c_kzg::Bytes48>(proofs);
225 Self { blobs, commitments, proofs }
226 }
227 }
228
229 #[cfg(feature = "kzg")]
243 pub fn validate(
244 &self,
245 blob_versioned_hashes: &[B256],
246 proof_settings: &c_kzg::KzgSettings,
247 ) -> Result<(), BlobTransactionValidationError> {
248 if blob_versioned_hashes.len() != self.commitments.len() {
250 return Err(c_kzg::Error::MismatchLength(format!(
251 "There are {} versioned commitment hashes and {} commitments",
252 blob_versioned_hashes.len(),
253 self.commitments.len()
254 ))
255 .into());
256 }
257
258 for (versioned_hash, commitment) in
260 blob_versioned_hashes.iter().zip(self.commitments.iter())
261 {
262 let calculated_versioned_hash = kzg_to_versioned_hash(commitment.as_slice());
264 if *versioned_hash != calculated_versioned_hash {
265 return Err(BlobTransactionValidationError::WrongVersionedHash {
266 have: *versioned_hash,
267 expected: calculated_versioned_hash,
268 });
269 }
270 }
271
272 let res = unsafe {
274 proof_settings.verify_blob_kzg_proof_batch(
275 core::mem::transmute::<&[Blob], &[c_kzg::Blob]>(self.blobs.as_slice()),
277 core::mem::transmute::<&[Bytes48], &[c_kzg::Bytes48]>(self.commitments.as_slice()),
279 core::mem::transmute::<&[Bytes48], &[c_kzg::Bytes48]>(self.proofs.as_slice()),
281 )
282 }
283 .map_err(BlobTransactionValidationError::KZGError)?;
284
285 res.then_some(()).ok_or(BlobTransactionValidationError::InvalidProof)
286 }
287
288 pub fn versioned_hashes(&self) -> VersionedHashIter<'_> {
290 VersionedHashIter::new(&self.commitments)
291 }
292
293 pub fn versioned_hash_for_blob(&self, blob_index: usize) -> Option<B256> {
296 self.commitments.get(blob_index).map(|c| kzg_to_versioned_hash(c.as_slice()))
297 }
298
299 pub fn versioned_hash_index(&self, hash: &B256) -> Option<usize> {
301 self.commitments
302 .iter()
303 .position(|commitment| kzg_to_versioned_hash(commitment.as_slice()) == *hash)
304 }
305
306 pub fn blob_by_versioned_hash(&self, hash: &B256) -> Option<&Blob> {
308 self.versioned_hash_index(hash).and_then(|index| self.blobs.get(index))
309 }
310
311 #[inline]
313 pub fn size(&self) -> usize {
314 self.blobs.len() * BYTES_PER_BLOB + self.commitments.len() * BYTES_PER_COMMITMENT + self.proofs.len() * BYTES_PER_PROOF }
318
319 #[cfg(all(feature = "kzg", any(test, feature = "arbitrary")))]
323 pub fn try_from_blobs_hex<I, B>(blobs: I) -> Result<Self, c_kzg::Error>
324 where
325 I: IntoIterator<Item = B>,
326 B: AsRef<str>,
327 {
328 let mut b = Vec::new();
329 for blob in blobs {
330 b.push(c_kzg::Blob::from_hex(blob.as_ref())?)
331 }
332 Self::try_from_blobs(b)
333 }
334
335 #[cfg(all(feature = "kzg", any(test, feature = "arbitrary")))]
339 pub fn try_from_blobs_bytes<I, B>(blobs: I) -> Result<Self, c_kzg::Error>
340 where
341 I: IntoIterator<Item = B>,
342 B: AsRef<[u8]>,
343 {
344 let mut b = Vec::new();
345 for blob in blobs {
346 b.push(c_kzg::Blob::from_bytes(blob.as_ref())?)
347 }
348 Self::try_from_blobs(b)
349 }
350
351 #[cfg(all(feature = "kzg", any(test, feature = "arbitrary")))]
356 pub fn try_from_blobs(blobs: Vec<c_kzg::Blob>) -> Result<Self, c_kzg::Error> {
357 use crate::eip4844::env_settings::EnvKzgSettings;
358
359 let kzg_settings = EnvKzgSettings::Default;
360
361 let commitments = blobs
362 .iter()
363 .map(|blob| {
364 kzg_settings.get().blob_to_kzg_commitment(&blob.clone()).map(|blob| blob.to_bytes())
365 })
366 .collect::<Result<Vec<_>, _>>()?;
367
368 let proofs = blobs
369 .iter()
370 .zip(commitments.iter())
371 .map(|(blob, commitment)| {
372 kzg_settings
373 .get()
374 .compute_blob_kzg_proof(blob, commitment)
375 .map(|blob| blob.to_bytes())
376 })
377 .collect::<Result<Vec<_>, _>>()?;
378
379 Ok(Self::from_kzg(blobs, commitments, proofs))
380 }
381
382 #[doc(hidden)]
385 pub fn rlp_encoded_fields_length(&self) -> usize {
386 self.blobs.length() + self.commitments.length() + self.proofs.length()
387 }
388
389 #[inline]
396 #[doc(hidden)]
397 pub fn rlp_encode_fields(&self, out: &mut dyn BufMut) {
398 self.blobs.encode(out);
400 self.commitments.encode(out);
401 self.proofs.encode(out);
402 }
403
404 fn rlp_header(&self) -> Header {
406 Header { list: true, payload_length: self.rlp_encoded_fields_length() }
407 }
408
409 pub fn rlp_encoded_length(&self) -> usize {
412 self.rlp_header().length() + self.rlp_encoded_fields_length()
413 }
414
415 pub fn rlp_encode(&self, out: &mut dyn BufMut) {
417 self.rlp_header().encode(out);
418 self.rlp_encode_fields(out);
419 }
420
421 #[doc(hidden)]
423 pub fn rlp_decode_fields(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
424 Ok(Self {
425 blobs: Decodable::decode(buf)?,
426 commitments: Decodable::decode(buf)?,
427 proofs: Decodable::decode(buf)?,
428 })
429 }
430
431 pub fn rlp_decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
433 let header = Header::decode(buf)?;
434 if !header.list {
435 return Err(alloy_rlp::Error::UnexpectedString);
436 }
437 if buf.len() < header.payload_length {
438 return Err(alloy_rlp::Error::InputTooShort);
439 }
440 let remaining = buf.len();
441 let this = Self::rlp_decode_fields(buf)?;
442
443 if buf.len() + header.payload_length != remaining {
444 return Err(alloy_rlp::Error::UnexpectedLength);
445 }
446
447 Ok(this)
448 }
449}
450
451impl Encodable for BlobTransactionSidecar {
452 fn encode(&self, out: &mut dyn BufMut) {
454 self.rlp_encode(out);
455 }
456
457 fn length(&self) -> usize {
458 self.rlp_encoded_length()
459 }
460}
461
462impl Decodable for BlobTransactionSidecar {
463 fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
465 Self::rlp_decode(buf)
466 }
467}
468
469impl Encodable7594 for BlobTransactionSidecar {
470 fn encode_7594_len(&self) -> usize {
471 self.rlp_encoded_fields_length()
472 }
473
474 fn encode_7594(&self, out: &mut dyn BufMut) {
475 self.rlp_encode_fields(out);
476 }
477}
478
479impl Decodable7594 for BlobTransactionSidecar {
480 fn decode_7594(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
481 Self::rlp_decode_fields(buf)
482 }
483}
484
485#[cfg(all(debug_assertions, feature = "serde"))]
487pub(crate) fn deserialize_blobs<'de, D>(deserializer: D) -> Result<Vec<Blob>, D::Error>
488where
489 D: serde::de::Deserializer<'de>,
490{
491 use serde::Deserialize;
492
493 let raw_blobs = Vec::<alloy_primitives::Bytes>::deserialize(deserializer)?;
494 let mut blobs = Vec::with_capacity(raw_blobs.len());
495 for blob in raw_blobs {
496 blobs.push(Blob::try_from(blob.as_ref()).map_err(serde::de::Error::custom)?);
497 }
498 Ok(blobs)
499}
500
501#[derive(Debug)]
503#[cfg(feature = "kzg")]
504pub enum BlobTransactionValidationError {
505 InvalidProof,
507 KZGError(c_kzg::Error),
509 NotBlobTransaction(u8),
511 MissingSidecar,
513 WrongVersionedHash {
515 have: B256,
517 expected: B256,
519 },
520}
521
522#[cfg(feature = "kzg")]
523impl core::error::Error for BlobTransactionValidationError {}
524
525#[cfg(feature = "kzg")]
526impl core::fmt::Display for BlobTransactionValidationError {
527 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
528 match self {
529 Self::InvalidProof => f.write_str("invalid KZG proof"),
530 Self::KZGError(err) => {
531 write!(f, "KZG error: {err:?}")
532 }
533 Self::NotBlobTransaction(err) => {
534 write!(f, "unable to verify proof for non blob transaction: {err}")
535 }
536 Self::MissingSidecar => {
537 f.write_str("eip4844 tx variant without sidecar being used for verification.")
538 }
539 Self::WrongVersionedHash { have, expected } => {
540 write!(f, "wrong versioned hash: have {have}, expected {expected}")
541 }
542 }
543 }
544}
545
546#[cfg(feature = "kzg")]
547impl From<c_kzg::Error> for BlobTransactionValidationError {
548 fn from(source: c_kzg::Error) -> Self {
549 Self::KZGError(source)
550 }
551}
552
553#[derive(Debug, Clone)]
555pub struct VersionedHashIter<'a> {
556 commitments: core::slice::Iter<'a, Bytes48>,
558}
559
560impl<'a> Iterator for VersionedHashIter<'a> {
561 type Item = B256;
562
563 fn next(&mut self) -> Option<Self::Item> {
564 self.commitments.next().map(|c| kzg_to_versioned_hash(c.as_slice()))
565 }
566}
567
568impl<'a> VersionedHashIter<'a> {
570 pub fn new(commitments: &'a [Bytes48]) -> Self {
572 Self { commitments: commitments.iter() }
573 }
574}
575
576#[cfg(test)]
577mod tests {
578 use super::*;
579 use arbitrary::Arbitrary;
580
581 #[test]
582 #[cfg(feature = "serde")]
583 fn deserialize_blob() {
584 let blob = BlobTransactionSidecar {
585 blobs: vec![Blob::default(), Blob::default(), Blob::default(), Blob::default()],
586 commitments: vec![
587 Bytes48::default(),
588 Bytes48::default(),
589 Bytes48::default(),
590 Bytes48::default(),
591 ],
592 proofs: vec![
593 Bytes48::default(),
594 Bytes48::default(),
595 Bytes48::default(),
596 Bytes48::default(),
597 ],
598 };
599
600 let s = serde_json::to_string(&blob).unwrap();
601 let deserialized: BlobTransactionSidecar = serde_json::from_str(&s).unwrap();
602 assert_eq!(blob, deserialized);
603 }
604
605 #[test]
606 fn test_arbitrary_blob() {
607 let mut unstructured = arbitrary::Unstructured::new(b"unstructured blob");
608 let _blob = BlobTransactionSidecar::arbitrary(&mut unstructured).unwrap();
609 }
610
611 #[test]
612 #[cfg(feature = "serde")]
613 fn test_blob_item_serde_roundtrip() {
614 let blob_item = BlobTransactionSidecarItem {
615 index: 0,
616 blob: Box::new(Blob::default()),
617 kzg_commitment: Bytes48::default(),
618 kzg_proof: Bytes48::default(),
619 };
620
621 let s = serde_json::to_string(&blob_item).unwrap();
622 let deserialized: BlobTransactionSidecarItem = serde_json::from_str(&s).unwrap();
623 assert_eq!(blob_item, deserialized);
624 }
625}