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