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

downloading & parsing blobs #75

Merged
merged 26 commits into from
Mar 21, 2024
Merged

downloading & parsing blobs #75

merged 26 commits into from
Mar 21, 2024

Conversation

vbar
Copy link
Collaborator

@vbar vbar commented Mar 19, 2024

No description provided.

@vbar vbar requested a review from zeapoz March 19, 2024 14:34
@@ -13,6 +13,9 @@ pub mod ethereum {

/// zkSync smart contract address.
pub const ZK_SYNC_ADDR: &str = "0x32400084C286CF3E17e7B677ea9583e60a000324";

/// Default Ethereum blob storage URL base.
pub const BLOBS_URL: &str = "https://api.blobscan.com/blobs/";
Copy link
Member

Choose a reason for hiding this comment

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

I'm wondering if we should supply a default URL at all. With the ETH-RPC URL, we made the decision to only give one as an example in the README, as opposed to hard-coding in a default one.

Of course this will depend on if blob providers can be counted on to use compatible formats. But, if it's possible, I'd prefer not tying ourselves to one specific provider.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We aren't tying ourselves - it's configurable... We're just being pushed, to the only blob archive there is... Anyway, this URL isn't directly analogical to the ETH-RPC URL, because it isn't always needed - if the user reconstructs only old blocks, it isn't necessary...

state-reconstruct-fetcher/src/constants.rs Outdated Show resolved Hide resolved
state-reconstruct-fetcher/src/l1_fetcher.rs Outdated Show resolved Hide resolved
state-reconstruct-fetcher/src/l1_fetcher.rs Outdated Show resolved Hide resolved
state-reconstruct-fetcher/src/l1_fetcher.rs Show resolved Hide resolved
}

async fn get_blob(
kzg_commitment: &[u8],
Copy link
Member

Choose a reason for hiding this comment

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

We should probably encode the KZG commitment before calling the function.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Why? The commitment is inherently binary data - naturally it does have a hexadecimal representation, but whether that representation is preceded by '0x', lower- or uppercase, or whether the blob storage server accepts base64 instead should be coded at the point of calling the server, not sooner...

state-reconstruct-fetcher/src/types/v3.rs Outdated Show resolved Hide resolved
state-reconstruct-fetcher/src/types/v3.rs Outdated Show resolved Hide resolved
}

let blobs_view = &blobs[..l];
let mut pointer = 0;
Copy link
Member

Choose a reason for hiding this comment

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

We're re-initializing the pointer a lot. Now that we have pretty modular parsing systems, we should not share pointers across functions. Instead, it makes more sense for the callers to always supply byte slices such that a function-local pointer will always be initialized at 0.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Frankly I don't think it ever made much sense to have functions taking explicit pointer - but since it's already implemented, I'm not keen to rewrite the code just to change the style...

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, no need to refactor it now since we'll probably end up rewriting a large chunk of this code in the future. Though, I'd recommend leaving some notes where this can be done so we don't forget to.

