Skip to content

Commit 69ee35d

Browse files
committed
handle top-ups to exiting/exited validators
1 parent f968d62 commit 69ee35d

File tree

2 files changed

+151
-4
lines changed

2 files changed

+151
-4
lines changed

specs/electra/beacon-chain.md

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -798,12 +798,27 @@ def process_pending_balance_deposits(state: BeaconState) -> None:
798798
available_for_processing = state.deposit_balance_to_consume + get_activation_exit_churn_limit(state)
799799
processed_amount = 0
800800
next_deposit_index = 0
801+
deposits_to_postpone = []
801802

802803
for deposit in state.pending_balance_deposits:
803-
if processed_amount + deposit.amount > available_for_processing:
804-
break
805-
increase_balance(state, deposit.index, deposit.amount)
806-
processed_amount += deposit.amount
804+
validator = state.validators[deposit.index]
805+
# Validator is exiting, postpone the deposit until after withdrawable epoch
806+
if validator.exit_epoch < FAR_FUTURE_EPOCH:
807+
if get_current_epoch(state) <= validator.withdrawable_epoch:
808+
deposits_to_postpone.append(deposit)
809+
# Deposited balance will never become active. Increase balance but do not consume churn
810+
else:
811+
increase_balance(state, deposit.index, deposit.amount)
812+
# Validator is not exiting, attempt to process deposit
813+
else:
814+
# Deposit does not fit in the churn, no more deposit processing in this epoch.
815+
if processed_amount + deposit.amount > available_for_processing:
816+
break
817+
# Deposit fits in the churn, process it. Increase balance and consume churn.
818+
else:
819+
increase_balance(state, deposit.index, deposit.amount)
820+
processed_amount += deposit.amount
821+
# Regardless of how the deposit was handled, we move on in the queue.
807822
next_deposit_index += 1
808823

