From d7f6a42729a7e734a51d3e5434ebe194f5edddaf Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Tue, 30 Nov 2021 23:55:03 +0800 Subject: [PATCH 01/16] [WIP] Add ex-ante fork choice test cases --- specs/phase0/fork-choice.md | 1 + tests/core/pyspec/eth2spec/test/context.py | 5 +- .../eth2spec/test/helpers/fork_choice.py | 10 +- .../test/phase0/fork_choice/test_ex_ante.py | 290 ++++++++++++++++++ 4 files changed, 301 insertions(+), 5 deletions(-) create mode 100644 tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py diff --git a/specs/phase0/fork-choice.md b/specs/phase0/fork-choice.md index d082ede30..ca38926fc 100644 --- a/specs/phase0/fork-choice.md +++ b/specs/phase0/fork-choice.md @@ -263,6 +263,7 @@ def get_head(store: Store) -> Root: if len(children) == 0: return head # Sort by latest attesting balance with ties broken lexicographically + # Ties broken by favoring block with lexicographically higher root head = max(children, key=lambda root: (get_latest_attesting_balance(store, root), root)) ``` diff --git a/tests/core/pyspec/eth2spec/test/context.py b/tests/core/pyspec/eth2spec/test/context.py index 184c0d609..346d2fc70 100644 --- a/tests/core/pyspec/eth2spec/test/context.py +++ b/tests/core/pyspec/eth2spec/test/context.py @@ -484,8 +484,9 @@ def with_config_overrides(config_overrides): # Retain types of all config values test_config = {k: config_types[k](v) for k, v in tmp_config.items()} - # Output the config for test vectors (TODO: check config YAML encoding) - yield 'config', 'data', test_config + # FIXME: config YAML encoding issue + # Output the config for test vectors + # yield 'config', 'data', test_config spec.config = spec.Configuration(**test_config) diff --git a/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py b/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py index 0b06f283f..f056b9acd 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py +++ b/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py @@ -42,6 +42,12 @@ def tick_and_add_block(spec, store, signed_block, test_steps, valid=True, return post_state +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 + test_steps.append({'attestation': get_attestation_file_name(attestation)}) + + def tick_and_run_on_attestation(spec, store, attestation, test_steps, is_from_block=False): parent_block = store.blocks[attestation.data.beacon_block_root] pre_state = store.block_states[spec.hash_tree_root(parent_block)] @@ -52,9 +58,7 @@ def tick_and_run_on_attestation(spec, store, attestation, test_steps, is_from_bl spec.on_tick(store, next_epoch_time) test_steps.append({'tick': int(next_epoch_time)}) - spec.on_attestation(store, attestation, is_from_block=is_from_block) - yield get_attestation_file_name(attestation), attestation - test_steps.append({'attestation': get_attestation_file_name(attestation)}) + yield from add_attestation(spec, store, attestation, test_steps, is_from_block) def run_on_attestation(spec, store, attestation, is_from_block=False, valid=True): diff --git a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py new file mode 100644 index 000000000..d19cb69b3 --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py @@ -0,0 +1,290 @@ +from eth2spec.test.context import ( + MAINNET, + spec_configured_state_test, + spec_state_test, + with_all_phases, + with_presets, +) +from eth2spec.test.helpers.attestations import ( + get_valid_attestation, + sign_attestation, +) +from eth2spec.test.helpers.block import ( + build_empty_block, +) +from eth2spec.test.helpers.fork_choice import ( + get_genesis_forkchoice_store_and_block, + on_tick_and_append_step, + add_attestation, + add_block, + tick_and_add_block, +) +from eth2spec.test.helpers.state import ( + state_transition_and_sign_block, +) + + +def _apply_base_block_a(spec, state, store, test_steps): + # On receiving block A at slot `N` + block = build_empty_block(spec, state, slot=state.slot + 1) + signed_block_a = state_transition_and_sign_block(spec, state, block) + yield from tick_and_add_block(spec, store, signed_block_a, test_steps) + assert spec.get_head(store) == signed_block_a.message.hash_tree_root() + + +@with_all_phases +@spec_state_test +def test_ex_ante_secnario_1_with_boost(spec, state): + """ + With a single adversarial attestation + + Block A - slot N + Block B (parent A) - slot N+1 + Block C (parent A) - slot N+2 + Attestation_1 (Block B) - slot N+1 – size 1 + """ + 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 block A at slot `N` + yield from _apply_base_block_a(spec, state, store, test_steps) + state_a = state.copy() + + # Block B at slot `N + 1`, parent is A + state_b = state_a.copy() + block = build_empty_block(spec, state_a, slot=state_a.slot + 1) + signed_block_b = state_transition_and_sign_block(spec, state_b, block) + + # Block C at slot `N + 2`, parent is A + state_c = state_a.copy() + block = build_empty_block(spec, state_c, slot=state_a.slot + 2) + signed_block_c = state_transition_and_sign_block(spec, state_c, block) + + # Attestation_1 received at N+2 — B is head due to boost proposer + def _filter_participant_set(participants): + return [next(iter(participants))] + + attestation = get_valid_attestation( + spec, state_b, slot=state_b.slot, signed=False, filter_participant_set=_filter_participant_set + ) + attestation.data.beacon_block_root = signed_block_b.message.hash_tree_root() + assert len([i for i in attestation.aggregation_bits if i == 1]) == 1 + sign_attestation(spec, state_b, attestation) + + # Block C received at N+2 — C is head + time = state_c.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, time, test_steps) + yield from add_block(spec, store, signed_block_c, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Block B received at N+2 — C is head that has higher proposer score boost + yield from add_block(spec, store, signed_block_b, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Attestation_1 received at N+2 — C is head + yield from add_attestation(spec, store, attestation, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + yield 'steps', test_steps + + +@with_all_phases +@spec_configured_state_test({ + 'PROPOSER_SCORE_BOOST': 0, +}) +def test_ex_ante_secnario_1_without_boost(spec, state): + """ + With a single adversarial attestation + + NOTE: this case disabled proposer score boost by setting config `PROPOSER_SCORE_BOOST` to `0` + + Block A - slot N + Block B (parent A) - slot N+1 + Block C (parent A) - slot N+2 + Attestation_1 (Block B) - slot N+1 – size 1 + """ + # For testing `PROPOSER_SCORE_BOOST = 0` case + yield 'PROPOSER_SCORE_BOOST', 'meta', 0 + + 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 block A at slot `N` + yield from _apply_base_block_a(spec, state, store, test_steps) + state_a = state.copy() + + # Block B at slot `N + 1`, parent is A + state_b = state_a.copy() + block = build_empty_block(spec, state_a, slot=state_a.slot + 1) + signed_block_b = state_transition_and_sign_block(spec, state_b, block) + + # Block C at slot `N + 2`, parent is A + state_c = state_a.copy() + block = build_empty_block(spec, state_c, slot=state_a.slot + 2) + signed_block_c = state_transition_and_sign_block(spec, state_c, block) + + # Attestation_1 received at N+2 — B is head due to boost proposer + def _filter_participant_set(participants): + return [next(iter(participants))] + + attestation = get_valid_attestation( + spec, state_b, slot=state_b.slot, signed=False, filter_participant_set=_filter_participant_set + ) + attestation.data.beacon_block_root = signed_block_b.message.hash_tree_root() + assert len([i for i in attestation.aggregation_bits if i == 1]) == 1 + sign_attestation(spec, state_b, attestation) + + # Block C received at N+2 — C is head + time = state_c.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, time, test_steps) + yield from add_block(spec, store, signed_block_c, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Block B received at N+2 + # Block B and C has the same score 0. Use a lexicographical order for tie-breaking. + yield from add_block(spec, store, signed_block_b, test_steps) + if signed_block_b.message.hash_tree_root() >= signed_block_c.message.hash_tree_root(): + assert spec.get_head(store) == signed_block_b.message.hash_tree_root() + else: + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Attestation_1 received at N+2 — B is head + yield from add_attestation(spec, store, attestation, test_steps) + assert spec.get_head(store) == signed_block_b.message.hash_tree_root() + + yield 'steps', test_steps + + +@with_all_phases +@with_presets([MAINNET], reason="to create larger committee") +@spec_state_test +def test_ex_ante_attestations_is_greater_than_proposer_boost_with_boost(spec, state): + """ + Adversarial attestations > proposer boost + + Block A - slot N + Block B (parent A) - slot N+1 + Block C (parent A) - slot N+2 + Attestation_1 (Block B) - slot N+1 – size > proposer_boost + """ + 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 block A at slot `N` + yield from _apply_base_block_a(spec, state, store, test_steps) + state_a = state.copy() + + # Block B at slot `N + 1`, parent is A + state_b = state_a.copy() + block = build_empty_block(spec, state_a, slot=state_a.slot + 1) + signed_block_b = state_transition_and_sign_block(spec, state_b, block) + + # Block C at slot `N + 2`, parent is A + state_c = state_a.copy() + block = build_empty_block(spec, state_c, slot=state_a.slot + 2) + signed_block_c = state_transition_and_sign_block(spec, state_c, block) + + # Full attestation received at N+2 — B is head due to boost proposer + attestation = get_valid_attestation(spec, state_b, slot=state_b.slot, signed=False) + attestation.data.beacon_block_root = signed_block_b.message.hash_tree_root() + assert len([i for i in attestation.aggregation_bits if i == 1]) > 1 + sign_attestation(spec, state_b, attestation) + + # Block C received at N+2 — C is head + time = state_c.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, time, test_steps) + yield from add_block(spec, store, signed_block_c, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Block B received at N+2 — C is head that has higher proposer score boost + yield from add_block(spec, store, signed_block_b, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Attestation_1 received at N+2 — B is head because B's attestation_score > C's proposer_score. + # (B's proposer_score = C's attestation_score = 0) + yield from add_attestation(spec, store, attestation, test_steps) + assert spec.get_head(store) == signed_block_b.message.hash_tree_root() + + yield 'steps', test_steps + + +@with_all_phases +@spec_configured_state_test({ + 'PROPOSER_SCORE_BOOST': 0, +}) +@with_presets([MAINNET], reason="to create larger committee") +def test_ex_ante_attestations_is_greater_than_proposer_boost_without_boost(spec, state): + """ + Adversarial attestations > proposer boost + + Block A - slot N + Block B (parent A) - slot N+1 + Block C (parent A) - slot N+2 + Attestation_1 (Block B) - slot N+1 – size > proposer_boost + """ + 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 block A at slot `N` + yield from _apply_base_block_a(spec, state, store, test_steps) + state_a = state.copy() + + # Block B at slot `N + 1`, parent is A + state_b = state_a.copy() + block = build_empty_block(spec, state_a, slot=state_a.slot + 1) + signed_block_b = state_transition_and_sign_block(spec, state_b, block) + + # Block C at slot `N + 2`, parent is A + state_c = state_a.copy() + block = build_empty_block(spec, state_c, slot=state_a.slot + 2) + signed_block_c = state_transition_and_sign_block(spec, state_c, block) + + # Full attestation received at N+2 — B is head due to boost proposer + attestation = get_valid_attestation(spec, state_b, slot=state_b.slot, signed=False) + attestation.data.beacon_block_root = signed_block_b.message.hash_tree_root() + assert len([i for i in attestation.aggregation_bits if i == 1]) > 1 + sign_attestation(spec, state_b, attestation) + + # Block C received at N+2 — C is head + time = state_c.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, time, test_steps) + yield from add_block(spec, store, signed_block_c, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Block B received at N+2 + # Block B and C has the same score 0. Use a lexicographical order for tie-breaking. + yield from add_block(spec, store, signed_block_b, test_steps) + if signed_block_b.message.hash_tree_root() >= signed_block_c.message.hash_tree_root(): + assert spec.get_head(store) == signed_block_b.message.hash_tree_root() + else: + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Attestation_1 received at N+2 — B is head because B's attestation_score > C's attestation_score + yield from add_attestation(spec, store, attestation, test_steps) + assert spec.get_head(store) == signed_block_b.message.hash_tree_root() + + yield 'steps', test_steps From bb8168fd956d753ea5ff84f94d6f0b334fbad61d Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Thu, 2 Dec 2021 00:46:19 +0800 Subject: [PATCH 02/16] Make config dict compatible with yaml.dump --- tests/core/pyspec/eth2spec/test/context.py | 24 +++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/tests/core/pyspec/eth2spec/test/context.py b/tests/core/pyspec/eth2spec/test/context.py index 346d2fc70..2a6f2e324 100644 --- a/tests/core/pyspec/eth2spec/test/context.py +++ b/tests/core/pyspec/eth2spec/test/context.py @@ -1,6 +1,7 @@ import pytest from copy import deepcopy from dataclasses import dataclass +from eth_utils import encode_hex from eth2spec.phase0 import mainnet as spec_phase0_mainnet, minimal as spec_phase0_minimal from eth2spec.altair import mainnet as spec_altair_mainnet, minimal as spec_altair_minimal @@ -464,6 +465,22 @@ def with_presets(preset_bases, reason=None): return decorator +def _get_basic_dict(ssz_dict: Dict[str, Any]) -> Dict[str, Any]: + """ + Get dict of Python built-in types from a dict of SSZ objects. + """ + result = {} + for k, v in ssz_dict.items(): + if isinstance(v, int): + value = int(v) + elif isinstance(v, bytes): + value = encode_hex(v) + else: + value = str(v) + result[k] = value + return result + + def with_config_overrides(config_overrides): """ WARNING: the spec_test decorator must wrap this, to ensure the decorated test actually runs. @@ -484,9 +501,10 @@ def with_config_overrides(config_overrides): # Retain types of all config values test_config = {k: config_types[k](v) for k, v in tmp_config.items()} - # FIXME: config YAML encoding issue - # Output the config for test vectors - # yield 'config', 'data', test_config + # To output the changed config to could be serialized with yaml test vectors, + # the dict SSZ objects have to be converted into Python built-in types. + output_config = _get_basic_dict(test_config) + yield 'config', 'data', output_config spec.config = spec.Configuration(**test_config) From dc52b351457e11be14a40cf79873903b5d8ad368 Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Thu, 2 Dec 2021 01:37:01 +0800 Subject: [PATCH 03/16] Add `PROPOSER_SCORE_BOOST` to meta.yaml and fix comments --- .../eth2spec/test/phase0/fork_choice/test_ex_ante.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py index d19cb69b3..806fe7ec4 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py +++ b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py @@ -153,7 +153,7 @@ def test_ex_ante_secnario_1_without_boost(spec, state): assert spec.get_head(store) == signed_block_c.message.hash_tree_root() # Block B received at N+2 - # Block B and C has the same score 0. Use a lexicographical order for tie-breaking. + # Block B and C have the same score 0. Use a lexicographical order for tie-breaking. yield from add_block(spec, store, signed_block_b, test_steps) if signed_block_b.message.hash_tree_root() >= signed_block_c.message.hash_tree_root(): assert spec.get_head(store) == signed_block_b.message.hash_tree_root() @@ -240,6 +240,9 @@ def test_ex_ante_attestations_is_greater_than_proposer_boost_without_boost(spec, Block C (parent A) - slot N+2 Attestation_1 (Block B) - slot N+1 – size > proposer_boost """ + # For testing `PROPOSER_SCORE_BOOST = 0` case + yield 'PROPOSER_SCORE_BOOST', 'meta', 0 + test_steps = [] # Initialization store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) @@ -276,7 +279,7 @@ def test_ex_ante_attestations_is_greater_than_proposer_boost_without_boost(spec, assert spec.get_head(store) == signed_block_c.message.hash_tree_root() # Block B received at N+2 - # Block B and C has the same score 0. Use a lexicographical order for tie-breaking. + # Block B and C have the same score 0. Use a lexicographical order for tie-breaking. yield from add_block(spec, store, signed_block_b, test_steps) if signed_block_b.message.hash_tree_root() >= signed_block_c.message.hash_tree_root(): assert spec.get_head(store) == signed_block_b.message.hash_tree_root() From 6308cee7a64d1820a38af0a3968481000a7a3081 Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Fri, 3 Dec 2021 02:17:57 +0800 Subject: [PATCH 04/16] Fix typo --- .../pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py index 806fe7ec4..c47ccefda 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py +++ b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py @@ -34,7 +34,7 @@ def _apply_base_block_a(spec, state, store, test_steps): @with_all_phases @spec_state_test -def test_ex_ante_secnario_1_with_boost(spec, state): +def test_ex_ante_scenario_1_with_boost(spec, state): """ With a single adversarial attestation @@ -98,7 +98,7 @@ def test_ex_ante_secnario_1_with_boost(spec, state): @spec_configured_state_test({ 'PROPOSER_SCORE_BOOST': 0, }) -def test_ex_ante_secnario_1_without_boost(spec, state): +def test_ex_ante_scenario_1_without_boost(spec, state): """ With a single adversarial attestation From 9dc1b7af7e6f4a0b3fa79aa10a4deff71fb50c37 Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Tue, 7 Dec 2021 00:23:01 +0800 Subject: [PATCH 05/16] Test with strict `proposer_boost + 1` attestation participants --- .../test/phase0/fork_choice/test_ex_ante.py | 64 +++++++++++++++---- 1 file changed, 50 insertions(+), 14 deletions(-) diff --git a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py index c47ccefda..b3a8b38fb 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py +++ b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py @@ -167,6 +167,26 @@ def test_ex_ante_scenario_1_without_boost(spec, state): yield 'steps', test_steps +def _get_greater_than_proposer_boost_score(spec, store, state, proposer_boost_root, root): + """ + Return the minimum attestation participant count such that attestation_score > proposer_score + """ + # calculate proposer boost score + block = store.blocks[root] + proposer_score = 0 + if spec.get_ancestor(store, root, block.slot) == proposer_boost_root: + num_validators = len(spec.get_active_validator_indices(state, spec.get_current_epoch(state))) + avg_balance = spec.get_total_active_balance(state) // num_validators + committee_size = num_validators // spec.SLOTS_PER_EPOCH + committee_weight = committee_size * avg_balance + proposer_score = (committee_weight * spec.config.PROPOSER_SCORE_BOOST) // 100 + + # calculate minimum participant count such that attestation_score > proposer_score + base_effective_balance = state.validators[0].effective_balance + + return proposer_score // base_effective_balance + 1 + + @with_all_phases @with_presets([MAINNET], reason="to create larger committee") @spec_state_test @@ -177,7 +197,7 @@ def test_ex_ante_attestations_is_greater_than_proposer_boost_with_boost(spec, st Block A - slot N Block B (parent A) - slot N+1 Block C (parent A) - slot N+2 - Attestation_1 (Block B) - slot N+1 – size > proposer_boost + Attestation_1 (Block B) - slot N+1 – proposer_boost + 1 participants """ test_steps = [] # Initialization @@ -202,12 +222,6 @@ def test_ex_ante_attestations_is_greater_than_proposer_boost_with_boost(spec, st block = build_empty_block(spec, state_c, slot=state_a.slot + 2) signed_block_c = state_transition_and_sign_block(spec, state_c, block) - # Full attestation received at N+2 — B is head due to boost proposer - attestation = get_valid_attestation(spec, state_b, slot=state_b.slot, signed=False) - attestation.data.beacon_block_root = signed_block_b.message.hash_tree_root() - assert len([i for i in attestation.aggregation_bits if i == 1]) > 1 - sign_attestation(spec, state_b, attestation) - # Block C received at N+2 — C is head time = state_c.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time on_tick_and_append_step(spec, store, time, test_steps) @@ -218,6 +232,20 @@ def test_ex_ante_attestations_is_greater_than_proposer_boost_with_boost(spec, st yield from add_block(spec, store, signed_block_b, test_steps) assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + # Attestation of proposer_boost + 1 participants + proposer_boost_root = signed_block_b.message.hash_tree_root() + root = signed_block_b.message.hash_tree_root() + participant_num = _get_greater_than_proposer_boost_score(spec, store, state, proposer_boost_root, root) + + def _filter_participant_set(participants): + return [index for i, index in enumerate(participants) if i < participant_num] + + attestation = get_valid_attestation( + spec, state_b, slot=state_b.slot, signed=False, filter_participant_set=_filter_participant_set + ) + attestation.data.beacon_block_root = signed_block_b.message.hash_tree_root() + sign_attestation(spec, state_b, attestation) + # Attestation_1 received at N+2 — B is head because B's attestation_score > C's proposer_score. # (B's proposer_score = C's attestation_score = 0) yield from add_attestation(spec, store, attestation, test_steps) @@ -238,7 +266,7 @@ def test_ex_ante_attestations_is_greater_than_proposer_boost_without_boost(spec, Block A - slot N Block B (parent A) - slot N+1 Block C (parent A) - slot N+2 - Attestation_1 (Block B) - slot N+1 – size > proposer_boost + Attestation_1 (Block B) - slot N+1 – proposer_boost + 1 participants """ # For testing `PROPOSER_SCORE_BOOST = 0` case yield 'PROPOSER_SCORE_BOOST', 'meta', 0 @@ -266,12 +294,6 @@ def test_ex_ante_attestations_is_greater_than_proposer_boost_without_boost(spec, block = build_empty_block(spec, state_c, slot=state_a.slot + 2) signed_block_c = state_transition_and_sign_block(spec, state_c, block) - # Full attestation received at N+2 — B is head due to boost proposer - attestation = get_valid_attestation(spec, state_b, slot=state_b.slot, signed=False) - attestation.data.beacon_block_root = signed_block_b.message.hash_tree_root() - assert len([i for i in attestation.aggregation_bits if i == 1]) > 1 - sign_attestation(spec, state_b, attestation) - # Block C received at N+2 — C is head time = state_c.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time on_tick_and_append_step(spec, store, time, test_steps) @@ -286,6 +308,20 @@ def test_ex_ante_attestations_is_greater_than_proposer_boost_without_boost(spec, else: assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + # Attestation of proposer_boost + 1 participants + proposer_boost_root = signed_block_b.message.hash_tree_root() + root = signed_block_b.message.hash_tree_root() + participant_num = _get_greater_than_proposer_boost_score(spec, store, state, proposer_boost_root, root) + + def _filter_participant_set(participants): + return [index for i, index in enumerate(participants) if i < participant_num] + + attestation = get_valid_attestation( + spec, state_b, slot=state_b.slot, signed=False, filter_participant_set=_filter_participant_set + ) + attestation.data.beacon_block_root = signed_block_b.message.hash_tree_root() + sign_attestation(spec, state_b, attestation) + # Attestation_1 received at N+2 — B is head because B's attestation_score > C's attestation_score yield from add_attestation(spec, store, attestation, test_steps) assert spec.get_head(store) == signed_block_b.message.hash_tree_root() From 57be9d064f933626aaa0c3d6bbcd713ccc59289d Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Tue, 7 Dec 2021 16:51:11 +0100 Subject: [PATCH 06/16] update per-test config to be unique per-test --- tests/core/pyspec/eth2spec/test/context.py | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/tests/core/pyspec/eth2spec/test/context.py b/tests/core/pyspec/eth2spec/test/context.py index 2a6f2e324..4916e008c 100644 --- a/tests/core/pyspec/eth2spec/test/context.py +++ b/tests/core/pyspec/eth2spec/test/context.py @@ -491,22 +491,18 @@ def with_config_overrides(config_overrides): """ def decorator(fn): def wrapper(*args, spec: Spec, **kw): - # remember the old config - old_config = spec.config + spec = deepcopy(spec) # apply our overrides to a copy of it, and apply it to the spec - tmp_config = deepcopy(old_config._asdict()) # not a private method, there are multiple - tmp_config.update(config_overrides) - config_types = spec.Configuration.__annotations__ - # Retain types of all config values - test_config = {k: config_types[k](v) for k, v in tmp_config.items()} + spec.config.update(config_overrides) # To output the changed config to could be serialized with yaml test vectors, # the dict SSZ objects have to be converted into Python built-in types. - output_config = _get_basic_dict(test_config) + output_config = _get_basic_dict(spec.config) yield 'config', 'data', output_config - spec.config = spec.Configuration(**test_config) + # Output the config for test vectors (TODO: check config YAML encoding) + yield 'config', 'data', spec.config # Run the function out = fn(*args, spec=spec, **kw) @@ -514,10 +510,6 @@ def with_config_overrides(config_overrides): # it's generating things, and we need to complete it before setting back the config. if out is not None: yield from out - - # Restore the old config and apply it - spec.config = old_config - return wrapper return decorator From 733f37715e342823a0a9a93810051e573d62b0a4 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Tue, 7 Dec 2021 17:51:32 +0100 Subject: [PATCH 07/16] use `importlib` to perform an actual spec copy --- tests/core/pyspec/eth2spec/test/context.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/tests/core/pyspec/eth2spec/test/context.py b/tests/core/pyspec/eth2spec/test/context.py index 4916e008c..0764bbe4a 100644 --- a/tests/core/pyspec/eth2spec/test/context.py +++ b/tests/core/pyspec/eth2spec/test/context.py @@ -1,6 +1,6 @@ import pytest -from copy import deepcopy from dataclasses import dataclass +import importlib from eth_utils import encode_hex from eth2spec.phase0 import mainnet as spec_phase0_mainnet, minimal as spec_phase0_minimal @@ -481,6 +481,16 @@ def _get_basic_dict(ssz_dict: Dict[str, Any]) -> Dict[str, Any]: return result +def _get_copy_of_spec(spec): + fork = spec.fork + preset = spec.config.PRESET_BASE + path = f"eth2spec.{fork}.{preset}" + + module_spec = importlib.util.find_spec(path) + module = importlib.util.module_from_spec(module_spec) + return module + + def with_config_overrides(config_overrides): """ WARNING: the spec_test decorator must wrap this, to ensure the decorated test actually runs. @@ -491,7 +501,7 @@ def with_config_overrides(config_overrides): """ def decorator(fn): def wrapper(*args, spec: Spec, **kw): - spec = deepcopy(spec) + spec = _get_copy_of_spec(spec) # apply our overrides to a copy of it, and apply it to the spec spec.config.update(config_overrides) @@ -501,9 +511,6 @@ def with_config_overrides(config_overrides): output_config = _get_basic_dict(spec.config) yield 'config', 'data', output_config - # Output the config for test vectors (TODO: check config YAML encoding) - yield 'config', 'data', spec.config - # Run the function out = fn(*args, spec=spec, **kw) # If it's not returning None like a normal test function, From 55c9c03f08696693af9f4e30e235d50e799f0bcd Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Tue, 7 Dec 2021 18:11:22 +0100 Subject: [PATCH 08/16] simply module import and fix config adjustment --- tests/core/pyspec/eth2spec/test/context.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/tests/core/pyspec/eth2spec/test/context.py b/tests/core/pyspec/eth2spec/test/context.py index 0764bbe4a..726723992 100644 --- a/tests/core/pyspec/eth2spec/test/context.py +++ b/tests/core/pyspec/eth2spec/test/context.py @@ -484,10 +484,10 @@ def _get_basic_dict(ssz_dict: Dict[str, Any]) -> Dict[str, Any]: def _get_copy_of_spec(spec): fork = spec.fork preset = spec.config.PRESET_BASE - path = f"eth2spec.{fork}.{preset}" - - module_spec = importlib.util.find_spec(path) + module_path = f"eth2spec.{fork}.{preset}" + module_spec = importlib.util.find_spec(module_path) module = importlib.util.module_from_spec(module_spec) + module_spec.loader.exec_module(module) return module @@ -504,13 +504,18 @@ def with_config_overrides(config_overrides): spec = _get_copy_of_spec(spec) # apply our overrides to a copy of it, and apply it to the spec - spec.config.update(config_overrides) + config = spec.config._asdict() + config.update(config_overrides) + config_types = spec.Configuration.__annotations__ + modified_config = {k: config_types[k](v) for k, v in config.items()} # To output the changed config to could be serialized with yaml test vectors, # the dict SSZ objects have to be converted into Python built-in types. - output_config = _get_basic_dict(spec.config) + output_config = _get_basic_dict(modified_config) yield 'config', 'data', output_config + spec.config = spec.Configuration(**modified_config) + # Run the function out = fn(*args, spec=spec, **kw) # If it's not returning None like a normal test function, From db2be42baaf763d44b409deff4c84700143d70cf Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Tue, 7 Dec 2021 18:50:00 +0100 Subject: [PATCH 09/16] use a specific `spec` rather than the pre-defined phases --- tests/core/pyspec/eth2spec/test/context.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/core/pyspec/eth2spec/test/context.py b/tests/core/pyspec/eth2spec/test/context.py index 726723992..260cb4d7d 100644 --- a/tests/core/pyspec/eth2spec/test/context.py +++ b/tests/core/pyspec/eth2spec/test/context.py @@ -86,10 +86,9 @@ class SpecForks(TypedDict, total=False): def _prepare_state(balances_fn: Callable[[Any], Sequence[int]], threshold_fn: Callable[[Any], int], spec: Spec, phases: SpecForks): - phase = phases[spec.fork] - balances = balances_fn(phase) - activation_threshold = threshold_fn(phase) - state = create_genesis_state(spec=phase, validator_balances=balances, + balances = balances_fn(spec) + activation_threshold = threshold_fn(spec) + state = create_genesis_state(spec=spec, validator_balances=balances, activation_threshold=activation_threshold) return state From dc9db78a67e68e69503e29e20d66aa9c855546cb Mon Sep 17 00:00:00 2001 From: Caspar Schwarz-Schilling Date: Wed, 8 Dec 2021 17:05:44 +0100 Subject: [PATCH 10/16] Add sandwich test scenarios --- .../test/phase0/fork_choice/test_ex_ante.py | 391 +++++++++++++++++- 1 file changed, 387 insertions(+), 4 deletions(-) diff --git a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py index b3a8b38fb..a200e10af 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py +++ b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py @@ -60,7 +60,7 @@ def test_ex_ante_scenario_1_with_boost(spec, state): state_b = state_a.copy() block = build_empty_block(spec, state_a, slot=state_a.slot + 1) signed_block_b = state_transition_and_sign_block(spec, state_b, block) - + # Block C at slot `N + 2`, parent is A state_c = state_a.copy() block = build_empty_block(spec, state_c, slot=state_a.slot + 2) @@ -89,7 +89,7 @@ def test_ex_ante_scenario_1_with_boost(spec, state): # Attestation_1 received at N+2 — C is head yield from add_attestation(spec, store, attestation, test_steps) - assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() yield 'steps', test_steps @@ -188,7 +188,7 @@ def _get_greater_than_proposer_boost_score(spec, store, state, proposer_boost_ro @with_all_phases -@with_presets([MAINNET], reason="to create larger committee") +@with_presets([MAINNET], reason="to create non-duplicate committee") @spec_state_test def test_ex_ante_attestations_is_greater_than_proposer_boost_with_boost(spec, state): """ @@ -258,7 +258,7 @@ def test_ex_ante_attestations_is_greater_than_proposer_boost_with_boost(spec, st @spec_configured_state_test({ 'PROPOSER_SCORE_BOOST': 0, }) -@with_presets([MAINNET], reason="to create larger committee") +@with_presets([MAINNET], reason="to create non-duplicate committee") def test_ex_ante_attestations_is_greater_than_proposer_boost_without_boost(spec, state): """ Adversarial attestations > proposer boost @@ -327,3 +327,386 @@ def test_ex_ante_attestations_is_greater_than_proposer_boost_without_boost(spec, assert spec.get_head(store) == signed_block_b.message.hash_tree_root() yield 'steps', test_steps + +@with_all_phases +@spec_state_test +def test_ex_ante_sandwich_without_attestations_with_boost(spec, state): + """ + Simple Sandwich test with boost and no attestations. + Obejcts: + Block A - slot N + Block B (parent A) - slot N+1 + Block C (parent A) - slot N+2 + Block D (parent B) - slot N+3 + Steps: + Block A received at N — A is head + Block C received at N+2 — C is head + Block B received at N+2 — C is head (with boost) + Block D received at N+3 — D is head (with boost) + """ + 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 block A at slot `N` + yield from _apply_base_block_a(spec, state, store, test_steps) + state_a = state.copy() + + # Block B at slot `N + 1`, parent is A + state_b = state_a.copy() + block = build_empty_block(spec, state_a, slot=state_a.slot + 1) + signed_block_b = state_transition_and_sign_block(spec, state_b, block) + + # Block C at slot `N + 2`, parent is A + state_c = state_a.copy() + block = build_empty_block(spec, state_c, slot=state_a.slot + 2) + signed_block_c = state_transition_and_sign_block(spec, state_c, block) + + # Block D at slot `N + 3`, parent is B + state_d = state_b.copy() + block = build_empty_block(spec, state_d, slot=state_a.slot + 3) + signed_block_d = state_transition_and_sign_block(spec, state_d, block) + + # Block C received at N+2 — C is head + time = state_c.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, time, test_steps) + yield from add_block(spec, store, signed_block_c, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Block B received at N+2 — C is head, it has proposer score boost + yield from add_block(spec, store, signed_block_b, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Block D received at N+3 - D is head, it has proposer score boost + time = state_d.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, time, test_steps) + yield from add_block(spec, store, signed_block_d, test_steps) + assert spec.get_head(store) == signed_block_d.message.hash_tree_root() + + yield 'steps', test_steps + +@with_all_phases +@spec_configured_state_test({ + 'PROPOSER_SCORE_BOOST': 0, +}) +def test_ex_ante_sandwich_without_attestations_without_boost(spec, state): + """ + Simple Sandwich test with no boost and no attestations. + Obejcts: + Block A - slot N + Block B (parent A) - slot N+1 + Block C (parent A) - slot N+2 + Block D (parent B) - slot N+3 + Steps: + Block A received at N — A is head + Block C received at N+2 — C is head + Block B received at N+2 — B or C is head (chosen lexicographically; without boost) + Block D received at N+3 — D or C is head (chosen lexicographically; without boost) + """ + 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 block A at slot `N` + yield from _apply_base_block_a(spec, state, store, test_steps) + state_a = state.copy() + + # Block B at slot `N + 1`, parent is A + state_b = state_a.copy() + block = build_empty_block(spec, state_a, slot=state_a.slot + 1) + signed_block_b = state_transition_and_sign_block(spec, state_b, block) + + # Block C at slot `N + 2`, parent is A + state_c = state_a.copy() + block = build_empty_block(spec, state_c, slot=state_a.slot + 2) + signed_block_c = state_transition_and_sign_block(spec, state_c, block) + + # Block D at slot `N + 3`, parent is B + state_d = state_b.copy() + block = build_empty_block(spec, state_d, slot=state_a.slot + 3) + signed_block_d = state_transition_and_sign_block(spec, state_d, block) + + # Block C received at N+2 — C is head + time = state_c.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, time, test_steps) + yield from add_block(spec, store, signed_block_c, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Block B received at N+2 + # Block B and C have the same score 0. Use a lexicographical order for tie-breaking. + yield from add_block(spec, store, signed_block_b, test_steps) + if signed_block_b.message.hash_tree_root() >= signed_block_c.message.hash_tree_root(): + assert spec.get_head(store) == signed_block_b.message.hash_tree_root() + else: + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Block D received at N+3 + # Block D and C have the same score 0. Use a lexicographical order for tie-breaking. + time = state_d.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, time, test_steps) + yield from add_block(spec, store, signed_block_d, test_steps) + if signed_block_d.message.hash_tree_root() >= signed_block_c.message.hash_tree_root(): + assert spec.get_head(store) == signed_block_d.message.hash_tree_root() + else: + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + yield 'steps', test_steps + +@with_all_phases +@spec_state_test +def test_ex_ante_sandwich_with_honest_attestation_with_boost(spec, state): + """ + Boosting necessary to sandwich attack. + Objects: + Block A - slot N + Block B (parent A) - slot N+1 + Block C (parent A) - slot N+2 + Block D (parent B) - slot N+3 + Attestation_1 (Block C); size 1 - slot N+2 (honest) + Steps: + Block A received at N — A is head + Block C received at N+2 — C is head + Block B received at N+2 — C is head + Attestation_1 received at N+3 — C is head + Block D received at N+3 — D is head + + """ + 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 block A at slot `N` + yield from _apply_base_block_a(spec, state, store, test_steps) + state_a = state.copy() + + # Block B at slot `N + 1`, parent is A + state_b = state_a.copy() + block = build_empty_block(spec, state_a, slot=state_a.slot + 1) + signed_block_b = state_transition_and_sign_block(spec, state_b, block) + + # Block C at slot `N + 2`, parent is A + state_c = state_a.copy() + block = build_empty_block(spec, state_c, slot=state_a.slot + 2) + signed_block_c = state_transition_and_sign_block(spec, state_c, block) + + # Attestation_1 at N+2 voting for block C + def _filter_participant_set(participants): + return [next(iter(participants))] + + attestation = get_valid_attestation( + spec, state_c, slot=state_c.slot, signed=False, filter_participant_set=_filter_participant_set + ) + attestation.data.beacon_block_root = signed_block_c.message.hash_tree_root() + assert len([i for i in attestation.aggregation_bits if i == 1]) == 1 + sign_attestation(spec, state_c, attestation) + + # Block D at slot `N + 3`, parent is B + state_d = state_b.copy() + block = build_empty_block(spec, state_d, slot=state_a.slot + 3) + signed_block_d = state_transition_and_sign_block(spec, state_d, block) + + # Block C received at N+2 — C is head + time = state_c.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, time, test_steps) + yield from add_block(spec, store, signed_block_c, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Block B received at N+2 — C is head, it has proposer score boost + yield from add_block(spec, store, signed_block_b, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Attestation_1 received at N+3 — C is head + time = state_d.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, time, test_steps) + yield from add_attestation(spec, store, attestation, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Block D received at N+3 - D is head, it has proposer score boost + yield from add_block(spec, store, signed_block_d, test_steps) + assert spec.get_head(store) == signed_block_d.message.hash_tree_root() + + yield 'steps', test_steps + +@with_all_phases +@spec_configured_state_test({ + 'PROPOSER_SCORE_BOOST': 0, +}) +def test_ex_ante_sandwich_with_honest_attestation_without_boost(spec, state): + """ + Boost necessary to sandwich attack: no boost, so not successful here. + Objects: + Block A - slot N + Block B (parent A) - slot N+1 + Block C (parent A) - slot N+2 + Block D (parent B) - slot N+3 + Attestation_1 (Block C); size 1 - slot N+2 (honest) + Steps: + Block A received at N — A is head + Block C received at N+2 — C is head + Block B received at N+2 — B or C is head (chosen lexicographically) + Attestation_1 received at N+3 — C is head + Block D received at N+3 — C is head + """ + 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 block A at slot `N` + yield from _apply_base_block_a(spec, state, store, test_steps) + state_a = state.copy() + + # Block B at slot `N + 1`, parent is A + state_b = state_a.copy() + block = build_empty_block(spec, state_a, slot=state_a.slot + 1) + signed_block_b = state_transition_and_sign_block(spec, state_b, block) + + # Block C at slot `N + 2`, parent is A + state_c = state_a.copy() + block = build_empty_block(spec, state_c, slot=state_a.slot + 2) + signed_block_c = state_transition_and_sign_block(spec, state_c, block) + + # Block D at slot `N + 3`, parent is B + state_d = state_b.copy() + block = build_empty_block(spec, state_d, slot=state_a.slot + 3) + signed_block_d = state_transition_and_sign_block(spec, state_d, block) + + # Block C received at N+2 — C is head + time = state_c.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, time, test_steps) + yield from add_block(spec, store, signed_block_c, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Block B received at N+2 + # Block B and C have the same score, 0. Use a lexicographical order for tie-breaking. + yield from add_block(spec, store, signed_block_b, test_steps) + if signed_block_b.message.hash_tree_root() >= signed_block_c.message.hash_tree_root(): + assert spec.get_head(store) == signed_block_b.message.hash_tree_root() + else: + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Attestation_1 at N+2 voting for block C + def _filter_participant_set(participants): + return [next(iter(participants))] + + attestation = get_valid_attestation( + spec, state_c, slot=state_c.slot, signed=False, filter_participant_set=_filter_participant_set + ) + attestation.data.beacon_block_root = signed_block_c.message.hash_tree_root() + assert len([i for i in attestation.aggregation_bits if i == 1]) == 1 + sign_attestation(spec, state_c, attestation) + + # Attestation_1 received at N+3 - C is head + time = state_d.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, time, test_steps) + yield from add_attestation(spec, store, attestation, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Block D received at N+3 - C is head, because block D has no proposer boost + yield from add_block(spec, store, signed_block_d, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + yield 'steps', test_steps + +@with_all_phases +@with_presets([MAINNET], reason="to create non-duplicate committee") +@spec_state_test +def test_ex_ante_sandwich_with_boost_not_sufficient(spec, state): + """ + Boost not sufficient to sandwich attack. + Objects: + Block A - slot N + Block B (parent A) - slot N+1 + Block C (parent A) - slot N+2 + Block D (parent B) - slot N+3 + Attestation_set_1 (Block C); size proposer_boost + 1 - slot N+2 + Steps: + Block A received at N — A is head + Block C received at N+2 — C is head + Block B received at N+2 — C is head + Attestation_set_1 received — C is head + Block D received at N+3 — C is head + """ + 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 block A at slot `N` + yield from _apply_base_block_a(spec, state, store, test_steps) + state_a = state.copy() + + # Block B at slot `N + 1`, parent is A + state_b = state_a.copy() + block = build_empty_block(spec, state_a, slot=state_a.slot + 1) + signed_block_b = state_transition_and_sign_block(spec, state_b, block) + + # Block C at slot `N + 2`, parent is A + state_c = state_a.copy() + block = build_empty_block(spec, state_c, slot=state_a.slot + 2) + signed_block_c = state_transition_and_sign_block(spec, state_c, block) + + # Block D at slot `N + 3`, parent is B + state_d = state_b.copy() + block = build_empty_block(spec, state_d, slot=state_a.slot + 3) + signed_block_d = state_transition_and_sign_block(spec, state_d, block) + + # Block C received at N+2 — C is head + time = state_c.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, time, test_steps) + yield from add_block(spec, store, signed_block_c, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Block B received at N+2 — C is head, it has proposer score boost + yield from add_block(spec, store, signed_block_b, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Attestation_set_1 at N+2 voting for block C + proposer_boost_root = signed_block_c.message.hash_tree_root() + root = signed_block_c.message.hash_tree_root() + participant_num = _get_greater_than_proposer_boost_score(spec, store, state, proposer_boost_root, root) + + def _filter_participant_set(participants): + return [index for i, index in enumerate(participants) if i < participant_num] + + attestation = get_valid_attestation( + spec, state_c, slot=state_c.slot, signed=False, filter_participant_set=_filter_participant_set + ) + attestation.data.beacon_block_root = signed_block_c.message.hash_tree_root() + sign_attestation(spec, state_c, attestation) + + # Attestation_1 received at N+3 — B is head because B's attestation_score > C's proposer_score. + # (B's proposer_score = C's attestation_score = 0) + time = state_d.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, time, test_steps) + yield from add_attestation(spec, store, attestation, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Block D received at N+3 - C is head, D's boost not sufficient! + yield from add_block(spec, store, signed_block_d, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + yield 'steps', test_steps From 79e1d5e0216320ffef462a8f08839e56d0614106 Mon Sep 17 00:00:00 2001 From: Caspar Schwarz-Schilling Date: Wed, 8 Dec 2021 18:14:46 +0100 Subject: [PATCH 11/16] Fix head view comparison --- .../pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py index a200e10af..bf526642a 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py +++ b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py @@ -455,7 +455,7 @@ def test_ex_ante_sandwich_without_attestations_without_boost(spec, state): time = state_d.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time on_tick_and_append_step(spec, store, time, test_steps) yield from add_block(spec, store, signed_block_d, test_steps) - if signed_block_d.message.hash_tree_root() >= signed_block_c.message.hash_tree_root(): + if signed_block_b.message.hash_tree_root() >= signed_block_c.message.hash_tree_root(): assert spec.get_head(store) == signed_block_d.message.hash_tree_root() else: assert spec.get_head(store) == signed_block_c.message.hash_tree_root() From 9f614fca83b0e974f25488f9fc523702660b2a42 Mon Sep 17 00:00:00 2001 From: Caspar Schwarz-Schilling Date: Wed, 8 Dec 2021 18:31:14 +0100 Subject: [PATCH 12/16] Fix linting --- .../test/phase0/fork_choice/test_ex_ante.py | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py index bf526642a..e85e9e937 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py +++ b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py @@ -60,7 +60,7 @@ def test_ex_ante_scenario_1_with_boost(spec, state): state_b = state_a.copy() block = build_empty_block(spec, state_a, slot=state_a.slot + 1) signed_block_b = state_transition_and_sign_block(spec, state_b, block) - + # Block C at slot `N + 2`, parent is A state_c = state_a.copy() block = build_empty_block(spec, state_c, slot=state_a.slot + 2) @@ -89,7 +89,7 @@ def test_ex_ante_scenario_1_with_boost(spec, state): # Attestation_1 received at N+2 — C is head yield from add_attestation(spec, store, attestation, test_steps) - assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() yield 'steps', test_steps @@ -328,6 +328,7 @@ def test_ex_ante_attestations_is_greater_than_proposer_boost_without_boost(spec, yield 'steps', test_steps + @with_all_phases @spec_state_test def test_ex_ante_sandwich_without_attestations_with_boost(spec, state): @@ -371,7 +372,7 @@ def test_ex_ante_sandwich_without_attestations_with_boost(spec, state): state_d = state_b.copy() block = build_empty_block(spec, state_d, slot=state_a.slot + 3) signed_block_d = state_transition_and_sign_block(spec, state_d, block) - + # Block C received at N+2 — C is head time = state_c.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time on_tick_and_append_step(spec, store, time, test_steps) @@ -390,6 +391,7 @@ def test_ex_ante_sandwich_without_attestations_with_boost(spec, state): yield 'steps', test_steps + @with_all_phases @spec_configured_state_test({ 'PROPOSER_SCORE_BOOST': 0, @@ -435,7 +437,7 @@ def test_ex_ante_sandwich_without_attestations_without_boost(spec, state): state_d = state_b.copy() block = build_empty_block(spec, state_d, slot=state_a.slot + 3) signed_block_d = state_transition_and_sign_block(spec, state_d, block) - + # Block C received at N+2 — C is head time = state_c.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time on_tick_and_append_step(spec, store, time, test_steps) @@ -462,6 +464,7 @@ def test_ex_ante_sandwich_without_attestations_without_boost(spec, state): yield 'steps', test_steps + @with_all_phases @spec_state_test def test_ex_ante_sandwich_with_honest_attestation_with_boost(spec, state): @@ -519,7 +522,7 @@ def test_ex_ante_sandwich_with_honest_attestation_with_boost(spec, state): state_d = state_b.copy() block = build_empty_block(spec, state_d, slot=state_a.slot + 3) signed_block_d = state_transition_and_sign_block(spec, state_d, block) - + # Block C received at N+2 — C is head time = state_c.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time on_tick_and_append_step(spec, store, time, test_steps) @@ -542,6 +545,7 @@ def test_ex_ante_sandwich_with_honest_attestation_with_boost(spec, state): yield 'steps', test_steps + @with_all_phases @spec_configured_state_test({ 'PROPOSER_SCORE_BOOST': 0, @@ -589,7 +593,7 @@ def test_ex_ante_sandwich_with_honest_attestation_without_boost(spec, state): state_d = state_b.copy() block = build_empty_block(spec, state_d, slot=state_a.slot + 3) signed_block_d = state_transition_and_sign_block(spec, state_d, block) - + # Block C received at N+2 — C is head time = state_c.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time on_tick_and_append_step(spec, store, time, test_steps) @@ -614,7 +618,7 @@ def test_ex_ante_sandwich_with_honest_attestation_without_boost(spec, state): attestation.data.beacon_block_root = signed_block_c.message.hash_tree_root() assert len([i for i in attestation.aggregation_bits if i == 1]) == 1 sign_attestation(spec, state_c, attestation) - + # Attestation_1 received at N+3 - C is head time = state_d.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time on_tick_and_append_step(spec, store, time, test_steps) @@ -627,6 +631,7 @@ def test_ex_ante_sandwich_with_honest_attestation_without_boost(spec, state): yield 'steps', test_steps + @with_all_phases @with_presets([MAINNET], reason="to create non-duplicate committee") @spec_state_test @@ -673,7 +678,7 @@ def test_ex_ante_sandwich_with_boost_not_sufficient(spec, state): state_d = state_b.copy() block = build_empty_block(spec, state_d, slot=state_a.slot + 3) signed_block_d = state_transition_and_sign_block(spec, state_d, block) - + # Block C received at N+2 — C is head time = state_c.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time on_tick_and_append_step(spec, store, time, test_steps) From addc03cb7c1111726fb838b67d8d9b69b97a0312 Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Thu, 9 Dec 2021 17:47:20 +0800 Subject: [PATCH 13/16] Enable ex-ante tests in testgen and minor fixes --- .../test/phase0/fork_choice/test_ex_ante.py | 13 +++++++++++-- tests/generators/fork_choice/main.py | 1 + 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py index e85e9e937..b4013c18d 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py +++ b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py @@ -34,7 +34,7 @@ def _apply_base_block_a(spec, state, store, test_steps): @with_all_phases @spec_state_test -def test_ex_ante_scenario_1_with_boost(spec, state): +def test_ex_ante_vanilla_with_boost(spec, state): """ With a single adversarial attestation @@ -98,7 +98,7 @@ def test_ex_ante_scenario_1_with_boost(spec, state): @spec_configured_state_test({ 'PROPOSER_SCORE_BOOST': 0, }) -def test_ex_ante_scenario_1_without_boost(spec, state): +def test_ex_ante_vanilla_without_boost(spec, state): """ With a single adversarial attestation @@ -244,6 +244,7 @@ def test_ex_ante_attestations_is_greater_than_proposer_boost_with_boost(spec, st spec, state_b, slot=state_b.slot, signed=False, filter_participant_set=_filter_participant_set ) attestation.data.beacon_block_root = signed_block_b.message.hash_tree_root() + assert len([i for i in attestation.aggregation_bits if i == 1]) == participant_num sign_attestation(spec, state_b, attestation) # Attestation_1 received at N+2 — B is head because B's attestation_score > C's proposer_score. @@ -320,6 +321,7 @@ def test_ex_ante_attestations_is_greater_than_proposer_boost_without_boost(spec, spec, state_b, slot=state_b.slot, signed=False, filter_participant_set=_filter_participant_set ) attestation.data.beacon_block_root = signed_block_b.message.hash_tree_root() + assert len([i for i in attestation.aggregation_bits if i == 1]) == participant_num sign_attestation(spec, state_b, attestation) # Attestation_1 received at N+2 — B is head because B's attestation_score > C's attestation_score @@ -410,6 +412,9 @@ def test_ex_ante_sandwich_without_attestations_without_boost(spec, state): Block B received at N+2 — B or C is head (chosen lexicographically; without boost) Block D received at N+3 — D or C is head (chosen lexicographically; without boost) """ + # For testing `PROPOSER_SCORE_BOOST = 0` case + yield 'PROPOSER_SCORE_BOOST', 'meta', 0 + test_steps = [] # Initialization store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) @@ -566,6 +571,9 @@ def test_ex_ante_sandwich_with_honest_attestation_without_boost(spec, state): Attestation_1 received at N+3 — C is head Block D received at N+3 — C is head """ + # For testing `PROPOSER_SCORE_BOOST = 0` case + yield 'PROPOSER_SCORE_BOOST', 'meta', 0 + test_steps = [] # Initialization store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) @@ -701,6 +709,7 @@ def test_ex_ante_sandwich_with_boost_not_sufficient(spec, state): spec, state_c, slot=state_c.slot, signed=False, filter_participant_set=_filter_participant_set ) attestation.data.beacon_block_root = signed_block_c.message.hash_tree_root() + assert len([i for i in attestation.aggregation_bits if i == 1]) == participant_num sign_attestation(spec, state_c, attestation) # Attestation_1 received at N+3 — B is head because B's attestation_score > C's proposer_score. diff --git a/tests/generators/fork_choice/main.py b/tests/generators/fork_choice/main.py index 562f851d1..b194dc3bd 100644 --- a/tests/generators/fork_choice/main.py +++ b/tests/generators/fork_choice/main.py @@ -6,6 +6,7 @@ if __name__ == "__main__": phase_0_mods = {key: 'eth2spec.test.phase0.fork_choice.test_' + key for key in [ 'get_head', 'on_block', + 'ex_ante', ]} # No additional Altair specific finality tests, yet. altair_mods = phase_0_mods From 11a42f8d0661ddcc56831d1788b3066bd1d06372 Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Thu, 9 Dec 2021 17:58:31 +0800 Subject: [PATCH 14/16] Remove `*_without_boost` tests --- .../test/phase0/fork_choice/test_ex_ante.py | 315 ------------------ 1 file changed, 315 deletions(-) diff --git a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py index b4013c18d..00d58356f 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py +++ b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py @@ -1,6 +1,5 @@ from eth2spec.test.context import ( MAINNET, - spec_configured_state_test, spec_state_test, with_all_phases, with_presets, @@ -94,79 +93,6 @@ def test_ex_ante_vanilla_with_boost(spec, state): yield 'steps', test_steps -@with_all_phases -@spec_configured_state_test({ - 'PROPOSER_SCORE_BOOST': 0, -}) -def test_ex_ante_vanilla_without_boost(spec, state): - """ - With a single adversarial attestation - - NOTE: this case disabled proposer score boost by setting config `PROPOSER_SCORE_BOOST` to `0` - - Block A - slot N - Block B (parent A) - slot N+1 - Block C (parent A) - slot N+2 - Attestation_1 (Block B) - slot N+1 – size 1 - """ - # For testing `PROPOSER_SCORE_BOOST = 0` case - yield 'PROPOSER_SCORE_BOOST', 'meta', 0 - - 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 block A at slot `N` - yield from _apply_base_block_a(spec, state, store, test_steps) - state_a = state.copy() - - # Block B at slot `N + 1`, parent is A - state_b = state_a.copy() - block = build_empty_block(spec, state_a, slot=state_a.slot + 1) - signed_block_b = state_transition_and_sign_block(spec, state_b, block) - - # Block C at slot `N + 2`, parent is A - state_c = state_a.copy() - block = build_empty_block(spec, state_c, slot=state_a.slot + 2) - signed_block_c = state_transition_and_sign_block(spec, state_c, block) - - # Attestation_1 received at N+2 — B is head due to boost proposer - def _filter_participant_set(participants): - return [next(iter(participants))] - - attestation = get_valid_attestation( - spec, state_b, slot=state_b.slot, signed=False, filter_participant_set=_filter_participant_set - ) - attestation.data.beacon_block_root = signed_block_b.message.hash_tree_root() - assert len([i for i in attestation.aggregation_bits if i == 1]) == 1 - sign_attestation(spec, state_b, attestation) - - # Block C received at N+2 — C is head - time = state_c.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time - on_tick_and_append_step(spec, store, time, test_steps) - yield from add_block(spec, store, signed_block_c, test_steps) - assert spec.get_head(store) == signed_block_c.message.hash_tree_root() - - # Block B received at N+2 - # Block B and C have the same score 0. Use a lexicographical order for tie-breaking. - yield from add_block(spec, store, signed_block_b, test_steps) - if signed_block_b.message.hash_tree_root() >= signed_block_c.message.hash_tree_root(): - assert spec.get_head(store) == signed_block_b.message.hash_tree_root() - else: - assert spec.get_head(store) == signed_block_c.message.hash_tree_root() - - # Attestation_1 received at N+2 — B is head - yield from add_attestation(spec, store, attestation, test_steps) - assert spec.get_head(store) == signed_block_b.message.hash_tree_root() - - yield 'steps', test_steps - - def _get_greater_than_proposer_boost_score(spec, store, state, proposer_boost_root, root): """ Return the minimum attestation participant count such that attestation_score > proposer_score @@ -255,82 +181,6 @@ def test_ex_ante_attestations_is_greater_than_proposer_boost_with_boost(spec, st yield 'steps', test_steps -@with_all_phases -@spec_configured_state_test({ - 'PROPOSER_SCORE_BOOST': 0, -}) -@with_presets([MAINNET], reason="to create non-duplicate committee") -def test_ex_ante_attestations_is_greater_than_proposer_boost_without_boost(spec, state): - """ - Adversarial attestations > proposer boost - - Block A - slot N - Block B (parent A) - slot N+1 - Block C (parent A) - slot N+2 - Attestation_1 (Block B) - slot N+1 – proposer_boost + 1 participants - """ - # For testing `PROPOSER_SCORE_BOOST = 0` case - yield 'PROPOSER_SCORE_BOOST', 'meta', 0 - - 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 block A at slot `N` - yield from _apply_base_block_a(spec, state, store, test_steps) - state_a = state.copy() - - # Block B at slot `N + 1`, parent is A - state_b = state_a.copy() - block = build_empty_block(spec, state_a, slot=state_a.slot + 1) - signed_block_b = state_transition_and_sign_block(spec, state_b, block) - - # Block C at slot `N + 2`, parent is A - state_c = state_a.copy() - block = build_empty_block(spec, state_c, slot=state_a.slot + 2) - signed_block_c = state_transition_and_sign_block(spec, state_c, block) - - # Block C received at N+2 — C is head - time = state_c.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time - on_tick_and_append_step(spec, store, time, test_steps) - yield from add_block(spec, store, signed_block_c, test_steps) - assert spec.get_head(store) == signed_block_c.message.hash_tree_root() - - # Block B received at N+2 - # Block B and C have the same score 0. Use a lexicographical order for tie-breaking. - yield from add_block(spec, store, signed_block_b, test_steps) - if signed_block_b.message.hash_tree_root() >= signed_block_c.message.hash_tree_root(): - assert spec.get_head(store) == signed_block_b.message.hash_tree_root() - else: - assert spec.get_head(store) == signed_block_c.message.hash_tree_root() - - # Attestation of proposer_boost + 1 participants - proposer_boost_root = signed_block_b.message.hash_tree_root() - root = signed_block_b.message.hash_tree_root() - participant_num = _get_greater_than_proposer_boost_score(spec, store, state, proposer_boost_root, root) - - def _filter_participant_set(participants): - return [index for i, index in enumerate(participants) if i < participant_num] - - attestation = get_valid_attestation( - spec, state_b, slot=state_b.slot, signed=False, filter_participant_set=_filter_participant_set - ) - attestation.data.beacon_block_root = signed_block_b.message.hash_tree_root() - assert len([i for i in attestation.aggregation_bits if i == 1]) == participant_num - sign_attestation(spec, state_b, attestation) - - # Attestation_1 received at N+2 — B is head because B's attestation_score > C's attestation_score - yield from add_attestation(spec, store, attestation, test_steps) - assert spec.get_head(store) == signed_block_b.message.hash_tree_root() - - yield 'steps', test_steps - - @with_all_phases @spec_state_test def test_ex_ante_sandwich_without_attestations_with_boost(spec, state): @@ -394,82 +244,6 @@ def test_ex_ante_sandwich_without_attestations_with_boost(spec, state): yield 'steps', test_steps -@with_all_phases -@spec_configured_state_test({ - 'PROPOSER_SCORE_BOOST': 0, -}) -def test_ex_ante_sandwich_without_attestations_without_boost(spec, state): - """ - Simple Sandwich test with no boost and no attestations. - Obejcts: - Block A - slot N - Block B (parent A) - slot N+1 - Block C (parent A) - slot N+2 - Block D (parent B) - slot N+3 - Steps: - Block A received at N — A is head - Block C received at N+2 — C is head - Block B received at N+2 — B or C is head (chosen lexicographically; without boost) - Block D received at N+3 — D or C is head (chosen lexicographically; without boost) - """ - # For testing `PROPOSER_SCORE_BOOST = 0` case - yield 'PROPOSER_SCORE_BOOST', 'meta', 0 - - 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 block A at slot `N` - yield from _apply_base_block_a(spec, state, store, test_steps) - state_a = state.copy() - - # Block B at slot `N + 1`, parent is A - state_b = state_a.copy() - block = build_empty_block(spec, state_a, slot=state_a.slot + 1) - signed_block_b = state_transition_and_sign_block(spec, state_b, block) - - # Block C at slot `N + 2`, parent is A - state_c = state_a.copy() - block = build_empty_block(spec, state_c, slot=state_a.slot + 2) - signed_block_c = state_transition_and_sign_block(spec, state_c, block) - - # Block D at slot `N + 3`, parent is B - state_d = state_b.copy() - block = build_empty_block(spec, state_d, slot=state_a.slot + 3) - signed_block_d = state_transition_and_sign_block(spec, state_d, block) - - # Block C received at N+2 — C is head - time = state_c.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time - on_tick_and_append_step(spec, store, time, test_steps) - yield from add_block(spec, store, signed_block_c, test_steps) - assert spec.get_head(store) == signed_block_c.message.hash_tree_root() - - # Block B received at N+2 - # Block B and C have the same score 0. Use a lexicographical order for tie-breaking. - yield from add_block(spec, store, signed_block_b, test_steps) - if signed_block_b.message.hash_tree_root() >= signed_block_c.message.hash_tree_root(): - assert spec.get_head(store) == signed_block_b.message.hash_tree_root() - else: - assert spec.get_head(store) == signed_block_c.message.hash_tree_root() - - # Block D received at N+3 - # Block D and C have the same score 0. Use a lexicographical order for tie-breaking. - time = state_d.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time - on_tick_and_append_step(spec, store, time, test_steps) - yield from add_block(spec, store, signed_block_d, test_steps) - if signed_block_b.message.hash_tree_root() >= signed_block_c.message.hash_tree_root(): - assert spec.get_head(store) == signed_block_d.message.hash_tree_root() - else: - assert spec.get_head(store) == signed_block_c.message.hash_tree_root() - - yield 'steps', test_steps - - @with_all_phases @spec_state_test def test_ex_ante_sandwich_with_honest_attestation_with_boost(spec, state): @@ -551,95 +325,6 @@ def test_ex_ante_sandwich_with_honest_attestation_with_boost(spec, state): yield 'steps', test_steps -@with_all_phases -@spec_configured_state_test({ - 'PROPOSER_SCORE_BOOST': 0, -}) -def test_ex_ante_sandwich_with_honest_attestation_without_boost(spec, state): - """ - Boost necessary to sandwich attack: no boost, so not successful here. - Objects: - Block A - slot N - Block B (parent A) - slot N+1 - Block C (parent A) - slot N+2 - Block D (parent B) - slot N+3 - Attestation_1 (Block C); size 1 - slot N+2 (honest) - Steps: - Block A received at N — A is head - Block C received at N+2 — C is head - Block B received at N+2 — B or C is head (chosen lexicographically) - Attestation_1 received at N+3 — C is head - Block D received at N+3 — C is head - """ - # For testing `PROPOSER_SCORE_BOOST = 0` case - yield 'PROPOSER_SCORE_BOOST', 'meta', 0 - - 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 block A at slot `N` - yield from _apply_base_block_a(spec, state, store, test_steps) - state_a = state.copy() - - # Block B at slot `N + 1`, parent is A - state_b = state_a.copy() - block = build_empty_block(spec, state_a, slot=state_a.slot + 1) - signed_block_b = state_transition_and_sign_block(spec, state_b, block) - - # Block C at slot `N + 2`, parent is A - state_c = state_a.copy() - block = build_empty_block(spec, state_c, slot=state_a.slot + 2) - signed_block_c = state_transition_and_sign_block(spec, state_c, block) - - # Block D at slot `N + 3`, parent is B - state_d = state_b.copy() - block = build_empty_block(spec, state_d, slot=state_a.slot + 3) - signed_block_d = state_transition_and_sign_block(spec, state_d, block) - - # Block C received at N+2 — C is head - time = state_c.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time - on_tick_and_append_step(spec, store, time, test_steps) - yield from add_block(spec, store, signed_block_c, test_steps) - assert spec.get_head(store) == signed_block_c.message.hash_tree_root() - - # Block B received at N+2 - # Block B and C have the same score, 0. Use a lexicographical order for tie-breaking. - yield from add_block(spec, store, signed_block_b, test_steps) - if signed_block_b.message.hash_tree_root() >= signed_block_c.message.hash_tree_root(): - assert spec.get_head(store) == signed_block_b.message.hash_tree_root() - else: - assert spec.get_head(store) == signed_block_c.message.hash_tree_root() - - # Attestation_1 at N+2 voting for block C - def _filter_participant_set(participants): - return [next(iter(participants))] - - attestation = get_valid_attestation( - spec, state_c, slot=state_c.slot, signed=False, filter_participant_set=_filter_participant_set - ) - attestation.data.beacon_block_root = signed_block_c.message.hash_tree_root() - assert len([i for i in attestation.aggregation_bits if i == 1]) == 1 - sign_attestation(spec, state_c, attestation) - - # Attestation_1 received at N+3 - C is head - time = state_d.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time - on_tick_and_append_step(spec, store, time, test_steps) - yield from add_attestation(spec, store, attestation, test_steps) - assert spec.get_head(store) == signed_block_c.message.hash_tree_root() - - # Block D received at N+3 - C is head, because block D has no proposer boost - yield from add_block(spec, store, signed_block_d, test_steps) - assert spec.get_head(store) == signed_block_c.message.hash_tree_root() - - yield 'steps', test_steps - - @with_all_phases @with_presets([MAINNET], reason="to create non-duplicate committee") @spec_state_test From 4cea972cc0f70e5f64e42d1832fdce3b0470af8a Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Sat, 11 Dec 2021 09:01:16 +0800 Subject: [PATCH 15/16] Apply suggestions from code review Co-authored-by: Caspar Schwarz-Schilling <31305984+casparschwa@users.noreply.github.com> --- .../test/phase0/fork_choice/test_ex_ante.py | 46 +++++++++++-------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py index 00d58356f..660b00eb7 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py +++ b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py @@ -33,14 +33,19 @@ def _apply_base_block_a(spec, state, store, test_steps): @with_all_phases @spec_state_test -def test_ex_ante_vanilla_with_boost(spec, state): +def test_ex_ante_vanilla(spec, state): """ With a single adversarial attestation - - Block A - slot N - Block B (parent A) - slot N+1 - Block C (parent A) - slot N+2 - Attestation_1 (Block B) - slot N+1 – size 1 + Objects: + Block A - slot N + Block B (parent A) - slot N+1 + Block C (parent A) - slot N+2 + Attestation_1 (Block B); size `1` - slot N+1 + Steps: + Block A received at N — A is head + Block C received at N+2 — C is head + Block B received at N+2 — C is head + Attestation_1 received at N+2 — C is head """ test_steps = [] # Initialization @@ -65,7 +70,7 @@ def test_ex_ante_vanilla_with_boost(spec, state): block = build_empty_block(spec, state_c, slot=state_a.slot + 2) signed_block_c = state_transition_and_sign_block(spec, state_c, block) - # Attestation_1 received at N+2 — B is head due to boost proposer + # Attestation_1 at slot `N + 1` voting for block B def _filter_participant_set(participants): return [next(iter(participants))] @@ -82,7 +87,7 @@ def test_ex_ante_vanilla_with_boost(spec, state): yield from add_block(spec, store, signed_block_c, test_steps) assert spec.get_head(store) == signed_block_c.message.hash_tree_root() - # Block B received at N+2 — C is head that has higher proposer score boost + # Block B received at N+2 — C is head due to proposer score boost yield from add_block(spec, store, signed_block_b, test_steps) assert spec.get_head(store) == signed_block_c.message.hash_tree_root() @@ -119,11 +124,16 @@ def _get_greater_than_proposer_boost_score(spec, store, state, proposer_boost_ro def test_ex_ante_attestations_is_greater_than_proposer_boost_with_boost(spec, state): """ Adversarial attestations > proposer boost - - Block A - slot N - Block B (parent A) - slot N+1 - Block C (parent A) - slot N+2 - Attestation_1 (Block B) - slot N+1 – proposer_boost + 1 participants + Objects: + Block A - slot N + Block B (parent A) - slot N+1 + Block C (parent A) - slot N+2 + Attestation_set_1 (Block B); size `proposer_boost + 1` - slot N+1 + Steps: + Block A received at N — A is head + Block C received at N+2 — C is head + Block B received at N+2 — C is head + Attestation_1 received at N+2 — B is head """ test_steps = [] # Initialization @@ -154,11 +164,11 @@ def test_ex_ante_attestations_is_greater_than_proposer_boost_with_boost(spec, st yield from add_block(spec, store, signed_block_c, test_steps) assert spec.get_head(store) == signed_block_c.message.hash_tree_root() - # Block B received at N+2 — C is head that has higher proposer score boost + # Block B received at N+2 — C is head due to proposer score boost yield from add_block(spec, store, signed_block_b, test_steps) assert spec.get_head(store) == signed_block_c.message.hash_tree_root() - # Attestation of proposer_boost + 1 participants + # Attestation_set_1 at slot `N + 1` voting for block B proposer_boost_root = signed_block_b.message.hash_tree_root() root = signed_block_b.message.hash_tree_root() participant_num = _get_greater_than_proposer_boost_score(spec, store, state, proposer_boost_root, root) @@ -173,7 +183,7 @@ def test_ex_ante_attestations_is_greater_than_proposer_boost_with_boost(spec, st assert len([i for i in attestation.aggregation_bits if i == 1]) == participant_num sign_attestation(spec, state_b, attestation) - # Attestation_1 received at N+2 — B is head because B's attestation_score > C's proposer_score. + # Attestation_set_1 received at N+2 — B is head because B's attestation_score > C's proposer_score. # (B's proposer_score = C's attestation_score = 0) yield from add_attestation(spec, store, attestation, test_steps) assert spec.get_head(store) == signed_block_b.message.hash_tree_root() @@ -183,7 +193,7 @@ def test_ex_ante_attestations_is_greater_than_proposer_boost_with_boost(spec, st @with_all_phases @spec_state_test -def test_ex_ante_sandwich_without_attestations_with_boost(spec, state): +def test_ex_ante_sandwich_without_attestations(spec, state): """ Simple Sandwich test with boost and no attestations. Obejcts: @@ -246,7 +256,7 @@ def test_ex_ante_sandwich_without_attestations_with_boost(spec, state): @with_all_phases @spec_state_test -def test_ex_ante_sandwich_with_honest_attestation_with_boost(spec, state): +def test_ex_ante_sandwich_with_honest_attestation(spec, state): """ Boosting necessary to sandwich attack. Objects: From 6edf840992af06f1aac868fc3a3a1e28e09c8098 Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Sat, 11 Dec 2021 09:06:56 +0800 Subject: [PATCH 16/16] fix lint --- .../pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py index 660b00eb7..d93101156 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py +++ b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py @@ -36,7 +36,7 @@ def _apply_base_block_a(spec, state, store, test_steps): def test_ex_ante_vanilla(spec, state): """ With a single adversarial attestation - Objects: + Objects: Block A - slot N Block B (parent A) - slot N+1 Block C (parent A) - slot N+2 @@ -124,7 +124,7 @@ def _get_greater_than_proposer_boost_score(spec, store, state, proposer_boost_ro def test_ex_ante_attestations_is_greater_than_proposer_boost_with_boost(spec, state): """ Adversarial attestations > proposer boost - Objects: + Objects: Block A - slot N Block B (parent A) - slot N+1 Block C (parent A) - slot N+2