-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Refactor EIP-8025 specs #4828
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
Refactor EIP-8025 specs #4828
Changes from all commits
f45e6ce
18bff2d
d046541
d833689
a7136ef
ab94ca9
e9a8f8e
27514cb
d5e64ff
089b17d
401b25f
b11df60
aacb0e1
35bf664
30efa02
4d45977
4885119
9bf6076
b08d0ce
1d8e7e3
23b0334
6bdb7d0
7fd6b51
eaa9c23
aae1f78
614679c
b746a88
f29f71d
be1a7e0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,43 +8,49 @@ | |
|
|
||
| - [Table of contents](#table-of-contents) | ||
| - [Introduction](#introduction) | ||
| - [Types](#types) | ||
| - [Constants](#constants) | ||
| - [Execution](#execution) | ||
| - [Domains](#domains) | ||
| - [Configuration](#configuration) | ||
| - [Containers](#containers) | ||
| - [New containers](#new-containers) | ||
| - [`ExecutionProof`](#executionproof) | ||
| - [`SignedExecutionProof`](#signedexecutionproof) | ||
| - [Extended containers](#extended-containers) | ||
| - [Helpers](#helpers) | ||
| - [Execution proof functions](#execution-proof-functions) | ||
| - [`verify_execution_proof`](#verify_execution_proof) | ||
| - [`verify_execution_proofs`](#verify_execution_proofs) | ||
| - [New `PublicInput`](#new-publicinput) | ||
| - [New `ExecutionProof`](#new-executionproof) | ||
| - [New `SignedExecutionProof`](#new-signedexecutionproof) | ||
| - [Beacon chain state transition function](#beacon-chain-state-transition-function) | ||
| - [Execution payload processing](#execution-payload-processing) | ||
| - [Modified `process_execution_payload`](#modified-process_execution_payload) | ||
| - [Block processing](#block-processing) | ||
| - [Modified `process_block`](#modified-process_block) | ||
| - [Execution payload](#execution-payload) | ||
| - [New `NewPayloadRequestHeader`](#new-newpayloadrequestheader) | ||
| - [Modified `process_execution_payload`](#modified-process_execution_payload) | ||
| - [Execution proof](#execution-proof) | ||
| - [New `process_execution_proof`](#new-process_execution_proof) | ||
|
|
||
| <!-- mdformat-toc end --> | ||
|
|
||
| ## Introduction | ||
|
|
||
| These are the beacon-chain specifications to add EIP-8025. This enables | ||
| stateless validation of execution payloads through cryptographic proofs. | ||
| These are the beacon-chain specifications to add EIP-8025, enabling stateless | ||
| validation of execution payloads through execution proofs. | ||
|
|
||
| *Note*: This specification is built upon [Fulu](../../fulu/beacon-chain.md). | ||
| *Note*: This specification is built upon [Fulu](../../fulu/beacon-chain.md) and | ||
| imports proof types from [proof-engine.md](./proof-engine.md). | ||
|
|
||
| *Note*: This specification assumes the reader is familiar with the | ||
| [public zkEVM methods exposed](./zkevm.md). | ||
| ## Types | ||
|
|
||
| | Name | SSZ equivalent | Description | | ||
| | ----------- | -------------- | --------------------------------- | | ||
| | `ProofType` | `uint8` | The type identifier for the proof | | ||
|
|
||
| ## Constants | ||
|
|
||
| ### Execution | ||
|
|
||
| | Name | Value | | ||
| | ---------------------------------- | -------------------------------------- | | ||
| | `MAX_EXECUTION_PROOFS_PER_PAYLOAD` | `uint64(4)` | | ||
| | `PROGRAM` | `ProgramBytecode(b"DEFAULT__PROGRAM")` | | ||
| *Note*: The execution values are not definitive. | ||
|
|
||
| | Name | Value | | ||
| | ---------------- | ------------------- | | ||
| | `MAX_PROOF_SIZE` | `307200` (= 300KiB) | | ||
|
|
||
| ### Domains | ||
|
|
||
|
|
@@ -56,110 +62,83 @@ stateless validation of execution payloads through cryptographic proofs. | |
|
|
||
| *Note*: The configuration values are not definitive. | ||
|
|
||
| | Name | Value | | ||
| | ------------------------------- | ----------- | | ||
| | `MIN_REQUIRED_EXECUTION_PROOFS` | `uint64(1)` | | ||
| | Name | Value | | ||
| | ------------------------- | ------------- | | ||
| | `MAX_WHITELISTED_PROVERS` | `uint64(256)` | | ||
|
|
||
| ## Containers | ||
|
|
||
| ### New containers | ||
| ### New `PublicInput` | ||
|
|
||
| ```python | ||
| class PublicInput(Container): | ||
| new_payload_request_root: Root | ||
| ``` | ||
|
|
||
| #### `ExecutionProof` | ||
| ### New `ExecutionProof` | ||
|
|
||
| ```python | ||
| class ExecutionProof(Container): | ||
| beacon_root: Root | ||
| zk_proof: ZKEVMProof | ||
| validator_index: ValidatorIndex | ||
| proof_data: ByteList[MAX_PROOF_SIZE] | ||
| proof_type: ProofType | ||
| public_input: PublicInput | ||
| ``` | ||
|
|
||
| #### `SignedExecutionProof` | ||
| ### New `SignedExecutionProof` | ||
|
|
||
| ```python | ||
| class SignedExecutionProof(Container): | ||
| message: ExecutionProof | ||
| prover_pubkey: BLSPubkey | ||
| signature: BLSSignature | ||
| ``` | ||
|
|
||
| ### Extended containers | ||
|
|
||
| *Note*: `BeaconState` and `BeaconBlockBody` remain the same. No modifications | ||
| are required for execution proofs since they are handled externally. | ||
| ## Beacon chain state transition function | ||
|
|
||
| ## Helpers | ||
| ### Block processing | ||
|
|
||
| ### Execution proof functions | ||
| #### Modified `process_block` | ||
|
|
||
| #### `verify_execution_proof` | ||
| *Note*: `process_block` is modified in EIP-8025 to pass `PROOF_ENGINE` to | ||
| `process_execution_payload`. | ||
|
|
||
| ```python | ||
| def verify_execution_proof( | ||
| signed_proof: SignedExecutionProof, | ||
| parent_hash: Hash32, | ||
| block_hash: Hash32, | ||
| state: BeaconState, | ||
| el_program: ProgramBytecode, | ||
| ) -> bool: | ||
| """ | ||
| Verify an execution proof against a payload header using zkEVM verification. | ||
| """ | ||
|
|
||
| # Note: signed_proof.message.beacon_root verification will be done at a higher level | ||
|
|
||
| # Verify the validator signature | ||
| proof_message = signed_proof.message | ||
| validator = state.validators[proof_message.validator_index] | ||
| signing_root = compute_signing_root(proof_message, get_domain(state, DOMAIN_EXECUTION_PROOF)) | ||
| if not bls.Verify(validator.pubkey, signing_root, signed_proof.signature): | ||
| return False | ||
|
|
||
| # Derive program bytecode from the EL program identifier and proof type | ||
| program_bytecode = ProgramBytecode( | ||
| el_program + proof_message.zk_proof.proof_type.to_bytes(1, "little") | ||
| ) | ||
|
|
||
| return verify_zkevm_proof(proof_message.zk_proof, parent_hash, block_hash, program_bytecode) | ||
| def process_block(state: BeaconState, block: BeaconBlock) -> None: | ||
| process_block_header(state, block) | ||
| process_withdrawals(state, block.body.execution_payload) | ||
| # [Modified in EIP8025] | ||
| process_execution_payload(state, block.body, EXECUTION_ENGINE, PROOF_ENGINE) | ||
| process_randao(state, block.body) | ||
| process_eth1_data(state, block.body) | ||
| process_operations(state, block.body) | ||
| process_sync_aggregate(state, block.body.sync_aggregate) | ||
| ``` | ||
|
|
||
| #### `verify_execution_proofs` | ||
| #### Execution payload | ||
|
|
||
| ##### New `NewPayloadRequestHeader` | ||
|
|
||
| ```python | ||
| def verify_execution_proofs(parent_hash: Hash32, block_hash: Hash32, state: BeaconState) -> bool: | ||
| """ | ||
| Verify that execution proofs are available and valid for an execution payload. | ||
| """ | ||
| # `retrieve_execution_proofs` is implementation and context dependent. | ||
| # It returns all execution proofs for the given payload block hash. | ||
| signed_execution_proofs = retrieve_execution_proofs(block_hash) | ||
|
|
||
| # Verify there are sufficient proofs | ||
| if len(signed_execution_proofs) < MIN_REQUIRED_EXECUTION_PROOFS: | ||
| return False | ||
|
|
||
| # Verify all execution proofs | ||
| for signed_proof in signed_execution_proofs: | ||
| if not verify_execution_proof(signed_proof, parent_hash, block_hash, state, PROGRAM): | ||
| return False | ||
|
|
||
| return True | ||
| @dataclass | ||
| class NewPayloadRequestHeader(object): | ||
| execution_payload_header: ExecutionPayloadHeader | ||
| versioned_hashes: Sequence[VersionedHash] | ||
| parent_beacon_block_root: Root | ||
| execution_requests: ExecutionRequests | ||
| ``` | ||
|
|
||
| ## Beacon chain state transition function | ||
|
|
||
| ### Execution payload processing | ||
| ##### Modified `process_execution_payload` | ||
|
|
||
| #### Modified `process_execution_payload` | ||
| *Note*: `process_execution_payload` is modified in EIP-8025 to require both | ||
| `ExecutionEngine` and `ProofEngine` for validation. | ||
|
|
||
| ```python | ||
| def process_execution_payload( | ||
| state: BeaconState, | ||
| body: BeaconBlockBody, | ||
| execution_engine: ExecutionEngine, | ||
| stateless_validation: bool = False, | ||
| proof_engine: ProofEngine, | ||
| ) -> None: | ||
| """ | ||
| Note: This function is modified to support optional stateless validation with execution proofs. | ||
| """ | ||
| payload = body.execution_payload | ||
|
|
||
| # Verify consistency of the parent hash with respect to the previous execution payload header | ||
|
|
@@ -174,24 +153,48 @@ def process_execution_payload( | |
| <= get_blob_parameters(get_current_epoch(state)).max_blobs_per_block | ||
| ) | ||
|
|
||
| if stateless_validation: | ||
| # Stateless validation using execution proofs | ||
| assert verify_execution_proofs(payload.parent_hash, payload.block_hash, state) | ||
| else: | ||
| # Compute list of versioned hashes | ||
| versioned_hashes = [ | ||
| kzg_commitment_to_versioned_hash(commitment) for commitment in body.blob_kzg_commitments | ||
| ] | ||
|
|
||
| # Verify the execution payload is valid | ||
| assert execution_engine.verify_and_notify_new_payload( | ||
| NewPayloadRequest( | ||
| execution_payload=payload, | ||
| versioned_hashes=versioned_hashes, | ||
| parent_beacon_block_root=state.latest_block_header.parent_root, | ||
| execution_requests=body.execution_requests, | ||
| ) | ||
| # Compute list of versioned hashes | ||
| versioned_hashes = [ | ||
| kzg_commitment_to_versioned_hash(commitment) for commitment in body.blob_kzg_commitments | ||
| ] | ||
|
|
||
| # Verify the execution payload is valid via ExecutionEngine | ||
| assert execution_engine.verify_and_notify_new_payload( | ||
| NewPayloadRequest( | ||
| execution_payload=payload, | ||
| versioned_hashes=versioned_hashes, | ||
| parent_beacon_block_root=state.latest_block_header.parent_root, | ||
| execution_requests=body.execution_requests, | ||
| ) | ||
| ) | ||
|
|
||
| # [New in EIP8025] | ||
| # Verify via ProofEngine | ||
| new_payload_request_header = NewPayloadRequestHeader( | ||
| execution_payload_header=ExecutionPayloadHeader( | ||
| parent_hash=payload.parent_hash, | ||
| fee_recipient=payload.fee_recipient, | ||
| state_root=payload.state_root, | ||
| receipts_root=payload.receipts_root, | ||
| logs_bloom=payload.logs_bloom, | ||
| prev_randao=payload.prev_randao, | ||
| block_number=payload.block_number, | ||
| gas_limit=payload.gas_limit, | ||
| gas_used=payload.gas_used, | ||
| timestamp=payload.timestamp, | ||
| extra_data=payload.extra_data, | ||
| base_fee_per_gas=payload.base_fee_per_gas, | ||
| block_hash=payload.block_hash, | ||
| transactions_root=hash_tree_root(payload.transactions), | ||
| withdrawals_root=hash_tree_root(payload.withdrawals), | ||
| blob_gas_used=payload.blob_gas_used, | ||
| excess_blob_gas=payload.excess_blob_gas, | ||
| ), | ||
| versioned_hashes=versioned_hashes, | ||
| parent_beacon_block_root=state.latest_block_header.parent_root, | ||
| execution_requests=body.execution_requests, | ||
| ) | ||
| assert proof_engine.verify_new_payload_request_header(new_payload_request_header) | ||
frisitano marked this conversation as resolved.
Show resolved
Hide resolved
jihoonsong marked this conversation as resolved.
Show resolved
Hide resolved
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If I'm not wrong, the right modifications here at the high-level are:
This basically merges One easy solution comes to me is, we introduce a deadline for proofs, which is set before the block's deadline. e.g., proofs deadline is 7s while block deadline is 8s on top of Fulu.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was thinking this could be done async and left as an implementation detail (left unspecified). If proofs do not arrive before
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you explain what does
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It must be specified, especially if it touches fork choice.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I don't follow this at all. ePBS is not underspecified with regard to block process and fork choice from my understanding.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. By the way I've written up some considerations on consensus for fulu and gloas here - https://hackmd.io/@frisitano/BJ9P2IpQ-e#Consensus-Considerations
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, is it because proofs are stored in
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This makes sense. I will introduce this.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, multiple factors came up to my mind. Please wait a bit. Let me think more about this. |
||
|
|
||
| # Cache execution payload header | ||
| state.latest_execution_payload_header = ExecutionPayloadHeader( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can reuse
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. good catch. |
||
|
|
@@ -214,3 +217,30 @@ def process_execution_payload( | |
| excess_blob_gas=payload.excess_blob_gas, | ||
| ) | ||
| ``` | ||
|
|
||
| ### Execution proof | ||
|
|
||
| *Note*: Proof storage is implementation-dependent, managed by the `ProofEngine`. | ||
|
|
||
| #### New `process_execution_proof` | ||
|
|
||
| ```python | ||
| def process_execution_proof( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I also think |
||
| state: BeaconState, | ||
| signed_proof: SignedExecutionProof, | ||
| proof_engine: ProofEngine, | ||
| ) -> None: | ||
| proof_message = signed_proof.message | ||
| prover_pubkey = signed_proof.prover_pubkey | ||
|
|
||
| # Verify prover is whitelisted | ||
| validator_pubkeys = [v.pubkey for v in state.validators] | ||
| assert prover_pubkey in validator_pubkeys | ||
|
|
||
| domain = get_domain(state, DOMAIN_EXECUTION_PROOF, compute_epoch_at_slot(state.slot)) | ||
| signing_root = compute_signing_root(proof_message, domain) | ||
| assert bls.Verify(prover_pubkey, signing_root, signed_proof.signature) | ||
|
|
||
| # Verify the execution proof | ||
| assert proof_engine.verify_execution_proof(proof_message) | ||
| ``` | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wasn't the whole purpose of execution proofs to let lightweight nodes run without an execution client (or at least with a stateless one)?
If so, why require both an
ExecutionEngineand aProofEnginefor a lightweight client?Conversely, EIP-8025 is titled "Optional Execution Proofs."
Yet with this PR, all nodes, including stateful, classic ones, will need execution proofs to import a block.
The EIP states:
This PR seems to make an EL mandatory for all node types.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe these specs represent what full nodes & validators would implement. There could be another node type for lightweight verifiers which are not connected to an EL client.
Edit: Yeah, see the "Proof Verification Flow" diagram in the PR's description.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we should add back a stateless validation flag like on master here to either verify using the execution engine or veify a proof using the proof engine @frisitano ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The high-level idea was that a node can be spun up with a
ProofEngine, anExecutionEngine, or both. If we only have aProofEngine, then theExecutionEngineinvocation would be a no-op. Similarly, if we only have anExecutionEngine, then theProofEnginecall would be a no-op.I agree that this spec doesn't capture this construct well and isn't clear. In regard to options as I see them:
process_execution_payloadback to default, and instead add a comment toExecutionEngine::verify_and_notify_new_payloadthat it should invoke theProofEngine(if configured) as well as an external re-execution node (if configured).ProofEngine, we extend theExecutionEnginewith methods for proofs and leave the method implementations as an implementation detail.I tend to be in favour of 3 or 4 but keen to hear others' opinions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think replacing
ExecutionEnginewithProofEnginewould be the ideal especially in terms of forward-compatibility.process_execution_payloadcan receive proof engine instead of execution engine, and replace the call to execution engine with proof engine.What do people feel about this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that sounds like a good approach. I agree.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Bridging my comment from Discord)