diff --git a/Makefile b/Makefile index b45cec410..88f17dcf9 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ clean: # runs a limited set of tests against a minimal config # run pytest with `-m` option to full suite test: - pytest -m "sanity and minimal_config" tests/ + pytest -m minimal_config tests/ $(BUILD_DIR)/phase0: diff --git a/tests/phase0/block_processing/test_process_block_header.py b/tests/phase0/block_processing/test_process_block_header.py new file mode 100644 index 000000000..241466437 --- /dev/null +++ b/tests/phase0/block_processing/test_process_block_header.py @@ -0,0 +1,60 @@ +from copy import deepcopy +import pytest + + +from build.phase0.spec import ( + get_beacon_proposer_index, + cache_state, + advance_slot, + process_block_header, +) +from tests.phase0.helpers import ( + build_empty_block_for_next_slot, +) + +# mark entire file as 'header' +pytestmark = pytest.mark.header + + +def prepare_state_for_header_processing(state): + cache_state(state) + advance_slot(state) + + +def run_block_header_processing(state, block, valid=True): + """ + Run ``process_block_header`` returning the pre and post state. + If ``valid == False``, run expecting ``AssertionError`` + """ + prepare_state_for_header_processing(state) + post_state = deepcopy(state) + + if not valid: + with pytest.raises(AssertionError): + process_block_header(post_state, block) + return state, None + + process_block_header(post_state, block) + return state, post_state + + +def test_success(state): + block = build_empty_block_for_next_slot(state) + pre_state, post_state = run_block_header_processing(state, block) + return state, block, post_state + + +def test_invalid_slot(state): + block = build_empty_block_for_next_slot(state) + block.slot = state.slot + 2 # invalid slot + + pre_state, post_state = run_block_header_processing(state, block, valid=False) + return pre_state, block, None + + +def test_invalid_previous_block_root(state): + block = build_empty_block_for_next_slot(state) + block.previous_block_root = b'\12'*32 # invalid prev root + + pre_state, post_state = run_block_header_processing(state, block, valid=False) + return pre_state, block, None diff --git a/tests/phase0/block_processing/test_process_deposit.py b/tests/phase0/block_processing/test_process_deposit.py new file mode 100644 index 000000000..297ad37f1 --- /dev/null +++ b/tests/phase0/block_processing/test_process_deposit.py @@ -0,0 +1,132 @@ +from copy import deepcopy +import pytest + +import build.phase0.spec as spec + +from build.phase0.spec import ( + Deposit, + process_deposit, +) +from tests.phase0.helpers import ( + build_deposit, +) + + +# mark entire file as 'voluntary_exits' +pytestmark = pytest.mark.voluntary_exits + + +def test_success(state, deposit_data_leaves, pubkeys, privkeys): + pre_state = deepcopy(state) + + index = len(deposit_data_leaves) + pubkey = pubkeys[index] + privkey = privkeys[index] + deposit, root, deposit_data_leaves = build_deposit( + pre_state, + deposit_data_leaves, + pubkey, + privkey, + spec.MAX_DEPOSIT_AMOUNT, + ) + + pre_state.latest_eth1_data.deposit_root = root + pre_state.latest_eth1_data.deposit_count = len(deposit_data_leaves) + + post_state = deepcopy(pre_state) + + process_deposit(post_state, deposit) + + assert len(post_state.validator_registry) == len(state.validator_registry) + 1 + assert len(post_state.validator_balances) == len(state.validator_balances) + 1 + assert post_state.validator_registry[index].pubkey == pubkeys[index] + assert post_state.deposit_index == post_state.latest_eth1_data.deposit_count + + return pre_state, deposit, post_state + + +def test_success_top_up(state, deposit_data_leaves, pubkeys, privkeys): + pre_state = deepcopy(state) + + validator_index = 0 + amount = spec.MAX_DEPOSIT_AMOUNT // 4 + pubkey = pubkeys[validator_index] + privkey = privkeys[validator_index] + deposit, root, deposit_data_leaves = build_deposit( + pre_state, + deposit_data_leaves, + pubkey, + privkey, + amount, + ) + + pre_state.latest_eth1_data.deposit_root = root + pre_state.latest_eth1_data.deposit_count = len(deposit_data_leaves) + pre_balance = pre_state.validator_balances[validator_index] + + post_state = deepcopy(pre_state) + + process_deposit(post_state, deposit) + + assert len(post_state.validator_registry) == len(state.validator_registry) + assert len(post_state.validator_balances) == len(state.validator_balances) + assert post_state.deposit_index == post_state.latest_eth1_data.deposit_count + assert post_state.validator_balances[validator_index] == pre_balance + amount + + return pre_state, deposit, post_state + + +def test_wrong_index(state, deposit_data_leaves, pubkeys, privkeys): + pre_state = deepcopy(state) + + index = len(deposit_data_leaves) + pubkey = pubkeys[index] + privkey = privkeys[index] + deposit, root, deposit_data_leaves = build_deposit( + pre_state, + deposit_data_leaves, + pubkey, + privkey, + spec.MAX_DEPOSIT_AMOUNT, + ) + + # mess up deposit_index + deposit.index = pre_state.deposit_index + 1 + + pre_state.latest_eth1_data.deposit_root = root + pre_state.latest_eth1_data.deposit_count = len(deposit_data_leaves) + + post_state = deepcopy(pre_state) + + with pytest.raises(AssertionError): + process_deposit(post_state, deposit) + + return pre_state, deposit, None + + +def test_bad_merkle_proof(state, deposit_data_leaves, pubkeys, privkeys): + pre_state = deepcopy(state) + + index = len(deposit_data_leaves) + pubkey = pubkeys[index] + privkey = privkeys[index] + deposit, root, deposit_data_leaves = build_deposit( + pre_state, + deposit_data_leaves, + pubkey, + privkey, + spec.MAX_DEPOSIT_AMOUNT, + ) + + # mess up merkle branch + deposit.proof[-1] = spec.ZERO_HASH + + pre_state.latest_eth1_data.deposit_root = root + pre_state.latest_eth1_data.deposit_count = len(deposit_data_leaves) + + post_state = deepcopy(pre_state) + + with pytest.raises(AssertionError): + process_deposit(post_state, deposit) + + return pre_state, deposit, None diff --git a/tests/phase0/block_processing/test_voluntary_exit.py b/tests/phase0/block_processing/test_voluntary_exit.py new file mode 100644 index 000000000..0801e4292 --- /dev/null +++ b/tests/phase0/block_processing/test_voluntary_exit.py @@ -0,0 +1,174 @@ +from copy import deepcopy +import pytest + +import build.phase0.spec as spec + +from build.phase0.spec import ( + get_active_validator_indices, + get_current_epoch, + process_voluntary_exit, +) +from tests.phase0.helpers import ( + build_voluntary_exit, +) + + +# mark entire file as 'voluntary_exits' +pytestmark = pytest.mark.voluntary_exits + + +def test_success(state, pub_to_priv): + pre_state = deepcopy(state) + # + # setup pre_state + # + # move state forward PERSISTENT_COMMITTEE_PERIOD epochs to allow for exit + pre_state.slot += spec.PERSISTENT_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH + + # + # build voluntary exit + # + current_epoch = get_current_epoch(pre_state) + validator_index = get_active_validator_indices(pre_state.validator_registry, current_epoch)[0] + privkey = pub_to_priv[pre_state.validator_registry[validator_index].pubkey] + + voluntary_exit = build_voluntary_exit( + pre_state, + current_epoch, + validator_index, + privkey, + ) + + post_state = deepcopy(pre_state) + + # + # test valid exit + # + process_voluntary_exit(post_state, voluntary_exit) + + assert not pre_state.validator_registry[validator_index].initiated_exit + assert post_state.validator_registry[validator_index].initiated_exit + + return pre_state, voluntary_exit, post_state + + +def test_validator_not_active(state, pub_to_priv): + pre_state = deepcopy(state) + current_epoch = get_current_epoch(pre_state) + validator_index = get_active_validator_indices(pre_state.validator_registry, current_epoch)[0] + privkey = pub_to_priv[pre_state.validator_registry[validator_index].pubkey] + + # + # setup pre_state + # + pre_state.validator_registry[validator_index].activation_epoch = spec.FAR_FUTURE_EPOCH + + # + # build and test voluntary exit + # + voluntary_exit = build_voluntary_exit( + pre_state, + current_epoch, + validator_index, + privkey, + ) + + with pytest.raises(AssertionError): + process_voluntary_exit(pre_state, voluntary_exit) + + return pre_state, voluntary_exit, None + + +def test_validator_already_exited(state, pub_to_priv): + pre_state = deepcopy(state) + # + # setup pre_state + # + # move state forward PERSISTENT_COMMITTEE_PERIOD epochs to allow validator able to exit + pre_state.slot += spec.PERSISTENT_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH + + current_epoch = get_current_epoch(pre_state) + validator_index = get_active_validator_indices(pre_state.validator_registry, current_epoch)[0] + privkey = pub_to_priv[pre_state.validator_registry[validator_index].pubkey] + + # but validator already has exited + pre_state.validator_registry[validator_index].exit_epoch = current_epoch + 2 + + # + # build voluntary exit + # + voluntary_exit = build_voluntary_exit( + pre_state, + current_epoch, + validator_index, + privkey, + ) + + with pytest.raises(AssertionError): + process_voluntary_exit(pre_state, voluntary_exit) + + return pre_state, voluntary_exit, None + + +def test_validator_already_initiated_exit(state, pub_to_priv): + pre_state = deepcopy(state) + # + # setup pre_state + # + # move state forward PERSISTENT_COMMITTEE_PERIOD epochs to allow validator able to exit + pre_state.slot += spec.PERSISTENT_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH + + current_epoch = get_current_epoch(pre_state) + validator_index = get_active_validator_indices(pre_state.validator_registry, current_epoch)[0] + privkey = pub_to_priv[pre_state.validator_registry[validator_index].pubkey] + + # but validator already has initiated exit + pre_state.validator_registry[validator_index].initiated_exit = True + + # + # build voluntary exit + # + voluntary_exit = build_voluntary_exit( + pre_state, + current_epoch, + validator_index, + privkey, + ) + + with pytest.raises(AssertionError): + process_voluntary_exit(pre_state, voluntary_exit) + + return pre_state, voluntary_exit, None + + +def test_validator_not_active_long_enough(state, pub_to_priv): + pre_state = deepcopy(state) + # + # setup pre_state + # + current_epoch = get_current_epoch(pre_state) + validator_index = get_active_validator_indices(pre_state.validator_registry, current_epoch)[0] + privkey = pub_to_priv[pre_state.validator_registry[validator_index].pubkey] + + # but validator already has initiated exit + pre_state.validator_registry[validator_index].initiated_exit = True + + # + # build voluntary exit + # + voluntary_exit = build_voluntary_exit( + pre_state, + current_epoch, + validator_index, + privkey, + ) + + assert ( + current_epoch - pre_state.validator_registry[validator_index].activation_epoch < + spec.PERSISTENT_COMMITTEE_PERIOD + ) + + with pytest.raises(AssertionError): + process_voluntary_exit(pre_state, voluntary_exit) + + return pre_state, voluntary_exit, None diff --git a/tests/phase0/conftest.py b/tests/phase0/conftest.py index e92896e92..395929028 100644 --- a/tests/phase0/conftest.py +++ b/tests/phase0/conftest.py @@ -5,6 +5,7 @@ from build.phase0 import spec from tests.phase0.helpers import ( privkeys_list, pubkeys_list, + pubkey_to_privkey, create_genesis_state, ) @@ -34,6 +35,11 @@ def pubkeys(): return pubkeys_list +@pytest.fixture +def pub_to_priv(): + return pubkey_to_privkey + + def overwrite_spec_config(config): for field in config: setattr(spec, field, config[field]) diff --git a/tests/phase0/helpers.py b/tests/phase0/helpers.py index 16f5a5ea4..134fa797d 100644 --- a/tests/phase0/helpers.py +++ b/tests/phase0/helpers.py @@ -13,6 +13,7 @@ from build.phase0.spec import ( DepositInput, DepositData, Eth1Data, + VoluntaryExit, # functions get_block_root, get_current_epoch, @@ -71,17 +72,29 @@ def create_mock_genesis_validator_deposits(num_validators, deposit_data_leaves): def create_genesis_state(num_validators, deposit_data_leaves): - initial_deposits, deposit_root = create_mock_genesis_validator_deposits(num_validators, deposit_data_leaves) + initial_deposits, deposit_root = create_mock_genesis_validator_deposits( + num_validators, + deposit_data_leaves, + ) return get_genesis_beacon_state( initial_deposits, genesis_time=0, genesis_eth1_data=Eth1Data( deposit_root=deposit_root, + deposit_count=len(initial_deposits), block_hash=spec.ZERO_HASH, ), ) +def force_registry_change_at_next_epoch(state): + # artificially trigger registry update at next epoch transition + state.finalized_epoch = get_current_epoch(state) - 1 + for crosslink in state.latest_crosslinks: + crosslink.epoch = state.finalized_epoch + state.validator_registry_update_epoch = state.finalized_epoch - 1 + + def build_empty_block_for_next_slot(state): empty_block = get_empty_block() empty_block.slot = state.slot + 1 @@ -143,3 +156,46 @@ def build_attestation_data(state, slot, shard): crosslink_data_root=spec.ZERO_HASH, previous_crosslink=deepcopy(state.latest_crosslinks[shard]), ) + + +def build_voluntary_exit(state, epoch, validator_index, privkey): + voluntary_exit = VoluntaryExit( + epoch=epoch, + validator_index=validator_index, + signature=EMPTY_SIGNATURE, + ) + voluntary_exit.signature = bls.sign( + message_hash=signed_root(voluntary_exit), + privkey=privkey, + domain=get_domain( + fork=state.fork, + epoch=epoch, + domain_type=spec.DOMAIN_VOLUNTARY_EXIT, + ) + ) + + return voluntary_exit + + +def build_deposit(state, + deposit_data_leaves, + pubkey, + privkey, + amount): + deposit_data = build_deposit_data(state, pubkey, privkey, amount) + + item = hash(deposit_data.serialize()) + index = len(deposit_data_leaves) + deposit_data_leaves.append(item) + tree = calc_merkle_tree_from_leaves(tuple(deposit_data_leaves)) + root = get_merkle_root((tuple(deposit_data_leaves))) + proof = list(get_merkle_proof(tree, item_index=index)) + assert verify_merkle_branch(item, proof, spec.DEPOSIT_CONTRACT_TREE_DEPTH, index, root) + + deposit = Deposit( + proof=list(proof), + index=index, + deposit_data=deposit_data, + ) + + return deposit, root, deposit_data_leaves diff --git a/utils/phase0/minimal_ssz.py b/utils/phase0/minimal_ssz.py index 08bd68357..c4828d08f 100644 --- a/utils/phase0/minimal_ssz.py +++ b/utils/phase0/minimal_ssz.py @@ -39,15 +39,22 @@ def SSZType(fields): return SSZObject -class Vector(list): - def __init__(self, x): - list.__init__(self, x) - self.length = len(x) +class Vector(): + def __init__(self, items): + self.items = items + self.length = len(items) - def append(*args): - raise Exception("Cannot change the length of a vector") + def __getitem__(self, key): + return self.items[key] - remove = clear = extend = pop = insert = append + def __setitem__(self, key, value): + self.items[key] = value + + def __iter__(self): + return iter(self.items) + + def __len__(self): + return self.length def is_basic(typ):