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

fix(alloy-eips): SimpleCoder::decode_one() should return Ok(None) #1818

Merged
merged 11 commits into from
Dec 24, 2024

Conversation

StackOverflowExcept1on
Copy link
Contributor

@StackOverflowExcept1on StackOverflowExcept1on commented Dec 22, 2024

Motivation

Resolves #1388

For example you want to decode 4096 FEs. In this case the first time decode_one() will return data, but the second time (because of loop) it will return Err(()) and it will fail the whole decode_all().

fn decode_all(&mut self, blobs: &[Blob]) -> Option<Vec<Vec<u8>>> {
let mut fes =
blobs.iter().flat_map(|blob| blob.chunks(32).map(WholeFe::new)).map(Option::unwrap);
let mut res = Vec::new();
loop {
match Self::decode_one(&mut fes) {
Ok(Some(data)) => res.push(data),
Ok(None) => break,
Err(()) => return None,
}
}
Some(res)
}

Solution

Returns Ok(None) if there is no data (empty iterator, length prefix is 0).

PR Checklist

  • Added Tests
  • Added Documentation
  • Breaking changes

@@ -434,6 +436,17 @@ mod tests {
assert_eq!(decoded, data);
}

#[test]
fn big_ingestion_strategy() {
let data = vec![1u8; 126_945];
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blob has size of 128 * 1024 bytes. First 32 bytes are used as length, thus leaving 4095 FEs / 4096 FEs. This constant was calculated as 128 * 1024 - 32 - 4095 (4095 zeros in each FE).

Comment on lines 254 to 264
if blobs
.iter()
.flat_map(|blob| blob.chunks(FIELD_ELEMENT_BYTES).map(WholeFe::new))
.any(|fe| fe.is_none())
{
return None;
}

let mut fes = blobs
.iter()
.flat_map(|blob| blob.chunks(FIELD_ELEMENT_BYTES).map(WholeFe::new_unchecked));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously it didn't work correctly because .map(WholeFe::new)).map(Option::unwrap) was executed lazily (when decode_one() reached the wrong WholeFe, it would panic).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I decided to check this in a non-lazy way to do not pass Option<Option<Option<Option<WholeFe>> to decode_one().

Copy link
Member

@mattsse mattsse left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tysm for looking into this,

all of this looks fine to me, although not an expert when it comes to the internals, but these changes match with what I discovered when I looked into the issue earlier

some nits

crates/eips/src/eip4844/mod.rs Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no functional changes here, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could leave it u64, but then I'd have to make as usize in all usages, which isn't very convenient. Maybe it makes sense to introduce FIELD_ELEMENT_BYTES_USIZE: usize = FIELD_ELEMENT_BYTES as usize? Yes, there are no other functional changes.

Comment on lines +100 to +101
let last_unused_blob_index = self.fe / FIELD_ELEMENTS_PER_BLOB as usize;
self.blobs.get_mut(last_unused_blob_index).expect("never empty")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this makes sense

Comment on lines +201 to +203
let Some(first) = fes.next() else {
return Ok(None);
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this also makes sense, because before this would always return an error for iters len > 1, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this returned an error when there was no more data left in the iterator. but that's a little different from what we want from decode_all().

Comment on lines 252 to 254
if !matches!(blobs.len(), 1..=MAX_BLOBS_PER_BLOCK) {
return None;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this restriction will be problematic soon, because this constant should no longer be used, so I'd rather remove this check entirely

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the problem? if the number of blobs is increased, we just update the constants and everything will work.

Copy link
Member

@mattsse mattsse Dec 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the max number of blobs will be decoupled from this constant ethereum/EIPs#9129

@klkvr should we use the eip7840::BlobParams::cancun() for now or remove this entirely, I guess the proper way of doing this would be to pass around the max_block setting but this seems a bit much...
ref https://github.com/alloy-rs/alloy/pull/1828/files

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah don't think we need this check

custom implementations can store the constant in coder if needed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I'll remove the use of MAX_BLOBS_PER_BLOCK in this place and here for now:

// if there are too many bytes
if num_bytes > BYTES_PER_BLOB * MAX_BLOBS_PER_BLOCK {
return Err(());
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could then do a check that blobs: &[Blob] is not empty and that num_bytes does not exceed 2 MiB (it's limit of 16 blobs against the current 6 blobs and is quite normal practice to defend against attacks of allocating too much memory).

Comment on lines +256 to +266
if blobs
.iter()
.flat_map(|blob| blob.chunks(FIELD_ELEMENT_BYTES_USIZE).map(WholeFe::new))
.any(|fe| fe.is_none())
{
return None;
}

let mut fes = blobs
.iter()
.flat_map(|blob| blob.chunks(FIELD_ELEMENT_BYTES_USIZE).map(WholeFe::new_unchecked));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah as pointed out, this overhead should be fine

@mattsse mattsse merged commit eb1798a into alloy-rs:main Dec 24, 2024
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Bug] SidecarBuilder fails to encode anything bigger than 1 blob of data
3 participants