Merge pull request #3463 from ethereum/deneb-fc-tests-take-2
Deneb fork choice tests - take 2
This commit is contained in:
commit
56d6d1a51e
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
```
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = {
|
||||
|
|
Loading…
Reference in New Issue