diff --git a/tests/core/pyspec/eth2spec/test/deneb/fork_choice/__init__.py b/tests/core/pyspec/eth2spec/test/deneb/fork_choice/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/core/pyspec/eth2spec/test/deneb/fork_choice/test_on_block.py b/tests/core/pyspec/eth2spec/test/deneb/fork_choice/test_on_block.py new file mode 100644 index 000000000..85610675c --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/deneb/fork_choice/test_on_block.py @@ -0,0 +1,105 @@ +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.fork_choice import ( + BlobData, + get_genesis_forkchoice_store_and_block, + on_tick_and_append_step, + tick_and_add_block, + with_blob_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) + + def run_func_1(): + yield from tick_and_add_block(spec, store, signed_block, test_steps, blob_data=blob_data) + + yield from with_blob_data(spec, blob_data, run_func_1) + + 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) + + def run_func_2(): + yield from tick_and_add_block(spec, store, signed_block, test_steps, blob_data=blob_data) + + yield from with_blob_data(spec, blob_data, run_func_2) + + 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, blob_kzg_proofs = 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) + + def run_func_1(): + yield from tick_and_add_block(spec, store, signed_block, test_steps, blob_data=blob_data, valid=False) + + yield from with_blob_data(spec, blob_data, run_func_1) + + assert spec.get_head(store) != signed_block.message.hash_tree_root() + + yield 'steps', test_steps diff --git a/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py b/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py index af231d87f..d73a9a01b 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py +++ b/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py @@ -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,33 @@ from eth2spec.test.helpers.attestations import ( ) +class BlobData(NamedTuple): + blobs: Sequence[Any] + proofs: Sequence[bytes] + + +def with_blob_data(spec, blob_data, func): + 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 +44,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,6 +60,7 @@ 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 @@ -94,6 +125,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 +157,53 @@ 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: + assert len(blob_data.blobs) == len(blob_data.proofs) + 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: diff --git a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_get_head.py b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_get_head.py index 30f94b854..886fcbd20 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_get_head.py +++ b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_get_head.py @@ -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) diff --git a/tests/formats/fork_choice/README.md b/tests/formats/fork_choice/README.md index 3b28837de..bfc8a423d 100644 --- a/tests/formats/fork_choice/README.md +++ b/tests/formats/fork_choice/README.md @@ -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 + + + + +- [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) + + + + ## 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 are the expected values from `retrieve_blobs_and_proofs()` helper inside `is_data_available()` helper. + After this step, the `store` object may have been updated. #### `on_merge_block` execution step diff --git a/tests/generators/fork_choice/main.py b/tests/generators/fork_choice/main.py index b0c9a9bb9..7ff028cd8 100644 --- a/tests/generators/fork_choice/main.py +++ b/tests/generators/fork_choice/main.py @@ -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 = {