Skip to content

Commit 5efd6e1

Browse files
authored
Merge pull request #3776 from fradamt/top-ups-exited
Handle top-ups to exiting/exited validators
2 parents 0de1252 + 4223bc0 commit 5efd6e1

File tree

2 files changed

+183
-22
lines changed

2 files changed

+183
-22
lines changed

specs/electra/beacon-chain.md

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -811,12 +811,27 @@ def process_pending_balance_deposits(state: BeaconState) -> None:
811811
available_for_processing = state.deposit_balance_to_consume + get_activation_exit_churn_limit(state)
812812
processed_amount = 0
813813
next_deposit_index = 0
814+
deposits_to_postpone = []
814815

815816
for deposit in state.pending_balance_deposits:
816-
if processed_amount + deposit.amount > available_for_processing:
817-
break
818-
increase_balance(state, deposit.index, deposit.amount)
819-
processed_amount += deposit.amount
817+
validator = state.validators[deposit.index]
818+
# Validator is exiting, postpone the deposit until after withdrawable epoch
819+
if validator.exit_epoch < FAR_FUTURE_EPOCH:
820+
if get_current_epoch(state) <= validator.withdrawable_epoch:
821+
deposits_to_postpone.append(deposit)
822+
# Deposited balance will never become active. Increase balance but do not consume churn
823+
else:
824+
increase_balance(state, deposit.index, deposit.amount)
825+
# Validator is not exiting, attempt to process deposit
826+
else:
827+
# Deposit does not fit in the churn, no more deposit processing in this epoch.
828+
if processed_amount + deposit.amount > available_for_processing:
829+
break
830+
# Deposit fits in the churn, process it. Increase balance and consume churn.
831+
else:
832+
increase_balance(state, deposit.index, deposit.amount)
833+
processed_amount += deposit.amount
834+
# Regardless of how the deposit was handled, we move on in the queue.
820835
next_deposit_index += 1
821836