809824
state.pending_balance_deposits = state.pending_balance_deposits[next_deposit_index:]
@@ -812,6 +827,8 @@ def process_pending_balance_deposits(state: BeaconState) -> None:
812827
state.deposit_balance_to_consume = Gwei(0)
813828
else:
814829
state.deposit_balance_to_consume = available_for_processing - processed_amount
830+
831+
state.pending_balance_deposits += deposits_to_postpone
815832
```
816833

817834
#### New `process_pending_consolidations`

tests/core/pyspec/eth2spec/test/electra/epoch_processing/test_process_pending_balance_deposits.py

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,3 +132,133 @@ def test_multiple_pending_deposits_above_churn(spec, state):
132132
assert state.pending_balance_deposits == [
133133
spec.PendingBalanceDeposit(index=2, amount=amount)
134134
]
135+
136+
137+
@with_electra_and_later
138+
@spec_state_test
139+
def test_skipped_deposit_exiting_validator(spec, state):
140+
index = 0
141+
amount = spec.MIN_ACTIVATION_BALANCE
142+
state.pending_balance_deposits.append(spec.PendingBalanceDeposit(index=index, amount=amount))
143+
pre_pending_balance_deposits = state.pending_balance_deposits.copy()
144+
pre_balance = state.balances[index]
145+
# Initiate the validator's exit
146+
spec.initiate_validator_exit(state, index)
147+
yield from run_epoch_processing_with(spec, state, 'process_pending_balance_deposits')
148+
# Deposit is skipped because validator is exiting
149+
assert state.balances[index] == pre_balance
150+
# All deposits either processed or postponed, no leftover deposit balance to consume
151+
assert state.deposit_balance_to_consume == 0
152+
# The deposit is still in the queue
153+
assert state.pending_balance_deposits == pre_pending_balance_deposits
154+
155+
156+
@with_electra_and_later
157+
@spec_state_test
158+
def test_multiple_skipped_deposits_exiting_validators(spec, state):
159+
amount = spec.EFFECTIVE_BALANCE_INCREMENT
160+
for i in [0, 1, 2]:
161+
# Append pending deposit for validator i
162+
state.pending_balance_deposits.append(spec.PendingBalanceDeposit(index=i, amount=amount))
163+
164+
# Initiate the exit of validator i
165+
spec.initiate_validator_exit(state, i)
166+
pre_pending_balance_deposits = state.pending_balance_deposits.copy()
167+
pre_balances = state.balances.copy()
168+
yield from run_epoch_processing_with(spec, state, 'process_pending_balance_deposits')
169+
# All deposits are postponed, no balance changes
170+
assert state.balances == pre_balances
171+
# All deposits are postponed, no leftover deposit balance to consume
172+
assert state.deposit_balance_to_consume == 0
173+
# All deposits still in the queue, in the same order
174+
assert state.pending_balance_deposits == pre_pending_balance_deposits
175+
176+
177+
@with_electra_and_later
178+
@spec_state_test
179+
def test_multiple_pending_one_skipped(spec, state):
180+
amount = spec.EFFECTIVE_BALANCE_INCREMENT
181+
for i in [0, 1, 2]:
182+
state.pending_balance_deposits.append(spec.PendingBalanceDeposit(index=i, amount=amount))
183+
pre_balances = state.balances.copy()
184+
# Initiate the second validator's exit
185+
spec.initiate_validator_exit(state, 1)
186+
yield from run_epoch_processing_with(spec, state, 'process_pending_balance_deposits')
187+
# First and last deposit are processed, second is not because of exiting
188+
for i in [0, 2]:
189+
assert state.balances[i] == pre_balances[i] + amount
190+
assert state.balances[1] == pre_balances[1]
191+
# All deposits either processed or postponed, no leftover deposit balance to consume
192+
assert state.deposit_balance_to_consume == 0
193+
# second deposit is still in the queue
194+
assert state.pending_balance_deposits == [spec.PendingBalanceDeposit(index=1, amount=amount)]
195+
196+
197+
@with_electra_and_later
198+
@spec_state_test
199+
def test_mixture_of_skipped_and_above_churn(spec, state):
200+
amount01 = spec.EFFECTIVE_BALANCE_INCREMENT
201+
amount2 = spec.MAX_EFFECTIVE_BALANCE_ELECTRA
202+
# First two validators have small deposit, third validators a large one
203+
for i in [0, 1]:
204+
state.pending_balance_deposits.append(spec.PendingBalanceDeposit(index=i, amount=amount01))
205+
state.pending_balance_deposits.append(spec.PendingBalanceDeposit(index=2, amount=amount2))
206+
pre_balances = state.balances.copy()
207+
# Initiate the second validator's exit
208+
spec.initiate_validator_exit(state, 1)
209+
yield from run_epoch_processing_with(spec, state, 'process_pending_balance_deposits')
210+
# First deposit is processed
211+
assert state.balances[0] == pre_balances[0] + amount01
212+
# Second deposit is postponed, third is above churn
213+
for i in [1, 2]:
214+
assert state.balances[i] == pre_balances[i]
215+
# First deposit consumes some deposit balance
216+
# Deposit balance to consume is not reset because third deposit is not processed
217+
assert state.deposit_balance_to_consume == spec.get_activation_exit_churn_limit(state) - amount01
218+
# second and third deposit still in the queue, but second is appended at the end
219+
assert state.pending_balance_deposits == [spec.PendingBalanceDeposit(index=2, amount=amount2),
220+
spec.PendingBalanceDeposit(index=1, amount=amount01)]
221+
222+
223+
@with_electra_and_later
224+
@spec_state_test
225+
def test_processing_deposit_of_withdrawable_validator(spec, state):
226+
index = 0
227+
amount = spec.MIN_ACTIVATION_BALANCE
228+
state.pending_balance_deposits.append(spec.PendingBalanceDeposit(index=index, amount=amount))
229+
pre_balance = state.balances[index]
230+
# Initiate the validator's exit
231+
spec.initiate_validator_exit(state, index)
232+
# Set epoch to withdrawable epoch + 1 to allow processing of the deposit
233+
state.slot = spec.SLOTS_PER_EPOCH * (state.validators[index].withdrawable_epoch + 1)
234+
yield from run_epoch_processing_with(spec, state, 'process_pending_balance_deposits')
235+
# Deposit is correctly processed
236+
assert state.balances[index] == pre_balance + amount
237+
# No leftover deposit balance to consume when there are no deposits left to process
238+
assert state.deposit_balance_to_consume == 0
239+
assert state.pending_balance_deposits == []
240+
241+
242+
@with_electra_and_later
243+
@spec_state_test
244+
def test_processing_deposit_of_withdrawable_validator_does_not_get_churned(spec, state):
245+
amount = spec.MAX_EFFECTIVE_BALANCE_ELECTRA
246+
for i in [0, 1]:
247+
state.pending_balance_deposits.append(spec.PendingBalanceDeposit(index=i, amount=amount))
248+
pre_balances = state.balances.copy()
249+
# Initiate the first validator's exit
250+
spec.initiate_validator_exit(state, 0)
251+
# Set epoch to withdrawable epoch + 1 to allow processing of the deposit
252+
state.slot = spec.SLOTS_PER_EPOCH * (state.validators[0].withdrawable_epoch + 1)
253+
# Don't use run_epoch_processing_with to avoid penalties being applied
254+
yield 'pre', state
255+
spec.process_pending_balance_deposits(state)
256+
yield 'post', state
257+
# First deposit is processed though above churn limit, because validator is withdrawable
258+
assert state.balances[0] == pre_balances[0] + amount
259+
# Second deposit is not processed because above churn
260+
assert state.balances[1] == pre_balances[1]
261+
# Second deposit is not processed, so there's leftover deposit balance to consume.
262+
# First deposit does not consume any.
263+
assert state.deposit_balance_to_consume == spec.get_activation_exit_churn_limit(state)
264+
assert state.pending_balance_deposits == [spec.PendingBalanceDeposit(index=1, amount=amount)]

0 commit comments

Comments
 (0)