Skip to content

Commit f4e3908

Browse files
authored
Merge pull request #3868 from mkalinin/fix-consolidation
Fix off-by-one in process_pending_consolidations
2 parents bd396a6 + 024ee04 commit f4e3908

File tree

4 files changed

+182
-3
lines changed

4 files changed

+182
-3
lines changed

specs/electra/beacon-chain.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -854,6 +854,7 @@ def process_registry_updates(state: BeaconState) -> None:
854854

855855
```python
856856
def process_pending_balance_deposits(state: BeaconState) -> None:
857+
next_epoch = Epoch(get_current_epoch(state) + 1)
857858
available_for_processing = state.deposit_balance_to_consume + get_activation_exit_churn_limit(state)
858859
processed_amount = 0
859860
next_deposit_index = 0
@@ -863,7 +864,7 @@ def process_pending_balance_deposits(state: BeaconState) -> None:
863864
validator = state.validators[deposit.index]
864865
# Validator is exiting, postpone the deposit until after withdrawable epoch
865866
if validator.exit_epoch < FAR_FUTURE_EPOCH:
866-
if get_current_epoch(state) <= validator.withdrawable_epoch:
867+
if next_epoch <= validator.withdrawable_epoch:
867868
deposits_to_postpone.append(deposit)
868869
# Deposited balance will never become active. Increase balance but do not consume churn
869870
else:
@@ -894,13 +895,14 @@ def process_pending_balance_deposits(state: BeaconState) -> None:
894895

895896
```python
896897
def process_pending_consolidations(state: BeaconState) -> None:
898+
next_epoch = Epoch(get_current_epoch(state) + 1)
897899
next_pending_consolidation = 0
898900
for pending_consolidation in state.pending_consolidations:
899901
source_validator = state.validators[pending_consolidation.source_index]
900902
if source_validator.slashed:
901903
next_pending_consolidation += 1
902904
continue
903-
if source_validator.withdrawable_epoch > get_current_epoch(state):
905+
if source_validator.withdrawable_epoch > next_epoch:
904906
break
905907

906908
# Churn any target excess active balance of target and raise its max

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

Lines changed: 162 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
1-
from eth2spec.test.helpers.epoch_processing import run_epoch_processing_with
1+
from eth2spec.test.helpers.epoch_processing import (
2+
run_epoch_processing_with,
3+
compute_state_by_epoch_processing_to,
4+
)
25
from eth2spec.test.context import (
36
spec_state_test,
47
with_electra_and_later,
58
)
9+
from eth2spec.test.helpers.state import (
10+
next_epoch_with_full_participation,
11+
)
612

