[WIP] Add new transition tests

This commit is contained in:
Hsiao-Wei Wang 2021-10-12 20:35:18 +08:00
parent aa592b008c
commit 4dd8b7c98a
No known key found for this signature in database
GPG Key ID: 1111A8A81778319E
4 changed files with 438 additions and 19 deletions

View File

@ -1,7 +1,11 @@
from random import Random
from eth2spec.test.context import spec_state_test, with_altair_and_later
from eth2spec.test.helpers.inactivity_scores import randomize_inactivity_scores, zero_inactivity_scores
from eth2spec.test.helpers.inactivity_scores import (
randomize_inactivity_scores,
zero_inactivity_scores,
slash_some_validators_for_inactivity_scores_test,
)
from eth2spec.test.helpers.state import (
next_epoch,
next_epoch_via_block,
@ -201,20 +205,6 @@ def test_random_inactivity_scores_full_participation_leaking(spec, state):
assert spec.is_in_inactivity_leak(state)
def slash_some_validators_for_inactivity_scores_test(spec, state, rng=Random(40404040)):
# ``run_inactivity_scores_test`` runs at the next epoch from `state`.
# We retrieve the proposer of this future state to avoid
# accidentally slashing that validator
future_state = state.copy()
next_epoch_via_block(spec, future_state)
proposer_index = spec.get_beacon_proposer_index(future_state)
# Slash ~1/4 of validaors
for validator_index in range(len(state.validators)):
if rng.choice(range(4)) == 0 and validator_index != proposer_index:
spec.slash_validator(state, validator_index)
@with_altair_and_later
@spec_state_test
def test_some_slashed_zero_scores_full_participation(spec, state):

View File

@ -1,12 +1,24 @@
import random
from eth2spec.test.context import fork_transition_test
from eth2spec.test.helpers.constants import PHASE0, ALTAIR
from eth2spec.test.helpers.state import state_transition_and_sign_block, next_slot, next_epoch_via_signed_block
from eth2spec.test.helpers.state import (
next_epoch_via_signed_block,
next_slot,
state_transition_and_sign_block,
transition_to,
)
from eth2spec.test.helpers.block import build_empty_block_for_next_slot, build_empty_block, sign_block
from eth2spec.test.helpers.deposits import (
prepare_state_and_deposit,
)
from eth2spec.test.helpers.attestations import next_slots_with_attestations
from eth2spec.test.helpers.random import set_some_new_deposits
from eth2spec.test.helpers.inactivity_scores import (
slash_some_validators_for_inactivity_scores_test,
)
def _state_transition_and_sign_block_at_slot(spec, state):
def _state_transition_and_sign_block_at_slot(spec, state, deposits=None):
"""
Cribbed from ``transition_unsigned_block`` helper
where the early parts of the state transition have already
@ -15,6 +27,9 @@ def _state_transition_and_sign_block_at_slot(spec, state):
Used to produce a block during an irregular state transition.
"""
block = build_empty_block(spec, state)
# FIXME: not just passing `deposits`
if deposits is not None:
block.body.deposits = deposits
assert state.latest_block_header.slot < block.slot
assert state.slot == block.slot
@ -62,7 +77,29 @@ def _state_transition_across_slots(spec, state, to_slot, block_filter=_all_block
next_slot(spec, state)
def _do_altair_fork(state, spec, post_spec, fork_epoch, with_block=True):
def _state_transition_across_slots_with_ignoring_proposers(spec, state, to_slot, ignoring_proposers):
"""
The slashed validators can't be proposers. Here we ignore the given `ignoring_proposers`
and ensure that the result state was computed with a block with slot >= to_slot.
"""
assert state.slot < to_slot
found_valid = False
while state.slot < to_slot or not found_valid:
future_state = state.copy()
next_slot(spec, future_state)
proposer_index = spec.get_beacon_proposer_index(future_state)
if proposer_index not in ignoring_proposers:
block = build_empty_block_for_next_slot(spec, state)
signed_block = state_transition_and_sign_block(spec, state, block)
yield signed_block
if state.slot >= to_slot:
found_valid = True
else:
next_slot(spec, state)
def _do_altair_fork(state, spec, post_spec, fork_epoch, with_block=True, deposits=None):
spec.process_slots(state, state.slot + 1)
assert state.slot % spec.SLOTS_PER_EPOCH == 0
@ -75,11 +112,25 @@ def _do_altair_fork(state, spec, post_spec, fork_epoch, with_block=True):
assert state.fork.current_version == post_spec.config.ALTAIR_FORK_VERSION
if with_block:
return state, _state_transition_and_sign_block_at_slot(post_spec, state)
return state, _state_transition_and_sign_block_at_slot(post_spec, state, deposits=deposits)
else:
return state, None
def _set_validators_exit_epoch(spec, state, exit_epoch, rng=random.Random(40404040), fraction=0.25):
"""
Set some valdiators' exit_epoch.
"""
selected_count = int(len(state.validators) * fraction)
selected_indices = rng.sample(range(len(state.validators)), selected_count)
for validator_index in selected_indices:
state.validators[validator_index].exit_epoch = exit_epoch
state.validators[validator_index].withdrawable_epoch = (
exit_epoch + spec.config.MIN_VALIDATOR_WITHDRAWABILITY_DELAY
)
return selected_indices
@fork_transition_test(PHASE0, ALTAIR, fork_epoch=2)
def test_normal_transition(state, fork_epoch, spec, post_spec, pre_tag, post_tag):
"""
@ -434,3 +485,353 @@ def test_transition_with_no_attestations_until_after_fork(state, fork_epoch, spe
yield "blocks", blocks
yield "post", state
@fork_transition_test(PHASE0, ALTAIR, fork_epoch=1)
def test_transition_with_one_fourth_slashed_active_validators_pre_fork(
state, fork_epoch, spec, post_spec, pre_tag, post_tag):
"""
1/4 validators are slashed but still active at the fork transition.
"""
# slash 1/4 validators
selected_indices = slash_some_validators_for_inactivity_scores_test(
spec, state, rng=random.Random(5566), fraction=0.25)
assert len(selected_indices) > 0
# check if some validators are slashed but still active
for validator_index in selected_indices:
validator = state.validators[validator_index]
assert validator.slashed
assert spec.is_active_validator(validator, spec.get_current_epoch(state))
assert not spec.is_in_inactivity_leak(state)
# regular state transition until fork:
to_slot = fork_epoch * spec.SLOTS_PER_EPOCH - 1
transition_to(spec, state, to_slot)
assert spec.get_current_epoch(state) < fork_epoch
yield "pre", state
# irregular state transition to handle fork:
blocks = []
state, block = _do_altair_fork(state, spec, post_spec, fork_epoch)
blocks.append(post_tag(block))
# continue regular state transition with new spec into next epoch
to_slot = post_spec.SLOTS_PER_EPOCH + state.slot
# since the proposer might have been slashed, here we only create blocks with non-slashed proposers
blocks.extend([
post_tag(block) for block in
_state_transition_across_slots_with_ignoring_proposers(post_spec, state, to_slot, selected_indices)
])
# check post state
for validator in state.validators:
assert post_spec.is_active_validator(validator, post_spec.get_current_epoch(state))
assert not post_spec.is_in_inactivity_leak(state)
yield "blocks", blocks
yield "post", state
@fork_transition_test(PHASE0, ALTAIR, fork_epoch=2)
def test_transition_with_one_fourth_exiting_validators_exit_post_fork(
state, fork_epoch, spec, post_spec, pre_tag, post_tag):
"""
1/4 exiting but still active validators at the fork transition.
"""
exited_indices = _set_validators_exit_epoch(spec, state, exit_epoch=10, rng=random.Random(5566), fraction=0.25)
# regular state transition until fork:
to_slot = fork_epoch * spec.SLOTS_PER_EPOCH - 1
transition_to(spec, state, to_slot)
# check pre state
assert len(exited_indices) > 0
for index in exited_indices:
validator = state.validators[index]
assert not validator.slashed
assert fork_epoch < validator.exit_epoch < spec.FAR_FUTURE_EPOCH
assert spec.is_active_validator(validator, spec.get_current_epoch(state))
assert not spec.is_in_inactivity_leak(state)
assert spec.get_current_epoch(state) < fork_epoch
yield "pre", state
# irregular state transition to handle fork:
blocks = []
state, block = _do_altair_fork(state, spec, post_spec, fork_epoch)
blocks.append(post_tag(block))
# ensure that some of the current sync committee members are exiting
exited_pubkeys = [state.validators[index].pubkey for index in exited_indices]
assert any(set(exited_pubkeys).intersection(list(state.current_sync_committee.pubkeys)))
# continue regular state transition with new spec into next epoch
to_slot = post_spec.SLOTS_PER_EPOCH + state.slot
blocks.extend([
post_tag(block) for block in
_state_transition_across_slots(post_spec, state, to_slot)
])
# check state
for index in exited_indices:
validator = state.validators[index]
assert not validator.slashed
assert post_spec.is_active_validator(validator, post_spec.get_current_epoch(state))
assert not post_spec.is_in_inactivity_leak(state)
yield "blocks", blocks
yield "post", state
@fork_transition_test(PHASE0, ALTAIR, fork_epoch=2)
def test_transition_with_one_fourth_exiting_validators_exit_at_fork(
state, fork_epoch, spec, post_spec, pre_tag, post_tag):
"""
1/4 exiting but still active validators at the fork transition.
"""
exited_indices = _set_validators_exit_epoch(spec, state, exit_epoch=2, rng=random.Random(5566), fraction=0.25)
# regular state transition until fork:
to_slot = fork_epoch * spec.SLOTS_PER_EPOCH - 1
transition_to(spec, state, to_slot)
# check pre state
assert len(exited_indices) > 0
for index in exited_indices:
validator = state.validators[index]
assert not validator.slashed
assert fork_epoch == validator.exit_epoch < spec.FAR_FUTURE_EPOCH
assert spec.is_active_validator(validator, spec.get_current_epoch(state))
assert not spec.is_in_inactivity_leak(state)
assert spec.get_current_epoch(state) < fork_epoch
yield "pre", state
# irregular state transition to handle fork:
blocks = []
state, block = _do_altair_fork(state, spec, post_spec, fork_epoch)
blocks.append(post_tag(block))
# check post transition state
for index in exited_indices:
validator = state.validators[index]
assert not validator.slashed
assert not post_spec.is_active_validator(validator, post_spec.get_current_epoch(state))
assert not post_spec.is_in_inactivity_leak(state)
# ensure that none of the current sync committee members are exited validators
exited_pubkeys = [state.validators[index].pubkey for index in exited_indices]
assert not any(set(exited_pubkeys).intersection(list(state.current_sync_committee.pubkeys)))
# continue regular state transition with new spec into next epoch
to_slot = post_spec.SLOTS_PER_EPOCH + state.slot
blocks.extend([
post_tag(block) for block in
_state_transition_across_slots(post_spec, state, to_slot)
])
yield "blocks", blocks
yield "post", state
@fork_transition_test(PHASE0, ALTAIR, fork_epoch=7)
def test_transition_with_leaking_pre_fork(state, fork_epoch, spec, post_spec, pre_tag, post_tag):
"""
Leaking starts at epoch 6 (MIN_EPOCHS_TO_INACTIVITY_PENALTY + 2).
The leaking starts before the fork transition in this case.
"""
# regular state transition until fork:
to_slot = fork_epoch * spec.SLOTS_PER_EPOCH - 1
transition_to(spec, state, to_slot)
assert spec.is_in_inactivity_leak(state)
assert spec.get_current_epoch(state) < fork_epoch
yield "pre", state
# irregular state transition to handle fork:
blocks = []
state, block = _do_altair_fork(state, spec, post_spec, fork_epoch)
blocks.append(post_tag(block))
# check post transition state
assert spec.is_in_inactivity_leak(state)
# continue regular state transition with new spec into next epoch
to_slot = post_spec.SLOTS_PER_EPOCH + state.slot
blocks.extend([
post_tag(block) for block in
_state_transition_across_slots(post_spec, state, to_slot)
])
yield "blocks", blocks
yield "post", state
@fork_transition_test(PHASE0, ALTAIR, fork_epoch=6)
def test_transition_with_leaking_at_fork(state, fork_epoch, spec, post_spec, pre_tag, post_tag):
"""
Leaking starts at epoch 6 (MIN_EPOCHS_TO_INACTIVITY_PENALTY + 2).
The leaking starts at the fork transition in this case.
"""
# regular state transition until fork:
to_slot = fork_epoch * spec.SLOTS_PER_EPOCH - 1
transition_to(spec, state, to_slot)
assert not spec.is_in_inactivity_leak(state)
assert spec.get_current_epoch(state) < fork_epoch
yield "pre", state
# irregular state transition to handle fork:
blocks = []
state, block = _do_altair_fork(state, spec, post_spec, fork_epoch)
blocks.append(post_tag(block))
# check post transition state
assert spec.is_in_inactivity_leak(state)
# continue regular state transition with new spec into next epoch
to_slot = post_spec.SLOTS_PER_EPOCH + state.slot
blocks.extend([
post_tag(block) for block in
_state_transition_across_slots(post_spec, state, to_slot)
])
yield "blocks", blocks
yield "post", state
@fork_transition_test(PHASE0, ALTAIR, fork_epoch=5)
def test_transition_with_leaking_post_fork(state, fork_epoch, spec, post_spec, pre_tag, post_tag):
"""
Leaking starts at epoch 6 (MIN_EPOCHS_TO_INACTIVITY_PENALTY + 2).
The leaking starts after the fork transition in this case.
"""
# regular state transition until fork:
to_slot = fork_epoch * spec.SLOTS_PER_EPOCH - 1
transition_to(spec, state, to_slot)
assert not spec.is_in_inactivity_leak(state)
assert spec.get_current_epoch(state) < fork_epoch
yield "pre", state
# irregular state transition to handle fork:
blocks = []
state, block = _do_altair_fork(state, spec, post_spec, fork_epoch)
blocks.append(post_tag(block))
# check post transition state
assert not spec.is_in_inactivity_leak(state)
# continue regular state transition with new spec into next epoch
to_slot = post_spec.SLOTS_PER_EPOCH + state.slot
blocks.extend([
post_tag(block) for block in
_state_transition_across_slots(post_spec, state, to_slot)
])
# check state again
assert spec.is_in_inactivity_leak(state)
yield "blocks", blocks
yield "post", state
@fork_transition_test(PHASE0, ALTAIR, fork_epoch=10)
def test_transition_with_non_empty_activation_queue(state, fork_epoch, spec, post_spec, pre_tag, post_tag):
"""
Create some deposits before the transition
"""
# regular state transition until fork:
to_slot = fork_epoch * spec.SLOTS_PER_EPOCH - 1
transition_to(spec, state, to_slot)
_, queuing_indices = set_some_new_deposits(spec, state, rng=random.Random(5566))
assert spec.get_current_epoch(state) < fork_epoch
assert len(queuing_indices) > 0
for validator_index in queuing_indices:
assert not spec.is_active_validator(state.validators[validator_index], spec.get_current_epoch(state))
yield "pre", state
# irregular state transition to handle fork:
blocks = []
state, block = _do_altair_fork(state, spec, post_spec, fork_epoch)
blocks.append(post_tag(block))
# continue regular state transition with new spec into next epoch
to_slot = post_spec.SLOTS_PER_EPOCH + state.slot
blocks.extend([
post_tag(block) for block in
_state_transition_across_slots(post_spec, state, to_slot)
])
yield "blocks", blocks
yield "post", state
@fork_transition_test(PHASE0, ALTAIR, fork_epoch=10)
def test_transition_with_deposit_at_fork(state, fork_epoch, spec, post_spec, pre_tag, post_tag):
"""
Create a deposit at the transition
"""
# regular state transition until fork:
to_slot = fork_epoch * spec.SLOTS_PER_EPOCH - 1
transition_to(spec, state, to_slot)
yield "pre", state
# create a new deposit
validator_index = len(state.validators)
amount = post_spec.MAX_EFFECTIVE_BALANCE
deposit = prepare_state_and_deposit(post_spec, state, validator_index, amount, signed=True)
# irregular state transition to handle fork:
state, block = _do_altair_fork(state, spec, post_spec, fork_epoch, deposits=[deposit])
blocks = []
blocks.append(post_tag(block))
assert not post_spec.is_active_validator(state.validators[validator_index], post_spec.get_current_epoch(state))
# continue regular state transition with new spec into next epoch
to_slot = post_spec.SLOTS_PER_EPOCH + state.slot
blocks.extend([
post_tag(block) for block in
_state_transition_across_slots(post_spec, state, to_slot)
])
# finalize activation_eligibility_epoch
_, blocks_in_epoch, state = next_slots_with_attestations(
post_spec,
state,
spec.SLOTS_PER_EPOCH * 2,
fill_cur_epoch=True,
fill_prev_epoch=True,
)
blocks.extend([pre_tag(block) for block in blocks_in_epoch])
assert state.finalized_checkpoint.epoch == state.validators[validator_index].activation_eligibility_epoch
# continue regular state transition with new spec into next epoch
to_slot = post_spec.SLOTS_PER_EPOCH + state.slot
blocks.extend([
post_tag(block) for block in
_state_transition_across_slots(post_spec, state, to_slot)
])
assert state.validators[validator_index].activation_epoch < post_spec.FAR_FUTURE_EPOCH
to_slot = state.validators[validator_index].activation_epoch * post_spec.SLOTS_PER_EPOCH
blocks.extend([
post_tag(block) for block in
_state_transition_across_slots(post_spec, state, to_slot)
])
assert post_spec.is_active_validator(state.validators[validator_index], post_spec.get_current_epoch(state))
yield "blocks", blocks
yield "post", state

View File

@ -1,5 +1,9 @@
from random import Random
from eth2spec.test.helpers.state import (
next_epoch_via_block,
)
def randomize_inactivity_scores(spec, state, minimum=0, maximum=50000, rng=Random(4242)):
state.inactivity_scores = [rng.randint(minimum, maximum) for _ in range(len(state.validators))]
@ -7,3 +11,22 @@ def randomize_inactivity_scores(spec, state, minimum=0, maximum=50000, rng=Rando
def zero_inactivity_scores(spec, state, rng=None):
state.inactivity_scores = [0] * len(state.validators)
def slash_some_validators_for_inactivity_scores_test(spec, state, rng=Random(40404040), fraction=0.25):
"""
``run_inactivity_scores_test`` runs at the next epoch from `state`.
# We retrieve the proposer of this future state to avoid
# accidentally slashing that validator
"""
future_state = state.copy()
next_epoch_via_block(spec, future_state)
proposer_index = spec.get_beacon_proposer_index(future_state)
selected_count = int(len(state.validators) * fraction)
selected_indices = rng.sample(range(len(state.validators)), selected_count)
if proposer_index in selected_indices:
selected_indices.remove(proposer_index)
for validator_index in selected_indices:
spec.slash_validator(state, validator_index)
return selected_indices

View File

@ -7,6 +7,7 @@ from eth2spec.test.helpers.state import next_epoch
def set_some_new_deposits(spec, state, rng):
eligible_indices = queuing_indices = []
num_validators = len(state.validators)
# Set ~1/10 to just recently deposited
for index in range(num_validators):
@ -18,6 +19,10 @@ def set_some_new_deposits(spec, state, rng):
# Set ~half of selected to eligible for activation
if rng.choice([True, False]):
state.validators[index].activation_eligibility_epoch = spec.get_current_epoch(state)
eligible_indices.append(index)
else:
queuing_indices.append(index)
return eligible_indices, queuing_indices
def exit_random_validators(spec, state, rng, fraction=None):