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

Use engine_newPayloadV3 to pass versioned_hashes to EL for validation #3359

Merged
merged 16 commits into from
May 24, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -578,7 +578,7 @@ def get_pow_chain_head() -> PowBlock:

class NoopExecutionEngine(ExecutionEngine):

def notify_new_payload(self: ExecutionEngine, execution_payload: ExecutionPayload) -> bool:
def notify_new_payload(self: ExecutionEngine, new_payload_request: NewPayloadRequest) -> bool:
return True

def notify_forkchoice_updated(self: ExecutionEngine,
Expand Down
12 changes: 8 additions & 4 deletions specs/_features/eip6110/beacon-chain.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,12 +178,11 @@ def process_block(state: BeaconState, block: BeaconBlock) -> None:
process_block_header(state, block)
if is_execution_enabled(state, block.body):
process_withdrawals(state, block.body.execution_payload)
process_execution_payload(state, block.body.execution_payload, EXECUTION_ENGINE) # [Modified in EIP6110]
process_execution_payload(state, block.body, EXECUTION_ENGINE) # [Modified in EIP6110]
process_randao(state, block.body)
process_eth1_data(state, block.body)
process_operations(state, block.body) # [Modified in EIP6110]
process_sync_aggregate(state, block.body.sync_aggregate)
process_blob_kzg_commitments(block.body)
```

#### Modified `process_operations`
Expand Down Expand Up @@ -238,7 +237,9 @@ def process_deposit_receipt(state: BeaconState, deposit_receipt: DepositReceipt)
*Note*: The function `process_execution_payload` is modified to use the new `ExecutionPayloadHeader` type.

```python
def process_execution_payload(state: BeaconState, payload: ExecutionPayload, execution_engine: ExecutionEngine) -> None:
def process_execution_payload(state: BeaconState, body: BeaconBlockBody, execution_engine: ExecutionEngine) -> None:
payload = body.execution_payload

# Verify consistency of the parent hash with respect to the previous execution payload header
if is_merge_transition_complete(state):
assert payload.parent_hash == state.latest_execution_payload_header.block_hash
Expand All @@ -247,7 +248,10 @@ def process_execution_payload(state: BeaconState, payload: ExecutionPayload, exe
# Verify timestamp
assert payload.timestamp == compute_timestamp_at_slot(state, state.slot)
# Verify the execution payload is valid
assert execution_engine.notify_new_payload(payload)
versioned_hashes = [kzg_commitment_to_versioned_hash(commitment) for commitment in body.blob_kzg_commitments]
assert execution_engine.notify_new_payload(
NewPayloadRequest(execution_payload=payload, versioned_hashes=versioned_hashes)
)
# Cache execution payload header
state.latest_execution_payload_header = ExecutionPayloadHeader(
parent_hash=payload.parent_hash,
Expand Down
27 changes: 22 additions & 5 deletions specs/bellatrix/beacon-chain.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
- [Modified `slash_validator`](#modified-slash_validator)
- [Beacon chain state transition function](#beacon-chain-state-transition-function)
- [Execution engine](#execution-engine)
- [Request data](#request-data)
- [`NewPayloadRequest`](#newpayloadrequest)
- [Engine APIs](#engine-apis)
- [`notify_new_payload`](#notify_new_payload)
- [Block processing](#block-processing)
- [Execution payload](#execution-payload)
Expand Down Expand Up @@ -300,6 +303,18 @@ def slash_validator(state: BeaconState,

### Execution engine

#### Request data

##### `NewPayloadRequest`

```python
@dataclass
class NewPayloadRequest(object):
execution_payload: ExecutionPayload
```

#### Engine APIs

The implementation-dependent `ExecutionEngine` protocol encapsulates the execution sub-system logic via:

* a state object `self.execution_state` of type `ExecutionState`
Expand All @@ -313,9 +328,9 @@ The Engine API may be used to implement this and similarly defined functions via
#### `notify_new_payload`

```python
def notify_new_payload(self: ExecutionEngine, execution_payload: ExecutionPayload) -> bool:
def notify_new_payload(self: ExecutionEngine, new_payload_request: NewPayloadRequest) -> bool:
"""
Return ``True`` if and only if ``execution_payload`` is valid with respect to ``self.execution_state``.
Return ``True`` if and only if ``new_payload_request`` is valid with respect to ``self.execution_state``.
"""
...
Copy link
Contributor

Choose a reason for hiding this comment

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

What does the ... imply here?

Copy link
Contributor Author

@hwwhww hwwhww May 24, 2023

Choose a reason for hiding this comment

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

I wanted to imply that this is a pseudo-code-like function and is client implementation dependent. 😅

```
Expand All @@ -328,7 +343,7 @@ def notify_new_payload(self: ExecutionEngine, execution_payload: ExecutionPayloa
def process_block(state: BeaconState, block: BeaconBlock) -> None:
process_block_header(state, block)
if is_execution_enabled(state, block.body):
process_execution_payload(state, block.body.execution_payload, EXECUTION_ENGINE) # [New in Bellatrix]
process_execution_payload(state, block.body, EXECUTION_ENGINE) # [New in Bellatrix]
process_randao(state, block.body)
process_eth1_data(state, block.body)
process_operations(state, block.body)
Expand All @@ -340,7 +355,9 @@ def process_block(state: BeaconState, block: BeaconBlock) -> None:
##### `process_execution_payload`

```python
def process_execution_payload(state: BeaconState, payload: ExecutionPayload, execution_engine: ExecutionEngine) -> None:
def process_execution_payload(state: BeaconState, body: BeaconBlockBody, execution_engine: ExecutionEngine) -> None:
payload = body.execution_payload

# Verify consistency of the parent hash with respect to the previous execution payload header
if is_merge_transition_complete(state):
assert payload.parent_hash == state.latest_execution_payload_header.block_hash
Expand All @@ -349,7 +366,7 @@ def process_execution_payload(state: BeaconState, payload: ExecutionPayload, exe
# Verify timestamp
assert payload.timestamp == compute_timestamp_at_slot(state, state.slot)
# Verify the execution payload is valid
assert execution_engine.notify_new_payload(payload)
assert execution_engine.notify_new_payload(NewPayloadRequest(execution_payload=payload))
# Cache execution payload header
state.latest_execution_payload_header = ExecutionPayloadHeader(
parent_hash=payload.parent_hash,
Expand Down
8 changes: 5 additions & 3 deletions specs/capella/beacon-chain.md
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ def process_block(state: BeaconState, block: BeaconBlock) -> None:
process_block_header(state, block)
if is_execution_enabled(state, block.body):
process_withdrawals(state, block.body.execution_payload) # [New in Capella]
process_execution_payload(state, block.body.execution_payload, EXECUTION_ENGINE) # [Modified in Capella]
process_execution_payload(state, block.body, EXECUTION_ENGINE) # [Modified in Capella]
process_randao(state, block.body)
process_eth1_data(state, block.body)
process_operations(state, block.body) # [Modified in Capella]
Expand Down Expand Up @@ -407,7 +407,9 @@ def process_withdrawals(state: BeaconState, payload: ExecutionPayload) -> None:
*Note*: The function `process_execution_payload` is modified to use the new `ExecutionPayloadHeader` type.

```python
def process_execution_payload(state: BeaconState, payload: ExecutionPayload, execution_engine: ExecutionEngine) -> None:
def process_execution_payload(state: BeaconState, body: BeaconBlockBody, execution_engine: ExecutionEngine) -> None:
payload = body.execution_payload

# Verify consistency of the parent hash with respect to the previous execution payload header
if is_merge_transition_complete(state):
assert payload.parent_hash == state.latest_execution_payload_header.block_hash
Expand All @@ -416,7 +418,7 @@ def process_execution_payload(state: BeaconState, payload: ExecutionPayload, exe
# Verify timestamp
assert payload.timestamp == compute_timestamp_at_slot(state, state.slot)
# Verify the execution payload is valid
assert execution_engine.notify_new_payload(payload)
assert execution_engine.notify_new_payload(NewPayloadRequest(execution_payload=payload))
# Cache execution payload header
state.latest_execution_payload_header = ExecutionPayloadHeader(
parent_hash=payload.parent_hash,
Expand Down
81 changes: 28 additions & 53 deletions specs/deneb/beacon-chain.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,14 @@
- [Helper functions](#helper-functions)
- [Misc](#misc)
- [`kzg_commitment_to_versioned_hash`](#kzg_commitment_to_versioned_hash)
- [`tx_peek_blob_versioned_hashes`](#tx_peek_blob_versioned_hashes)
- [`verify_kzg_commitments_against_transactions`](#verify_kzg_commitments_against_transactions)
- [Beacon chain state transition function](#beacon-chain-state-transition-function)
- [Block processing](#block-processing)
- [Execution engine](#execution-engine)
- [Request data](#request-data)
- [Modified `NewPayloadRequest`](#modified-newpayloadrequest)
- [Engine APIs](#engine-apis)
- [Modified `notify_new_payload`](#modified-notify_new_payload)
- [Execution payload](#execution-payload)
- [`process_execution_payload`](#process_execution_payload)
- [Blob KZG commitments](#blob-kzg-commitments)
- [Testing](#testing)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->
Expand Down Expand Up @@ -158,64 +159,41 @@ def kzg_commitment_to_versioned_hash(kzg_commitment: KZGCommitment) -> Versioned
return VERSIONED_HASH_VERSION_KZG + hash(kzg_commitment)[1:]
```

#### `tx_peek_blob_versioned_hashes`
## Beacon chain state transition function

This function retrieves the hashes from the `SignedBlobTransaction` as defined in Deneb, using SSZ offsets.
Offsets are little-endian `uint32` values, as defined in the [SSZ specification](../../ssz/simple-serialize.md).
See [the full details of `blob_versioned_hashes` offset calculation](https://gist.github.com/protolambda/23bd106b66f6d4bb854ce46044aa3ca3).
### Execution engine

```python
def tx_peek_blob_versioned_hashes(opaque_tx: Transaction) -> Sequence[VersionedHash]:
assert opaque_tx[0] == BLOB_TX_TYPE
message_offset = 1 + uint32.decode_bytes(opaque_tx[1:5])
# field offset: 32 + 8 + 32 + 32 + 8 + 4 + 32 + 4 + 4 + 32 = 188
blob_versioned_hashes_offset = (
message_offset
+ uint32.decode_bytes(opaque_tx[(message_offset + 188):(message_offset + 192)])
)
# `VersionedHash` is a 32-byte object
assert (len(opaque_tx) - blob_versioned_hashes_offset) % 32 == 0
return [
VersionedHash(opaque_tx[x:(x + 32)])
for x in range(blob_versioned_hashes_offset, len(opaque_tx), 32)
]
```
#### Request data

#### `verify_kzg_commitments_against_transactions`
##### Modified `NewPayloadRequest`

```python
def verify_kzg_commitments_against_transactions(transactions: Sequence[Transaction],
kzg_commitments: Sequence[KZGCommitment]) -> bool:
all_versioned_hashes: List[VersionedHash] = []
for tx in transactions:
if tx[0] == BLOB_TX_TYPE:
all_versioned_hashes += tx_peek_blob_versioned_hashes(tx)
return all_versioned_hashes == [kzg_commitment_to_versioned_hash(commitment) for commitment in kzg_commitments]
@dataclass
class NewPayloadRequest(object):
execution_payload: ExecutionPayload
versioned_hashes: Sequence[VersionedHash]
```

## Beacon chain state transition function
#### Engine APIs

### Block processing
hwwhww marked this conversation as resolved.
Show resolved Hide resolved
#### Modified `notify_new_payload`

```python
def process_block(state: BeaconState, block: BeaconBlock) -> None:
process_block_header(state, block)
if is_execution_enabled(state, block.body):
process_withdrawals(state, block.body.execution_payload)
process_execution_payload(state, block.body.execution_payload, EXECUTION_ENGINE) # [Modified in Deneb]
process_randao(state, block.body)
process_eth1_data(state, block.body)
process_operations(state, block.body)
process_sync_aggregate(state, block.body.sync_aggregate)
process_blob_kzg_commitments(block.body) # [New in Deneb]
def notify_new_payload(self: ExecutionEngine, new_payload_request: NewPayloadRequest) -> bool:
"""
Return ``True`` if and only if ``new_payload_request`` is valid with respect to ``self.execution_state``.
"""
...
```

#### Execution payload

##### `process_execution_payload`

```python
def process_execution_payload(state: BeaconState, payload: ExecutionPayload, execution_engine: ExecutionEngine) -> None:
def process_execution_payload(state: BeaconState, body: BeaconBlockBody, execution_engine: ExecutionEngine) -> None:
payload = body.execution_payload

# Verify consistency of the parent hash with respect to the previous execution payload header
if is_merge_transition_complete(state):
assert payload.parent_hash == state.latest_execution_payload_header.block_hash
Expand All @@ -224,7 +202,11 @@ def process_execution_payload(state: BeaconState, payload: ExecutionPayload, exe
# Verify timestamp
assert payload.timestamp == compute_timestamp_at_slot(state, state.slot)
# Verify the execution payload is valid
assert execution_engine.notify_new_payload(payload)
# [Modified in Deneb]
versioned_hashes = [kzg_commitment_to_versioned_hash(commitment) for commitment in body.blob_kzg_commitments]
assert execution_engine.notify_new_payload(
Copy link
Contributor

@djrtwo djrtwo May 18, 2023

Choose a reason for hiding this comment

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

another strategy is to keep notify_new_payload() as the same and to insert another function execution_engine.verify_versioned_hashes(versioned_hashes, payload.transactions). This would be divergent from the engine API but is very explicit.

In fact if we went that path, I would also break out the verification of the block_hash -- e.g.

assert execution_engine.is_valid_block_hash(payload)
assert execution_engine.is_valid_versioned_hashes(versioned_hashes, payload.transactions)
assert execution_engine.notify_new_payload(payload)

and i would maybe keep it in process_blob_kzg_commitments along with the length check discussed in #3338

Copy link
Contributor

Choose a reason for hiding this comment

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

cc: @mkalinin

Copy link
Collaborator

@mkalinin mkalinin May 19, 2023

Choose a reason for hiding this comment

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

I really like this strategy as making these checks explicit brings more clarity to the spec. Can't see anything that could be broken by doing it this way, I was thinking about optimistic sync tests but they shouldn't be affected afaics

Copy link
Contributor Author

Choose a reason for hiding this comment

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

But in client implementations, the validation will still use the one Engine API call response, correct? Aren't different API abstractions make it more difficult to test? e.g., we already yield execution_valid in test vectors, if we want to add what's proposed in #3359 (comment) + add execution_engine.is_valid_versioned_hashes() helper in specs, we have to yield an is_valid_versioned_hashes field in test vectors.

I'd like to confirm if clients indeed prefer parsing this themselves. 🤔

/cc @terencechain

Copy link
Contributor

@g11tech g11tech May 20, 2023

Choose a reason for hiding this comment

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

my view: we would just like it as a single call with length check before the call (check should come before versioned hashes computation but suggested here for the conversation context)

Suggested change
assert execution_engine.notify_new_payload(
asset len(body.blob_kzg_commitments) <= MAX_BLOBS_PER_BLOCK
assert execution_engine.notify_new_payload(

Copy link
Contributor

Choose a reason for hiding this comment

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

Given this is a specification to show complete state transition validation and mutation logic and not an implementation guide that knows or cares about the engine api (Engine API is just one architecture of a valid full client), I very much like explicitly calling out the various groupings of validations instead of just hiding it behind one.

That said, at this point in the process, if it is going to cause too much additional overhead (e.g. with changing test formats), I'm not going to push too hard for it.

Copy link
Contributor Author

@hwwhww hwwhww May 23, 2023

Choose a reason for hiding this comment

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

@g11tech @ppopth

The extra length check is unnecessary since the SSZ container has defined field blob_kzg_commitments: List[KZGCommitment, MAX_BLOBS_PER_BLOCK].

Copy link
Contributor Author

@hwwhww hwwhww May 23, 2023

Choose a reason for hiding this comment

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

@djrtwo @mkalinin

I agree the explicitness is good, but CL is just the caller and has no control over executing the validation. I support @ppopth's proposal of renaming the API name or creating a new API to replace the old one in Deneb.

Another way is to make it clear in the abstract API:

def verify_and_notify_new_payload(self: ExecutionEngine,
                                  new_payload_request: NewPayloadRequest) -> bool:
    """
    Return ``True`` if and only if ``new_payload_request`` is valid with respect to ``self.execution_state``.
    """
    assert self.is_valid_block_hash(new_payload_request.execution_payload)
    assert self.is_valid_versioned_hashes(new_payload_request)
    assert self.notify_new_payload(new_payload_request.execution_payload)

And we stick to using the single execution_valid return value of notify_new_payload_and_verify_versioned_hashes in testing.

Note: we override this API in setup.py, so the content doesn't matter for pyspec itself.

Does it make sense to rename the Engine API side as well?

Edited: verify_and_notify_new_payload approach https://github.com/ethereum/consensus-specs/compare/engine-versioned-hashes-explicit

Copy link
Member

Choose a reason for hiding this comment

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

The extra length check is unnecessary since the SSZ container has defined field blob_kzg_commitments: List[KZGCommitment, MAX_BLOBS_PER_BLOCK].

It will be changed in this PR #3338

Copy link
Contributor Author

@hwwhww hwwhww May 23, 2023

Choose a reason for hiding this comment

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

@ppopth @g11tech
Ah...! but this PR will get merged first, so #3338 will have to address it. 😅

NewPayloadRequest(execution_payload=payload, versioned_hashes=versioned_hashes)
)

# Cache execution payload header
state.latest_execution_payload_header = ExecutionPayloadHeader(
Expand All @@ -247,13 +229,6 @@ def process_execution_payload(state: BeaconState, payload: ExecutionPayload, exe
)
```

#### Blob KZG commitments

```python
def process_blob_kzg_commitments(body: BeaconBlockBody) -> None:
assert verify_kzg_commitments_against_transactions(body.execution_payload.transactions, body.blob_kzg_commitments)
```

## Testing

*Note*: The function `initialize_beacon_state_from_eth1` is modified for pure Deneb testing only.
Expand Down
6 changes: 1 addition & 5 deletions specs/deneb/validator.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,13 +104,9 @@ via `get_payload(payload_id).blobs_bundle`.
2. Validate `blobs` and `blob_kzg_commitments`:

```python
def validate_blobs_and_kzg_commitments(execution_payload: ExecutionPayload,
blobs: Sequence[Blob],
def validate_blobs_and_kzg_commitments(blobs: Sequence[Blob],
blob_kzg_commitments: Sequence[KZGCommitment],
blob_kzg_proofs: Sequence[KZGProof]) -> None:
# Optionally sanity-check that the KZG commitments match the versioned hashes in the transactions
assert verify_kzg_commitments_against_transactions(execution_payload.transactions, blob_kzg_commitments)

# Optionally sanity-check that the KZG commitments match the blobs (as produced by the execution engine)
assert len(blob_kzg_commitments) == len(blobs) == len(blob_kzg_proofs)
assert verify_blob_kzg_proof_batch(blobs, blob_kzg_commitments, blob_kzg_proofs)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,33 +21,35 @@ def run_execution_payload_processing(spec, state, execution_payload, valid=True,
- post-state ('post').
If ``valid == False``, run expecting ``AssertionError``
"""
# Before Deneb, only `body.execution_payload` matters. `BeaconBlockBody` is just a wrapper.
body = spec.BeaconBlockBody(execution_payload=execution_payload)

yield 'pre', state
yield 'execution', {'execution_valid': execution_valid}
yield 'execution_payload', execution_payload
yield 'body', body

called_new_block = False

class TestEngine(spec.NoopExecutionEngine):
def notify_new_payload(self, payload) -> bool:
def notify_new_payload(self, new_payload_request) -> bool:
nonlocal called_new_block, execution_valid
called_new_block = True
assert payload == execution_payload
assert new_payload_request.execution_payload == body.execution_payload
return execution_valid

if not valid:
expect_assertion_error(lambda: spec.process_execution_payload(state, execution_payload, TestEngine()))
expect_assertion_error(lambda: spec.process_execution_payload(state, body, TestEngine()))
yield 'post', None
return

spec.process_execution_payload(state, execution_payload, TestEngine())
spec.process_execution_payload(state, body, TestEngine())

# Make sure we called the engine
assert called_new_block

yield 'post', state

assert state.latest_execution_payload_header == get_execution_payload_header(spec, execution_payload)
assert state.latest_execution_payload_header == get_execution_payload_header(spec, body.execution_payload)


def run_success_test(spec, state):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def verify_post_state(state, spec, expected_withdrawals,
def run_withdrawals_processing(spec, state, execution_payload, num_expected_withdrawals=None,
fully_withdrawable_indices=None, partial_withdrawals_indices=None, valid=True):
"""
Run ``process_execution_payload``, yielding:
Run ``process_withdrawals``, yielding:
- pre-state ('pre')
- execution payload ('execution_payload')
- post-state ('post').
Expand Down
Empty file.
Loading