Merge pull request #3463 from ethereum/deneb-fc-tests-take-2

Deneb fork choice tests - take 2
This commit is contained in:
Hsiao-Wei Wang 2023-08-03 21:40:24 +08:00 committed by GitHub
commit 56d6d1a51e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 307 additions and 26 deletions

View File

@ -21,9 +21,9 @@ T = TypeVar('T') # For generic function
@classmethod @classmethod
def sundry_functions(cls) -> str: def sundry_functions(cls) -> str:
return ''' return '''
def retrieve_blobs_and_proofs(beacon_block_root: Root) -> PyUnion[Tuple[Blob, KZGProof], Tuple[str, str]]: def retrieve_blobs_and_proofs(beacon_block_root: Root) -> Tuple[Sequence[Blob], Sequence[KZGProof]]:
# pylint: disable=unused-argument # pylint: disable=unused-argument
return ("TEST", "TEST")''' return [], []'''
@classmethod @classmethod
def execution_engine_cls(cls) -> str: def execution_engine_cls(cls) -> str:

View File

@ -55,11 +55,6 @@ def is_data_available(beacon_block_root: Root, blob_kzg_commitments: Sequence[KZ
# `MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS` # `MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS`
blobs, proofs = retrieve_blobs_and_proofs(beacon_block_root) blobs, proofs = retrieve_blobs_and_proofs(beacon_block_root)
# For testing, `retrieve_blobs_and_proofs` returns ("TEST", "TEST").
# TODO: Remove it once we have a way to inject `BlobSidecar` into tests.
if isinstance(blobs, str) or isinstance(proofs, str):
return True
return verify_blob_kzg_proof_batch(blobs, blob_kzg_commitments, proofs) return verify_blob_kzg_proof_batch(blobs, blob_kzg_commitments, proofs)
``` ```

View File

@ -0,0 +1,182 @@
from random import Random
from eth2spec.test.context import (
spec_state_test,
with_deneb_and_later,
)
from eth2spec.test.helpers.block import (
build_empty_block_for_next_slot,
)
from eth2spec.test.helpers.execution_payload import (
compute_el_block_hash,
)
from eth2spec.test.helpers.fork_choice import (
BlobData,
get_genesis_forkchoice_store_and_block,
on_tick_and_append_step,
tick_and_add_block_with_data,
)
from eth2spec.test.helpers.state import (
state_transition_and_sign_block,
)
from eth2spec.test.helpers.sharding import (
get_sample_opaque_tx,
)
def get_block_with_blob(spec, state, rng=None):
block = build_empty_block_for_next_slot(spec, state)
opaque_tx, blobs, blob_kzg_commitments, blob_kzg_proofs = get_sample_opaque_tx(spec, blob_count=1, rng=rng)
block.body.execution_payload.transactions = [opaque_tx]
block.body.execution_payload.block_hash = compute_el_block_hash(spec, block.body.execution_payload)
block.body.blob_kzg_commitments = blob_kzg_commitments
return block, blobs, blob_kzg_proofs
@with_deneb_and_later
@spec_state_test
def test_simple_blob_data(spec, state):
rng = Random(1234)
test_steps = []
# Initialization
store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state)
yield 'anchor_state', state
yield 'anchor_block', anchor_block
current_time = state.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time
on_tick_and_append_step(spec, store, current_time, test_steps)
assert store.time == current_time
# On receiving a block of `GENESIS_SLOT + 1` slot
block, blobs, blob_kzg_proofs = get_block_with_blob(spec, state, rng=rng)
signed_block = state_transition_and_sign_block(spec, state, block)
blob_data = BlobData(blobs, blob_kzg_proofs)
yield from tick_and_add_block_with_data(spec, store, signed_block, test_steps, blob_data)
assert spec.get_head(store) == signed_block.message.hash_tree_root()
# On receiving a block of next epoch
store.time = current_time + spec.config.SECONDS_PER_SLOT * spec.SLOTS_PER_EPOCH
block, blobs, blob_kzg_proofs = get_block_with_blob(spec, state, rng=rng)
signed_block = state_transition_and_sign_block(spec, state, block)
blob_data = BlobData(blobs, blob_kzg_proofs)
yield from tick_and_add_block_with_data(spec, store, signed_block, test_steps, blob_data)
assert spec.get_head(store) == signed_block.message.hash_tree_root()
yield 'steps', test_steps
@with_deneb_and_later
@spec_state_test
def test_invalid_incorrect_proof(spec, state):
rng = Random(1234)
test_steps = []
# Initialization
store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state)
yield 'anchor_state', state
yield 'anchor_block', anchor_block
current_time = state.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time
on_tick_and_append_step(spec, store, current_time, test_steps)
assert store.time == current_time
# On receiving a block of `GENESIS_SLOT + 1` slot
block, blobs, _ = get_block_with_blob(spec, state, rng=rng)
signed_block = state_transition_and_sign_block(spec, state, block)
# Insert incorrect proof
blob_kzg_proofs = [b'\xc0' + b'\x00' * 47]
blob_data = BlobData(blobs, blob_kzg_proofs)
yield from tick_and_add_block_with_data(spec, store, signed_block, test_steps, blob_data, valid=False)
assert spec.get_head(store) != signed_block.message.hash_tree_root()
yield 'steps', test_steps
@with_deneb_and_later
@spec_state_test
def test_invalid_data_unavailable(spec, state):
rng = Random(1234)
test_steps = []
# Initialization
store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state)
yield 'anchor_state', state
yield 'anchor_block', anchor_block
current_time = state.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time
on_tick_and_append_step(spec, store, current_time, test_steps)
assert store.time == current_time
# On receiving a block of `GENESIS_SLOT + 1` slot
block, _, _ = get_block_with_blob(spec, state, rng=rng)
signed_block = state_transition_and_sign_block(spec, state, block)
# data unavailable
blob_data = BlobData([], [])
yield from tick_and_add_block_with_data(spec, store, signed_block, test_steps, blob_data, valid=False)
assert spec.get_head(store) != signed_block.message.hash_tree_root()
yield 'steps', test_steps
@with_deneb_and_later
@spec_state_test
def test_invalid_wrong_proofs_length(spec, state):
rng = Random(1234)
test_steps = []
# Initialization
store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state)
yield 'anchor_state', state
yield 'anchor_block', anchor_block
current_time = state.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time
on_tick_and_append_step(spec, store, current_time, test_steps)
assert store.time == current_time
# On receiving a block of `GENESIS_SLOT + 1` slot
block, blobs, _ = get_block_with_blob(spec, state, rng=rng)
signed_block = state_transition_and_sign_block(spec, state, block)
# unavailable proofs
blob_data = BlobData(blobs, [])
yield from tick_and_add_block_with_data(spec, store, signed_block, test_steps, blob_data, valid=False)
assert spec.get_head(store) != signed_block.message.hash_tree_root()
yield 'steps', test_steps
@with_deneb_and_later
@spec_state_test
def test_invalid_wrong_blobs_length(spec, state):
rng = Random(1234)
test_steps = []
# Initialization
store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state)
yield 'anchor_state', state
yield 'anchor_block', anchor_block
current_time = state.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time
on_tick_and_append_step(spec, store, current_time, test_steps)
assert store.time == current_time
# On receiving a block of `GENESIS_SLOT + 1` slot
block, _, blob_kzg_proofs = get_block_with_blob(spec, state, rng=rng)
signed_block = state_transition_and_sign_block(spec, state, block)
# unavailable blobs
blob_data = BlobData([], blob_kzg_proofs)
yield from tick_and_add_block_with_data(spec, store, signed_block, test_steps, blob_data, valid=False)
assert spec.get_head(store) != signed_block.message.hash_tree_root()
yield 'steps', test_steps

