diff --git a/specs/electra/beacon-chain.md b/specs/electra/beacon-chain.md index 62da89114..b09cf6906 100644 --- a/specs/electra/beacon-chain.md +++ b/specs/electra/beacon-chain.md @@ -798,12 +798,27 @@ def process_pending_balance_deposits(state: BeaconState) -> None: available_for_processing = state.deposit_balance_to_consume + get_activation_exit_churn_limit(state) processed_amount = 0 next_deposit_index = 0 + deposits_to_postpone = [] for deposit in state.pending_balance_deposits: - if processed_amount + deposit.amount > available_for_processing: - break - increase_balance(state, deposit.index, deposit.amount) - processed_amount += deposit.amount + validator = state.validators[deposit.index] + # Validator is exiting, postpone the deposit until after withdrawable epoch + if validator.exit_epoch < FAR_FUTURE_EPOCH: + if get_current_epoch(state) <= validator.withdrawable_epoch: + deposits_to_postpone.append(deposit) + # Deposited balance will never become active. Increase balance but do not consume churn + else: + increase_balance(state, deposit.index, deposit.amount) + # Validator is not exiting, attempt to process deposit + else: + # Deposit does not fit in the churn, no more deposit processing in this epoch. + if processed_amount + deposit.amount > available_for_processing: + break + # Deposit fits in the churn, process it. Increase balance and consume churn. + else: + increase_balance(state, deposit.index, deposit.amount) + processed_amount += deposit.amount + # Regardless of how the deposit was handled, we move on in the queue. next_deposit_index += 1 state.pending_balance_deposits = state.pending_balance_deposits[next_deposit_index:] @@ -812,6 +827,8 @@ def process_pending_balance_deposits(state: BeaconState) -> None: state.deposit_balance_to_consume = Gwei(0) else: state.deposit_balance_to_consume = available_for_processing - processed_amount + + state.pending_balance_deposits += deposits_to_postpone ``` #### New `process_pending_consolidations` diff --git a/tests/core/pyspec/eth2spec/test/electra/epoch_processing/test_process_pending_balance_deposits.py b/tests/core/pyspec/eth2spec/test/electra/epoch_processing/test_process_pending_balance_deposits.py index 981851bc8..e3f852691 100644 --- a/tests/core/pyspec/eth2spec/test/electra/epoch_processing/test_process_pending_balance_deposits.py +++ b/tests/core/pyspec/eth2spec/test/electra/epoch_processing/test_process_pending_balance_deposits.py @@ -132,3 +132,133 @@ def test_multiple_pending_deposits_above_churn(spec, state): assert state.pending_balance_deposits == [ spec.PendingBalanceDeposit(index=2, amount=amount) ] + + +@with_electra_and_later +@spec_state_test +def test_skipped_deposit_exiting_validator(spec, state): + index = 0 + amount = spec.MIN_ACTIVATION_BALANCE + state.pending_balance_deposits.append(spec.PendingBalanceDeposit(index=index, amount=amount)) + pre_pending_balance_deposits = state.pending_balance_deposits.copy() + pre_balance = state.balances[index] + # Initiate the validator's exit + spec.initiate_validator_exit(state, index) + yield from run_epoch_processing_with(spec, state, 'process_pending_balance_deposits') + # Deposit is skipped because validator is exiting + assert state.balances[index] == pre_balance + # All deposits either processed or postponed, no leftover deposit balance to consume + assert state.deposit_balance_to_consume == 0 + # The deposit is still in the queue + assert state.pending_balance_deposits == pre_pending_balance_deposits + + +@with_electra_and_later +@spec_state_test +def test_multiple_skipped_deposits_exiting_validators(spec, state): + amount = spec.EFFECTIVE_BALANCE_INCREMENT + for i in [0, 1, 2]: + # Append pending deposit for validator i + state.pending_balance_deposits.append(spec.PendingBalanceDeposit(index=i, amount=amount)) + + # Initiate the exit of validator i + spec.initiate_validator_exit(state, i) + pre_pending_balance_deposits = state.pending_balance_deposits.copy() + pre_balances = state.balances.copy() + yield from run_epoch_processing_with(spec, state, 'process_pending_balance_deposits') + # All deposits are postponed, no balance changes + assert state.balances == pre_balances + # All deposits are postponed, no leftover deposit balance to consume + assert state.deposit_balance_to_consume == 0 + # All deposits still in the queue, in the same order + assert state.pending_balance_deposits == pre_pending_balance_deposits + + +@with_electra_and_later +@spec_state_test +def test_multiple_pending_one_skipped(spec, state): + amount = spec.EFFECTIVE_BALANCE_INCREMENT + for i in [0, 1, 2]: + state.pending_balance_deposits.append(spec.PendingBalanceDeposit(index=i, amount=amount)) + pre_balances = state.balances.copy() + # Initiate the second validator's exit + spec.initiate_validator_exit(state, 1) + yield from run_epoch_processing_with(spec, state, 'process_pending_balance_deposits') + # First and last deposit are processed, second is not because of exiting + for i in [0, 2]: + assert state.balances[i] == pre_balances[i] + amount + assert state.balances[1] == pre_balances[1] + # All deposits either processed or postponed, no leftover deposit balance to consume + assert state.deposit_balance_to_consume == 0 + # second deposit is still in the queue + assert state.pending_balance_deposits == [spec.PendingBalanceDeposit(index=1, amount=amount)] + + +@with_electra_and_later +@spec_state_test +def test_mixture_of_skipped_and_above_churn(spec, state): + amount01 = spec.EFFECTIVE_BALANCE_INCREMENT + amount2 = spec.MAX_EFFECTIVE_BALANCE_ELECTRA + # First two validators have small deposit, third validators a large one + for i in [0, 1]: + state.pending_balance_deposits.append(spec.PendingBalanceDeposit(index=i, amount=amount01)) + state.pending_balance_deposits.append(spec.PendingBalanceDeposit(index=2, amount=amount2)) + pre_balances = state.balances.copy() + # Initiate the second validator's exit + spec.initiate_validator_exit(state, 1) + yield from run_epoch_processing_with(spec, state, 'process_pending_balance_deposits') + # First deposit is processed + assert state.balances[0] == pre_balances[0] + amount01 + # Second deposit is postponed, third is above churn + for i in [1, 2]: + assert state.balances[i] == pre_balances[i] + # First deposit consumes some deposit balance + # Deposit balance to consume is not reset because third deposit is not processed + assert state.deposit_balance_to_consume == spec.get_activation_exit_churn_limit(state) - amount01 + # second and third deposit still in the queue, but second is appended at the end + assert state.pending_balance_deposits == [spec.PendingBalanceDeposit(index=2, amount=amount2), + spec.PendingBalanceDeposit(index=1, amount=amount01)] + + +@with_electra_and_later +@spec_state_test +def test_processing_deposit_of_withdrawable_validator(spec, state): + index = 0 + amount = spec.MIN_ACTIVATION_BALANCE + state.pending_balance_deposits.append(spec.PendingBalanceDeposit(index=index, amount=amount)) + pre_balance = state.balances[index] + # Initiate the validator's exit + spec.initiate_validator_exit(state, index) + # Set epoch to withdrawable epoch + 1 to allow processing of the deposit + state.slot = spec.SLOTS_PER_EPOCH * (state.validators[index].withdrawable_epoch + 1) + yield from run_epoch_processing_with(spec, state, 'process_pending_balance_deposits') + # Deposit is correctly processed + assert state.balances[index] == pre_balance + amount + # No leftover deposit balance to consume when there are no deposits left to process + assert state.deposit_balance_to_consume == 0 + assert state.pending_balance_deposits == [] + + +@with_electra_and_later +@spec_state_test +def test_processing_deposit_of_withdrawable_validator_does_not_get_churned(spec, state): + amount = spec.MAX_EFFECTIVE_BALANCE_ELECTRA + for i in [0, 1]: + state.pending_balance_deposits.append(spec.PendingBalanceDeposit(index=i, amount=amount)) + pre_balances = state.balances.copy() + # Initiate the first validator's exit + spec.initiate_validator_exit(state, 0) + # Set epoch to withdrawable epoch + 1 to allow processing of the deposit + state.slot = spec.SLOTS_PER_EPOCH * (state.validators[0].withdrawable_epoch + 1) + # Don't use run_epoch_processing_with to avoid penalties being applied + yield 'pre', state + spec.process_pending_balance_deposits(state) + yield 'post', state + # First deposit is processed though above churn limit, because validator is withdrawable + assert state.balances[0] == pre_balances[0] + amount + # Second deposit is not processed because above churn + assert state.balances[1] == pre_balances[1] + # Second deposit is not processed, so there's leftover deposit balance to consume. + # First deposit does not consume any. + assert state.deposit_balance_to_consume == spec.get_activation_exit_churn_limit(state) + assert state.pending_balance_deposits == [spec.PendingBalanceDeposit(index=1, amount=amount)]