713
# ***********************
814
# * CONSOLIDATION TESTS *
@@ -185,3 +191,158 @@ def test_all_consolidation_cases_together(spec, state):
185191
assert state.balances[target_index[i]] == pre_balances[target_index[i]]
186192
# First consolidation is processed, second is skipped, last two are left in the queue
187193
state.pending_consolidations = pre_pending_consolidations[2:]
194+
195+
196+
@with_electra_and_later
197+
@spec_state_test
198+
def test_pending_consolidation_future_epoch(spec, state):
199+
current_epoch = spec.get_current_epoch(state)
200+
source_index = spec.get_active_validator_indices(state, current_epoch)[0]
201+
target_index = spec.get_active_validator_indices(state, current_epoch)[1]
202+
# initiate source exit
203+
spec.initiate_validator_exit(state, source_index)
204+
# set withdrawable_epoch to exit_epoch + 1
205+
state.validators[source_index].withdrawable_epoch = state.validators[source_index].exit_epoch + spec.Epoch(1)
206+
# append pending consolidation
207+
state.pending_consolidations.append(
208+
spec.PendingConsolidation(source_index=source_index, target_index=target_index)
209+
)
210+
# Set the target withdrawal credential to eth1
211+
eth1_withdrawal_credential = (
212+
spec.ETH1_ADDRESS_WITHDRAWAL_PREFIX + b"\x00" * 11 + b"\x11" * 20
213+
)
214+
state.validators[target_index].withdrawal_credentials = eth1_withdrawal_credential
215+
216+
# Advance to withdrawable_epoch - 1 with full participation
217+
target_epoch = state.validators[source_index].withdrawable_epoch - spec.Epoch(1)
218+
while spec.get_current_epoch(state) < target_epoch:
219+
next_epoch_with_full_participation(spec, state)
220+
221+
# Obtain state before the call to process_pending_consolidations
222+
state_before_consolidation = compute_state_by_epoch_processing_to(spec, state, "process_pending_consolidations")
223+
224+
yield from run_epoch_processing_with(spec, state, "process_pending_consolidations")
225+
226+
# Pending consolidation was successfully processed
227+
expected_source_balance = state_before_consolidation.balances[source_index] - spec.MIN_ACTIVATION_BALANCE
228+
assert (
229+
state.validators[target_index].withdrawal_credentials[:1]
230+
== spec.COMPOUNDING_WITHDRAWAL_PREFIX
231+
)
232+
assert state.balances[target_index] == 2 * spec.MIN_ACTIVATION_BALANCE
233+
assert state.balances[source_index] == expected_source_balance
234+
assert state.pending_consolidations == []
235+
236+
# Pending balance deposit to the target is created as part of `switch_to_compounding_validator`.
237+
# The excess balance to queue are the rewards accumulated over the previous epoch transitions.
238+
expected_pending_balance = state_before_consolidation.balances[target_index] - spec.MIN_ACTIVATION_BALANCE
239+
assert len(state.pending_balance_deposits) > 0
240+
pending_balance_deposit = state.pending_balance_deposits[len(state.pending_balance_deposits) - 1]
241+
assert pending_balance_deposit.index == target_index
242+
assert pending_balance_deposit.amount == expected_pending_balance
243+
244+
245+
@with_electra_and_later
246+
@spec_state_test
247+
def test_pending_consolidation_compounding_creds(spec, state):
248+
current_epoch = spec.get_current_epoch(state)
249+
source_index = spec.get_active_validator_indices(state, current_epoch)[0]
250+
target_index = spec.get_active_validator_indices(state, current_epoch)[1]
251+
# initiate source exit
252+
spec.initiate_validator_exit(state, source_index)
253+
# set withdrawable_epoch to exit_epoch + 1
254+
state.validators[source_index].withdrawable_epoch = state.validators[source_index].exit_epoch + spec.Epoch(1)
255+
# append pending consolidation
256+
state.pending_consolidations.append(
257+
spec.PendingConsolidation(source_index=source_index, target_index=target_index)
258+
)
259+
# Set the source and the target withdrawal credential to compounding
260+
state.validators[source_index].withdrawal_credentials = (
261+
spec.COMPOUNDING_WITHDRAWAL_PREFIX + b"\x00" * 11 + b"\x11" * 20
262+
)
263+
state.validators[target_index].withdrawal_credentials = (
264+
spec.COMPOUNDING_WITHDRAWAL_PREFIX + b"\x00" * 11 + b"\x12" * 20
265+
)
266+
267+
# Advance to withdrawable_epoch - 1 with full participation
268+
target_epoch = state.validators[source_index].withdrawable_epoch - spec.Epoch(1)
269+
while spec.get_current_epoch(state) < target_epoch:
270+
next_epoch_with_full_participation(spec, state)
271+
272+
# Obtain state before the call to process_pending_consolidations
273+
state_before_consolidation = compute_state_by_epoch_processing_to(spec, state, "process_pending_consolidations")
274+
275+
yield from run_epoch_processing_with(spec, state, "process_pending_consolidations")
276+
277+
# Pending consolidation was successfully processed
278+
expected_target_balance = (
279+
state_before_consolidation.balances[source_index] + state_before_consolidation.balances[target_index]
280+
)
281+
assert (
282+
state.validators[target_index].withdrawal_credentials[:1]
283+
== spec.COMPOUNDING_WITHDRAWAL_PREFIX
284+
)
285+
assert state.balances[target_index] == expected_target_balance
286+
# All source balance is active and moved to the target,
287+
# because the source validator has compounding credentials
288+
assert state.balances[source_index] == 0
289+
assert state.pending_consolidations == []
290+
291+
# Pending balance deposit to the target is not created,
292+
# because the target already has compounding credentials
293+
assert len(state.pending_balance_deposits) == 0
294+
295+
296+
@with_electra_and_later
297+
@spec_state_test
298+
def test_pending_consolidation_with_pending_deposit(spec, state):
299+
current_epoch = spec.get_current_epoch(state)
300+
source_index = spec.get_active_validator_indices(state, current_epoch)[0]
301+
target_index = spec.get_active_validator_indices(state, current_epoch)[1]
302+
# initiate source exit
303+
spec.initiate_validator_exit(state, source_index)
304+
# set withdrawable_epoch to exit_epoch + 1
305+
state.validators[source_index].withdrawable_epoch = state.validators[source_index].exit_epoch + spec.Epoch(1)
306+
# append pending consolidation
307+
state.pending_consolidations.append(
308+
spec.PendingConsolidation(source_index=source_index, target_index=target_index)
309+
)
310+
# append pending deposit
311+
state.pending_balance_deposits.append(
312+
spec.PendingBalanceDeposit(index=source_index, amount=spec.MIN_ACTIVATION_BALANCE)
313+
)
314+
# Set the source and the target withdrawal credential to compounding
315+
state.validators[source_index].withdrawal_credentials = (
316+
spec.COMPOUNDING_WITHDRAWAL_PREFIX + b"\x00" * 11 + b"\x11" * 20
317+
)
318+
state.validators[target_index].withdrawal_credentials = (
319+
spec.COMPOUNDING_WITHDRAWAL_PREFIX + b"\x00" * 11 + b"\x12" * 20
320+
)
321+
322+
# Advance to withdrawable_epoch - 1 with full participation
323+
target_epoch = state.validators[source_index].withdrawable_epoch - spec.Epoch(1)
324+
while spec.get_current_epoch(state) < target_epoch:
325+
next_epoch_with_full_participation(spec, state)
326+
327+
# Obtain state before the call to process_pending_balance_deposits
328+
state_before_consolidation = compute_state_by_epoch_processing_to(spec, state, "process_pending_balance_deposits")
329+
330+
yield from run_epoch_processing_with(spec, state, "process_pending_consolidations")
331+
332+
# Pending consolidation was successfully processed
333+
expected_target_balance = (
334+
state_before_consolidation.balances[source_index] + state_before_consolidation.balances[target_index]
335+
)
336+
assert (
337+
state.validators[target_index].withdrawal_credentials[:1]
338+
== spec.COMPOUNDING_WITHDRAWAL_PREFIX
339+
)
340+
assert state.balances[target_index] == expected_target_balance
341+
assert state.balances[source_index] == 0
342+
assert state.pending_consolidations == []
343+
344+
# Pending balance deposit to the source was not processed.
345+
# It should only be processed in the next epoch transition
346+
assert len(state.pending_balance_deposits) == 1
347+
assert state.pending_balance_deposits[0] == spec.PendingBalanceDeposit(
348+
index=source_index, amount=spec.MIN_ACTIVATION_BALANCE)

