diff --git a/specs/capella/beacon-chain.md b/specs/capella/beacon-chain.md index 81a7c1707..ff93bec9c 100644 --- a/specs/capella/beacon-chain.md +++ b/specs/capella/beacon-chain.md @@ -9,17 +9,22 @@ - [Introduction](#introduction) - [Custom types](#custom-types) - [Constants](#constants) + - [Domain types](#domain-types) - [Preset](#preset) - [State list lengths](#state-list-lengths) + - [Max operations per block](#max-operations-per-block) - [Execution](#execution) - [Configuration](#configuration) - [Containers](#containers) - [New containers](#new-containers) - [`Withdrawal`](#withdrawal) + - [`BLSToExecutionChange`](#blstoexecutionchange) + - [`SignedBLSToExecutionChange`](#signedblstoexecutionchange) - [Extended Containers](#extended-containers) - [`ExecutionPayload`](#executionpayload) - [`ExecutionPayloadHeader`](#executionpayloadheader) - [`Validator`](#validator) + - [`BeaconBlockBody`](#beaconblockbody) - [`BeaconState`](#beaconstate) - [Helpers](#helpers) - [Beacon state mutators](#beacon-state-mutators) @@ -32,6 +37,8 @@ - [Block processing](#block-processing) - [New `process_withdrawals`](#new-process_withdrawals) - [Modified `process_execution_payload`](#modified-process_execution_payload) + - [Modified `process_operations`](#modified-process_operations) + - [New `process_bls_to_execution_change`](#new-process_bls_to_execution_change) @@ -47,14 +54,20 @@ to validator withdrawals. Including: ## Custom types -## Constants - We define the following Python custom types for type hinting and readability: | Name | SSZ equivalent | Description | | - | - | - | | `WithdrawalIndex` | `uint64` | an index of a `Withdrawal`| +## Constants + +### Domain types + +| Name | Value | +| - | - | +| `DOMAIN_BLS_TO_EXECUTION_CHANGE` | `DomainType('0x0A000000')` | + ## Preset ### State list lengths @@ -63,6 +76,12 @@ We define the following Python custom types for type hinting and readability: | - | - | :-: | :-: | | `WITHDRAWALS_QUEUE_LIMIT` | `uint64(2**40)` (= 1,099,511,627,776) | withdrawals enqueued in state| +### Max operations per block + +| Name | Value | +| - | - | +| `MAX_BLS_TO_EXECUTION_CHANGES` | `2**4` (= 16) | + ### Execution | Name | Value | Description | @@ -84,6 +103,23 @@ class Withdrawal(Container): amount: Gwei ``` +#### `BLSToExecutionChange` + +```python +class BLSToExecutionChange(Container): + validator_index: ValidatorIndex + from_bls_pubkey: BLSPubkey + to_execution_address: ExecutionAddress +``` + +#### `SignedBLSToExecutionChange` + +```python +class SignedBLSToExecutionChange(Container): + message: BLSToExecutionChange + signature: BLSSignature +``` + ### Extended Containers #### `ExecutionPayload` @@ -148,6 +184,26 @@ class Validator(Container): fully_withdrawn_epoch: Epoch # [New in Capella] ``` +#### `BeaconBlockBody` + +```python +class BeaconBlockBody(Container): + randao_reveal: BLSSignature + eth1_data: Eth1Data # Eth1 data vote + graffiti: Bytes32 # Arbitrary data + # Operations + proposer_slashings: List[ProposerSlashing, MAX_PROPOSER_SLASHINGS] + attester_slashings: List[AttesterSlashing, MAX_ATTESTER_SLASHINGS] + attestations: List[Attestation, MAX_ATTESTATIONS] + deposits: List[Deposit, MAX_DEPOSITS] + voluntary_exits: List[SignedVoluntaryExit, MAX_VOLUNTARY_EXITS] + sync_aggregate: SyncAggregate + # Execution + execution_payload: ExecutionPayload + # Capella operations + bls_to_execution_changes: List[SignedBLSToExecutionChange, MAX_BLS_TO_EXECUTION_CHANGES] # [New in Capella] +``` + #### `BeaconState` ```python @@ -324,3 +380,49 @@ def process_execution_payload(state: BeaconState, payload: ExecutionPayload, exe withdrawals_root=hash_tree_root(payload.withdrawals), # [New in Capella] ) ``` + +#### Modified `process_operations` + +*Note*: The function `process_operations` is modified to process `BLSToExecutionChange` operations included in the block. + +```python +def process_operations(state: BeaconState, body: BeaconBlockBody) -> None: + # Verify that outstanding deposits are processed up to the maximum number of deposits + assert len(body.deposits) == min(MAX_DEPOSITS, state.eth1_data.deposit_count - state.eth1_deposit_index) + + def for_ops(operations: Sequence[Any], fn: Callable[[BeaconState, Any], None]) -> None: + for operation in operations: + fn(state, operation) + + for_ops(body.proposer_slashings, process_proposer_slashing) + for_ops(body.attester_slashings, process_attester_slashing) + for_ops(body.attestations, process_attestation) + for_ops(body.deposits, process_deposit) + for_ops(body.voluntary_exits, process_voluntary_exit) + for_ops(body.bls_to_execution_changes, process_bls_to_execution_change) # [New in Capella] +``` + +#### New `process_bls_to_execution_change` + +```python +def process_bls_to_execution_change(state: BeaconState, + signed_address_change: SignedBLSToExecutionChange) -> None: + address_change = signed_address_change.message + + assert address_change.validator_index < len(state.validators) + + validator = state.validators[address_change.validator_index] + + assert validator.withdrawal_credentials[:1] == BLS_WITHDRAWAL_PREFIX + assert validator.withdrawal_credentials[1:] == hash(address_change.from_bls_pubkey)[1:] + + domain = get_domain(state, DOMAIN_BLS_TO_EXECUTION_CHANGE) + signing_root = compute_signing_root(address_change, domain) + assert bls.Verify(address_change.from_bls_pubkey, signing_root, signed_address_change.signature) + + validator.withdrawal_credentials = ( + ETH1_ADDRESS_WITHDRAWAL_PREFIX + + (0x00).to_bytes(11, 'little') + + address_change.to_execution_address + ) +``` diff --git a/tests/core/pyspec/eth2spec/test/capella/block_processing/test_process_bls_to_execution_change.py b/tests/core/pyspec/eth2spec/test/capella/block_processing/test_process_bls_to_execution_change.py new file mode 100644 index 000000000..4b69e04a6 --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/capella/block_processing/test_process_bls_to_execution_change.py @@ -0,0 +1,191 @@ +from eth2spec.utils import bls +from eth2spec.test.helpers.keys import pubkeys, privkeys, pubkey_to_privkey + +from eth2spec.test.context import spec_state_test, expect_assertion_error, with_capella_and_later, always_bls + + +def run_bls_to_execution_change_processing(spec, state, signed_address_change, valid=True): + """ + Run ``process_bls_to_execution_change``, yielding: + - pre-state ('pre') + - address-change ('address_change') + - post-state ('post'). + If ``valid == False``, run expecting ``AssertionError`` + """ + # yield pre-state + yield 'pre', state + + yield 'address_change', signed_address_change + + # If the address_change is invalid, processing is aborted, and there is no post-state. + if not valid: + expect_assertion_error(lambda: spec.process_bls_to_execution_change(state, signed_address_change)) + yield 'post', None + return + + # process address change + spec.process_bls_to_execution_change(state, signed_address_change) + + # Make sure the address change has been processed + validator_index = signed_address_change.message.validator_index + validator = state.validators[validator_index] + assert validator.withdrawal_credentials[:1] == spec.ETH1_ADDRESS_WITHDRAWAL_PREFIX + assert validator.withdrawal_credentials[1:12] == b'\x00' * 11 + assert validator.withdrawal_credentials[12:] == signed_address_change.message.to_execution_address + + # yield post-state + yield 'post', state + + +def get_signed_address_change(spec, state, validator_index=None, withdrawal_pubkey=None): + if validator_index is None: + validator_index = 0 + + if withdrawal_pubkey is None: + key_index = -1 - validator_index + withdrawal_pubkey = pubkeys[key_index] + withdrawal_privkey = privkeys[key_index] + else: + withdrawal_privkey = pubkey_to_privkey[withdrawal_pubkey] + + domain = spec.get_domain(state, spec.DOMAIN_BLS_TO_EXECUTION_CHANGE) + address_change = spec.BLSToExecutionChange( + validator_index=validator_index, + from_bls_pubkey=withdrawal_pubkey, + to_execution_address=b'\x42' * 20, + ) + + signing_root = spec.compute_signing_root(address_change, domain) + return spec.SignedBLSToExecutionChange( + message=address_change, + signature=bls.Sign(withdrawal_privkey, signing_root), + ) + + +@with_capella_and_later +@spec_state_test +def test_success(spec, state): + signed_address_change = get_signed_address_change(spec, state) + yield from run_bls_to_execution_change_processing(spec, state, signed_address_change) + + +@with_capella_and_later +@spec_state_test +def test_success_not_activated(spec, state): + validator_index = 3 + validator = state.validators[validator_index] + validator.activation_eligibility_epoch += 4 + validator.activation_epoch = spec.FAR_FUTURE_EPOCH + + assert not spec.is_active_validator(validator, spec.get_current_epoch(state)) + + signed_address_change = get_signed_address_change(spec, state) + yield from run_bls_to_execution_change_processing(spec, state, signed_address_change) + + assert not spec.is_fully_withdrawable_validator(state.validators[validator_index], spec.get_current_epoch(state)) + + +@with_capella_and_later +@spec_state_test +def test_success_in_activation_queue(spec, state): + validator_index = 3 + validator = state.validators[validator_index] + validator.activation_eligibility_epoch = spec.get_current_epoch(state) + validator.activation_epoch += 4 + + assert not spec.is_active_validator(validator, spec.get_current_epoch(state)) + + signed_address_change = get_signed_address_change(spec, state) + yield from run_bls_to_execution_change_processing(spec, state, signed_address_change) + + assert not spec.is_fully_withdrawable_validator(state.validators[validator_index], spec.get_current_epoch(state)) + + +@with_capella_and_later +@spec_state_test +def test_success_in_exit_queue(spec, state): + validator_index = 3 + spec.initiate_validator_exit(state, validator_index) + + assert spec.is_active_validator(state.validators[validator_index], spec.get_current_epoch(state)) + assert spec.get_current_epoch(state) < state.validators[validator_index].exit_epoch + + signed_address_change = get_signed_address_change(spec, state, validator_index=validator_index) + yield from run_bls_to_execution_change_processing(spec, state, signed_address_change) + + +@with_capella_and_later +@spec_state_test +def test_success_exited(spec, state): + validator_index = 4 + validator = state.validators[validator_index] + validator.exit_epoch = spec.get_current_epoch(state) + + assert not spec.is_active_validator(validator, spec.get_current_epoch(state)) + + signed_address_change = get_signed_address_change(spec, state, validator_index=validator_index) + yield from run_bls_to_execution_change_processing(spec, state, signed_address_change) + + assert not spec.is_fully_withdrawable_validator(state.validators[validator_index], spec.get_current_epoch(state)) + + +@with_capella_and_later +@spec_state_test +def test_success_withdrawable(spec, state): + validator_index = 4 + validator = state.validators[validator_index] + validator.exit_epoch = spec.get_current_epoch(state) + validator.withdrawable_epoch = spec.get_current_epoch(state) + + assert not spec.is_active_validator(validator, spec.get_current_epoch(state)) + + signed_address_change = get_signed_address_change(spec, state, validator_index=validator_index) + yield from run_bls_to_execution_change_processing(spec, state, signed_address_change) + + assert spec.is_fully_withdrawable_validator(state.validators[validator_index], spec.get_current_epoch(state)) + + +@with_capella_and_later +@spec_state_test +def test_fail_val_index_out_of_range(spec, state): + # Create for one validator beyond the validator list length + signed_address_change = get_signed_address_change(spec, state, validator_index=len(state.validators)) + + yield from run_bls_to_execution_change_processing(spec, state, signed_address_change, valid=False) + + +@with_capella_and_later +@spec_state_test +def test_fail_already_0x01(spec, state): + # Create for one validator beyond the validator list length + validator_index = len(state.validators) // 2 + validator = state.validators[validator_index] + validator.withdrawal_credentials = b'\x01' + b'\x00' * 11 + b'\x23' * 20 + signed_address_change = get_signed_address_change(spec, state, validator_index=validator_index) + + yield from run_bls_to_execution_change_processing(spec, state, signed_address_change, valid=False) + + +@with_capella_and_later +@spec_state_test +def test_fail_incorrect_from_bls_pubkey(spec, state): + # Create for one validator beyond the validator list length + validator_index = 2 + signed_address_change = get_signed_address_change( + spec, state, + validator_index=validator_index, + withdrawal_pubkey=pubkeys[0], + ) + + yield from run_bls_to_execution_change_processing(spec, state, signed_address_change, valid=False) + + +@with_capella_and_later +@spec_state_test +@always_bls +def test_fail_bad_signature(spec, state): + signed_address_change = get_signed_address_change(spec, state) + # Mutate sigature + signed_address_change.signature = spec.BLSSignature(b'\x42' * 96) + + yield from run_bls_to_execution_change_processing(spec, state, signed_address_change, valid=False) diff --git a/tests/core/pyspec/eth2spec/test/helpers/genesis.py b/tests/core/pyspec/eth2spec/test/helpers/genesis.py index 1ca408598..83994c409 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/genesis.py +++ b/tests/core/pyspec/eth2spec/test/helpers/genesis.py @@ -6,11 +6,12 @@ from eth2spec.test.helpers.keys import pubkeys def build_mock_validator(spec, i: int, balance: int): - pubkey = pubkeys[i] + active_pubkey = pubkeys[i] + withdrawal_pubkey = pubkeys[-1 - i] # insecurely use pubkey as withdrawal key as well - withdrawal_credentials = spec.BLS_WITHDRAWAL_PREFIX + spec.hash(pubkey)[1:] + withdrawal_credentials = spec.BLS_WITHDRAWAL_PREFIX + spec.hash(withdrawal_pubkey)[1:] validator = spec.Validator( - pubkey=pubkeys[i], + pubkey=active_pubkey, withdrawal_credentials=withdrawal_credentials, activation_eligibility_epoch=spec.FAR_FUTURE_EPOCH, activation_epoch=spec.FAR_FUTURE_EPOCH,