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
def sundry_functions(cls) -> str:
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
return ("TEST", "TEST")'''
return [], []'''
@classmethod
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`
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)
```

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 eth2spec.test.exceptions import BlockNotFoundException
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):
anchor_block_header = state.latest_block_header.copy()
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,
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]
if merge_block:
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,
block_not_found=block_not_found,
is_optimistic=is_optimistic,
blob_data=blob_data,
)
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):
spec.on_attestation(store, attestation, is_from_block=is_from_block)
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())}"
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):
spec.on_tick(store, time)
test_steps.append({'tick': int(time)})
@ -119,35 +171,52 @@ def add_block(spec,
test_steps,
valid=True,
block_not_found=False,
is_optimistic=False):
is_optimistic=False,
blob_data=None):
"""
Run on_block and on_attestation
"""
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 is_optimistic:
run_on_block(spec, store, signed_block, valid=True)
test_steps.append({
'block': get_block_file_name(signed_block),
'valid': False,
})
_append_step(is_blob_data_test, valid=False)
else:
try:
run_on_block(spec, store, signed_block, valid=True)
except (AssertionError, BlockNotFoundException) as e:
if isinstance(e, BlockNotFoundException) and not block_not_found:
assert False
test_steps.append({
'block': get_block_file_name(signed_block),
'valid': False,
})
_append_step(is_blob_data_test, valid=False)
return
else:
assert False
else:
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
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
@spec_state_test
def test_genesis(spec, state):
@ -271,6 +268,7 @@ def test_proposer_boost_correct_head(spec, state):
next_slots(spec, state_2, 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)
rng = random.Random(1001)
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))
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)
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)
rng = random.Random(1001)
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))
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.
## 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
### `meta.yaml`
@ -59,14 +83,20 @@ The parameter that is required for executing `on_block(store, block)`.
```yaml
{
block: string -- the name of the `block_<32-byte-root>.ssz_snappy` file.
To execute `on_block(store, block)` with the given attestation.
valid: bool -- optional, default to `true`.
If it's `false`, this execution step is expected to be invalid.
block: string -- the name of the `block_<32-byte-root>.ssz_snappy` file.
To execute `on_block(store, block)` with the given attestation.
blobs: string -- optional, the name of the `blobs_<32-byte-root>.ssz_snappy` file.
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).
`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.
#### `on_merge_block` execution step

View File

@ -19,7 +19,13 @@ if __name__ == "__main__":
]}
bellatrix_mods = combine_mods(_new_bellatrix_mods, altair_mods)
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
all_mods = {