Merge pull request #3979 from mkalinin/more-withdrawal-tests

eip7251: Bugfix and more withdrawal tests
This commit is contained in:
Justin Traglia 2024-10-30 16:33:44 -05:00 committed by GitHub
commit c060147812
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 387 additions and 41 deletions

View File

@ -1059,7 +1059,7 @@ def get_expected_withdrawals(state: BeaconState) -> Tuple[Sequence[Withdrawal],
withdrawal_index = state.next_withdrawal_index
validator_index = state.next_withdrawal_validator_index
withdrawals: List[Withdrawal] = []
partial_withdrawals_count = 0
processed_partial_withdrawals_count = 0
# [New in Electra:EIP7251] Consume pending partial withdrawals
for withdrawal in state.pending_partial_withdrawals:
@ -1079,13 +1079,16 @@ def get_expected_withdrawals(state: BeaconState) -> Tuple[Sequence[Withdrawal],
))
withdrawal_index += WithdrawalIndex(1)
partial_withdrawals_count += 1
processed_partial_withdrawals_count += 1
# Sweep for remaining.
bound = min(len(state.validators), MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP)
for _ in range(bound):
validator = state.validators[validator_index]
balance = state.balances[validator_index]
# [Modified in Electra:EIP7251]
partially_withdrawn_balance = sum(
withdrawal.amount for withdrawal in withdrawals if withdrawal.validator_index == validator_index)
balance = state.balances[validator_index] - partially_withdrawn_balance
if is_fully_withdrawable_validator(validator, balance, epoch):
withdrawals.append(Withdrawal(
index=withdrawal_index,
@ -1105,7 +1108,7 @@ def get_expected_withdrawals(state: BeaconState) -> Tuple[Sequence[Withdrawal],
if len(withdrawals) == MAX_WITHDRAWALS_PER_PAYLOAD:
break
validator_index = ValidatorIndex((validator_index + 1) % len(state.validators))
return withdrawals, partial_withdrawals_count
return withdrawals, processed_partial_withdrawals_count
```
##### Modified `process_withdrawals`
@ -1114,7 +1117,8 @@ def get_expected_withdrawals(state: BeaconState) -> Tuple[Sequence[Withdrawal],
```python
def process_withdrawals(state: BeaconState, payload: ExecutionPayload) -> None:
expected_withdrawals, partial_withdrawals_count = get_expected_withdrawals(state) # [Modified in Electra:EIP7251]
# [Modified in Electra:EIP7251]
expected_withdrawals, processed_partial_withdrawals_count = get_expected_withdrawals(state)
assert payload.withdrawals == expected_withdrawals
@ -1122,7 +1126,7 @@ def process_withdrawals(state: BeaconState, payload: ExecutionPayload) -> None:
decrease_balance(state, withdrawal.validator_index, withdrawal.amount)
# Update pending partial withdrawals [New in Electra:EIP7251]
state.pending_partial_withdrawals = state.pending_partial_withdrawals[partial_withdrawals_count:]
state.pending_partial_withdrawals = state.pending_partial_withdrawals[processed_partial_withdrawals_count:]
# Update the next withdrawal index if this block contained withdrawals
if len(expected_withdrawals) != 0:

View File

@ -11,7 +11,7 @@ from eth2spec.test.helpers.state import (
next_slot,
)
from eth2spec.test.helpers.withdrawals import (
prepare_expected_withdrawals_compounding,
prepare_expected_withdrawals,
run_withdrawals_processing,
set_compounding_withdrawal_credential_with_balance,
prepare_pending_withdrawal,
@ -23,11 +23,11 @@ from eth2spec.test.helpers.withdrawals import (
def test_success_mixed_fully_and_partial_withdrawable_compounding(spec, state):
num_full_withdrawals = spec.MAX_WITHDRAWALS_PER_PAYLOAD // 2
num_partial_withdrawals = spec.MAX_WITHDRAWALS_PER_PAYLOAD - num_full_withdrawals
fully_withdrawable_indices, partial_withdrawals_indices = prepare_expected_withdrawals_compounding(
fully_withdrawable_indices, partial_withdrawals_indices = prepare_expected_withdrawals(
spec, state,
rng=random.Random(42),
num_full_withdrawals=num_full_withdrawals,
num_partial_withdrawals_sweep=num_partial_withdrawals,
num_full_withdrawals_comp=num_full_withdrawals,
num_partial_withdrawals_comp=num_partial_withdrawals,
)
next_slot(spec, state)
@ -94,14 +94,351 @@ def test_pending_withdrawals_one_skipped_one_effective(spec, state):
index_0 = 3
index_1 = 5
withdrawal_0 = prepare_pending_withdrawal(spec, state, index_0)
withdrawal_1 = prepare_pending_withdrawal(spec, state, index_1)
pending_withdrawal_0 = prepare_pending_withdrawal(spec, state, index_0)
pending_withdrawal_1 = prepare_pending_withdrawal(spec, state, index_1)
# If validator doesn't have an excess balance pending withdrawal is skipped
state.balances[index_0] = spec.MIN_ACTIVATION_BALANCE
execution_payload = build_empty_execution_payload(spec, state)
assert state.pending_partial_withdrawals == [withdrawal_0, withdrawal_1]
yield from run_withdrawals_processing(spec, state, execution_payload, num_expected_withdrawals=1)
assert state.pending_partial_withdrawals == [pending_withdrawal_0, pending_withdrawal_1]
yield from run_withdrawals_processing(
spec, state,
execution_payload,
num_expected_withdrawals=1,
pending_withdrawal_requests=[pending_withdrawal_1]
)
assert state.pending_partial_withdrawals == []
@with_electra_and_later
@spec_state_test
def test_pending_withdrawals_next_epoch(spec, state):
validator_index = len(state.validators) // 2
next_epoch = spec.get_current_epoch(state) + 1
pending_withdrawal = prepare_pending_withdrawal(spec, state, validator_index, withdrawable_epoch=next_epoch)
execution_payload = build_empty_execution_payload(spec, state)
yield from run_withdrawals_processing(spec, state, execution_payload, num_expected_withdrawals=0)
assert state.pending_partial_withdrawals == [pending_withdrawal]
@with_electra_and_later
@spec_state_test
def test_pending_withdrawals_at_max(spec, state):
pending_withdrawal_requests = []
# Create spec.MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP + 1 partial withdrawals
for i in range(0, spec.MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP + 1):
pending_withdrawal = prepare_pending_withdrawal(spec, state, i)
pending_withdrawal_requests.append(pending_withdrawal)
assert len(state.pending_partial_withdrawals) == spec.MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP + 1
execution_payload = build_empty_execution_payload(spec, state)
yield from run_withdrawals_processing(
spec, state,
execution_payload,
num_expected_withdrawals=spec.MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP,
pending_withdrawal_requests=pending_withdrawal_requests[:spec.MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP]
)
withdrawals_exceeding_max = pending_withdrawal_requests[spec.MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP:]
assert state.pending_partial_withdrawals == withdrawals_exceeding_max
@with_electra_and_later
@spec_state_test
def test_pending_withdrawals_exiting_validator(spec, state):
validator_index = len(state.validators) // 2
pending_withdrawal = prepare_pending_withdrawal(spec, state, validator_index)
spec.initiate_validator_exit(state, pending_withdrawal.index)
execution_payload = build_empty_execution_payload(spec, state)
yield from run_withdrawals_processing(spec, state, execution_payload, num_expected_withdrawals=0)
assert state.pending_partial_withdrawals == []
@with_electra_and_later
@spec_state_test
def test_pending_withdrawals_low_effective_balance(spec, state):
validator_index = len(state.validators) // 2
pending_withdrawal = prepare_pending_withdrawal(spec, state, validator_index)
state.validators[pending_withdrawal.index].effective_balance = (
spec.MIN_ACTIVATION_BALANCE - spec.EFFECTIVE_BALANCE_INCREMENT
)
execution_payload = build_empty_execution_payload(spec, state)
yield from run_withdrawals_processing(spec, state, execution_payload, num_expected_withdrawals=0)
assert state.pending_partial_withdrawals == []
@with_electra_and_later
@spec_state_test
def test_pending_withdrawals_no_excess_balance(spec, state):
validator_index = len(state.validators) // 2
pending_withdrawal = prepare_pending_withdrawal(spec, state, validator_index)
state.balances[pending_withdrawal.index] = spec.MIN_ACTIVATION_BALANCE
execution_payload = build_empty_execution_payload(spec, state)
yield from run_withdrawals_processing(spec, state, execution_payload, num_expected_withdrawals=0)
assert state.pending_partial_withdrawals == []
@with_electra_and_later
@spec_state_test
def test_pending_withdrawals_with_ineffective_sweep_on_top(spec, state):
# Ensure validator will be processed by the sweep
validator_index = min(len(state.validators), spec.MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP) // 2
pending_withdrawal = prepare_pending_withdrawal(
spec, state,
validator_index,
effective_balance=spec.MAX_EFFECTIVE_BALANCE_ELECTRA,
)
# Check that validator is partially withdrawable before pending withdrawal is processed
assert spec.is_partially_withdrawable_validator(
state.validators[validator_index],
state.balances[validator_index]
)
# And is not partially withdrawable thereafter
assert not spec.is_partially_withdrawable_validator(
state.validators[validator_index],
state.balances[validator_index] - pending_withdrawal.amount
)
next_slot(spec, state)
execution_payload = build_empty_execution_payload(spec, state)
yield from run_withdrawals_processing(
spec, state,
execution_payload,
num_expected_withdrawals=1,
fully_withdrawable_indices=[],
partial_withdrawals_indices=[],
pending_withdrawal_requests=[pending_withdrawal]
)
assert state.pending_partial_withdrawals == []
@with_electra_and_later
@spec_state_test
def test_pending_withdrawals_with_ineffective_sweep_on_top_2(spec, state):
# Ensure validator will be processed by the sweep
validator_index = min(len(state.validators), spec.MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP) // 2
pending_withdrawal_0 = prepare_pending_withdrawal(
spec, state,
validator_index,
effective_balance=spec.MAX_EFFECTIVE_BALANCE_ELECTRA,
amount=spec.EFFECTIVE_BALANCE_INCREMENT // 2
)
pending_withdrawal_1 = prepare_pending_withdrawal(
spec, state,
validator_index,
effective_balance=spec.MAX_EFFECTIVE_BALANCE_ELECTRA,
amount=spec.EFFECTIVE_BALANCE_INCREMENT
)
# Set excess balance in a way that validator
# becomes not partially withdrawable only after the second pending withdrawal is processed
state.balances[validator_index] = spec.MAX_EFFECTIVE_BALANCE_ELECTRA + spec.EFFECTIVE_BALANCE_INCREMENT
assert spec.is_partially_withdrawable_validator(
state.validators[validator_index],
state.balances[validator_index] - pending_withdrawal_0.amount
)
assert not spec.is_partially_withdrawable_validator(
state.validators[validator_index],
state.balances[validator_index] - pending_withdrawal_0.amount - pending_withdrawal_1.amount
)
next_slot(spec, state)
execution_payload = build_empty_execution_payload(spec, state)
yield from run_withdrawals_processing(
spec, state,
execution_payload,
num_expected_withdrawals=2,
fully_withdrawable_indices=[],
partial_withdrawals_indices=[],
pending_withdrawal_requests=[pending_withdrawal_0, pending_withdrawal_1]
)
assert state.pending_partial_withdrawals == []
@with_electra_and_later
@spec_state_test
def test_pending_withdrawals_with_effective_sweep_on_top(spec, state):
# Ensure validator will be processed by the sweep
validator_index = min(len(state.validators), spec.MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP) // 2
pending_withdrawal_0 = prepare_pending_withdrawal(
spec, state,
validator_index,
effective_balance=spec.MAX_EFFECTIVE_BALANCE_ELECTRA,
amount=spec.EFFECTIVE_BALANCE_INCREMENT // 2
)
pending_withdrawal_1 = prepare_pending_withdrawal(
spec, state,
validator_index,
effective_balance=spec.MAX_EFFECTIVE_BALANCE_ELECTRA,
amount=spec.EFFECTIVE_BALANCE_INCREMENT
)
# Set excess balance to requested amount times three,
# so the validator is partially withdrawable after pending withdrawal is processed
state.balances[validator_index] = spec.MAX_EFFECTIVE_BALANCE_ELECTRA + spec.EFFECTIVE_BALANCE_INCREMENT * 2
assert spec.is_partially_withdrawable_validator(
state.validators[validator_index],
state.balances[validator_index] - pending_withdrawal_0.amount - pending_withdrawal_1.amount
)
next_slot(spec, state)
execution_payload = build_empty_execution_payload(spec, state)
yield from run_withdrawals_processing(
spec, state,
execution_payload,
num_expected_withdrawals=3,
fully_withdrawable_indices=[],
partial_withdrawals_indices=[validator_index],
pending_withdrawal_requests=[pending_withdrawal_0, pending_withdrawal_1]
)
assert state.pending_partial_withdrawals == []
@with_electra_and_later
@spec_state_test
def test_pending_withdrawals_with_sweep_different_validator(spec, state):
# Ensure validator will be processed by the sweep
validator_index_0 = min(len(state.validators), spec.MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP) // 2 - 1
validator_index_1 = min(len(state.validators), spec.MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP) // 2
# Initiate pending withdrawal for the first validator
pending_withdrawal_0 = prepare_pending_withdrawal(
spec, state,
validator_index_0,
effective_balance=spec.MAX_EFFECTIVE_BALANCE_ELECTRA,
amount=spec.EFFECTIVE_BALANCE_INCREMENT
)
# Make the second validator partially withdrawable by the sweep
set_compounding_withdrawal_credential_with_balance(
spec, state, validator_index_1,
effective_balance=spec.MAX_EFFECTIVE_BALANCE_ELECTRA,
balance=(spec.MAX_EFFECTIVE_BALANCE_ELECTRA + spec.EFFECTIVE_BALANCE_INCREMENT)
)
assert spec.is_partially_withdrawable_validator(
state.validators[validator_index_1],
state.balances[validator_index_1]
)
next_slot(spec, state)
execution_payload = build_empty_execution_payload(spec, state)
yield from run_withdrawals_processing(
spec, state,
execution_payload,
num_expected_withdrawals=2,
fully_withdrawable_indices=[],
partial_withdrawals_indices=[validator_index_1],
pending_withdrawal_requests=[pending_withdrawal_0]
)
assert state.pending_partial_withdrawals == []
@with_electra_and_later
@spec_state_test
def test_pending_withdrawals_mixed_with_sweep_and_fully_withdrawable(spec, state):
num_full_withdrawals = spec.MAX_WITHDRAWALS_PER_PAYLOAD // 4
num_partial_withdrawals = spec.MAX_WITHDRAWALS_PER_PAYLOAD // 4
num_full_withdrawals_comp = spec.MAX_WITHDRAWALS_PER_PAYLOAD // 4
num_partial_withdrawals_comp = spec.MAX_WITHDRAWALS_PER_PAYLOAD // 4
num_pending_withdrawal_requests = spec.MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP // 2
fully_withdrawable_indices, partial_withdrawals_indices = prepare_expected_withdrawals(
spec, state,
rng=random.Random(42),
num_full_withdrawals=num_full_withdrawals,
num_partial_withdrawals=num_partial_withdrawals,
num_full_withdrawals_comp=num_full_withdrawals_comp,
num_partial_withdrawals_comp=num_partial_withdrawals_comp,
)
pending_withdrawal_requests = []
for index in range(0, len(state.validators)):
if len(pending_withdrawal_requests) >= num_pending_withdrawal_requests:
break
if index in (fully_withdrawable_indices + partial_withdrawals_indices):
continue
pending_withdrawal = prepare_pending_withdrawal(spec, state, index)
pending_withdrawal_requests.append(pending_withdrawal)
next_slot(spec, state)
execution_payload = build_empty_execution_payload(spec, state)
yield from run_withdrawals_processing(
spec, state,
execution_payload,
num_expected_withdrawals=spec.MAX_WITHDRAWALS_PER_PAYLOAD,
fully_withdrawable_indices=fully_withdrawable_indices,
partial_withdrawals_indices=partial_withdrawals_indices,
pending_withdrawal_requests=pending_withdrawal_requests
)
assert state.pending_partial_withdrawals == []
@with_electra_and_later
@spec_state_test
def test_pending_withdrawals_at_max_mixed_with_sweep_and_fully_withdrawable(spec, state):
num_full_withdrawals = spec.MAX_WITHDRAWALS_PER_PAYLOAD // 4
num_partial_withdrawals = spec.MAX_WITHDRAWALS_PER_PAYLOAD // 4
num_full_withdrawals_comp = spec.MAX_WITHDRAWALS_PER_PAYLOAD // 4
num_partial_withdrawals_comp = spec.MAX_WITHDRAWALS_PER_PAYLOAD // 4
num_pending_withdrawal_requests = spec.MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP + 1
fully_withdrawable_indices, partial_withdrawals_indices = prepare_expected_withdrawals(
spec, state,
rng=random.Random(42),
num_full_withdrawals=num_full_withdrawals,
num_partial_withdrawals=num_partial_withdrawals,
num_full_withdrawals_comp=num_full_withdrawals_comp,
num_partial_withdrawals_comp=num_partial_withdrawals_comp,
)
pending_withdrawal_requests = []
for index in range(0, len(state.validators)):
if len(pending_withdrawal_requests) >= num_pending_withdrawal_requests:
break
if index in (fully_withdrawable_indices + partial_withdrawals_indices):
continue
pending_withdrawal = prepare_pending_withdrawal(spec, state, index)
pending_withdrawal_requests.append(pending_withdrawal)
next_slot(spec, state)
execution_payload = build_empty_execution_payload(spec, state)
yield from run_withdrawals_processing(
spec, state,
execution_payload,
num_expected_withdrawals=spec.MAX_WITHDRAWALS_PER_PAYLOAD,
fully_withdrawable_indices=fully_withdrawable_indices,
partial_withdrawals_indices=partial_withdrawals_indices,
pending_withdrawal_requests=pending_withdrawal_requests[:spec.MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP]
)
withdrawals_exceeding_max = pending_withdrawal_requests[spec.MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP:]
assert state.pending_partial_withdrawals == withdrawals_exceeding_max

View File

@ -63,11 +63,21 @@ def sample_withdrawal_indices(spec, state, rng, num_full_withdrawals, num_partia
def prepare_expected_withdrawals(spec, state, rng,
num_full_withdrawals=0, num_partial_withdrawals=0):
num_full_withdrawals=0, num_partial_withdrawals=0,
num_full_withdrawals_comp=0, num_partial_withdrawals_comp=0):
fully_withdrawable_indices, partial_withdrawals_indices = sample_withdrawal_indices(
spec, state, rng, num_full_withdrawals, num_partial_withdrawals
spec, state, rng,
num_full_withdrawals + num_full_withdrawals_comp,
num_partial_withdrawals + num_partial_withdrawals_comp
)
fully_withdrawable_indices_comp = rng.sample(fully_withdrawable_indices, num_full_withdrawals_comp)
partial_withdrawals_indices_comp = rng.sample(partial_withdrawals_indices, num_partial_withdrawals_comp)
for index in (fully_withdrawable_indices_comp + partial_withdrawals_indices_comp):
address = state.validators[index].withdrawal_credentials[12:]
set_compounding_withdrawal_credential_with_balance(spec, state, index, address=address)
for index in fully_withdrawable_indices:
set_validator_fully_withdrawable(spec, state, index)
for index in partial_withdrawals_indices:
@ -97,32 +107,13 @@ def set_compounding_withdrawal_credential_with_balance(spec, state, index,
state.balances[index] = balance
def prepare_expected_withdrawals_compounding(spec, state, rng,
num_full_withdrawals=0,
num_partial_withdrawals_sweep=0,
excess_balance=1000000000):
assert is_post_electra(spec)
fully_withdrawable_indices, partial_withdrawals_sweep_indices = sample_withdrawal_indices(
spec, state, rng, num_full_withdrawals, num_partial_withdrawals_sweep
)
for index in fully_withdrawable_indices + partial_withdrawals_sweep_indices:
address = state.validators[index].withdrawal_credentials[12:]
set_compounding_withdrawal_credential_with_balance(spec, state, index, address=address)
for index in fully_withdrawable_indices:
set_validator_fully_withdrawable(spec, state, index)
for index in partial_withdrawals_sweep_indices:
set_validator_partially_withdrawable(spec, state, index)
return fully_withdrawable_indices, partial_withdrawals_sweep_indices
def prepare_pending_withdrawal(spec, state, validator_index,
effective_balance=32_000_000_000, amount=1_000_000_000):
effective_balance=32_000_000_000, amount=1_000_000_000, withdrawable_epoch=None):
assert is_post_electra(spec)
if withdrawable_epoch is None:
withdrawable_epoch = spec.get_current_epoch(state)
balance = effective_balance + amount
set_compounding_withdrawal_credential_with_balance(
spec, state, validator_index, effective_balance, balance
@ -131,7 +122,7 @@ def prepare_pending_withdrawal(spec, state, validator_index,
withdrawal = spec.PendingPartialWithdrawal(
index=validator_index,
amount=amount,
withdrawable_epoch=spec.get_current_epoch(state),
withdrawable_epoch=withdrawable_epoch,
)
state.pending_partial_withdrawals.append(withdrawal)
@ -175,7 +166,8 @@ def verify_post_state(state, spec, expected_withdrawals,
def run_withdrawals_processing(spec, state, execution_payload, num_expected_withdrawals=None,
fully_withdrawable_indices=None, partial_withdrawals_indices=None, valid=True):
fully_withdrawable_indices=None, partial_withdrawals_indices=None,
pending_withdrawal_requests=None, valid=True):
"""
Run ``process_withdrawals``, yielding:
- pre-state ('pre')
@ -206,6 +198,11 @@ def run_withdrawals_processing(spec, state, execution_payload, num_expected_with
yield 'post', state
# Check withdrawal indices
assert state.next_withdrawal_index == pre_state.next_withdrawal_index + len(expected_withdrawals)
for index, withdrawal in enumerate(execution_payload.withdrawals):
assert withdrawal.index == pre_state.next_withdrawal_index + index
if len(expected_withdrawals) == 0:
next_withdrawal_validator_index = (
pre_state.next_withdrawal_validator_index + spec.MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP
@ -220,4 +217,12 @@ def run_withdrawals_processing(spec, state, execution_payload, num_expected_with
if fully_withdrawable_indices is not None or partial_withdrawals_indices is not None:
verify_post_state(state, spec, expected_withdrawals, fully_withdrawable_indices, partial_withdrawals_indices)
# Check withdrawal requests
if pending_withdrawal_requests is not None:
assert len(pending_withdrawal_requests) <= len(execution_payload.withdrawals)
for index, request in enumerate(pending_withdrawal_requests):
withdrawal = execution_payload.withdrawals[index]
assert withdrawal.validator_index == request.index
assert withdrawal.amount == request.amount
return expected_withdrawals