Comment on lines 197 to 199
Ok(response) => match response.text().await {
Ok(text) => match get_blob_data(&text) {
Ok(data) => {
Copy link
Member

Choose a reason for hiding this comment

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

(Ditto for V3::get_blob_data too)

This could benefit a lot from using the ? operator in combination with let else clauses, and using if let/match clauses more sparingly. At the moment this is quite hard to read due in part to the excessive indentation. If we don't need to actively handle the error (like when re-polling), I think it's better to use ? to return the error directly (like on get_blob_data(&text)) and make use of the #[from ..] macro to automatically convert errors from other crates like serde and reqwest with ? (via thiserror).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I want to keep the logging - is it possible to do that with the ? operator?

Copy link
Member

Choose a reason for hiding this comment

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

Not at the same level, but of course it's possible to still have logger at the caller level (by matching). If you absolutely need logging, I'd recommend sticking to let else.

Though, in my personal opinion, a warning trace saying data is not string followed by an error trace saying the exact same thing (just without the source string) seems a bit superfluous. I'd advise to remove the tracing::warn!s (from get_blob_data) and just keep the errors since they get traced at the caller level anyways.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

But, the source that couldn't be parsed is the most important thing for debugging - if we don't log it, we won't even know whether it's changed... There's a lot of logging here because there was (and is) a lot of problems parsing blob data...

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, but right now we're logging a warning, and then we also return an error that gets caught and logged by the caller, so essentially we're logging twice for every single one of those errors, right? I think it makes more sense to instead pass in the source string in the error and avoid logging those warnings.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Well yes, it's logged twice - like in lots of other places, e.g. in retry_call and its callers... But yes, the error data can be passed up before logging it...

@vbar vbar requested a review from zeapoz March 20, 2024 08:33
src/cli.rs Show resolved Hide resolved
state-reconstruct-fetcher/src/constants.rs Outdated Show resolved Hide resolved
state-reconstruct-fetcher/src/types/v2.rs Outdated Show resolved Hide resolved
state-reconstruct-fetcher/src/types/v2.rs Outdated Show resolved Hide resolved
state-reconstruct-fetcher/src/types/v1.rs Outdated Show resolved Hide resolved
state-reconstruct-fetcher/src/types/v3.rs Outdated Show resolved Hide resolved
state-reconstruct-fetcher/src/types/v3.rs Outdated Show resolved Hide resolved
state-reconstruct-fetcher/src/types/v3.rs Outdated Show resolved Hide resolved
vbar and others added 8 commits March 20, 2024 14:57
Co-authored-by: Jonathan <94441036+zeapoz@users.noreply.github.com>
Co-authored-by: Jonathan <94441036+zeapoz@users.noreply.github.com>
Co-authored-by: Jonathan <94441036+zeapoz@users.noreply.github.com>
Co-authored-by: Jonathan <94441036+zeapoz@users.noreply.github.com>
Co-authored-by: Jonathan <94441036+zeapoz@users.noreply.github.com>
tracing::error!("Failed to parse calldata: {e}");
cancellation_token.cancel();
return last_block_number_processed;
let blocks = loop {
Copy link
Member

Choose a reason for hiding this comment

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

We should not handle blob errors here, that should only be done within V3.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That isn't possible. On the one hand the program must retry on network errors, but on the other hand it cannot retry forever to remain cancellable - so there are two places where retrieval is retried: the high level has access to the CancellationToken while the low level has a hard-coded retry limit. I admit it's inelegant, but refactoring the whole fetcher around a network retrieval loop would IMHO be too drastic change... Note that retry_call works the same way.

Copy link
Member

Choose a reason for hiding this comment

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

I see, ideally we should couple all these function calls with something like a tokio::select! that will wait for whichever returns first, the retry function or the CancellationToken.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, ideally, but I'm afraid getting that through the layers of the ethers provider would be non-trivial...

Comment on lines +36 to +57
Ok(response) => match response.text().await {
Ok(text) => match get_blob_data(&text) {
Ok(data) => {
let plain = if let Some(p) = data.strip_prefix("0x") {
p
} else {
&data
};
return hex::decode(plain).map_err(|e| {
ParseError::BlobFormatError(plain.to_string(), e.to_string())
});
}
Err(e) => {
tracing::error!("failed parsing response of {url}");
return Err(e);
}
},
Err(e) => {
tracing::error!("attempt {}: {} failed: {:?}", attempt, url, e);
sleep(Duration::from_secs(FAILED_FETCH_RETRY_INTERVAL_S)).await;
}
},
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
Ok(response) => match response.text().await {
Ok(text) => match get_blob_data(&text) {
Ok(data) => {
let plain = if let Some(p) = data.strip_prefix("0x") {
p
} else {
&data
};
return hex::decode(plain).map_err(|e| {
ParseError::BlobFormatError(plain.to_string(), e.to_string())
});
}
Err(e) => {
tracing::error!("failed parsing response of {url}");
return Err(e);
}
},
Err(e) => {
tracing::error!("attempt {}: {} failed: {:?}", attempt, url, e);
sleep(Duration::from_secs(FAILED_FETCH_RETRY_INTERVAL_S)).await;
}
},
Ok(response) => {
let Ok(text) = response.text().await else {
continue;
};
let data = get_blob_data(&text)?;
let plain = if let Some(p) = data.strip_prefix("0x") {
p
} else {
&data
};
return hex::decode(plain)
.map_err(|e| ParseError::BlobFormatError(plain.to_string(), e.to_string())));
}

How does this look? It accomplishes pretty much the same thing as it did previously, but in my opinion it's a lot more readable this way.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

But it isn't the same thing at all... In case response cannot be retrieved, I want to see the error (I don't think I ever did - I suppose it's there for some complicated HTTP stuff that doesn't really happen, but who knows), not just try again...

Copy link
Member

Choose a reason for hiding this comment

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

It will give you the error if the response fails though, that part hasn't been changed at all.

However, another thing I thought of: we should actually be getting the response as a json directly instead of getting it as a string and then parsing it as one.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It will give you the error if the response fails though, that part hasn't been changed at all.

Well, no. Let's say I replace the Ok(text) = response.text() with Err(x) = response.text(), to simulate the failure (full executable code is in https://github.com/eqlabs/zksync-state-reconstruct/tree/inversion ). Then I can run cargo run reconstruct l1 --http-url https://eth.llamarpc.com --start-block 19428415 to check some blobs, and get

2024-03-21T06:57:31.341353Z  INFO url = https://api.blobscan.com/blobs/0xb1647613a7b52bae4aa8f359421d1c9e8144d62768ac00946afb81838d24a3f76172fdfd01ab09f5bfa2e1638954e513
2024-03-21T06:57:32.394116Z  INFO attempt 1: got response
2024-03-21T06:57:33.340344Z  INFO attempt 2: got response
2024-03-21T06:57:34.429335Z  INFO attempt 3: got response
2024-03-21T06:57:35.041117Z  INFO attempt 4: got response
2024-03-21T06:57:35.635881Z  INFO attempt 5: got response
2024-03-21T06:57:40.777653Z  INFO PROGRESS: [ 0%] CUR BLOCK L1: 19428483 L2: 460962 TOTAL BLOCKS PROCESSED L1: 68 L2: 0

No error is logged, and really I don't see how it could...

Copy link
Collaborator Author

@vbar vbar Mar 21, 2024

Choose a reason for hiding this comment

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

However, another thing I thought of: we should actually be getting the response as a json directly instead of getting it as a string and then parsing it as one.

That link is for sending JSON, but yes, something like https://docs.rs/reqwest/latest/reqwest/struct.Response.html#method.json should be possible - except I don't see how to get the response text after failing to parse it as JSON... Really, all I want here is error handling - get response, report error on failure, then parse it, report error on failure... If that's more elaborate than some online Rust examples, well, so what?

Copy link
Member

Choose a reason for hiding this comment

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

That code is testing that we can decode the response, not get the response. Getting the response is still handled with a match statement like it was previously.

I'll admit, I don't really see why we would need that level of error logging. Failing to parse it as JSON is in my mind more than good enough, since that tells the user exactly what went wrong - it's not a valid JSON response.

And for the error handling part, if you're insistent, I would highly recommend looking into more advanced thiserror uses, like the #[from] derive. That allows you to do what you want, without polluting the code with so much boilerplate. If you go down this road, I'd also suggest making BlobError it's own error type that can then be condensed to a ParseError.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

There are 2 async calls needed to get the response: send (inside BlobHttpClient::retrieve_url) and text. Both can fail, and both should be tested. I want to log the second one, precisely because I do not expect the error to happen.

I'll admit, I don't really see why we would need that level of error logging.

Well, to debug the problems that we've already had (e.g. the wrong URL returning JSON without a data attribute - looking at it, it's obvious it says NOT_FOUND, but a serde parser will loose that info).

Copy link
Member

Choose a reason for hiding this comment

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

Gotcha, sure. in that case we should definitely be using the ? operator in combination with thiserror-style error handling then since that would be an unrecoverable error. See my comment above for details.

vbar added a commit that referenced this pull request Mar 21, 2024
@vbar vbar merged commit 424cdfd into main Mar 21, 2024
4 checks passed
@vbar vbar deleted the feat/blob-resolve branch March 21, 2024 14:38
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.

2 participants