Implement EIP-8025 guest program design#7
Conversation
Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com>
… with network access
| exclude: | ||
| - guest: stateless-validator-ethrex | ||
| zkvm: airbender | ||
| - guest: stateless-validator-reth |
There was a problem hiding this comment.
As mentioned in the PR description, ethereum_ssz has some chain of crates dependencies which aren't no_std. Went into that rabbit hole a bit, and got pretty far but required at least these patches:
ethereum_ssz = { git = "https://github.com/han0110/ethereum_ssz", branch = "feature/no-std" }
ethereum_ssz_derive = { git = "https://github.com/han0110/ethereum_ssz", branch = "feature/no-std" }
ethereum_serde_utils = { git = "https://github.com/han0110/ethereum_serde_utils", branch = "feature/no-std" }
tree_hash = { git = "https://github.com/jsign/tree_hash", rev = "2263d501c0765837ef099796f153ba323939e614" }
tree_hash_derive = { git = "https://github.com/jsign/tree_hash", rev = "2263d501c0765837ef099796f153ba323939e614" }
ssz_types = { git = "https://github.com/jsign/ssz_types", rev = "679b7ba3e7402df8f8441af2242a6117c91fc84a" }
The first three are ones that we already did before for similar reasons, and bottom three are ones I had to do because of new needs. I got pretty close -- but there is still some extra things to do potentially. I prefered to stop and not include it in this PR since it was already quite big; but I think we can revisit this to see if we need to continue patching or there is other way.
| tree_hash = "0.10.0" | ||
| tree_hash_derive = "0.10.0" | ||
| ssz_types = { version = "0.11", default-features = false } | ||
| typenum = { version = "1.18", default-features = false } |
There was a problem hiding this comment.
Only rely on ssz and tree hash from Lighthouse, not on their types crates. All the types to be used are in stateless-validator-common.
| crypto-bigint = { git = "https://github.com/risc0/RustCrypto-crypto-bigint", tag = "v0.5.5-risczero.0" } | ||
| c-kzg = { git = "https://github.com/risc0/c-kzg-4844", tag = "c-kzg/v2.1.1-risczero.0" } | ||
| substrate-bn = { git = "https://github.com/risc0/paritytech-bn", tag = "v0.6.0-risczero.0" } | ||
| ethereum_hashing = { git = "https://github.com/jsign/ethereum_hashing", rev = "08c88633b700d7447ef840d19ce7b369c44311de" } |
There was a problem hiding this comment.
This is the only patched crate -- it is an easy one if you want to take a look, and shouldn't be maintained so feels okay. We can eventually polish and upstream it.
| } | ||
|
|
||
| /// Reads all stateless validator fixtures. | ||
| pub fn get_fixtures() -> Vec<StatelessValidatorFixture> { |
There was a problem hiding this comment.
Change things a bit so integration-tests call this directly, to avoid some repetition. And also clean up the previous blocks folder which remained.
| /// A platform that to run guests outside zkVMs. | ||
| #[derive(Debug)] | ||
| pub struct NoopPlatform; | ||
|
|
||
| impl Platform for NoopPlatform { | ||
| #[allow(unreachable_code)] | ||
| fn read_whole_input() -> impl std::ops::Deref<Target = [u8]> { | ||
| panic!("Can't read input in NoopPlatform"); | ||
| &[] as &[u8] | ||
| } | ||
|
|
||
| fn write_whole_output(_: &[u8]) { | ||
| panic!("Can't write output in NoopPlatform"); | ||
| } | ||
|
|
||
| fn print(message: &str) { | ||
| println!("{}", message); | ||
| } | ||
| } |
There was a problem hiding this comment.
This is just a helper ere Platform used to execute the guest program on the host.
| }; | ||
|
|
||
| /// Converts a [`NewPayloadRequest`] into an ethrex [`Block`]. | ||
| pub fn get_block_from_new_payload_request(req: NewPayloadRequest) -> Result<Block> { |
There was a problem hiding this comment.
This is the gist of the work that EL guests have to do for NewPayloadRequest to their own EL types.
It is "boring code" -- the only important thing was to try to avoid as many allocations and copies as possible, since we're in the guest.
| /// The stateless input for the stateless validation function. | ||
| pub stateless_input: StatelessInput, | ||
| /// New payload request data. | ||
| pub new_payload_request: NewPayloadRequest, | ||
| /// Execution witness for the EL block. | ||
| pub witness: ExecutionWitness, | ||
| /// Chain configuration for the stateless validation function | ||
| #[serde_as(as = "alloy_genesis::serde_bincode_compat::ChainConfig<'_>")] | ||
| pub chain_config: ChainConfig, |
There was a problem hiding this comment.
Similar as my comment in Ethrex. StatelessInput has the Reth Block type, and now they receive NewPayloadRequest. So now the guest input type is mainly the same as StatelessInput but with a new_payload_request field -- the rest is the same.
| let requests = get_requests(stateless_input, &signers, valid_block)?; | ||
| let new_payload_request = to_new_payload_request(stateless_input, requests)?; |
There was a problem hiding this comment.
In short, the new way that host constructs the private input part.
| } | ||
| } | ||
|
|
||
| fn get_requests( |
There was a problem hiding this comment.
Here is the point on how the host re-executes the block to get teh request bytes I mentioened before required in NewPayloadRequest.
| /// This method is copied from `reth-ethereum-payload-builder` crate. | ||
| /// https://github.com/paradigmxyz/reth/blob/8eecad3d1d433ed509373713c21c31504290d17d/crates/ethereum/payload/src/validator.rs#L66 | ||
| /// Unfortunately, that crate does not allow to have minimal activated | ||
| /// features in alloy-consensus to use directly without pulling in blst | ||
| /// and other non-friendly dependencies for zkVMs. | ||
| /// TODO: If we can upstream some changes to this crate accordingly, we | ||
| /// can remove this method and use directly from reth. | ||
| fn ensure_well_formed_payload<ChainSpec, T>( |
There was a problem hiding this comment.
This is the only method pulled from the Reth codebase -- by doing some changes in Reth, we can delete this method and use directly.
han0110
left a comment
There was a problem hiding this comment.
First round review, nice work! Left some comments and questions
| @@ -0,0 +1,321 @@ | |||
| //! Consensus types to support new payload requests. | |||
There was a problem hiding this comment.
Learned a lot about manually impl of rkyv!
han0110
left a comment
There was a problem hiding this comment.
LGTM!
(just 2 l2 stuff of ethrex needed to be cleanup, but we can do that later since we never use that feature)
Signed-off-by: jsign <jsign.uy@gmail.com>
This PR implements the current EIP-8025 draft. In this repo, today we support Ethrex and Reth so both work -- but this code can help other ELs to understand how EIP-8025 expects guests to work.
It is a big-ish PR, so I’ll summarize the key points here to keep the important parts concise and make the review feel natural (no quirks/hacks). Still, I will soon create PR comments to revisit the points below and help as much as possible with the review. (I think it is quite hard to do this in multiple PRs). I'll also create some issues to track many comments.
TL;DR:
NewPayloadRequest(+exec witness) instead. We support multipleNewPayloadRequestversions since this change per fork (I use an enum).(parent_hash, block_hash, valid/invalid)to(tree_hash_root(NewPayloadRequest), valid/invalid)block_hashcheck between theNewPayloadRequestand derived block, CLversioned_blob_hashesagainst the present ones in the EL transactions, etc.(tree_hash_root(NewPaylaodRequest), false)— this is a technical detail that is preferred now until there is more clarity reg panic behavior in zkVMs, since we have thetrue/falsefield in public inputs.Performance impact
Comparing the previous version to the current one is like comparing apples to oranges — in the previous version, we didn’t have many checks we knew were needed. Still, I ran the previous and new versions of Reth/Zisk over 50 mainnet blocks using an 8x5090 prover.
The average speedup is 0.96x — much better than I expected. For the SSZ hashing, the SHA2 zkVM precompile prob helped a lot, and I paid attention to our
NewPayloadRequesttransformation to the underlying EL block has as few allocations/copy as possible — this is usually possible since there is a quite direct map between our standardizedNewPayloadRequestand the underlying EL types (just had to be lucky enough to transform types with moves instead of copy).Mid-level important comments
Other important comments.
Regarding how to ingest
NewPayloadRequestinto the guest programNewPayloadRequestis not a data structure related to EL RPCs. Ideally, I wanted a way to still be able to generate proofs only relying on EL RPC (i.e.eth_getBlock+debug_executionWitness). We could rely on CL and EL RPC, but that would add an extra RPC dependency and introduce more edge cases with unsynced nodes.NewExecutionPayloadfrom an EL block. It is not enough to “map fields,” since, for example, therequest_bytesaren’t present in the EL block, but only in the root. To solve this, I execute the block on the host, pull the' request_bytes' result, and use it to fillNewExecutionPayload. For optional proofs, this adds an extra execution in the host, but we could eventually includerequest_bytesin a newly designeddebug_executionWitnessV2to avoid the extra execution (should be easy).Reg CL/EngineAPI types for
NewPayloadRequestand other downstream typesNewPayloadRequest,ExecutionPayloadV{1,2,3},Withdawals, etc.typescrate, which contains other CL types that need complicated dependencies for zkVMs (e.g. blst, kzg) without an easy way to feature-gate them.NewPayloadRequest->EL blockof the guest into their codebases (since we use Rust here at least). I doubt they will accept using Lighthouse crates if that might pull many other deps for them. I wouldn't complain at all if the Lighthouse team is okay with extracting what we need into a separate, minimal crate so we don't have to maintain our own :)ssz_typeswork.NewPayloadRequestis the same for Ethrex and Reth, compared to our previous reality, where each received their own “Block type”. This is nice since we’re moving closer to them receiving a standardized input.SSZ
ethereum_sszfrom Lighthouse, since it is properly isolated from kzg and blst dependencies. Although every code is critical for the guest program, def I think we don't need new SSZ impl if possible. The only side effect is that it is notno_stdwhich is bad for Airbender. I spent a while patching crates, but I already needed to patch 6 crates before I felt too uncomfortable having so many patches -- prob worth revisiting in a separate PR again to double-check if there's a simpler way.Reg public input verification
tree_hash_root(NewPayloadRequest)instead of our previousblock_hash, there wasn’t a clear way to verify that the guest program execution is correct since this root isn’t part of EL RPC (eth_getBlockanddebug_executionWitness). As in, there isn’t a clear way to generate the expected public input without relying on EL guest execution itself.hash_tree_root(NewPayaloadRequest). Using this as an independent source of truth, I compare EL guest execution public input against this map.Crate patching
ethereum_hashing, which is an indirect dependency fromethereum_sszif you rely onTreeHashderive.ethereum_hashingis only used as a proxy to decide which SHA256 crate is used for SSZ hashing. It unconditionally pulledringcrate, which uses a C-lib -- not feature gated, unfortunately. It has support forsha2(which we want since most zkVM precompiles patch it) but only enabled by CPU-feature detection, and not a direct way through feature-gating. In any case, it is an extremely simple patch and will require zero maintenance (but we could/should upstream at some point).Unmodified Reth and Ethrex repos
What did all this mean for core guest program logic
The only change that the existing guest program logic has compared to before this PR is:
NewPayloadRequestfrom private input into their corresponding EL block type (for Ethrex and Reth are distinct types ofc). I did this with a method that tries to avoid as many allocations/copies as possible and leverage existing code in their codebases (since they do this in the EngineAPI). As part of that, they have to do the Payload/Block validation checks I mentioned in the TL;DR.NewPayloadRequest.tree_hash_root()for the public input — very easy and transparent from the guest program perspective.