tests/core/pyspec/eth2spec/test/helpers/epoch_processing.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ def get_process_calls(spec):
2222
'charge_confirmed_header_fees', # sharding
2323
'reset_pending_headers', # sharding
2424
'process_eth1_data_reset',
25+
'process_pending_balance_deposits', # electra
26+
'process_pending_consolidations', # electra
2527
'process_effective_balance_updates',
2628
'process_slashings_reset',
2729
'process_randao_mixes_reset',
@@ -72,3 +74,9 @@ def run_epoch_processing_with(spec, state, process_name: str):
7274
yield 'pre', state
7375
getattr(spec, process_name)(state)
7476
yield 'post', state
77+
78+
79+
def compute_state_by_epoch_processing_to(spec, state, process_name: str):
80+
state_copy = state.copy()
81+
run_epoch_processing_to(spec, state_copy, process_name)
82+
return state_copy

tests/core/pyspec/eth2spec/test/helpers/state.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,14 @@ def next_epoch(spec, state):
6060
spec.process_slots(state, slot)
6161

6262

63+
def next_epoch_with_full_participation(spec, state):
64+
"""
65+
Transition to the start slot of the next epoch with full participation
66+
"""
67+
set_full_participation(spec, state)
68+
next_epoch(spec, state)
69+
70+
6371
def next_epoch_via_block(spec, state, insert_state_root=False):
6472
"""
6573
Transition to the start slot of the next epoch via a full block transition

0 commit comments

Comments
 (0)