View File

@ -1,3 +1,5 @@
from typing import NamedTuple, Sequence, Any
from eth_utils import encode_hex from eth_utils import encode_hex
from eth2spec.test.exceptions import BlockNotFoundException from eth2spec.test.exceptions import BlockNotFoundException
from eth2spec.test.helpers.attestations import ( from eth2spec.test.helpers.attestations import (
@ -7,6 +9,40 @@ from eth2spec.test.helpers.attestations import (
) )
class BlobData(NamedTuple):
"""
The return values of ``retrieve_blobs_and_proofs`` helper.
"""
blobs: Sequence[Any]
proofs: Sequence[bytes]
def with_blob_data(spec, blob_data, func):
"""
This helper runs the given ``func`` with monkeypatched ``retrieve_blobs_and_proofs``
that returns ``blob_data.blobs, blob_data.proofs``.
"""
def retrieve_blobs_and_proofs(beacon_block_root):
return blob_data.blobs, blob_data.proofs
retrieve_blobs_and_proofs_backup = spec.retrieve_blobs_and_proofs
spec.retrieve_blobs_and_proofs = retrieve_blobs_and_proofs
class AtomicBoolean():
value = False
is_called = AtomicBoolean()
def wrap(flag: AtomicBoolean):
yield from func()
flag.value = True
try:
yield from wrap(is_called)
finally:
spec.retrieve_blobs_and_proofs = retrieve_blobs_and_proofs_backup
assert is_called.value
def get_anchor_root(spec, state): def get_anchor_root(spec, state):
anchor_block_header = state.latest_block_header.copy() anchor_block_header = state.latest_block_header.copy()
if anchor_block_header.state_root == spec.Bytes32(): if anchor_block_header.state_root == spec.Bytes32():
@ -15,7 +51,8 @@ def get_anchor_root(spec, state):
def tick_and_add_block(spec, store, signed_block, test_steps, valid=True, def tick_and_add_block(spec, store, signed_block, test_steps, valid=True,
merge_block=False, block_not_found=False, is_optimistic=False): merge_block=False, block_not_found=False, is_optimistic=False,
blob_data=None):
pre_state = store.block_states[signed_block.message.parent_root] pre_state = store.block_states[signed_block.message.parent_root]
if merge_block: if merge_block:
assert spec.is_merge_transition_block(pre_state, signed_block.message.body) assert spec.is_merge_transition_block(pre_state, signed_block.message.body)
@ -30,11 +67,19 @@ def tick_and_add_block(spec, store, signed_block, test_steps, valid=True,
valid=valid, valid=valid,
block_not_found=block_not_found, block_not_found=block_not_found,
is_optimistic=is_optimistic, is_optimistic=is_optimistic,
blob_data=blob_data,
) )
return post_state return post_state
def tick_and_add_block_with_data(spec, store, signed_block, test_steps, blob_data, valid=True):
def run_func():
yield from tick_and_add_block(spec, store, signed_block, test_steps, blob_data=blob_data, valid=valid)
yield from with_blob_data(spec, blob_data, run_func)
def add_attestation(spec, store, attestation, test_steps, is_from_block=False): def add_attestation(spec, store, attestation, test_steps, is_from_block=False):
spec.on_attestation(store, attestation, is_from_block=is_from_block) spec.on_attestation(store, attestation, is_from_block=is_from_block)
yield get_attestation_file_name(attestation), attestation yield get_attestation_file_name(attestation), attestation
@ -94,6 +139,13 @@ def get_attester_slashing_file_name(attester_slashing):
return f"attester_slashing_{encode_hex(attester_slashing.hash_tree_root())}" return f"attester_slashing_{encode_hex(attester_slashing.hash_tree_root())}"
def get_blobs_file_name(blobs=None, blobs_root=None):
if blobs:
return f"blobs_{encode_hex(blobs.hash_tree_root())}"
else:
return f"blobs_{encode_hex(blobs_root)}"
def on_tick_and_append_step(spec, store, time, test_steps): def on_tick_and_append_step(spec, store, time, test_steps):
spec.on_tick(store, time) spec.on_tick(store, time)
test_steps.append({'tick': int(time)}) test_steps.append({'tick': int(time)})
@ -119,35 +171,52 @@ def add_block(spec,
test_steps, test_steps,
valid=True, valid=True,
block_not_found=False, block_not_found=False,
is_optimistic=False): is_optimistic=False,
blob_data=None):
""" """
Run on_block and on_attestation Run on_block and on_attestation
""" """
yield get_block_file_name(signed_block), signed_block yield get_block_file_name(signed_block), signed_block
# Check blob_data
if blob_data is not None:
blobs = spec.List[spec.Blob, spec.MAX_BLOBS_PER_BLOCK](blob_data.blobs)
blobs_root = blobs.hash_tree_root()
yield get_blobs_file_name(blobs_root=blobs_root), blobs
is_blob_data_test = blob_data is not None
def _append_step(is_blob_data_test, valid=True):
if is_blob_data_test:
test_steps.append({
'block': get_block_file_name(signed_block),
'blobs': get_blobs_file_name(blobs_root=blobs_root),
'proofs': [encode_hex(proof) for proof in blob_data.proofs],
'valid': valid,
})
else:
test_steps.append({
'block': get_block_file_name(signed_block),
'valid': valid,
})
if not valid: if not valid:
if is_optimistic: if is_optimistic:
run_on_block(spec, store, signed_block, valid=True) run_on_block(spec, store, signed_block, valid=True)
test_steps.append({ _append_step(is_blob_data_test, valid=False)
'block': get_block_file_name(signed_block),
'valid': False,
})
else: else:
try: try:
run_on_block(spec, store, signed_block, valid=True) run_on_block(spec, store, signed_block, valid=True)
except (AssertionError, BlockNotFoundException) as e: except (AssertionError, BlockNotFoundException) as e:
if isinstance(e, BlockNotFoundException) and not block_not_found: if isinstance(e, BlockNotFoundException) and not block_not_found:
assert False assert False
test_steps.append({ _append_step(is_blob_data_test, valid=False)
'block': get_block_file_name(signed_block),
'valid': False,
})
return return
else: else:
assert False assert False
else: else:
run_on_block(spec, store, signed_block, valid=True) run_on_block(spec, store, signed_block, valid=True)
test_steps.append({'block': get_block_file_name(signed_block)}) _append_step(is_blob_data_test)
# An on_block step implies receiving block's attestations # An on_block step implies receiving block's attestations
for attestation in signed_block.message.body.attestations: for attestation in signed_block.message.body.attestations:

