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

Merge test vectors: enable phase0 tests for Merge + start on new testing #2380

Merged
merged 17 commits into from
May 10, 2021
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
16 changes: 6 additions & 10 deletions specs/merge/beacon-chain.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,8 @@ def is_transition_completed(state: BeaconState) -> bool:
#### `is_transition_block`

```python
def is_transition_block(state: BeaconState, block_body: BeaconBlockBody) -> bool:
return not is_transition_completed(state) and block_body.execution_payload != ExecutionPayload()
def is_transition_block(state: BeaconState, block: BeaconBlock) -> bool:
return not is_transition_completed(state) and block.body.execution_payload != ExecutionPayload()
```

#### `compute_time_at_slot`
Expand All @@ -168,7 +168,9 @@ def process_block(state: BeaconState, block: BeaconBlock) -> None:
process_randao(state, block.body)
process_eth1_data(state, block.body)
process_operations(state, block.body)
process_execution_payload(state, block.body) # [New in Merge]
# Pre-merge, skip execution payload processing
if is_transition_completed(state) or is_transition_block(state, block):
process_execution_payload(state, block.body.execution_payload) # [New in Merge]
Copy link
Contributor

@hwwhww hwwhww May 5, 2021

Choose a reason for hiding this comment

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

Two issues:

  1. Looking at tests/formats/operations/README.md and I just realized that we put sync_aggregate as part of "operation" tests but process_sync_committee is not part of the process_operations function like execution_payload.
    1. IMO it's good to consider sync_aggregate as an operation.
    2. If we want to simplify it, I think execution_payload could also be considered as an operation since it's essentially "transactions".
  2. I can see why it looks good to extract the is_transition_completed(state) or is_transition_block(state, block) condition checks from process_execution_payload, but it makes it a bit unclear where/how to test these conditions in test cases.

