Isolate switch to compounding flow

This commit is contained in:
Mikhail Kalinin 2024-09-18 13:27:03 +04:00
parent 1bf8ca5777
commit b29a1d36d1
3 changed files with 425 additions and 57 deletions

View File

@ -61,6 +61,7 @@
- [Modified `get_next_sync_committee_indices`](#modified-get_next_sync_committee_indices)
- [Beacon state mutators](#beacon-state-mutators)
- [Modified `initiate_validator_exit`](#modified-initiate_validator_exit)
- [New `switch_to_compounding_validator`](#new-switch_to_compounding_validator)
- [New `queue_excess_active_balance`](#new-queue_excess_active_balance)
- [New `queue_entire_balance_and_reset_validator`](#new-queue_entire_balance_and_reset_validator)
- [New `compute_exit_epoch_and_update_churn`](#new-compute_exit_epoch_and_update_churn)
@ -96,6 +97,7 @@
- [Deposit requests](#deposit-requests)
- [New `process_deposit_request`](#new-process_deposit_request)
- [Execution layer consolidation requests](#execution-layer-consolidation-requests)
- [New `is_valid_switch_to_compounding_request`](#new-is_valid_switch_to_compounding_request)
- [New `process_consolidation_request`](#new-process_consolidation_request)
- [Testing](#testing)
@ -677,6 +679,15 @@ def initiate_validator_exit(state: BeaconState, index: ValidatorIndex) -> None:
validator.withdrawable_epoch = Epoch(validator.exit_epoch + MIN_VALIDATOR_WITHDRAWABILITY_DELAY)
```
#### New `switch_to_compounding_validator`
```python
def switch_to_compounding_validator(state: BeaconState, index: ValidatorIndex) -> None:
validator = state.validators[index]
validator.withdrawal_credentials = COMPOUNDING_WITHDRAWAL_PREFIX + validator.withdrawal_credentials[1:]
queue_excess_active_balance(state, index)
```
#### New `queue_excess_active_balance`
```python
@ -1383,6 +1394,45 @@ def process_deposit_request(state: BeaconState, deposit_request: DepositRequest)
##### Execution layer consolidation requests
###### New `is_valid_switch_to_compounding_request`
```python
def is_valid_switch_to_compounding_request(
state: BeaconState,
consolidation_request: ConsolidationRequest
) -> bool:
# Switch to compounding requires source and target be equal
if consolidation_request.source_pubkey != consolidation_request.target_pubkey:
return False
# Verify pubkey exists
source_pubkey = consolidation_request.source_pubkey
validator_pubkeys = [v.pubkey for v in state.validators]
if source_pubkey not in validator_pubkeys:
return False
source_validator = state.validators[ValidatorIndex(validator_pubkeys.index(source_pubkey))]
# Verify request has been authorized
if source_validator.withdrawal_credentials[12:] != consolidation_request.source_address:
return False
# Verify source withdrawal credentials
if not has_eth1_withdrawal_credential(source_validator):
return False
# Verify the source is active
current_epoch = get_current_epoch(state)
if not is_active_validator(source_validator, current_epoch):
return False
# Verify exit for source have not been initiated
if source_validator.exit_epoch != FAR_FUTURE_EPOCH:
return False
return True
```
###### New `process_consolidation_request`
```python
@ -1390,6 +1440,16 @@ def process_consolidation_request(
state: BeaconState,
consolidation_request: ConsolidationRequest
) -> None:
if is_valid_switch_to_compounding_request(state, consolidation_request):
validator_pubkeys = [v.pubkey for v in state.validators]
request_source_pubkey = consolidation_request.source_pubkey
source_index = ValidatorIndex(validator_pubkeys.index(request_source_pubkey))
switch_to_compounding_validator(state, source_index)
return
# Verify that source != target, so a consolidation cannot be used as an exit.
if consolidation_request.source_pubkey == consolidation_request.target_pubkey:
return
# If the pending consolidations queue is full, consolidation requests are ignored
if len(state.pending_consolidations) == PENDING_CONSOLIDATIONS_LIMIT:
return
@ -1434,28 +1494,21 @@ def process_consolidation_request(
if target_validator.exit_epoch != FAR_FUTURE_EPOCH:
return
# Churn any target excess active balance of target and raise its max
if has_eth1_withdrawal_credential(target_validator):
state.validators[target_index].withdrawal_credentials = (
COMPOUNDING_WITHDRAWAL_PREFIX + target_validator.withdrawal_credentials[1:]
)
queue_excess_active_balance(state, target_index)
# Verify that source != target, so a consolidation cannot be used as an exit.
if source_index == target_index:
return
# Initiate source validator exit and append pending consolidation
state.validators[source_index].exit_epoch = compute_consolidation_epoch_and_update_churn(
source_validator.exit_epoch = compute_consolidation_epoch_and_update_churn(
state, source_validator.effective_balance
)
state.validators[source_index].withdrawable_epoch = Epoch(
state.validators[source_index].exit_epoch + MIN_VALIDATOR_WITHDRAWABILITY_DELAY
source_validator.withdrawable_epoch = Epoch(
source_validator.exit_epoch + MIN_VALIDATOR_WITHDRAWABILITY_DELAY
)
state.pending_consolidations.append(PendingConsolidation(
source_index=source_index,
target_index=target_index
))
# Churn any target excess active balance of target and raise its max
if has_eth1_withdrawal_credential(target_validator):
switch_to_compounding_validator(state, target_index)
```
## Testing

View File

@ -66,6 +66,106 @@ def test_basic_consolidation_in_current_consolidation_epoch(spec, state):
assert state.validators[source_index].exit_epoch == expected_exit_epoch
@with_electra_and_later
@with_presets([MINIMAL], "need sufficient consolidation churn limit")
@with_custom_state(
balances_fn=scaled_churn_balances_exceed_activation_exit_churn_limit,
threshold_fn=default_activation_threshold,
)
@spec_test
@single_phase
def test_basic_consolidation_with_excess_target_balance(spec, state):
# This state has 256 validators each with 32 ETH in MINIMAL preset, 128 ETH consolidation churn
current_epoch = spec.get_current_epoch(state)
source_index = spec.get_active_validator_indices(state, current_epoch)[0]
target_index = spec.get_active_validator_indices(state, current_epoch)[1]
# Set source to eth1 credentials
source_address = b"\x22" * 20
set_eth1_withdrawal_credential_with_balance(
spec, state, source_index, address=source_address
)
# Make consolidation with source address
consolidation = spec.ConsolidationRequest(
source_address=source_address,
source_pubkey=state.validators[source_index].pubkey,
target_pubkey=state.validators[target_index].pubkey,
)
# Set target to eth1 credentials
set_eth1_withdrawal_credential_with_balance(spec, state, target_index)
# Set earliest consolidation epoch to the expected exit epoch
expected_exit_epoch = spec.compute_activation_exit_epoch(current_epoch)
state.earliest_consolidation_epoch = expected_exit_epoch
consolidation_churn_limit = spec.get_consolidation_churn_limit(state)
# Set the consolidation balance to consume equal to churn limit
state.consolidation_balance_to_consume = consolidation_churn_limit
# Add excess balance
state.balances[target_index] = state.balances[target_index] + spec.EFFECTIVE_BALANCE_INCREMENT
yield from run_consolidation_processing(spec, state, consolidation)
# Check consolidation churn is decremented correctly
assert (
state.consolidation_balance_to_consume
== consolidation_churn_limit - spec.MIN_ACTIVATION_BALANCE
)
# Check exit epoch
assert state.validators[source_index].exit_epoch == expected_exit_epoch
@with_electra_and_later
@with_presets([MINIMAL], "need sufficient consolidation churn limit")
@with_custom_state(
balances_fn=scaled_churn_balances_exceed_activation_exit_churn_limit,
threshold_fn=default_activation_threshold,
)
@spec_test
@single_phase
def test_basic_consolidation_with_excess_target_balance_and_compounding_credentials(spec, state):
# This state has 256 validators each with 32 ETH in MINIMAL preset, 128 ETH consolidation churn
current_epoch = spec.get_current_epoch(state)
source_index = spec.get_active_validator_indices(state, current_epoch)[0]
target_index = spec.get_active_validator_indices(state, current_epoch)[1]
# Set source to eth1 credentials
source_address = b"\x22" * 20
set_eth1_withdrawal_credential_with_balance(
spec, state, source_index, address=source_address
)
# Make consolidation with source address
consolidation = spec.ConsolidationRequest(
source_address=source_address,
source_pubkey=state.validators[source_index].pubkey,
target_pubkey=state.validators[target_index].pubkey,
)
# Set target to eth1 credentials
set_compounding_withdrawal_credential(spec, state, target_index)
# Set earliest consolidation epoch to the expected exit epoch
expected_exit_epoch = spec.compute_activation_exit_epoch(current_epoch)
state.earliest_consolidation_epoch = expected_exit_epoch
consolidation_churn_limit = spec.get_consolidation_churn_limit(state)
# Set the consolidation balance to consume equal to churn limit
state.consolidation_balance_to_consume = consolidation_churn_limit
# Add excess balance
state.balances[target_index] = state.balances[target_index] + spec.EFFECTIVE_BALANCE_INCREMENT
yield from run_consolidation_processing(spec, state, consolidation)
# Check consolidation churn is decremented correctly
assert (
state.consolidation_balance_to_consume
== consolidation_churn_limit - spec.MIN_ACTIVATION_BALANCE
)
# Check exit epoch
assert state.validators[source_index].exit_epoch == expected_exit_epoch
@with_electra_and_later
@with_presets([MINIMAL], "need sufficient consolidation churn limit")
@with_custom_state(
@ -235,7 +335,7 @@ def test_basic_consolidation_with_compounding_credentials(spec, state):
target_pubkey=state.validators[target_index].pubkey,
)
# Set target to eth1 credentials
# Set target to compounding credentials
set_compounding_withdrawal_credential(spec, state, target_index)
# Set the consolidation balance to consume equal to churn limit
@ -396,14 +496,8 @@ def test_consolidation_balance_through_two_churn_epochs(spec, state):
@with_electra_and_later
@with_presets([MINIMAL], "need sufficient consolidation churn limit")
@with_custom_state(
balances_fn=scaled_churn_balances_exceed_activation_exit_churn_limit,
threshold_fn=default_activation_threshold,
)
@spec_test
@single_phase
def test_source_equals_target_switches_to_compounding(spec, state):
@spec_state_test
def test_basic_switch_to_compounding(spec, state):
current_epoch = spec.get_current_epoch(state)
source_index = spec.get_active_validator_indices(state, current_epoch)[0]
@ -419,23 +513,14 @@ def test_source_equals_target_switches_to_compounding(spec, state):
target_pubkey=state.validators[source_index].pubkey,
)
# Check the the return condition
assert consolidation.source_pubkey == consolidation.target_pubkey
yield from run_consolidation_processing(
yield from run_switch_to_compounding_processing(
spec, state, consolidation, success=True
)
@with_electra_and_later
@with_presets([MINIMAL], "need sufficient consolidation churn limit")
@with_custom_state(
balances_fn=scaled_churn_balances_exceed_activation_exit_churn_limit,
threshold_fn=default_activation_threshold,
)
@spec_test
@single_phase
def test_source_equals_target_switches_to_compounding_with_excess(spec, state):
@spec_state_test
def test_switch_to_compounding_with_excess(spec, state):
current_epoch = spec.get_current_epoch(state)
source_index = spec.get_active_validator_indices(state, current_epoch)[0]
@ -453,10 +538,36 @@ def test_source_equals_target_switches_to_compounding_with_excess(spec, state):
target_pubkey=state.validators[source_index].pubkey,
)
# Check the the return condition
assert consolidation.source_pubkey == consolidation.target_pubkey
yield from run_switch_to_compounding_processing(
spec, state, consolidation, success=True
)
yield from run_consolidation_processing(
@with_electra_and_later
@spec_state_test
def test_switch_to_compounding_with_pending_consolidations_at_limit(spec, state):
state.pending_consolidations = [
spec.PendingConsolidation(source_index=0, target_index=1)
] * spec.PENDING_CONSOLIDATIONS_LIMIT
current_epoch = spec.get_current_epoch(state)
source_index = spec.get_active_validator_indices(state, current_epoch)[0]
# Set source to eth1 credentials
source_address = b"\x22" * 20
set_eth1_withdrawal_credential_with_balance(
spec, state, source_index, address=source_address
)
# Add excess balance
state.balances[source_index] = state.balances[source_index] + spec.EFFECTIVE_BALANCE_INCREMENT
# Make consolidation from source to source
consolidation = spec.ConsolidationRequest(
source_address=source_address,
source_pubkey=state.validators[source_index].pubkey,
target_pubkey=state.validators[source_index].pubkey,
)
yield from run_switch_to_compounding_processing(
spec, state, consolidation, success=True
)
@ -831,6 +942,156 @@ def test_incorrect_unknown_target_pubkey(spec, state):
)
@with_electra_and_later
@spec_state_test
def test_switch_to_compounding_exited_source(spec, state):
# Set up an otherwise correct request
current_epoch = spec.get_current_epoch(state)
source_index = spec.get_active_validator_indices(state, current_epoch)[0]
source_address = b"\x22" * 20
set_eth1_withdrawal_credential_with_balance(
spec, state, source_index, address=source_address
)
consolidation = spec.ConsolidationRequest(
source_address=source_address,
source_pubkey=state.validators[source_index].pubkey,
target_pubkey=state.validators[source_index].pubkey,
)
# exit source
spec.initiate_validator_exit(state, source_index)
# Check the the return condition
assert state.validators[source_index].exit_epoch != spec.FAR_FUTURE_EPOCH
yield from run_switch_to_compounding_processing(
spec, state, consolidation, success=False
)
@with_electra_and_later
@spec_state_test
def test_switch_to_compounding_inactive_source(spec, state):
# Set up an otherwise correct request
current_epoch = spec.get_current_epoch(state)
source_index = spec.get_active_validator_indices(state, current_epoch)[0]
source_address = b"\x22" * 20
set_eth1_withdrawal_credential_with_balance(
spec, state, source_index, address=source_address
)
consolidation = spec.ConsolidationRequest(
source_address=source_address,
source_pubkey=state.validators[source_index].pubkey,
target_pubkey=state.validators[source_index].pubkey,
)
# set source validator as not yet activated
state.validators[source_index].activation_epoch = spec.FAR_FUTURE_EPOCH
# Check the the return condition
assert not spec.is_active_validator(state.validators[source_index], current_epoch)
yield from run_switch_to_compounding_processing(
spec, state, consolidation, success=False
)
@with_electra_and_later
@spec_state_test
def test_switch_to_compounding_source_bls_withdrawal_credential(spec, state):
# Set up a correct request, but source does have
# a bls withdrawal credential
current_epoch = spec.get_current_epoch(state)
source_index = spec.get_active_validator_indices(state, current_epoch)[0]
source_address = b"\x22" * 20
consolidation = spec.ConsolidationRequest(
source_address=source_address,
source_pubkey=state.validators[source_index].pubkey,
target_pubkey=state.validators[source_index].pubkey,
)
# Check the the return condition
assert not spec.has_eth1_withdrawal_credential(state.validators[source_index])
yield from run_switch_to_compounding_processing(
spec, state, consolidation, success=False
)
@with_electra_and_later
@spec_state_test
def test_switch_to_compounding_source_coumpounding_withdrawal_credential(spec, state):
# Set up a correct request, but source does have
# a compounding withdrawal credential and excess balance
current_epoch = spec.get_current_epoch(state)
source_index = spec.get_active_validator_indices(state, current_epoch)[0]
source_address = b"\x22" * 20
consolidation = spec.ConsolidationRequest(
source_address=source_address,
source_pubkey=state.validators[source_index].pubkey,
target_pubkey=state.validators[source_index].pubkey,
)
set_compounding_withdrawal_credential(spec, state, source_index)
state.balances[source_index] = spec.MIN_ACTIVATION_BALANCE + spec.EFFECTIVE_BALANCE_INCREMENT
# Check the the return condition
assert not spec.has_eth1_withdrawal_credential(state.validators[source_index])
yield from run_switch_to_compounding_processing(
spec, state, consolidation, success=False
)
@with_electra_and_later
@spec_state_test
def test_switch_to_compounding_not_authorized(spec, state):
# Set up an otherwise correct request
current_epoch = spec.get_current_epoch(state)
source_index = spec.get_active_validator_indices(state, current_epoch)[0]
source_address = b"\x22" * 20
set_eth1_withdrawal_credential_with_balance(
spec, state, source_index, address=source_address
)
# Make request with different source address
consolidation = spec.ConsolidationRequest(
source_address=b"\x33" * 20,
source_pubkey=state.validators[source_index].pubkey,
target_pubkey=state.validators[source_index].pubkey,
)
# Check the the return condition
assert not state.validators[source_index].withdrawal_credentials[12:] == consolidation.source_address
yield from run_switch_to_compounding_processing(
spec, state, consolidation, success=False
)
@with_electra_and_later
@spec_state_test
def test_switch_to_compounding_unknown_source_pubkey(spec, state):
# Set up an otherwise correct request
current_epoch = spec.get_current_epoch(state)
source_index = spec.get_active_validator_indices(state, current_epoch)[0]
source_address = b"\x22" * 20
set_eth1_withdrawal_credential_with_balance(
spec, state, source_index, address=source_address
)
# Make consolidation with different source pubkey
consolidation = spec.ConsolidationRequest(
source_address=source_address,
source_pubkey=b"\x00" * 48,
target_pubkey=b"\x00" * 48,
)
# Check the the return condition
assert not state.validators[source_index].pubkey == consolidation.source_pubkey
yield from run_switch_to_compounding_processing(
spec, state, consolidation, success=False
)
def run_consolidation_processing(spec, state, consolidation, success=True):
"""
Run ``process_consolidation``, yielding:
@ -865,12 +1126,28 @@ def run_consolidation_processing(spec, state, consolidation, success=True):
# Check source has execution credentials
assert spec.has_execution_withdrawal_credential(source_validator)
# Check target has compounding credentials
assert spec.has_execution_withdrawal_credential(target_validator)
assert spec.has_compounding_withdrawal_credential(state.validators[target_index])
# Check source address in the consolidation fits the withdrawal credentials
assert source_validator.withdrawal_credentials[12:] == consolidation.source_address
# Check source and target are not the same
assert source_index != target_index
# Check source and target were not exiting
assert pre_exit_epoch_source == spec.FAR_FUTURE_EPOCH
assert pre_exit_epoch_target == spec.FAR_FUTURE_EPOCH
# Check source is now exiting
assert state.validators[source_index].exit_epoch < spec.FAR_FUTURE_EPOCH
# Check that the exit epoch matches earliest_consolidation_epoch
assert state.validators[source_index].exit_epoch == state.earliest_consolidation_epoch
# Check that the withdrawable_epoch is set correctly
assert state.validators[source_index].withdrawable_epoch == (
state.validators[source_index].exit_epoch + spec.config.MIN_VALIDATOR_WITHDRAWABILITY_DELAY
)
# Check that the correct consolidation has been appended
expected_new_pending_consolidation = spec.PendingConsolidation(
source_index=source_index,
target_index=target_index,
)
assert state.pending_consolidations == pre_pending_consolidations + [expected_new_pending_consolidation]
# Check excess balance is queued if the target switched to compounding
if pre_target_withdrawal_credentials[:1] == spec.ETH1_ADDRESS_WITHDRAWAL_PREFIX:
assert state.validators[target_index].withdrawal_credentials == (
@ -879,23 +1156,58 @@ def run_consolidation_processing(spec, state, consolidation, success=True):
if pre_target_balance > spec.MIN_ACTIVATION_BALANCE:
assert state.pending_balance_deposits == [spec.PendingBalanceDeposit(
index=target_index, amount=(pre_target_balance - spec.MIN_ACTIVATION_BALANCE))]
# If source and target are same, no consolidation must have been initiated
if source_index == target_index:
assert state.validators[source_index].exit_epoch == spec.FAR_FUTURE_EPOCH
assert state.pending_consolidations == []
else:
# Check source is now exiting
assert state.validators[source_index].exit_epoch < spec.FAR_FUTURE_EPOCH
# Check that the exit epoch matches earliest_consolidation_epoch
assert state.validators[source_index].exit_epoch == state.earliest_consolidation_epoch
# Check that the withdrawable_epoch is set correctly
assert state.validators[source_index].withdrawable_epoch == (
state.validators[source_index].exit_epoch + spec.config.MIN_VALIDATOR_WITHDRAWABILITY_DELAY)
# Check that the correct consolidation has been appended
expected_new_pending_consolidation = spec.PendingConsolidation(
source_index=source_index,
target_index=target_index,
)
assert state.pending_consolidations == pre_pending_consolidations + [expected_new_pending_consolidation]
assert state.balances[target_index] == pre_target_balance
else:
assert pre_state == state
def run_switch_to_compounding_processing(spec, state, consolidation, success=True):
"""
Run ``process_consolidation``, yielding:
- pre-state ('pre')
- consolidation_request ('consolidation_request')
- post-state ('post').
If ``success == False``, ``process_consolidation_request`` would return without any state change.
"""
if success:
validator_pubkeys = [v.pubkey for v in state.validators]
source_index = spec.ValidatorIndex(validator_pubkeys.index(consolidation.source_pubkey))
target_index = spec.ValidatorIndex(validator_pubkeys.index(consolidation.target_pubkey))
source_validator = state.validators[source_index]
pre_exit_epoch = source_validator.exit_epoch
pre_pending_consolidations = state.pending_consolidations.copy()
pre_withdrawal_credentials = source_validator.withdrawal_credentials
pre_balance = state.balances[source_index]
else:
pre_state = state.copy()
yield 'pre', state
yield 'consolidation_request', consolidation
spec.process_consolidation_request(state, consolidation)
yield 'post', state
if success:
# Check that source and target are same
assert source_index == target_index
# Check that the credentials before the switch are of ETH1 type
assert pre_withdrawal_credentials[:1] == spec.ETH1_ADDRESS_WITHDRAWAL_PREFIX
# Check source address in the consolidation fits the withdrawal credentials
assert state.validators[source_index].withdrawal_credentials[12:] == consolidation.source_address
# Check that the source has switched to compounding
assert state.validators[source_index].withdrawal_credentials == (
spec.COMPOUNDING_WITHDRAWAL_PREFIX + pre_withdrawal_credentials[1:]
)
# Check excess balance is queued
assert state.balances[source_index] == spec.MIN_ACTIVATION_BALANCE
if pre_balance > spec.MIN_ACTIVATION_BALANCE:
assert state.pending_balance_deposits == [spec.PendingBalanceDeposit(
index=source_index, amount=(pre_balance - spec.MIN_ACTIVATION_BALANCE))]
# Check no consolidation has been initiated
assert state.validators[source_index].exit_epoch == spec.FAR_FUTURE_EPOCH
assert state.pending_consolidations == pre_pending_consolidations
else:
assert pre_state == state

View File

@ -30,7 +30,10 @@ def set_validator_fully_withdrawable(spec, state, index, withdrawable_epoch=None
def set_eth1_withdrawal_credential_with_balance(spec, state, index, balance=None, address=None):
if balance is None:
balance = spec.MAX_EFFECTIVE_BALANCE
if is_post_electra(spec):
balance = spec.MIN_ACTIVATION_BALANCE
else:
balance = spec.MAX_EFFECTIVE_BALANCE
if address is None:
address = b'\x11' * 20