822837
state.pending_balance_deposits = state.pending_balance_deposits[next_deposit_index:]
@@ -825,6 +840,8 @@ def process_pending_balance_deposits(state: BeaconState) -> None:
825840
state.deposit_balance_to_consume = Gwei(0)
826841
else:
827842
state.deposit_balance_to_consume = available_for_processing - processed_amount
843+
844+
state.pending_balance_deposits += deposits_to_postpone
828845
```
829846

830847
#### New `process_pending_consolidations`

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

Lines changed: 162 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
)
66

77

8+
def run_process_pending_balance_deposits(spec, state):
9+
yield from run_epoch_processing_with(spec, state, 'process_pending_balance_deposits')
10+
11+
812
@with_electra_and_later
913
@spec_state_test
1014
def test_pending_deposit_min_activation_balance(spec, state):
@@ -14,9 +18,9 @@ def test_pending_deposit_min_activation_balance(spec, state):
1418
spec.PendingBalanceDeposit(index=index, amount=amount)
1519
)
1620
pre_balance = state.balances[index]
17-
yield from run_epoch_processing_with(
18-
spec, state, "process_pending_balance_deposits"
19-
)
21+
22+
yield from run_process_pending_balance_deposits(spec, state)
23+
2024
assert state.balances[index] == pre_balance + amount
2125
# No leftover deposit balance to consume when there are no deposits left to process
2226
assert state.deposit_balance_to_consume == 0
@@ -32,9 +36,9 @@ def test_pending_deposit_balance_equal_churn(spec, state):
3236
spec.PendingBalanceDeposit(index=index, amount=amount)
3337
)
3438
pre_balance = state.balances[index]
35-
yield from run_epoch_processing_with(
36-
spec, state, "process_pending_balance_deposits"
37-
)
39+
40+
yield from run_process_pending_balance_deposits(spec, state)
41+
3842
assert state.balances[index] == pre_balance + amount
3943
assert state.deposit_balance_to_consume == 0
4044
assert state.pending_balance_deposits == []
@@ -49,9 +53,9 @@ def test_pending_deposit_balance_above_churn(spec, state):
4953
spec.PendingBalanceDeposit(index=index, amount=amount)
5054
)
5155
pre_balance = state.balances[index]
52-
yield from run_epoch_processing_with(
53-
spec, state, "process_pending_balance_deposits"
54-
)
56+
57+
yield from run_process_pending_balance_deposits(spec, state)
58+
5559
# deposit was above churn, balance hasn't changed
5660
assert state.balances[index] == pre_balance
5761
# deposit balance to consume is the full churn limit
@@ -74,9 +78,9 @@ def test_pending_deposit_preexisting_churn(spec, state):
7478
spec.PendingBalanceDeposit(index=index, amount=amount)
7579
)
7680
pre_balance = state.balances[index]
77-
yield from run_epoch_processing_with(
78-
spec, state, "process_pending_balance_deposits"
79-
)
81+
82+
yield from run_process_pending_balance_deposits(spec, state)
83+
8084
# balance was deposited correctly
8185
assert state.balances[index] == pre_balance + amount
8286
# No leftover deposit balance to consume when there are no deposits left to process
@@ -96,9 +100,9 @@ def test_multiple_pending_deposits_below_churn(spec, state):
96100
spec.PendingBalanceDeposit(index=1, amount=amount)
97101
)
98102
pre_balances = state.balances.copy()
99-
yield from run_epoch_processing_with(
100-
spec, state, "process_pending_balance_deposits"
101-
)
103+
104+
yield from run_process_pending_balance_deposits(spec, state)
105+
102106
for i in [0, 1]:
103107
assert state.balances[i] == pre_balances[i] + amount
104108
# No leftover deposit balance to consume when there are no deposits left to process
@@ -116,9 +120,9 @@ def test_multiple_pending_deposits_above_churn(spec, state):
116120
spec.PendingBalanceDeposit(index=i, amount=amount)
117121
)
118122
pre_balances = state.balances.copy()
119-
yield from run_epoch_processing_with(
120-
spec, state, "process_pending_balance_deposits"
121-
)
123+
124+
yield from run_process_pending_balance_deposits(spec, state)
125+
122126
# First two deposits are processed, third is not because above churn
123127
for i in [0, 1]:
124128
assert state.balances[i] == pre_balances[i] + amount
@@ -132,3 +136,143 @@ def test_multiple_pending_deposits_above_churn(spec, state):
132136
assert state.pending_balance_deposits == [
133137
spec.PendingBalanceDeposit(index=2, amount=amount)
134138
]
139+
140+
141+
@with_electra_and_later
142+
@spec_state_test
143+
def test_skipped_deposit_exiting_validator(spec, state):
144+
index = 0
145+
amount = spec.MIN_ACTIVATION_BALANCE
146+
state.pending_balance_deposits.append(spec.PendingBalanceDeposit(index=index, amount=amount))
147+
pre_pending_balance_deposits = state.pending_balance_deposits.copy()
148+
pre_balance = state.balances[index]
149+
# Initiate the validator's exit
150+
spec.initiate_validator_exit(state, index)
151+
152+
yield from run_process_pending_balance_deposits(spec, state)
153+
154+
# Deposit is skipped because validator is exiting
155+
assert state.balances[index] == pre_balance
156+
# All deposits either processed or postponed, no leftover deposit balance to consume
157+
assert state.deposit_balance_to_consume == 0
158+
# The deposit is still in the queue
159+
assert state.pending_balance_deposits == pre_pending_balance_deposits
160+
161+
162+
@with_electra_and_later
163+
@spec_state_test
164+
def test_multiple_skipped_deposits_exiting_validators(spec, state):
165+
amount = spec.EFFECTIVE_BALANCE_INCREMENT
166+
for i in [0, 1, 2]:
167+
# Append pending deposit for validator i
168+
state.pending_balance_deposits.append(spec.PendingBalanceDeposit(index=i, amount=amount))
169+
170+
# Initiate the exit of validator i
171+
spec.initiate_validator_exit(state, i)
172+
pre_pending_balance_deposits = state.pending_balance_deposits.copy()
173+
pre_balances = state.balances.copy()
174+
175+
yield from run_process_pending_balance_deposits(spec, state)
176+
177+
# All deposits are postponed, no balance changes
178+
assert state.balances == pre_balances
179+
# All deposits are postponed, no leftover deposit balance to consume
180+
assert state.deposit_balance_to_consume == 0
181+
# All deposits still in the queue, in the same order
182+
assert state.pending_balance_deposits == pre_pending_balance_deposits
183+
184+
185+
@with_electra_and_later
186+
@spec_state_test
187+
def test_multiple_pending_one_skipped(spec, state):
188+
amount = spec.EFFECTIVE_BALANCE_INCREMENT
189+
for i in [0, 1, 2]:
190+
state.pending_balance_deposits.append(spec.PendingBalanceDeposit(index=i, amount=amount))
191+
pre_balances = state.balances.copy()
192+
# Initiate the second validator's exit
193+
spec.initiate_validator_exit(state, 1)
194+
195+
yield from run_process_pending_balance_deposits(spec, state)
196+
197+
# First and last deposit are processed, second is not because of exiting
198+
for i in [0, 2]:
199+
assert state.balances[i] == pre_balances[i] + amount
200+
assert state.balances[1] == pre_balances[1]
201+
# All deposits either processed or postponed, no leftover deposit balance to consume
202+
assert state.deposit_balance_to_consume == 0
203+
# second deposit is still in the queue
204+
assert state.pending_balance_deposits == [spec.PendingBalanceDeposit(index=1, amount=amount)]
205+
206+
207+
@with_electra_and_later
208+
@spec_state_test
209+
def test_mixture_of_skipped_and_above_churn(spec, state):
210+
amount01 = spec.EFFECTIVE_BALANCE_INCREMENT
211+
amount2 = spec.MAX_EFFECTIVE_BALANCE_ELECTRA
212+
# First two validators have small deposit, third validators a large one
213+
for i in [0, 1]:
214+
state.pending_balance_deposits.append(spec.PendingBalanceDeposit(index=i, amount=amount01))
215+
state.pending_balance_deposits.append(spec.PendingBalanceDeposit(index=2, amount=amount2))
216+
pre_balances = state.balances.copy()
217+
# Initiate the second validator's exit
218+
spec.initiate_validator_exit(state, 1)
219+
220+
yield from run_process_pending_balance_deposits(spec, state)
221+
222+
# First deposit is processed
223+
assert state.balances[0] == pre_balances[0] + amount01
224+
# Second deposit is postponed, third is above churn
225+
for i in [1, 2]:
226+
assert state.balances[i] == pre_balances[i]
227+
# First deposit consumes some deposit balance
228+
# Deposit balance to consume is not reset because third deposit is not processed
229+
assert state.deposit_balance_to_consume == spec.get_activation_exit_churn_limit(state) - amount01
230+
# second and third deposit still in the queue, but second is appended at the end
231+
assert state.pending_balance_deposits == [spec.PendingBalanceDeposit(index=2, amount=amount2),
232+
spec.PendingBalanceDeposit(index=1, amount=amount01)]
233+
234+
235+
@with_electra_and_later
236+
@spec_state_test
237+
def test_processing_deposit_of_withdrawable_validator(spec, state):
238+
index = 0
239+
amount = spec.MIN_ACTIVATION_BALANCE
240+
state.pending_balance_deposits.append(spec.PendingBalanceDeposit(index=index, amount=amount))
241+
pre_balance = state.balances[index]
242+
# Initiate the validator's exit
243+
spec.initiate_validator_exit(state, index)
244+
# Set epoch to withdrawable epoch + 1 to allow processing of the deposit
245+
state.slot = spec.SLOTS_PER_EPOCH * (state.validators[index].withdrawable_epoch + 1)
246+
247+
yield from run_process_pending_balance_deposits(spec, state)
248+
249+
# Deposit is correctly processed
250+
assert state.balances[index] == pre_balance + amount
251+
# No leftover deposit balance to consume when there are no deposits left to process
252+
assert state.deposit_balance_to_consume == 0
253+
assert state.pending_balance_deposits == []
254+
255+
256+
@with_electra_and_later
257+
@spec_state_test
258+
def test_processing_deposit_of_withdrawable_validator_does_not_get_churned(spec, state):
259+
amount = spec.MAX_EFFECTIVE_BALANCE_ELECTRA
260+
for i in [0, 1]:
261+
state.pending_balance_deposits.append(spec.PendingBalanceDeposit(index=i, amount=amount))
262+
pre_balances = state.balances.copy()
263+
# Initiate the first validator's exit
264+
spec.initiate_validator_exit(state, 0)
265+
# Set epoch to withdrawable epoch + 1 to allow processing of the deposit
266+
state.slot = spec.SLOTS_PER_EPOCH * (state.validators[0].withdrawable_epoch + 1)
267+
# Don't use run_epoch_processing_with to avoid penalties being applied
268+
yield 'pre', state
269+
spec.process_pending_balance_deposits(state)
270+
yield 'post', state
271+
# First deposit is processed though above churn limit, because validator is withdrawable
272+
assert state.balances[0] == pre_balances[0] + amount
273+
# Second deposit is not processed because above churn
274+
assert state.balances[1] == pre_balances[1]
275+
# Second deposit is not processed, so there's leftover deposit balance to consume.
276+
# First deposit does not consume any.
277+
assert state.deposit_balance_to_consume == spec.get_activation_exit_churn_limit(state)
278+
assert state.pending_balance_deposits == [spec.PendingBalanceDeposit(index=1, amount=amount)]

0 commit comments

Comments
 (0)