diff --git a/specs/core/0_beacon-chain.md b/specs/core/0_beacon-chain.md index 6aa562ca4..86a017508 100644 --- a/specs/core/0_beacon-chain.md +++ b/specs/core/0_beacon-chain.md @@ -2280,7 +2280,7 @@ def process_transfer(state: BeaconState, transfer: Transfer) -> None: assert ( get_current_epoch(state) >= state.validator_registry[transfer.sender].withdrawable_epoch or state.validator_registry[transfer.sender].activation_epoch == FAR_FUTURE_EPOCH or - transfer.amount + transfer.fee + MAX_EFFECTIVE_BALANCE <= get_balance(transfer.sender) + transfer.amount + transfer.fee + MAX_EFFECTIVE_BALANCE <= get_balance(state, transfer.sender) ) # Verify that the pubkey is valid assert ( diff --git a/test_libs/pyspec/tests/block_processing/test_process_transfer.py b/test_libs/pyspec/tests/block_processing/test_process_transfer.py new file mode 100644 index 000000000..df49aff98 --- /dev/null +++ b/test_libs/pyspec/tests/block_processing/test_process_transfer.py @@ -0,0 +1,143 @@ +from copy import deepcopy +import pytest + +import eth2spec.phase0.spec as spec + +from eth2spec.phase0.spec import ( + get_active_validator_indices, + get_balance, + get_beacon_proposer_index, + get_current_epoch, + process_transfer, + set_balance, +) +from tests.helpers import ( + get_valid_transfer, + next_epoch, +) + + +# mark entire file as 'transfers' +pytestmark = pytest.mark.transfers + + +def run_transfer_processing(state, transfer, valid=True): + """ + Run ``process_transfer`` returning the pre and post state. + If ``valid == False``, run expecting ``AssertionError`` + """ + post_state = deepcopy(state) + + if not valid: + with pytest.raises(AssertionError): + process_transfer(post_state, transfer) + return state, None + + + process_transfer(post_state, transfer) + + proposer_index = get_beacon_proposer_index(state) + pre_transfer_sender_balance = state.balances[transfer.sender] + pre_transfer_recipient_balance = state.balances[transfer.recipient] + pre_transfer_proposer_balance = state.balances[proposer_index] + sender_balance = post_state.balances[transfer.sender] + recipient_balance = post_state.balances[transfer.recipient] + assert sender_balance == pre_transfer_sender_balance - transfer.amount - transfer.fee + assert recipient_balance == pre_transfer_recipient_balance + transfer.amount + assert post_state.balances[proposer_index] == pre_transfer_proposer_balance + transfer.fee + + return state, post_state + + +def test_success_non_activated(state): + transfer = get_valid_transfer(state) + # un-activate so validator can transfer + state.validator_registry[transfer.sender].activation_epoch = spec.FAR_FUTURE_EPOCH + + pre_state, post_state = run_transfer_processing(state, transfer) + + return pre_state, transfer, post_state + + +def test_success_withdrawable(state): + next_epoch(state) + + transfer = get_valid_transfer(state) + + # withdrawable_epoch in past so can transfer + state.validator_registry[transfer.sender].withdrawable_epoch = get_current_epoch(state) - 1 + + pre_state, post_state = run_transfer_processing(state, transfer) + + return pre_state, transfer, post_state + + +def test_success_active_above_max_effective(state): + sender_index = get_active_validator_indices(state, get_current_epoch(state))[-1] + amount = spec.MAX_EFFECTIVE_BALANCE // 32 + set_balance(state, sender_index, spec.MAX_EFFECTIVE_BALANCE + amount) + transfer = get_valid_transfer(state, sender_index=sender_index, amount=amount, fee=0) + + pre_state, post_state = run_transfer_processing(state, transfer) + + return pre_state, transfer, post_state + + +def test_active_but_transfer_past_effective_balance(state): + sender_index = get_active_validator_indices(state, get_current_epoch(state))[-1] + amount = spec.MAX_EFFECTIVE_BALANCE // 32 + set_balance(state, sender_index, spec.MAX_EFFECTIVE_BALANCE) + transfer = get_valid_transfer(state, sender_index=sender_index, amount=amount, fee=0) + + pre_state, post_state = run_transfer_processing(state, transfer, False) + + return pre_state, transfer, post_state + + +def test_incorrect_slot(state): + transfer = get_valid_transfer(state, slot=state.slot+1) + # un-activate so validator can transfer + state.validator_registry[transfer.sender].activation_epoch = spec.FAR_FUTURE_EPOCH + + pre_state, post_state = run_transfer_processing(state, transfer, False) + + return pre_state, transfer, post_state + + +def test_insufficient_balance(state): + sender_index = get_active_validator_indices(state, get_current_epoch(state))[-1] + amount = spec.MAX_EFFECTIVE_BALANCE + set_balance(state, sender_index, spec.MAX_EFFECTIVE_BALANCE) + transfer = get_valid_transfer(state, sender_index=sender_index, amount=amount + 1, fee=0) + + # un-activate so validator can transfer + state.validator_registry[transfer.sender].activation_epoch = spec.FAR_FUTURE_EPOCH + + pre_state, post_state = run_transfer_processing(state, transfer, False) + + return pre_state, transfer, post_state + + +def test_no_dust(state): + sender_index = get_active_validator_indices(state, get_current_epoch(state))[-1] + balance = state.balances[sender_index] + transfer = get_valid_transfer(state, sender_index=sender_index, amount=balance - spec.MIN_DEPOSIT_AMOUNT + 1, fee=0) + + # un-activate so validator can transfer + state.validator_registry[transfer.sender].activation_epoch = spec.FAR_FUTURE_EPOCH + + pre_state, post_state = run_transfer_processing(state, transfer, False) + + return pre_state, transfer, post_state + + +def test_invalid_pubkey(state): + transfer = get_valid_transfer(state) + state.validator_registry[transfer.sender].withdrawal_credentials = spec.ZERO_HASH + + # un-activate so validator can transfer + state.validator_registry[transfer.sender].activation_epoch = spec.FAR_FUTURE_EPOCH + + pre_state, post_state = run_transfer_processing(state, transfer, False) + + return pre_state, transfer, post_state \ No newline at end of file diff --git a/test_libs/pyspec/tests/helpers.py b/test_libs/pyspec/tests/helpers.py index 9ef891219..650790b5a 100644 --- a/test_libs/pyspec/tests/helpers.py +++ b/test_libs/pyspec/tests/helpers.py @@ -21,11 +21,13 @@ from eth2spec.phase0.spec import ( DepositData, Eth1Data, ProposerSlashing, + Transfer, VoluntaryExit, # functions convert_to_indexed, get_active_validator_indices, get_attestation_participants, + get_balance, get_block_root, get_crosslink_committee_for_attestation, get_current_epoch, @@ -291,6 +293,48 @@ def get_valid_attestation(state, slot=None): return attestation +def get_valid_transfer(state, slot=None, sender_index=None, amount=None, fee=None): + if slot is None: + slot = state.slot + current_epoch = get_current_epoch(state) + if sender_index is None: + sender_index = get_active_validator_indices(state, current_epoch)[-1] + recipient_index = get_active_validator_indices(state, current_epoch)[0] + transfer_pubkey = pubkeys[-1] + transfer_privkey = privkeys[-1] + + if fee is None: + fee = get_balance(state, sender_index) // 32 + if amount is None: + amount = get_balance(state, sender_index) - fee + + transfer = Transfer( + sender=sender_index, + recipient=recipient_index, + amount=amount, + fee=fee, + slot=slot, + pubkey=transfer_pubkey, + signature=EMPTY_SIGNATURE, + ) + transfer.signature = bls.sign( + message_hash=signing_root(transfer), + privkey=transfer_privkey, + domain=get_domain( + fork=state.fork, + epoch=get_current_epoch(state), + domain_type=spec.DOMAIN_TRANSFER, + ) + ) + + # ensure withdrawal_credentials reproducable + state.validator_registry[transfer.sender].withdrawal_credentials = ( + spec.BLS_WITHDRAWAL_PREFIX_BYTE + spec.hash(transfer.pubkey)[1:] + ) + + return transfer + + def get_attestation_signature(state, attestation_data, privkey, custody_bit=0b0): message_hash = AttestationDataAndCustodyBit( data=attestation_data, @@ -311,3 +355,9 @@ def get_attestation_signature(state, attestation_data, privkey, custody_bit=0b0) def next_slot(state): block = build_empty_block_for_next_slot(state) state_transition(state, block) + + +def next_epoch(state): + block = build_empty_block_for_next_slot(state) + block.slot += spec.SLOTS_PER_EPOCH - (state.slot % spec.SLOTS_PER_EPOCH) + state_transition(state, block)