diff --git a/crates/eips/src/eip4844/builder.rs b/crates/eips/src/eip4844/builder.rs index ecd1e7b1b79..26846e0413a 100644 --- a/crates/eips/src/eip4844/builder.rs +++ b/crates/eips/src/eip4844/builder.rs @@ -45,7 +45,8 @@ impl PartialSidecar { /// an empty blob to it. pub fn with_capacity(capacity: usize) -> Self { let mut blobs = Vec::with_capacity(capacity); - blobs.push(Blob::new([0u8; BYTES_PER_BLOB])); + blobs.push(Blob::new([0u8; BYTES_PER_BLOB].into()).expect("Failed to create a Blob")); + Self { blobs, fe: 0 } } @@ -74,7 +75,7 @@ impl PartialSidecar { /// Push an empty blob to the builder. fn push_empty_blob(&mut self) { - self.blobs.push(Blob::new([0u8; BYTES_PER_BLOB])); + self.blobs.push(Blob::new([0u8; BYTES_PER_BLOB].into()).expect("Failed to create a Blob")); } /// Allocate enough space for the required number of new field elements. @@ -100,15 +101,26 @@ impl PartialSidecar { self.blobs.get_mut(last_unused_blob_index).expect("never empty") } - /// Get a mutable reference to the field element at the given index, in - /// the current blob. - fn fe_at_mut(&mut self, index: usize) -> &mut [u8] { - &mut self.current_blob_mut()[index * 32..(index + 1) * 32] + /// Get a mutable reference to the field element at the given index in the current blob. + fn fe_at_mut(&mut self, index: usize) -> Vec { + let range = index * 32..(index + 1) * 32; + + // Convert immutable `Bytes` to a mutable `Vec`. + let mut bytes = self.current_blob_mut().as_bytes().to_vec(); + + // Ensure the range is valid. + if range.end > bytes.len() { + panic!("Index out of bounds: field element range exceeds blob size"); + } + + // Get the slice from the mutable bytes, modify as needed, and return it as `Vec`. + let slice = &mut bytes[range]; + slice.to_vec() } - /// Get a mutable reference to the next unused field element. - fn next_unused_fe_mut(&mut self) -> &mut [u8] { - self.fe_at_mut(self.first_unused_fe_index_in_current_blob()) + /// Get the next unused field element as an owned `Vec`. + fn next_unused_fe_mut(&mut self) -> Vec { + self.fe_at_mut(self.first_unused_fe_index_in_current_blob()).to_vec() } /// Ingest a field element into the current blobs. @@ -126,7 +138,7 @@ impl PartialSidecar { /// encode the data. pub fn ingest_partial_fe(&mut self, data: &[u8]) { self.alloc_fes(1); - let fe = self.next_unused_fe_mut(); + let mut fe = self.next_unused_fe_mut(); fe[1..1 + data.len()].copy_from_slice(data); self.fe += 1; } @@ -253,17 +265,26 @@ impl SidecarCoder for SimpleCoder { return None; } - if blobs + // Collect chunks into a vector to ensure their lifetime is valid. + let chunks: Vec> = blobs .iter() - .flat_map(|blob| blob.chunks(FIELD_ELEMENT_BYTES_USIZE).map(WholeFe::new)) - .any(|fe| fe.is_none()) - { + .flat_map(|blob| { + blob.as_bytes() + .chunks(FIELD_ELEMENT_BYTES_USIZE) + .map(|chunk| chunk.to_vec()) + .collect::>() // Ensure iterator allocation + }) + .collect(); + + // Ensure all field elements are valid. + if chunks.iter().any(|chunk| WholeFe::new(chunk).is_none()) { return None; } - let mut fes = blobs - .iter() - .flat_map(|blob| blob.chunks(FIELD_ELEMENT_BYTES_USIZE).map(WholeFe::new_unchecked)); + // Map over chunks and decode. + let mut fes = chunks + .into_iter() + .map(|chunk| WholeFe::new_unchecked(Box::leak(chunk.into_boxed_slice()))); let mut res = Vec::new(); loop { @@ -473,7 +494,10 @@ mod tests { #[test] fn decode_all_rejects_invalid_data() { assert_eq!(SimpleCoder.decode_all(&[]), None); - assert_eq!(SimpleCoder.decode_all(&[Blob::new([0xffu8; BYTES_PER_BLOB])]), None); + assert_eq!( + SimpleCoder.decode_all(&[Blob::new([0xffu8; BYTES_PER_BLOB].into()).unwrap()]), + None + ); } #[test] diff --git a/crates/eips/src/eip4844/mod.rs b/crates/eips/src/eip4844/mod.rs index 1d6c3a5c8cc..231ea299567 100644 --- a/crates/eips/src/eip4844/mod.rs +++ b/crates/eips/src/eip4844/mod.rs @@ -14,15 +14,20 @@ pub mod builder; pub mod utils; mod engine; +use core::hash::Hash; + +use alloy_rlp::{RlpDecodable, RlpEncodable}; +use arbitrary::{Arbitrary, Unstructured}; pub use engine::*; /// Contains sidecar related types #[cfg(feature = "kzg-sidecar")] mod sidecar; + #[cfg(feature = "kzg-sidecar")] pub use sidecar::*; -use alloy_primitives::{b256, FixedBytes, B256, U256}; +use alloy_primitives::{b256, Bytes, FixedBytes, B256, U256}; use crate::eip7840; @@ -86,9 +91,146 @@ pub const BYTES_PER_COMMITMENT: usize = 48; /// How many bytes are in a proof pub const BYTES_PER_PROOF: usize = 48; +/// A fixed-size container for binary data, designed to hold exactly 131,072 bytes. +#[derive(Clone, Debug, RlpEncodable, RlpDecodable, Hash, PartialEq, Eq)] +#[cfg_attr(feature = "ssz", derive(ssz_derive::Encode, ssz_derive::Decode))] +pub struct Blob { + inner: Bytes, +} +#[cfg(any(test, feature = "arbitrary"))] +impl<'a> Arbitrary<'a> for Blob { + fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { + let data: [u8; BYTES_PER_BLOB] = u.arbitrary()?; + Ok(Self { inner: Bytes::from(data.to_vec()) }) + } +} +impl Default for Blob { + fn default() -> Self { + let default_bytes = Bytes::from(vec![0u8; BYTES_PER_BLOB]); + Self { inner: default_bytes } + } +} + +impl From<[u8; BYTES_PER_BLOB]> for Blob { + fn from(array: [u8; BYTES_PER_BLOB]) -> Self { + Self { inner: Bytes::from(array.to_vec()) } + } +} +#[cfg(feature = "serde")] +impl serde::Serialize for Blob { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + if self.inner.len() != BYTES_PER_BLOB { + return Err(serde::ser::Error::custom(format!( + "Invalid blob length: expected {}, got {}", + BYTES_PER_BLOB, + self.inner.len() + ))); + } + serializer.serialize_bytes(&self.inner) + } +} + +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for Blob { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let data: Vec = serde::Deserialize::deserialize(deserializer)?; + if data.len() != BYTES_PER_BLOB { + return Err(serde::de::Error::custom(format!( + "Invalid blob length: expected {}, got {}", + BYTES_PER_BLOB, + data.len() + ))); + } + Ok(Self { inner: Bytes::from(data) }) + } +} + +impl Blob { + /// Creates a new `Blob` from `data` if it is exactly 131,072 bytes. + pub fn new(data: Bytes) -> Result { + if data.len() != BYTES_PER_BLOB { + return Err(format!( + "Invalid blob length: expected {}, got {}", + BYTES_PER_BLOB, + data.len() + )); + } + Ok(Self { inner: data }) + } -/// A Blob serialized as 0x-prefixed hex string -pub type Blob = FixedBytes; + /// Creates a new `Blob` filled with the same byte, repeated 131,072 times. + pub fn repeat_byte(byte: u8) -> Result { + let data = vec![byte; BYTES_PER_BLOB]; + Self::new(Bytes::from(data)) + } + + /// Returns an immutable reference to the underlying `Bytes`. + pub const fn as_bytes(&self) -> &Bytes { + &self.inner + } + + /// Returns a copy of the underlying bytes (as a `Vec`). + pub fn to_vec(&self) -> Vec { + self.inner.to_vec() + } + + /// Overwrite this Blob with the provided bytes. + pub fn update_bytes(&mut self, new_data: Vec) -> Result<(), String> { + if new_data.len() != BYTES_PER_BLOB { + return Err(format!( + "Invalid blob length: expected {}, got {}", + BYTES_PER_BLOB, + new_data.len() + )); + } + self.inner = Bytes::from(new_data); + Ok(()) + } + + /// Returns the number of bytes in this blob . + pub fn len(&self) -> usize { + self.inner.len() + } + + /// Returns true if this blob is empty. + pub fn is_empty(&self) -> bool { + self.inner.is_empty() + } + + /// Returns the data as a standard slice. + pub fn as_slice(&self) -> &[u8] { + &self.inner + } +} +/// Converts a `Vec` into a `Blob`. +impl TryFrom> for Blob { + type Error = String; + + fn try_from(data: Vec) -> Result { + Self::try_from(data.as_slice()) + } +} +/// Converts a slice (`&[u8]`) into a `Blob`. +impl TryFrom<&[u8]> for Blob { + type Error = String; + + fn try_from(data: &[u8]) -> Result { + if data.len() != BYTES_PER_BLOB { + return Err(format!( + "Invalid blob length: expected {}, got {}", + BYTES_PER_BLOB, + data.len() + )); + } + Ok(Self { inner: Bytes::copy_from_slice(data) }) + } +} /// Helper function to deserialize boxed blobs. #[cfg(feature = "serde")] diff --git a/crates/eips/src/eip4844/sidecar.rs b/crates/eips/src/eip4844/sidecar.rs index 8273344dde0..8e3f268167f 100644 --- a/crates/eips/src/eip4844/sidecar.rs +++ b/crates/eips/src/eip4844/sidecar.rs @@ -69,7 +69,7 @@ impl BlobTransactionSidecar { versioned_hashes.iter().enumerate().filter_map(move |(j, target_hash)| { if blob_versioned_hash == *target_hash { if let Some((blob, proof)) = - self.blobs.get(i).copied().zip(self.proofs.get(i).copied()) + self.blobs.get(i).cloned().zip(self.proofs.get(i).cloned()) { return Some((j, BlobAndProofV1 { blob: Box::new(blob), proof })); } @@ -155,7 +155,7 @@ impl BlobTransactionSidecarItem { hash: &IndexedBlobHash, ) -> Result<(), BlobTransactionValidationError> { if self.index != hash.index { - let blob_hash_part = B256::from_slice(&self.blob[0..32]); + let blob_hash_part = B256::from_slice(&self.blob.as_ref().as_slice()[0..32]); return Err(BlobTransactionValidationError::WrongVersionedHash { have: blob_hash_part, expected: hash.hash, @@ -335,7 +335,7 @@ impl BlobTransactionSidecar { /// a RLP header. #[doc(hidden)] pub fn rlp_encoded_fields_length(&self) -> usize { - self.blobs.length() + self.commitments.length() + self.proofs.length() + self.blobs.len() + self.commitments.length() + self.proofs.length() } /// Encodes the inner [BlobTransactionSidecar] fields as RLP bytes, __without__ a RLP header. diff --git a/crates/rpc-types-eth/src/transaction/request.rs b/crates/rpc-types-eth/src/transaction/request.rs index 7c93d0bc078..e2ea189067c 100644 --- a/crates/rpc-types-eth/src/transaction/request.rs +++ b/crates/rpc-types-eth/src/transaction/request.rs @@ -1379,8 +1379,11 @@ mod tests { use alloy_eips::eip4844::{Blob, BlobTransactionSidecar}; // Positive case - let sidecar = - BlobTransactionSidecar::new(vec![Blob::repeat_byte(0xFA)], Vec::new(), Vec::new()); + let sidecar = BlobTransactionSidecar::new( + vec![Blob::repeat_byte(0xFA).unwrap()], + Vec::new(), + Vec::new(), + ); let eip4844_request = TransactionRequest { to: Some(TxKind::Call(Address::repeat_byte(0xDE))), max_fee_per_gas: Some(1234),