View File

@ -34,9 +34,6 @@ from eth2spec.test.helpers.state import (
) )
rng = random.Random(1001)
@with_altair_and_later @with_altair_and_later
@spec_state_test @spec_state_test
def test_genesis(spec, state): def test_genesis(spec, state):
@ -271,6 +268,7 @@ def test_proposer_boost_correct_head(spec, state):
next_slots(spec, state_2, 2) next_slots(spec, state_2, 2)
block_2 = build_empty_block_for_next_slot(spec, state_2) block_2 = build_empty_block_for_next_slot(spec, state_2)
signed_block_2 = state_transition_and_sign_block(spec, state_2.copy(), block_2) signed_block_2 = state_transition_and_sign_block(spec, state_2.copy(), block_2)
rng = random.Random(1001)
while spec.hash_tree_root(block_1) >= spec.hash_tree_root(block_2): while spec.hash_tree_root(block_1) >= spec.hash_tree_root(block_2):
block_2.body.graffiti = spec.Bytes32(hex(rng.getrandbits(8 * 32))[2:].zfill(64)) block_2.body.graffiti = spec.Bytes32(hex(rng.getrandbits(8 * 32))[2:].zfill(64))
signed_block_2 = state_transition_and_sign_block(spec, state_2.copy(), block_2) signed_block_2 = state_transition_and_sign_block(spec, state_2.copy(), block_2)
@ -339,6 +337,7 @@ def test_discard_equivocations_on_attester_slashing(spec, state):
next_slots(spec, state_2, 2) next_slots(spec, state_2, 2)
block_2 = build_empty_block_for_next_slot(spec, state_2) block_2 = build_empty_block_for_next_slot(spec, state_2)
signed_block_2 = state_transition_and_sign_block(spec, state_2.copy(), block_2) signed_block_2 = state_transition_and_sign_block(spec, state_2.copy(), block_2)
rng = random.Random(1001)
while spec.hash_tree_root(block_1) >= spec.hash_tree_root(block_2): while spec.hash_tree_root(block_1) >= spec.hash_tree_root(block_2):
block_2.body.graffiti = spec.Bytes32(hex(rng.getrandbits(8 * 32))[2:].zfill(64)) block_2.body.graffiti = spec.Bytes32(hex(rng.getrandbits(8 * 32))[2:].zfill(64))
signed_block_2 = state_transition_and_sign_block(spec, state_2.copy(), block_2) signed_block_2 = state_transition_and_sign_block(spec, state_2.copy(), block_2)