What do you think about:

  1. Move process_sync_committee and process_execution_payload calls into process_operations.
  2. Revert the if is_transition_completed(state) or is_transition_block(state, block) checks extraction.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

  1. Regarding process_sync_committee: sure, but let's keep that separate from merge-testing for now. Moving the process_execution_payload to process_operations would be good too. I'm honestly surprised we didn't do the same for the Randao and Eth1Data functionality.
  2. The if-statement was purposefully extracted from the process_execution_payload function: this way the function input can just be an ExecutionPayload, and not a complete beacon block body. This helps with future compatibility and reduces test complexity (We don't have to set up a complete beacon block for every single test case). We can test that one if-statement as part of the regular full beacon block testing. And in future phases after the merge, we may even remove the if-statement entirely, since the merge-transition check will always be true.

Copy link
Contributor

Choose a reason for hiding this comment

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

For (2), it might make sense to put it behind a function is_execution_enabled(state, block). This is semantically cleaner for any chain/setting that doesn't have the transition. They can just override the function to return True and as you mentioned, in the future we can just remove it.

```

#### Execution payload processing
Expand All @@ -181,16 +183,10 @@ The body of the function is implementation dependent.
##### `process_execution_payload`

```python
def process_execution_payload(state: BeaconState, body: BeaconBlockBody) -> None:
def process_execution_payload(state: BeaconState, execution_payload: ExecutionPayload) -> None:
"""
Note: This function is designed to be able to be run in parallel with the other `process_block` sub-functions
"""
# Pre-merge, skip processing
if not is_transition_completed(state) and not is_transition_block(state, body):
return

execution_payload = body.execution_payload

if is_transition_completed(state):
assert execution_payload.parent_hash == state.latest_execution_payload_header.block_hash
assert execution_payload.number == state.latest_execution_payload_header.number + 1
Expand Down
2 changes: 1 addition & 1 deletion specs/merge/fork-choice.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def on_block(store: Store, signed_block: SignedBeaconBlock) -> None:
assert get_ancestor(store, block.parent_root, finalized_slot) == store.finalized_checkpoint.root

# [New in Merge]
if is_transition_block(pre_state, block.body):
if is_transition_block(pre_state, block):
# Delay consideration of block until PoW block is processed by the PoW node
pow_block = get_pow_block(block.body.execution_payload.parent_hash)
assert pow_block.is_processed
Expand Down
8 changes: 7 additions & 1 deletion tests/core/pyspec/eth2spec/test/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from .exceptions import SkippedTest
from .helpers.constants import (
PHASE0, ALTAIR,
ALL_PHASES, FORKS_BEFORE_ALTAIR,
ALL_PHASES, FORKS_BEFORE_ALTAIR, FORKS_BEFORE_MERGE,
)
from .helpers.genesis import create_genesis_state
from .utils import vector_test, with_meta_tags
Expand Down Expand Up @@ -365,3 +365,9 @@ def is_post_altair(spec):
if spec.fork in FORKS_BEFORE_ALTAIR:
return False
return True


def is_post_merge(spec):
if spec.fork in FORKS_BEFORE_MERGE:
return False
return True
6 changes: 5 additions & 1 deletion tests/core/pyspec/eth2spec/test/helpers/block.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from eth2spec.test.context import is_post_altair
from eth2spec.test.context import is_post_altair, is_post_merge
from eth2spec.test.helpers.execution_payload import build_empty_execution_payload
from eth2spec.test.helpers.keys import privkeys
from eth2spec.utils import bls
from eth2spec.utils.bls import only_with_bls
Expand Down Expand Up @@ -94,6 +95,9 @@ def build_empty_block(spec, state, slot=None):
if is_post_altair(spec):
empty_block.body.sync_aggregate.sync_committee_signature = spec.G2_POINT_AT_INFINITY

if is_post_merge(spec):
empty_block.body.execution_payload = build_empty_execution_payload(spec, state)

apply_randao_reveal(spec, state, empty_block)
return empty_block

Expand Down
9 changes: 6 additions & 3 deletions tests/core/pyspec/eth2spec/test/helpers/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,24 @@
# Some of the Spec module functionality is exposed here to deal with phase-specific changes.
PHASE0 = SpecForkName('phase0')
ALTAIR = SpecForkName('altair')
MERGE = SpecForkName('merge')

# Experimental phases (not included in default "ALL_PHASES"):
MERGE = SpecForkName('merge')
SHARDING = SpecForkName('sharding')
CUSTODY_GAME = SpecForkName('custody_game')
DAS = SpecForkName('das')

# The forks that pytest runs with.
ALL_PHASES = (PHASE0, ALTAIR)
ALL_PHASES = (PHASE0, ALTAIR, MERGE)
# The forks that output to the test vectors.
TESTGEN_FORKS = (PHASE0, ALTAIR)
TESTGEN_FORKS = (PHASE0, ALTAIR, MERGE)
# TODO: everything runs in parallel to Altair.
# After features are rebased on the Altair fork, this can be reduced to just PHASE0.
FORKS_BEFORE_ALTAIR = (PHASE0, MERGE, SHARDING, CUSTODY_GAME, DAS)

# TODO: when rebasing Merge onto Altair, add ALTAIR to this tuple.
FORKS_BEFORE_MERGE = (PHASE0,)

#
# Config
#
Expand Down
26 changes: 26 additions & 0 deletions tests/core/pyspec/eth2spec/test/helpers/execution_payload.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@

def build_empty_execution_payload(spec, state):
"""
Assuming a pre-state of the same slot, build a valid ExecutionPayload without any transactions.
protolambda marked this conversation as resolved.
Show resolved Hide resolved
"""
latest = state.latest_execution_payload_header
timestamp = spec.compute_time_at_slot(state, state.slot)
empty_txs = spec.List[spec.OpaqueTransaction, spec.MAX_EXECUTION_TRANSACTIONS]()

payload = spec.ExecutionPayload(
block_hash=spec.Hash32(),
parent_hash=latest.block_hash,
coinbase=spec.Bytes20(),
protolambda marked this conversation as resolved.
Show resolved Hide resolved
state_root=latest.state_root, # no changes to the state
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this certain to be the case?

Does the consensus not write anything to state automatically? The recent block_roots being included in state is something that comes to mind, like EIP 210. I know 210 isn't deployed but I'm unsure if there isn't anything else like that in place

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There may be something yes, although I'm not entirely sure either. The account tree definitely does not change (no block rewards on eth1 state anymore), and the storage/values/code/nonce of accounts only changes with transactions. Can't find anything in the yellow paper that would otherwise touch the eth1 world state.

I'll open an issue to investigate this more. Maybe this new possibility of a duplicate state-root in eth1 blocks is untested/dangerous?

Copy link
Contributor

Choose a reason for hiding this comment

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

might be. at least it might not be expected

I think we've touched on this a tiny bit. Peter mentioned that it isn't the case today because of issuance/coinbase but that it might change in this context. I think they can handle it but it's something to highlight as a design consideration/edge-case for execution engines

number=latest.number + 1,
gas_limit=latest.gas_limit, # retain same limit
gas_used=0, # empty block, 0 gas
timestamp=timestamp,
receipt_root=b"no receipts here" + b"\x00"*16, # TODO: root of empty MPT may be better.
logs_bloom=spec.ByteVector[spec.BYTES_PER_LOGS_BLOOM](), # TODO: zeroed logs bloom for empty logs ok?
protolambda marked this conversation as resolved.
Show resolved Hide resolved
transactions_root=empty_txs,
)
# TODO: real RLP + block hash logic would be nice, requires RLP and keccak256 dependency however.
payload.block_hash = spec.Hash32(spec.hash(payload.hash_tree_root() + b"FAKE RLP HASH"))
Copy link
Contributor

Choose a reason for hiding this comment

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

The htr would not be used in the correct functionality, 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.

Right, it's just a short-cut to make a hash that is unique to the payload. Encoding and flat-hashing would work to. I think we may just want to do the real RLP thing though. The current thing works for now, but it's nice if it's correct, especially if we get eth1-eth2 combination clients that run these tests.

Copy link
Contributor

Choose a reason for hiding this comment

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

We might want to decouple this shortcut and use it as a helper.


return payload
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
from eth2spec.test.helpers.constants import PHASE0, ALTAIR
from eth2spec.test.context import spec_state_test, expect_assertion_error, always_bls, with_all_phases_except

with_merge_and_later = with_all_phases_except([PHASE0, ALTAIR])


def run_execution_payload_processing(spec, state, execution_payload, valid=True, execution_valid=True):
"""
Run ``process_execution_payload``, yielding:
- pre-state ('pre')
- execution payload ('execution_payload')
- execution details, to mock EVM execution ('execution.yml', a dict with 'execution_valid' key and boolean value)
- post-state ('post').
If ``valid == False``, run expecting ``AssertionError``
"""

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

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

spec.process_execution_payload(state, execution_payload)

yield 'post', state

# TODO: any assertions to make?
protolambda marked this conversation as resolved.
Show resolved Hide resolved


@with_merge_and_later
@spec_state_test
def test_success_first_payload(spec, state):
assert not spec.is_transition_completed(state)
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe assert that it is a transition block too

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There is no block processing here, just the execution payload, so as-is it cannot be asserted.

Maybe we should start using the new run_block_processing_to helper to simulate the block behavior up to the execution-payload processing, I can discuss this with Mikhail for the next PR, but I prefer to keep this PR simple.

Copy link
Contributor

Choose a reason for hiding this comment

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

Is this test case supposed to test the transition?

Copy link
Contributor

Choose a reason for hiding this comment

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

process_execution_payload has been refactored to not care about the transition logic. So yes, it tests the empty header to a real header but it doesn't test that the transition go/no-go logic works

Copy link
Contributor

Choose a reason for hiding this comment

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

A nit: then we likely should not use transition spec helpers to check the pre-state.

Copy link
Contributor

Choose a reason for hiding this comment

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

ah, but we want to test a range of tests/pre-states/inputs directly on this function. One of which when state has an empty header which maps to not spec.is_transition_completed. Even though process_execution_payload does not check this, we do want to ensure we have a test cast that is in this pre-condition.

If we don't use that helper, we'd want to check state.latest_execution_header == ExecutionHeader(). Or directly just set it.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok, I see. This is more likely to be done in a separate PR anyway. Though, I would rather set test case preliminaries in the test case itself than expect them from the outside.


# TODO: execution payload
execution_payload = spec.ExecutionPayload()

yield from run_execution_payload_processing(spec, state, execution_payload)


@with_merge_and_later
@spec_state_test
def test_success_regular_payload(spec, state):
# TODO: setup state
assert spec.is_transition_completed(state)

# TODO: execution payload
execution_payload = spec.ExecutionPayload()

yield from run_execution_payload_processing(spec, state, execution_payload)


@with_merge_and_later
@spec_state_test
def test_success_first_payload_with_gap_slot(spec, state):
# TODO: transition gap slot

assert not spec.is_transition_completed(state)

# TODO: execution payload
execution_payload = spec.ExecutionPayload()

yield from run_execution_payload_processing(spec, state, execution_payload)


@with_merge_and_later
@spec_state_test
def test_success_regular_payload_with_gap_slot(spec, state):
# TODO: setup state
assert spec.is_transition_completed(state)
# TODO: transition gap slot

# TODO: execution payload
execution_payload = spec.ExecutionPayload()

yield from run_execution_payload_processing(spec, state, execution_payload)


@with_merge_and_later
@spec_state_test
def test_bad_execution_first_payload(spec, state):
# completely valid payload, but execution itself fails (e.g. block exceeds gas limit)

# TODO: execution payload.
execution_payload = spec.ExecutionPayload()

yield from run_execution_payload_processing(spec, state, execution_payload, valid=False, execution_valid=False)


@with_merge_and_later
@spec_state_test
def test_bad_execution_regular_payload(spec, state):
# completely valid payload, but execution itself fails (e.g. block exceeds gas limit)

# TODO: execution payload
execution_payload = spec.ExecutionPayload()

yield from run_execution_payload_processing(spec, state, execution_payload, valid=False, execution_valid=False)


@with_merge_and_later
@spec_state_test
def test_bad_parent_hash_first_payload(spec, state):
# TODO: execution payload
execution_payload = spec.ExecutionPayload()

yield from run_execution_payload_processing(spec, state, execution_payload, valid=False)


@with_merge_and_later
@spec_state_test
def test_bad_number_first_payload(spec, state):
# TODO: execution payload
execution_payload = spec.ExecutionPayload()

yield from run_execution_payload_processing(spec, state, execution_payload, valid=False)


@with_merge_and_later
@spec_state_test
def test_bad_everything_first_payload(spec, state):
# TODO: execution payload
execution_payload = spec.ExecutionPayload()

yield from run_execution_payload_processing(spec, state, execution_payload, valid=False)


@with_merge_and_later
@spec_state_test
def test_bad_timestamp_first_payload(spec, state):
# TODO: execution payload
execution_payload = spec.ExecutionPayload()

yield from run_execution_payload_processing(spec, state, execution_payload, valid=False)


@with_merge_and_later
@spec_state_test
def test_bad_timestamp_regular_payload(spec, state):
# TODO: execution payload
execution_payload = spec.ExecutionPayload()

yield from run_execution_payload_processing(spec, state, execution_payload, valid=False)

Empty file.
25 changes: 25 additions & 0 deletions tests/core/pyspec/eth2spec/test/merge/sanity/test_blocks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from eth2spec.test.helpers.state import (
state_transition_and_sign_block
)
from eth2spec.test.helpers.block import (
build_empty_block_for_next_slot
)
from eth2spec.test.context import (
with_all_phases, spec_state_test
)


@with_all_phases
@spec_state_test
def test_empty_block_transition(spec, state):
yield 'pre', state

block = build_empty_block_for_next_slot(spec, state)
assert len(block.body.execution_payload.transactions) == 0

signed_block = state_transition_and_sign_block(spec, state, block)

yield 'blocks', [signed_block]
yield 'post', state

# TODO: tests with EVM, mock or replacement?
24 changes: 15 additions & 9 deletions tests/formats/operations/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,23 @@ This excludes the other parts of the block-transition.

Operations:

| *`operation-name`* | *`operation-object`* | *`input name`* | *`processing call`* |
|-------------------------|-----------------------|----------------------|-----------------------------------------------------------------|
| `attestation` | `Attestation` | `attestation` | `process_attestation(state, attestation)` |
| `attester_slashing` | `AttesterSlashing` | `attester_slashing` | `process_attester_slashing(state, attester_slashing)` |
| `block_header` | `BeaconBlock` | **`block`** | `process_block_header(state, block)` |
| `deposit` | `Deposit` | `deposit` | `process_deposit(state, deposit)` |
| `proposer_slashing` | `ProposerSlashing` | `proposer_slashing` | `process_proposer_slashing(state, proposer_slashing)` |
| `voluntary_exit` | `SignedVoluntaryExit` | `voluntary_exit` | `process_voluntary_exit(state, voluntary_exit)` |
| `sync_aggregate` | `SyncAggregate` | `sync_aggregate` | `process_sync_committee(state, sync_aggregate)` (new in Altair) |
| *`operation-name`* | *`operation-object`* | *`input name`* | *`processing call`* |
|-------------------------|-----------------------|----------------------|----------------------------------------------------------------------|
| `attestation` | `Attestation` | `attestation` | `process_attestation(state, attestation)` |
| `attester_slashing` | `AttesterSlashing` | `attester_slashing` | `process_attester_slashing(state, attester_slashing)` |
| `block_header` | `BeaconBlock` | **`block`** | `process_block_header(state, block)` |
| `deposit` | `Deposit` | `deposit` | `process_deposit(state, deposit)` |
| `proposer_slashing` | `ProposerSlashing` | `proposer_slashing` | `process_proposer_slashing(state, proposer_slashing)` |
| `voluntary_exit` | `SignedVoluntaryExit` | `voluntary_exit` | `process_voluntary_exit(state, voluntary_exit)` |
| `sync_aggregate` | `SyncAggregate` | `sync_aggregate` | `process_sync_committee(state, sync_aggregate)` (new in Altair) |
| `execution_payload` | `ExecutionPayload` | `execution_payload` | `process_execution_payload(state, execution_payload)` (new in Merge) |

Note that `block_header` is not strictly an operation (and is a full `Block`), but processed in the same manner, and hence included here.

The `execution_payload` processing normally requires a `verify_execution_state_transition(execution_payload)`,
a responsibility of an (external) execution engine.
During testing this execution is mocked, an `execution.yml` is provided instead:
a dict containing an `execution_valid` boolean field with the verification result.

The resulting state should match the expected `post` state, or if the `post` state is left blank,
the handler should reject the input operation as invalid.
10 changes: 8 additions & 2 deletions tests/generators/epoch_processing/main.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from eth2spec.gen_helpers.gen_from_tests.gen import run_state_test_generators
from eth2spec.phase0 import spec as spec_phase0
from eth2spec.altair import spec as spec_altair
from eth2spec.test.helpers.constants import PHASE0, ALTAIR
from eth2spec.merge import spec as spec_merge
from eth2spec.test.helpers.constants import PHASE0, ALTAIR, MERGE


specs = (spec_phase0, spec_altair)
specs = (spec_phase0, spec_altair, spec_merge)


if __name__ == "__main__":
Expand All @@ -27,6 +28,10 @@
**phase_0_mods,
} # also run the previous phase 0 tests

# No epoch-processing changes in Merge and previous testing repeats with new types, so no additional tests required.
# TODO: rebase onto Altair testing later.
merge_mods = phase_0_mods

# TODO Custody Game testgen is disabled for now
# custody_game_mods = {**{key: 'eth2spec.test.custody_game.epoch_processing.test_process_' + key for key in [
# 'reveal_deadlines',
Expand All @@ -37,6 +42,7 @@
all_mods = {
PHASE0: phase_0_mods,
ALTAIR: altair_mods,
MERGE: merge_mods,
}

run_state_test_generators(runner_name="epoch_processing", specs=specs, all_mods=all_mods)
Loading