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