View File

@ -2,6 +2,30 @@
The aim of the fork choice tests is to provide test coverage of the various components of the fork choice. The aim of the fork choice tests is to provide test coverage of the various components of the fork choice.
## Table of contents
<!-- TOC -->
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
- [Test case format](#test-case-format)
- [`meta.yaml`](#metayaml)
- [`anchor_state.ssz_snappy`](#anchor_statessz_snappy)
- [`anchor_block.ssz_snappy`](#anchor_blockssz_snappy)
- [`steps.yaml`](#stepsyaml)
- [`on_tick` execution step](#on_tick-execution-step)
- [`on_attestation` execution step](#on_attestation-execution-step)
- [`on_block` execution step](#on_block-execution-step)
- [`on_merge_block` execution step](#on_merge_block-execution-step)
- [`on_attester_slashing` execution step](#on_attester_slashing-execution-step)
- [`on_payload_info` execution step](#on_payload_info-execution-step)
- [Checks step](#checks-step)
- [`attestation_<32-byte-root>.ssz_snappy`](#attestation_32-byte-rootssz_snappy)
- [`block_<32-byte-root>.ssz_snappy`](#block_32-byte-rootssz_snappy)
- [Condition](#condition)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
<!-- /TOC -->
## Test case format ## Test case format
### `meta.yaml` ### `meta.yaml`
@ -59,14 +83,20 @@ The parameter that is required for executing `on_block(store, block)`.
```yaml ```yaml
{ {
block: string -- the name of the `block_<32-byte-root>.ssz_snappy` file. block: string -- the name of the `block_<32-byte-root>.ssz_snappy` file.
To execute `on_block(store, block)` with the given attestation. To execute `on_block(store, block)` with the given attestation.
valid: bool -- optional, default to `true`. blobs: string -- optional, the name of the `blobs_<32-byte-root>.ssz_snappy` file.
If it's `false`, this execution step is expected to be invalid. The blobs file content is a `List[Blob, MAX_BLOBS_PER_BLOCK]` SSZ object.
proofs: array of byte48 hex string -- optional, the proofs of blob commitments.
valid: bool -- optional, default to `true`.
If it's `false`, this execution step is expected to be invalid.
} }
``` ```
The file is located in the same folder (see below). The file is located in the same folder (see below).
`blobs` and `proofs` are new fields from Deneb EIP-4844. These fields indicate the expected values from `retrieve_blobs_and_proofs()` helper inside `is_data_available()` helper. If these two fields are not provided, `retrieve_blobs_and_proofs()` returns empty lists.
After this step, the `store` object may have been updated. After this step, the `store` object may have been updated.
#### `on_merge_block` execution step #### `on_merge_block` execution step

View File

@ -19,7 +19,13 @@ if __name__ == "__main__":
]} ]}
bellatrix_mods = combine_mods(_new_bellatrix_mods, altair_mods) bellatrix_mods = combine_mods(_new_bellatrix_mods, altair_mods)
capella_mods = bellatrix_mods # No additional Capella specific fork choice tests capella_mods = bellatrix_mods # No additional Capella specific fork choice tests
deneb_mods = capella_mods # No additional Deneb specific fork choice tests
# Deneb adds `is_data_available` tests
_new_deneb_mods = {key: 'eth2spec.test.deneb.fork_choice.test_' + key for key in [
'on_block',
]}
deneb_mods = combine_mods(_new_deneb_mods, capella_mods)
eip6110_mods = deneb_mods # No additional EIP6110 specific fork choice tests eip6110_mods = deneb_mods # No additional EIP6110 specific fork choice tests
all_mods = { all_mods = {