Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add Blob type that behaves like Bytes #1912

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
60 changes: 42 additions & 18 deletions crates/eips/src/eip4844/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
}

Expand Down Expand Up @@ -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.
Expand All @@ -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<u8> {
let range = index * 32..(index + 1) * 32;

// Convert immutable `Bytes` to a mutable `Vec<u8>`.
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<u8>`.
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<u8>`.
fn next_unused_fe_mut(&mut self) -> Vec<u8> {
self.fe_at_mut(self.first_unused_fe_index_in_current_blob()).to_vec()
}

/// Ingest a field element into the current blobs.
Expand All @@ -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;
}
Expand Down Expand Up @@ -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<Vec<u8>> = 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::<Vec<_>>() // 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 {
Expand Down Expand Up @@ -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]
Expand Down
148 changes: 145 additions & 3 deletions crates/eips/src/eip4844/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<Self> {
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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let data: Vec<u8> = 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<Self, String> {
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<BYTES_PER_BLOB>;
/// Creates a new `Blob` filled with the same byte, repeated 131,072 times.
pub fn repeat_byte(byte: u8) -> Result<Self, String> {
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<u8>`).
pub fn to_vec(&self) -> Vec<u8> {
self.inner.to_vec()
}

/// Overwrite this Blob with the provided bytes.
pub fn update_bytes(&mut self, new_data: Vec<u8>) -> 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<u8>` into a `Blob`.
impl TryFrom<Vec<u8>> for Blob {
type Error = String;

fn try_from(data: Vec<u8>) -> Result<Self, Self::Error> {
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<Self, Self::Error> {
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")]
Expand Down
6 changes: 3 additions & 3 deletions crates/eips/src/eip4844/sidecar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 }));
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down
7 changes: 5 additions & 2 deletions crates/rpc-types-eth/src/transaction/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
Loading