From 20d292f52c9f37eb2781d7d55c0c900ee17f9e50 Mon Sep 17 00:00:00 2001 From: Justin Traglia Date: Thu, 11 Jun 2026 15:01:32 -0500 Subject: [PATCH 01/14] Add EIP-8282 to Gloas --- presets/mainnet/gloas.yaml | 7 + presets/minimal/gloas.yaml | 7 + pysetup/spec_builders/heze.py | 2 + specs/_features/eip8148/beacon-chain.md | 8 +- specs/gloas/beacon-chain.md | 278 ++-- specs/gloas/builder.md | 40 +- specs/gloas/fork.md | 86 +- specs/gloas/validator.md | 76 + .../unittests/test_execution_requests.py | 2 +- .../test_process_builder_deposit_request.py | 689 +++++++++ .../test_process_builder_exit_request.py | 197 +++ .../test_process_deposit_request.py | 1230 +---------------- .../test_process_parent_execution_payload.py | 33 +- .../test_process_voluntary_exit.py | 181 +-- .../test/helpers/deposits.py | 56 +- tests/infra/helpers/deposit_requests.py | 117 +- 16 files changed, 1407 insertions(+), 1602 deletions(-) create mode 100644 tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_builder_deposit_request.py create mode 100644 tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_builder_exit_request.py diff --git a/presets/mainnet/gloas.yaml b/presets/mainnet/gloas.yaml index 599a426d506..884b539b8e6 100644 --- a/presets/mainnet/gloas.yaml +++ b/presets/mainnet/gloas.yaml @@ -10,6 +10,13 @@ PTC_SIZE: 512 # 2**2 (= 4) attestations MAX_PAYLOAD_ATTESTATIONS: 4 +# Execution +# --------------------------------------------------------------- +# 2**4 (= 16) builder deposit requests +MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD: 16 +# 2**4 (= 16) builder exit requests +MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD: 16 + # State list lengths # --------------------------------------------------------------- # 2**40 (= 1,099,511,627,776) builder spots diff --git a/presets/minimal/gloas.yaml b/presets/minimal/gloas.yaml index 559c2d46df7..1dbbc7c65c1 100644 --- a/presets/minimal/gloas.yaml +++ b/presets/minimal/gloas.yaml @@ -10,6 +10,13 @@ PTC_SIZE: 16 # 2**2 (= 4) attestations MAX_PAYLOAD_ATTESTATIONS: 4 +# Execution +# --------------------------------------------------------------- +# 2**4 (= 16) builder deposit requests +MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD: 16 +# 2**4 (= 16) builder exit requests +MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD: 16 + # State list lengths # --------------------------------------------------------------- # 2**40 (= 1,099,511,627,776) builder spots diff --git a/pysetup/spec_builders/heze.py b/pysetup/spec_builders/heze.py index f378571d7c4..79ddb341a3e 100644 --- a/pysetup/spec_builders/heze.py +++ b/pysetup/spec_builders/heze.py @@ -68,6 +68,8 @@ def is_inclusion_list_satisfied(self: ExecutionEngine, def deprecate_functions(cls) -> set[str]: return { "initialize_ptc_window", + "is_builder_withdrawal_credential", + "is_pending_validator", "onboard_builders_from_pending_deposits", "upgrade_to_gloas", } diff --git a/specs/_features/eip8148/beacon-chain.md b/specs/_features/eip8148/beacon-chain.md index 3ed35f425e4..e578c74bd3f 100644 --- a/specs/_features/eip8148/beacon-chain.md +++ b/specs/_features/eip8148/beacon-chain.md @@ -55,7 +55,7 @@ control their balance withdrawals more precisely. | Name | Value | | ------------------------------ | ---------------- | -| `SWEEP_THRESHOLD_REQUEST_TYPE` | `Bytes1('0x03')` | +| `SWEEP_THRESHOLD_REQUEST_TYPE` | `Bytes1('0x05')` | ### Sweep threshold validation @@ -137,6 +137,8 @@ class ExecutionRequests(Container): deposits: List[DepositRequest, MAX_DEPOSIT_REQUESTS_PER_PAYLOAD] withdrawals: List[WithdrawalRequest, MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD] consolidations: List[ConsolidationRequest, MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD] + builder_deposits: List[BuilderDepositRequest, MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD] + builder_exits: List[BuilderExitRequest, MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD] # [New in EIP8148] sweep_thresholds: List[SetSweepThresholdRequest, MAX_SET_SWEEP_THRESHOLD_REQUESTS_PER_PAYLOAD] ``` @@ -239,6 +241,8 @@ def get_execution_requests_list(execution_requests: ExecutionRequests) -> Sequen (DEPOSIT_REQUEST_TYPE, execution_requests.deposits), (WITHDRAWAL_REQUEST_TYPE, execution_requests.withdrawals), (CONSOLIDATION_REQUEST_TYPE, execution_requests.consolidations), + (BUILDER_DEPOSIT_REQUEST_TYPE, execution_requests.builder_deposits), + (BUILDER_EXIT_REQUEST_TYPE, execution_requests.builder_exits), # [New in EIP8148] (SWEEP_THRESHOLD_REQUEST_TYPE, execution_requests.sweep_thresholds), ] @@ -376,6 +380,8 @@ def apply_parent_execution_payload( for_ops(requests.deposits, process_deposit_request) for_ops(requests.withdrawals, process_withdrawal_request) for_ops(requests.consolidations, process_consolidation_request) + for_ops(requests.builder_deposits, process_builder_deposit_request) + for_ops(requests.builder_exits, process_builder_exit_request) # [New in EIP8148] for_ops(requests.sweep_thresholds, process_set_sweep_threshold_request) diff --git a/specs/gloas/beacon-chain.md b/specs/gloas/beacon-chain.md index 9de630c96a8..ad327a87307 100644 --- a/specs/gloas/beacon-chain.md +++ b/specs/gloas/beacon-chain.md @@ -10,10 +10,11 @@ - [Index flags](#index-flags) - [Domains](#domains) - [Misc](#misc) - - [Withdrawal prefixes](#withdrawal-prefixes) + - [Execution-layer triggered requests](#execution-layer-triggered-requests) - [Preset](#preset) - [Misc](#misc-1) - [Max operations per block](#max-operations-per-block) + - [Execution](#execution) - [State list lengths](#state-list-lengths) - [Withdrawals processing](#withdrawals-processing) - [Configuration](#configuration) @@ -24,6 +25,9 @@ - [`Builder`](#builder) - [`BuilderPendingPayment`](#builderpendingpayment) - [`BuilderPendingWithdrawal`](#builderpendingwithdrawal) + - [`BuilderDepositMessage`](#builderdepositmessage) + - [`BuilderDepositRequest`](#builderdepositrequest) + - [`BuilderExitRequest`](#builderexitrequest) - [`PayloadAttestationData`](#payloadattestationdata) - [`PayloadAttestation`](#payloadattestation) - [`PayloadAttestationMessage`](#payloadattestationmessage) @@ -36,6 +40,7 @@ - [`BeaconBlockBody`](#beaconblockbody) - [`BeaconState`](#beaconstate) - [`ExecutionPayload`](#executionpayload) + - [`ExecutionRequests`](#executionrequests) - [Dataclasses](#dataclasses) - [Modified dataclasses](#modified-dataclasses) - [`ExpectedWithdrawals`](#expectedwithdrawals) @@ -43,10 +48,8 @@ - [Predicates](#predicates) - [New `is_builder_index`](#new-is_builder_index) - [New `is_active_builder`](#new-is_active_builder) - - [New `is_builder_withdrawal_credential`](#new-is_builder_withdrawal_credential) - [New `is_attestation_same_slot`](#new-is_attestation_same_slot) - [New `is_valid_indexed_payload_attestation`](#new-is_valid_indexed_payload_attestation) - - [New `is_pending_validator`](#new-is_pending_validator) - [Misc](#misc-2) - [New `convert_builder_index_to_validator_index`](#new-convert_builder_index_to_validator_index) - [New `convert_validator_index_to_builder_index`](#new-convert_validator_index_to_builder_index) @@ -91,18 +94,19 @@ - [Modified `process_withdrawals`](#modified-process_withdrawals) - [Execution payload](#execution-payload) - [Removed `process_execution_payload`](#removed-process_execution_payload) + - [Modified `get_execution_requests_list`](#modified-get_execution_requests_list) - [Execution payload bid](#execution-payload-bid) - [New `verify_execution_payload_bid_signature`](#new-verify_execution_payload_bid_signature) - [New `process_execution_payload_bid`](#new-process_execution_payload_bid) - [Operations](#operations) - [Modified `process_operations`](#modified-process_operations) - - [Deposit requests](#deposit-requests) + - [Builder deposit requests](#builder-deposit-requests) + - [New `is_valid_builder_deposit_signature`](#new-is_valid_builder_deposit_signature) - [New `get_index_for_new_builder`](#new-get_index_for_new_builder) - [New `add_builder_to_registry`](#new-add_builder_to_registry) - - [New `apply_deposit_for_builder`](#new-apply_deposit_for_builder) - - [Modified `process_deposit_request`](#modified-process_deposit_request) - - [Voluntary exits](#voluntary-exits) - - [Modified `process_voluntary_exit`](#modified-process_voluntary_exit) + - [New `process_builder_deposit_request`](#new-process_builder_deposit_request) + - [Builder exit requests](#builder-exit-requests) + - [New `process_builder_exit_request`](#new-process_builder_exit_request) - [Attestations](#attestations) - [Modified `process_attestation`](#modified-process_attestation) - [Payload attestations](#payload-attestations) @@ -123,6 +127,8 @@ Gloas is a consensus-layer upgrade containing a number of features. Including: validators from proposing - [EIP-8061](https://eips.ethereum.org/EIPS/eip-8061): Increase exit and consolidation churn +- [EIP-8282](https://eips.ethereum.org/EIPS/eip-8282): Builder Execution + Requests ## Types @@ -155,11 +161,12 @@ Gloas is a consensus-layer upgrade containing a number of features. Including: | `BUILDER_PAYMENT_THRESHOLD_NUMERATOR` | `uint64(6)` | | | `BUILDER_PAYMENT_THRESHOLD_DENOMINATOR` | `uint64(10)` | | -### Withdrawal prefixes +### Execution-layer triggered requests -| Name | Value | Description | -| --------------------------- | ---------------- | ------------------------------------------ | -| `BUILDER_WITHDRAWAL_PREFIX` | `Bytes1('0x03')` | Withdrawal credential prefix for a builder | +| Name | Value | +| ------------------------------ | ---------------- | +| `BUILDER_DEPOSIT_REQUEST_TYPE` | `Bytes1('0x03')` | +| `BUILDER_EXIT_REQUEST_TYPE` | `Bytes1('0x04')` | ## Preset @@ -175,6 +182,13 @@ Gloas is a consensus-layer upgrade containing a number of features. Including: | -------------------------- | ----- | | `MAX_PAYLOAD_ATTESTATIONS` | `4` | +### Execution + +| Name | Value | Description | +| ------------------------------------------ | --------------------- | ---------------------------------------------------------- | +| `MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD` | `uint64(2**4)` (= 16) | Maximum number of builder deposit requests in each payload | +| `MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD` | `uint64(2**4)` (= 16) | Maximum number of builder exit requests in each payload | + ### State list lengths | Name | Value | Unit | @@ -237,6 +251,35 @@ class BuilderPendingWithdrawal(Container): builder_index: BuilderIndex ``` +#### `BuilderDepositMessage` + +```python +class BuilderDepositMessage(Container): + pubkey: BLSPubkey + version: uint8 + execution_address: ExecutionAddress + amount: Gwei +``` + +#### `BuilderDepositRequest` + +```python +class BuilderDepositRequest(Container): + pubkey: BLSPubkey + version: uint8 + execution_address: ExecutionAddress + amount: Gwei + signature: BLSSignature +``` + +#### `BuilderExitRequest` + +```python +class BuilderExitRequest(Container): + source_address: ExecutionAddress + pubkey: BLSPubkey +``` + #### `PayloadAttestationData` ```python @@ -442,6 +485,19 @@ class ExecutionPayload(Container): slot_number: uint64 ``` +#### `ExecutionRequests` + +```python +class ExecutionRequests(Container): + deposits: List[DepositRequest, MAX_DEPOSIT_REQUESTS_PER_PAYLOAD] + withdrawals: List[WithdrawalRequest, MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD] + consolidations: List[ConsolidationRequest, MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD] + # [New in Gloas:EIP8282] + builder_deposits: List[BuilderDepositRequest, MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD] + # [New in Gloas:EIP8282] + builder_exits: List[BuilderExitRequest, MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD] +``` + ## Dataclasses ### Modified dataclasses @@ -487,13 +543,6 @@ def is_active_builder(state: BeaconState, builder_index: BuilderIndex) -> bool: ) ``` -#### New `is_builder_withdrawal_credential` - -```python -def is_builder_withdrawal_credential(withdrawal_credentials: Bytes32) -> bool: - return withdrawal_credentials[:1] == BUILDER_WITHDRAWAL_PREFIX -``` - #### New `is_attestation_same_slot` ```python @@ -533,29 +582,6 @@ def is_valid_indexed_payload_attestation( return bls.FastAggregateVerify(pubkeys, signing_root, attestation.signature) ``` -#### New `is_pending_validator` - -*Note*: This function naively revalidates deposit signatures on every call. -Implementations SHOULD cache verification results to avoid repeated work. - -```python -def is_pending_validator(pending_deposits: Sequence[PendingDeposit], pubkey: BLSPubkey) -> bool: - """ - Check if a pending deposit with a valid signature is in the queue for the given pubkey. - """ - for pending_deposit in pending_deposits: - if pending_deposit.pubkey != pubkey: - continue - if is_valid_deposit_signature( - pending_deposit.pubkey, - pending_deposit.withdrawal_credentials, - pending_deposit.amount, - pending_deposit.signature, - ): - return True - return False -``` - ### Misc #### New `convert_builder_index_to_validator_index` @@ -1146,6 +1172,10 @@ def apply_parent_execution_payload( for_ops(requests.deposits, process_deposit_request) for_ops(requests.withdrawals, process_withdrawal_request) for_ops(requests.consolidations, process_consolidation_request) + # [New in Gloas:EIP8282] + for_ops(requests.builder_deposits, process_builder_deposit_request) + # [New in Gloas:EIP8282] + for_ops(requests.builder_exits, process_builder_exit_request) # Settle the builder payment if parent_epoch == get_current_epoch(state): @@ -1422,6 +1452,27 @@ def process_withdrawals( `on_execution_payload_envelope`. Payload processing is deferred to the next beacon block via `process_parent_execution_payload`. +##### Modified `get_execution_requests_list` + +```python +def get_execution_requests_list(execution_requests: ExecutionRequests) -> Sequence[bytes]: + requests = [ + (DEPOSIT_REQUEST_TYPE, execution_requests.deposits), + (WITHDRAWAL_REQUEST_TYPE, execution_requests.withdrawals), + (CONSOLIDATION_REQUEST_TYPE, execution_requests.consolidations), + # [New in Gloas:EIP8282] + (BUILDER_DEPOSIT_REQUEST_TYPE, execution_requests.builder_deposits), + # [New in Gloas:EIP8282] + (BUILDER_EXIT_REQUEST_TYPE, execution_requests.builder_exits), + ] + + return [ + request_type + ssz_serialize(request_data) + for request_type, request_data in requests + if len(request_data) != 0 + ] +``` + #### Execution payload bid ##### New `verify_execution_payload_bid_signature` @@ -1510,7 +1561,6 @@ def process_operations(state: BeaconState, body: BeaconBlockBody) -> None: for_ops(body.attester_slashings, process_attester_slashing) # [Modified in Gloas:EIP7732] for_ops(body.attestations, process_attestation) - # [Modified in Gloas:EIP7732] for_ops(body.voluntary_exits, process_voluntary_exit) for_ops(body.bls_to_execution_changes, process_bls_to_execution_change) # [Modified in Gloas:EIP7732] @@ -1523,7 +1573,22 @@ def process_operations(state: BeaconState, body: BeaconBlockBody) -> None: for_ops(body.payload_attestations, process_payload_attestation) ``` -##### Deposit requests +##### Builder deposit requests + +###### New `is_valid_builder_deposit_signature` + +```python +def is_valid_builder_deposit_signature(request: BuilderDepositRequest) -> bool: + deposit_message = BuilderDepositMessage( + pubkey=request.pubkey, + version=request.version, + execution_address=request.execution_address, + amount=request.amount, + ) + domain = compute_domain(DOMAIN_DEPOSIT) + signing_root = compute_signing_root(deposit_message, domain) + return bls.Verify(request.pubkey, signing_root, request.signature) +``` ###### New `get_index_for_new_builder` @@ -1541,7 +1606,8 @@ def get_index_for_new_builder(state: BeaconState) -> BuilderIndex: def add_builder_to_registry( state: BeaconState, pubkey: BLSPubkey, - withdrawal_credentials: Bytes32, + version: uint8, + execution_address: ExecutionAddress, amount: uint64, slot: Slot, ) -> None: @@ -1550,8 +1616,8 @@ def add_builder_to_registry( get_index_for_new_builder(state), Builder( pubkey=pubkey, - version=uint8(withdrawal_credentials[0]), - execution_address=ExecutionAddress(withdrawal_credentials[12:]), + version=version, + execution_address=execution_address, balance=amount, deposit_epoch=compute_epoch_at_slot(slot), withdrawable_epoch=FAR_FUTURE_EPOCH, @@ -1559,7 +1625,7 @@ def add_builder_to_registry( ) ``` -###### New `apply_deposit_for_builder` +###### New `process_builder_deposit_request` *Note*: Builder indices are reusable. When a builder exits, its index may later be reassigned to a different builder with a new public key. Any deposit sent to @@ -1569,108 +1635,44 @@ may have previously appeared in the builder set. Implementations that rely on caching should account for this behavior. ```python -def apply_deposit_for_builder( - state: BeaconState, - pubkey: BLSPubkey, - withdrawal_credentials: Bytes32, - amount: uint64, - signature: BLSSignature, - slot: Slot, -) -> None: +def process_builder_deposit_request(state: BeaconState, request: BuilderDepositRequest) -> None: builder_pubkeys = [b.pubkey for b in state.builders] - if pubkey not in builder_pubkeys: - # Verify the deposit signature (proof of possession) which is not checked by the deposit contract - if is_valid_deposit_signature(pubkey, withdrawal_credentials, amount, signature): - add_builder_to_registry(state, pubkey, withdrawal_credentials, amount, slot) + if request.pubkey not in builder_pubkeys: + if is_valid_builder_deposit_signature(request): + add_builder_to_registry( + state, + request.pubkey, + request.version, + request.execution_address, + request.amount, + state.slot, + ) else: - # Increase balance by deposit amount - builder_index = builder_pubkeys.index(pubkey) - state.builders[builder_index].balance += amount + builder_index = builder_pubkeys.index(request.pubkey) + state.builders[builder_index].balance += request.amount ``` -###### Modified `process_deposit_request` +##### Builder exit requests + +###### New `process_builder_exit_request` ```python -def process_deposit_request(state: BeaconState, deposit_request: DepositRequest) -> None: - # [New in Gloas:EIP7732] +def process_builder_exit_request(state: BeaconState, request: BuilderExitRequest) -> None: builder_pubkeys = [b.pubkey for b in state.builders] - validator_pubkeys = [v.pubkey for v in state.validators] - - # [New in Gloas:EIP7732] - # Regardless of the withdrawal credentials prefix, if a builder/validator - # already exists with this pubkey, apply the deposit to their balance - is_builder = deposit_request.pubkey in builder_pubkeys - is_validator = deposit_request.pubkey in validator_pubkeys - if is_builder or ( - is_builder_withdrawal_credential(deposit_request.withdrawal_credentials) - and not is_validator - and not is_pending_validator(state.pending_deposits, deposit_request.pubkey) - ): - # Apply builder deposits immediately - apply_deposit_for_builder( - state, - deposit_request.pubkey, - deposit_request.withdrawal_credentials, - deposit_request.amount, - deposit_request.signature, - state.slot, - ) + if request.pubkey not in builder_pubkeys: return - # Add validator deposits to the queue - state.pending_deposits.append( - PendingDeposit( - pubkey=deposit_request.pubkey, - withdrawal_credentials=deposit_request.withdrawal_credentials, - amount=deposit_request.amount, - signature=deposit_request.signature, - slot=state.slot, - ) - ) -``` - -##### Voluntary exits - -###### Modified `process_voluntary_exit` - -```python -def process_voluntary_exit(state: BeaconState, signed_voluntary_exit: SignedVoluntaryExit) -> None: - voluntary_exit = signed_voluntary_exit.message - domain = compute_domain( - DOMAIN_VOLUNTARY_EXIT, CAPELLA_FORK_VERSION, state.genesis_validators_root - ) - signing_root = compute_signing_root(voluntary_exit, domain) - - # Exits must specify an epoch when they become valid; they are not valid before then - assert get_current_epoch(state) >= voluntary_exit.epoch + builder_index = BuilderIndex(builder_pubkeys.index(request.pubkey)) + builder = state.builders[builder_index] - # [New in Gloas:EIP7732] - if is_builder_index(voluntary_exit.validator_index): - builder_index = convert_validator_index_to_builder_index(voluntary_exit.validator_index) - # Verify the builder is active - assert is_active_builder(state, builder_index) - # Only exit builder if it has no pending withdrawals in the queue - assert get_pending_balance_to_withdraw_for_builder(state, builder_index) == 0 - # Verify signature - pubkey = state.builders[builder_index].pubkey - assert bls.Verify(pubkey, signing_root, signed_voluntary_exit.signature) - # Initiate exit - initiate_builder_exit(state, builder_index) + if not is_active_builder(state, builder_index): + return + if builder.execution_address != request.source_address: + return + if get_pending_balance_to_withdraw_for_builder(state, builder_index) != 0: return - validator = state.validators[voluntary_exit.validator_index] - # Verify the validator is active - assert is_active_validator(validator, get_current_epoch(state)) - # Verify exit has not been initiated - assert validator.exit_epoch == FAR_FUTURE_EPOCH - # Verify the validator has been active long enough - assert get_current_epoch(state) >= validator.activation_epoch + SHARD_COMMITTEE_PERIOD - # Only exit validator if it has no pending withdrawals in the queue - assert get_pending_balance_to_withdraw(state, voluntary_exit.validator_index) == 0 - # Verify signature - assert bls.Verify(validator.pubkey, signing_root, signed_voluntary_exit.signature) - # Initiate exit - initiate_validator_exit(state, voluntary_exit.validator_index) + initiate_builder_exit(state, builder_index) ``` ##### Attestations diff --git a/specs/gloas/builder.md b/specs/gloas/builder.md index af465eede42..f5bda48e631 100644 --- a/specs/gloas/builder.md +++ b/specs/gloas/builder.md @@ -6,7 +6,6 @@ - [Introduction](#introduction) - [Becoming a builder](#becoming-a-builder) - - [Builder withdrawal credentials](#builder-withdrawal-credentials) - [Submit deposit](#submit-deposit) - [Process deposit](#process-deposit) - [Builder index](#builder-index) @@ -35,37 +34,30 @@ builders. ## Becoming a builder -### Builder withdrawal credentials - -When submitting a deposit to the deposit contract, the `withdrawal_credentials` -field determines whether the staked actor will be a validator or a builder. To -be recognized as a builder, the `withdrawal_credentials` must use the -`BUILDER_WITHDRAWAL_PREFIX`. - -The `withdrawal_credentials` field must be: - -- `withdrawal_credentials[:1] == BUILDER_WITHDRAWAL_PREFIX` (`0x03`) -- `withdrawal_credentials[1:12] == b'\x00' * 11` -- `withdrawal_credentials[12:] == builder_execution_address` - -Where `builder_execution_address` is an execution-layer address that will -receive withdrawals. - ### Submit deposit -Builders follow the same deposit process as validators, but with the -builder-specific withdrawal credentials. The deposit must include: +Builders are created by submitting a builder deposit request to the builder +deposit contract on the execution layer, as defined in +[EIP-8282](https://eips.ethereum.org/EIPS/eip-8282). The request must include: - `pubkey`: The builder's BLS public key. -- `withdrawal_credentials`: With the `BUILDER_WITHDRAWAL_PREFIX` (`0x03`) - prefix. +- `version`: The builder version, `0x00`. +- `execution_address`: The execution-layer address that will receive + withdrawals. - `amount`: At least `MIN_DEPOSIT_AMOUNT` gwei. -- `signature`: BLS signature over the deposit data. +- `signature`: BLS proof of possession over the corresponding + `BuilderDepositMessage` under `DOMAIN_DEPOSIT`. + +*Note*: Builders may be onboarded at the fork by submitting a deposit to the +validator deposit contract with a 0x03 withdrawal credential. This must be done +late enough that the deposit is still pending at the fork. Such a deposit signs +over `DepositMessage` under `DOMAIN_DEPOSIT`, with withdrawal credentials of the +form `BUILDER_WITHDRAWAL_PREFIX + b"\x00" * 11 + execution_address`. ### Process deposit -The beacon chain processes builder deposits identically to validator deposits, -with the withdrawal credentials using `BUILDER_WITHDRAWAL_PREFIX`. +A builder deposit request for a new pubkey registers a builder. A request for an +existing builder's pubkey tops up its balance. ### Builder index diff --git a/specs/gloas/fork.md b/specs/gloas/fork.md index 92111488d97..2095ac53a29 100644 --- a/specs/gloas/fork.md +++ b/specs/gloas/fork.md @@ -5,9 +5,13 @@ - [Introduction](#introduction) +- [Constants](#constants) + - [Withdrawal prefixes](#withdrawal-prefixes) - [Configuration](#configuration) - [Helpers](#helpers) - [New `initialize_ptc_window`](#new-initialize_ptc_window) + - [New `is_builder_withdrawal_credential`](#new-is_builder_withdrawal_credential) + - [New `is_pending_validator`](#new-is_pending_validator) - [New `onboard_builders_from_pending_deposits`](#new-onboard_builders_from_pending_deposits) - [Fork to Gloas](#fork-to-gloas) - [Fork trigger](#fork-trigger) @@ -19,6 +23,18 @@ This document describes the process of the Gloas upgrade. +## Constants + +### Withdrawal prefixes + +*Note*: `BUILDER_WITHDRAWAL_PREFIX` is a temporary constant which is only used +to onboard builders at the fork. It will be deprecated after the upgrade and a +future validator withdrawal prefix may reuse this value. + +| Name | Value | Description | +| --------------------------- | ---------------- | ------------------------------------------ | +| `BUILDER_WITHDRAWAL_PREFIX` | `Bytes1('0x03')` | Withdrawal credential prefix for a builder | + ## Configuration Warning: this configuration is not definitive. @@ -55,12 +71,50 @@ def initialize_ptc_window( return empty_previous_epoch + ptcs ``` +### New `is_builder_withdrawal_credential` + +```python +def is_builder_withdrawal_credential(withdrawal_credentials: Bytes32) -> bool: + return withdrawal_credentials[:1] == BUILDER_WITHDRAWAL_PREFIX +``` + +### New `is_pending_validator` + +*Note*: This function naively revalidates deposit signatures on every call. +Implementations SHOULD cache verification results to avoid repeated work. + +```python +def is_pending_validator(pending_deposits: Sequence[PendingDeposit], pubkey: BLSPubkey) -> bool: + """ + Check if a pending deposit with a valid signature is in the queue for the given pubkey. + """ + for pending_deposit in pending_deposits: + if pending_deposit.pubkey != pubkey: + continue + if is_valid_deposit_signature( + pending_deposit.pubkey, + pending_deposit.withdrawal_credentials, + pending_deposit.amount, + pending_deposit.signature, + ): + return True + return False +``` + ### New `onboard_builders_from_pending_deposits` +*Note*: This one-time onboarding is the only path through the validator deposit +contract that creates builders. From the fork onward, builders are created and +topped up only via `BuilderDepositRequest`. + *Note*: In the slots leading up to the fork, implementations SHOULD validate pending deposit signatures and cache the results. The pending deposit queue might be large and verifying many signatures at the fork could be slow. +*Note*: Builders onboarded at the fork are registered with a `version` of +`0x00`, rather than the `BUILDER_WITHDRAWAL_PREFIX` from their deposit's +withdrawal credentials. + ```python def onboard_builders_from_pending_deposits(state: BeaconState) -> None: """ @@ -76,9 +130,8 @@ def onboard_builders_from_pending_deposits(state: BeaconState) -> None: pending_deposits.append(deposit) continue - # Note that the function apply_deposit_for_builder can mutate the - # state and may add a builder to the registry. For this reason, the - # list of builder pubkeys must be recomputed each iteration. + # Since deposits may add builders to the registry, the list of + # builder pubkeys must be recomputed each iteration. builder_pubkeys = [b.pubkey for b in state.builders] # Deposits for non-builders stay in the pending queue. If there is a @@ -91,15 +144,26 @@ def onboard_builders_from_pending_deposits(state: BeaconState) -> None: if is_pending_validator(pending_deposits, deposit.pubkey): pending_deposits.append(deposit) continue + if not is_valid_deposit_signature( + deposit.pubkey, + deposit.withdrawal_credentials, + deposit.amount, + deposit.signature, + ): + continue - apply_deposit_for_builder( - state, - deposit.pubkey, - deposit.withdrawal_credentials, - deposit.amount, - deposit.signature, - deposit.slot, - ) + add_builder_to_registry( + state, + deposit.pubkey, + uint8(0x00), + ExecutionAddress(deposit.withdrawal_credentials[12:]), + deposit.amount, + deposit.slot, + ) + else: + # Increase balance by deposit amount + builder_index = builder_pubkeys.index(deposit.pubkey) + state.builders[builder_index].balance += deposit.amount state.pending_deposits = pending_deposits ``` diff --git a/specs/gloas/validator.md b/specs/gloas/validator.md index 87f907618c8..768f3d6c965 100644 --- a/specs/gloas/validator.md +++ b/specs/gloas/validator.md @@ -19,6 +19,7 @@ - [Signed execution payload bid](#signed-execution-payload-bid) - [Payload attestations](#payload-attestations) - [Parent execution requests](#parent-execution-requests) + - [Execution requests](#execution-requests) - [ExecutionPayload](#executionpayload) - [Voluntary exits](#voluntary-exits) - [Payload timeliness attestation](#payload-timeliness-attestation) @@ -245,6 +246,81 @@ parent's execution payload. The proposer constructs this field as follows: - Otherwise (the proposer is building on the parent's empty variant), set `parent_execution_requests` to an empty `ExecutionRequests()`. +##### Execution requests + +*Note*: In Gloas, execution requests are carried in the +`ExecutionPayloadEnvelope` rather than the `BeaconBlockBody`. The function +`get_execution_requests` is modified to parse the builder deposit and builder +exit request types, and is used by the builder when constructing the envelope. + +```python +def get_execution_requests(execution_requests_list: Sequence[bytes]) -> ExecutionRequests: + deposits = [] + withdrawals = [] + consolidations = [] + # [New in Gloas:EIP8282] + builder_deposits = [] + # [New in Gloas:EIP8282] + builder_exits = [] + + request_types = [ + DEPOSIT_REQUEST_TYPE, + WITHDRAWAL_REQUEST_TYPE, + CONSOLIDATION_REQUEST_TYPE, + # [New in Gloas:EIP8282] + BUILDER_DEPOSIT_REQUEST_TYPE, + # [New in Gloas:EIP8282] + BUILDER_EXIT_REQUEST_TYPE, + ] + + prev_request_type = None + for request in execution_requests_list: + request_type, request_data = request[0:1], request[1:] + + # Check that the request type is valid + assert request_type in request_types + # Check that the request data is not empty + assert len(request_data) != 0 + # Check that requests are in strictly ascending order + # Each successive type must be greater than the last with no duplicates + assert prev_request_type is None or prev_request_type < request_type + prev_request_type = request_type + + if request_type == DEPOSIT_REQUEST_TYPE: + deposits = ssz_deserialize( + List[DepositRequest, MAX_DEPOSIT_REQUESTS_PER_PAYLOAD], request_data + ) + elif request_type == WITHDRAWAL_REQUEST_TYPE: + withdrawals = ssz_deserialize( + List[WithdrawalRequest, MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD], request_data + ) + elif request_type == CONSOLIDATION_REQUEST_TYPE: + consolidations = ssz_deserialize( + List[ConsolidationRequest, MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD], request_data + ) + # [New in Gloas:EIP8282] + elif request_type == BUILDER_DEPOSIT_REQUEST_TYPE: + builder_deposits = ssz_deserialize( + List[BuilderDepositRequest, MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD], + request_data, + ) + # [New in Gloas:EIP8282] + elif request_type == BUILDER_EXIT_REQUEST_TYPE: + builder_exits = ssz_deserialize( + List[BuilderExitRequest, MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD], request_data + ) + + return ExecutionRequests( + deposits=deposits, + withdrawals=withdrawals, + consolidations=consolidations, + # [New in Gloas:EIP8282] + builder_deposits=builder_deposits, + # [New in Gloas:EIP8282] + builder_exits=builder_exits, + ) +``` + ##### ExecutionPayload *Note*: `prepare_execution_payload` is modified to build on the parent's full diff --git a/tests/core/pyspec/eth_consensus_specs/test/electra/unittests/test_execution_requests.py b/tests/core/pyspec/eth_consensus_specs/test/electra/unittests/test_execution_requests.py index d2153ddf9e8..2bfac7856ba 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/electra/unittests/test_execution_requests.py +++ b/tests/core/pyspec/eth_consensus_specs/test/electra/unittests/test_execution_requests.py @@ -99,6 +99,6 @@ def test_requests_deserialize__reject_empty_request(spec): @single_phase def test_requests_deserialize__reject_unexpected_request_type(spec): serialized_execution_requests = [ - b"\x03\xff\xff\xff", + b"\xff\xff\xff\xff", ] expect_assertion_error(lambda: spec.get_execution_requests(serialized_execution_requests)) diff --git a/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_builder_deposit_request.py b/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_builder_deposit_request.py new file mode 100644 index 00000000000..9a7a683766e --- /dev/null +++ b/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_builder_deposit_request.py @@ -0,0 +1,689 @@ +from eth_consensus_specs.test.context import always_bls, spec_state_test, with_gloas_and_later +from eth_consensus_specs.test.helpers.deposits import prepare_builder_deposit_request +from eth_consensus_specs.test.helpers.keys import privkeys, pubkeys +from tests.infra.helpers.deposit_requests import ( + assert_process_deposit_request, + prepare_process_builder_deposit_request, + run_builder_deposit_request_processing, +) + + +def run_builder_deposit_processing( + spec, state, builder_deposit_request, is_new_builder=True, valid=True +): + """ + Run ``process_builder_deposit_request``, yielding: + - pre-state ('pre') + - builder_deposit_request ('builder_deposit_request') + - post-state ('post'). + + Args: + is_new_builder: If True, expect a new builder to be created. + If False, expect a top-up of an existing builder. + valid: If True, expect the deposit to be applied (new builder or top-up). + If False, expect no changes (invalid signature, wrong credentials, etc). + """ + pre_state = state.copy() + pre_builder_count = len(state.builders) + + yield from run_builder_deposit_request_processing(spec, state, builder_deposit_request) + + if not valid: + # Invalid deposit should not change state (builder not created) + assert_process_deposit_request( + spec, + state, + pre_state, + state_unchanged=True, + ) + elif is_new_builder: + # New builder should be added to registry + assert_process_deposit_request( + spec, + state, + pre_state, + deposit_request=builder_deposit_request, + is_builder_deposit=True, + expected_builder_balance=builder_deposit_request.amount, + expected_execution_address=builder_deposit_request.execution_address, + expected_builder_withdrawable_epoch=spec.FAR_FUTURE_EPOCH, + ) + else: + # Top-up should increase balance + assert_process_deposit_request( + spec, + state, + pre_state, + deposit_request=builder_deposit_request, + is_builder_deposit=True, + expected_builder_count=pre_builder_count, + expected_builder_balance_delta=builder_deposit_request.amount, + ) + + +# +# New builder deposits +# + + +@with_gloas_and_later +@spec_state_test +def test_process_builder_deposit_request__new_builder(spec, state): + """Test fresh builder deposit creates a new builder.""" + amount = spec.MIN_DEPOSIT_AMOUNT + builder_deposit_request = prepare_builder_deposit_request(spec, state, amount, signed=True) + + yield from run_builder_deposit_processing(spec, state, builder_deposit_request) + + +@with_gloas_and_later +@spec_state_test +def test_process_builder_deposit_request__new_builder_nonzero_version(spec, state): + """ + Test fresh builder deposit with a non-zero version. + + The version is not constrained: it is recorded on the builder verbatim and + is committed to by the proof of possession. + """ + amount = spec.MIN_DEPOSIT_AMOUNT + version = spec.uint8(7) + builder_deposit_request = prepare_builder_deposit_request( + spec, state, amount, version=version, signed=True + ) + pre_state = state.copy() + + yield from run_builder_deposit_request_processing(spec, state, builder_deposit_request) + + assert_process_deposit_request( + spec, + state, + pre_state, + deposit_request=builder_deposit_request, + is_builder_deposit=True, + expected_builder_balance=amount, + expected_execution_address=builder_deposit_request.execution_address, + expected_builder_withdrawable_epoch=spec.FAR_FUTURE_EPOCH, + ) + + builder_index = [b.pubkey for b in state.builders].index(builder_deposit_request.pubkey) + assert state.builders[builder_index].version == version + + +@with_gloas_and_later +@spec_state_test +def test_process_builder_deposit_request__new_builder_large_amount(spec, state): + """Test fresh builder deposit with a large amount.""" + # 1000 ETH deposit + amount = spec.Gwei(1_000 * spec.ETH_TO_GWEI) + builder_deposit_request = prepare_builder_deposit_request(spec, state, amount, signed=True) + + yield from run_builder_deposit_processing(spec, state, builder_deposit_request) + + +@with_gloas_and_later +@spec_state_test +def test_process_builder_deposit_request__new_builder_very_large_amount(spec, state): + """Test fresh builder deposit with a very large amount.""" + # 10k ETH deposit + amount = spec.Gwei(10_000 * spec.ETH_TO_GWEI) + builder_deposit_request = prepare_builder_deposit_request(spec, state, amount, signed=True) + + yield from run_builder_deposit_processing(spec, state, builder_deposit_request) + + +@with_gloas_and_later +@spec_state_test +def test_process_builder_deposit_request__new_builder_extra_gwei(spec, state): + """ + Test builder deposit with non-round amount (extra gwei). + + Input State Configured: + - Valid beacon state with existing builders + - Builder deposit request with amount = MIN_DEPOSIT_AMOUNT + 1 (non-round) + + Output State Verified: + - New builder created + - Builder balance equals exact deposit amount (including extra gwei) + """ + amount = spec.MIN_DEPOSIT_AMOUNT + spec.Gwei(1) + builder_deposit_request = prepare_process_builder_deposit_request( + spec, state, amount=amount, signed=True + ) + pre_state = state.copy() + + yield from run_builder_deposit_request_processing(spec, state, builder_deposit_request) + + assert_process_deposit_request( + spec, + state, + pre_state, + deposit_request=builder_deposit_request, + is_builder_deposit=True, + expected_builder_balance=amount, + ) + + +@with_gloas_and_later +@spec_state_test +def test_process_builder_deposit_request__new_builder_max_minus_one(spec, state): + """ + Test builder deposit with amount = MAX_EFFECTIVE_BALANCE - 1. + + Input State Configured: + - Valid beacon state with existing builders + - Builder deposit request with amount just below max effective balance + + Output State Verified: + - New builder created + - Builder balance equals MAX_EFFECTIVE_BALANCE - 1 + """ + amount = spec.MAX_EFFECTIVE_BALANCE - 1 + builder_deposit_request = prepare_process_builder_deposit_request( + spec, state, amount=amount, signed=True + ) + pre_state = state.copy() + + yield from run_builder_deposit_request_processing(spec, state, builder_deposit_request) + + assert_process_deposit_request( + spec, + state, + pre_state, + deposit_request=builder_deposit_request, + is_builder_deposit=True, + expected_builder_balance=amount, + ) + + +@with_gloas_and_later +@spec_state_test +def test_process_builder_deposit_request__new_builder_empty_registry(spec, state): + """ + Test new builder deposit when state.builders is empty. + + Input State Configured: + - Empty builders registry + + Output State Verified: + - New builder appended as index 0 + - Builder has correct fields including withdrawable_epoch = FAR_FUTURE_EPOCH + """ + amount = spec.MIN_DEPOSIT_AMOUNT + # Clear builders registry via the helper + builder_deposit_request = prepare_process_builder_deposit_request( + spec, state, amount=amount, signed=True, builders=[] + ) + pre_state = state.copy() + + yield from run_builder_deposit_request_processing(spec, state, builder_deposit_request) + + assert_process_deposit_request( + spec, + state, + pre_state, + deposit_request=builder_deposit_request, + is_builder_deposit=True, + expected_builder_count=1, + expected_builder_index=0, + expected_builder_balance=amount, + expected_builder_withdrawable_epoch=spec.FAR_FUTURE_EPOCH, + ) + + +@with_gloas_and_later +@spec_state_test +def test_process_builder_deposit_request__new_builder_pubkey_is_validator(spec, state): + """ + Test that a builder deposit request registers a builder even when the + pubkey already belongs to a validator. The validator and builder + registries are keyed independently, so one pubkey may be both. + """ + validator_pubkey = state.validators[0].pubkey + assert validator_pubkey == pubkeys[0] + amount = spec.MIN_DEPOSIT_AMOUNT + pre_builder_count = len(state.builders) + + builder_deposit_request = prepare_builder_deposit_request( + spec, state, amount, pubkey=validator_pubkey, privkey=privkeys[0], signed=True + ) + pre_state = state.copy() + + yield from run_builder_deposit_request_processing(spec, state, builder_deposit_request) + + assert_process_deposit_request( + spec, + state, + pre_state, + deposit_request=builder_deposit_request, + is_builder_deposit=True, + expected_builder_count=pre_builder_count + 1, + expected_builder_balance=amount, + ) + + +# +# Top-up deposits for existing builders +# + + +@with_gloas_and_later +@spec_state_test +def test_process_builder_deposit_request__top_up(spec, state): + """ + Test builder top-up with builder credentials. + + Input State Configured: + - Existing builder pubkey + - Builder withdrawal credentials (0x03 prefix) + + Output State Verified: + - Builder balance increased (top-up) + - No pending deposits added + """ + builder_pubkey = state.builders[0].pubkey + amount = spec.MIN_DEPOSIT_AMOUNT + pre_balance = state.builders[0].balance + pre_builder_count = len(state.builders) + + builder_deposit_request = prepare_builder_deposit_request( + spec, state, amount, pubkey=builder_pubkey, signed=True + ) + pre_state = state.copy() + + yield from run_builder_deposit_request_processing(spec, state, builder_deposit_request) + + assert_process_deposit_request( + spec, + state, + pre_state, + deposit_request=builder_deposit_request, + is_builder_deposit=True, + expected_builder_count=pre_builder_count, + expected_builder_balance=pre_balance + amount, + ) + + +@with_gloas_and_later +@spec_state_test +def test_process_builder_deposit_request__top_up_large(spec, state): + """Test large top-up deposit for an existing builder.""" + # Large top-up (500 ETH) + amount = spec.Gwei(500 * spec.ETH_TO_GWEI) + pubkey = state.builders[0].pubkey + builder_deposit_request = prepare_builder_deposit_request( + spec, state, amount, pubkey=pubkey, signed=True + ) + + yield from run_builder_deposit_processing( + spec, state, builder_deposit_request, is_new_builder=False + ) + + +@with_gloas_and_later +@spec_state_test +def test_process_builder_deposit_request__top_up_single_builder(spec, state): + """ + Test top-up with single builder in registry. + + Input State Configured: + - Exactly one builder in registry + + Output State Verified: + - Builder balance increased + - Builder count unchanged (still 1) + """ + # Keep only one builder + first_builder = state.builders[0] + pre_balance = first_builder.balance + amount = spec.MIN_DEPOSIT_AMOUNT + + # Top-up existing builder at index 0, keeping only one builder + builder_deposit_request = prepare_process_builder_deposit_request( + spec, + state, + builder_index=0, + amount=amount, + signed=True, + builders=[first_builder], + ) + pre_state = state.copy() + + yield from run_builder_deposit_request_processing(spec, state, builder_deposit_request) + + assert_process_deposit_request( + spec, + state, + pre_state, + deposit_request=builder_deposit_request, + is_builder_deposit=True, + expected_builder_count=1, + expected_builder_balance=pre_balance + amount, + ) + + +@with_gloas_and_later +@spec_state_test +def test_process_builder_deposit_request__top_up_last_index(spec, state): + """ + Test top-up targeting last builder in registry. + + Input State Configured: + - Multiple builders in registry + - Top-up targets last builder + + Output State Verified: + - Last builder's balance increased + - Other builders unchanged (verified by assert_process_deposit_request invariant) + """ + last_index = len(state.builders) - 1 + pubkey = state.builders[last_index].pubkey + pre_balance = state.builders[last_index].balance + pre_builder_count = len(state.builders) + amount = spec.MIN_DEPOSIT_AMOUNT + + builder_deposit_request = prepare_builder_deposit_request( + spec, state, amount, pubkey=pubkey, signed=True + ) + pre_state = state.copy() + + yield from run_builder_deposit_request_processing(spec, state, builder_deposit_request) + + # assert_process_deposit_request verifies other builders unchanged for top-ups + assert_process_deposit_request( + spec, + state, + pre_state, + deposit_request=builder_deposit_request, + is_builder_deposit=True, + expected_builder_count=pre_builder_count, + expected_builder_index=last_index, + expected_builder_balance=pre_balance + amount, + ) + + +@with_gloas_and_later +@spec_state_test +def test_process_builder_deposit_request__top_up_ignores_request_fields(spec, state): + """ + Test that a top-up for an existing builder ignores the supplied version + and execution address. The existing registration is unchanged. + """ + builder_pubkey = state.builders[0].pubkey + amount = spec.MIN_DEPOSIT_AMOUNT + pre_balance = state.builders[0].balance + pre_builder_count = len(state.builders) + + # Use a version and execution address that differ from the registration + version = spec.uint8(7) + execution_address = spec.ExecutionAddress(b"\x42" * 20) + assert state.builders[0].version != version + assert state.builders[0].execution_address != execution_address + + builder_deposit_request = prepare_builder_deposit_request( + spec, + state, + amount, + pubkey=builder_pubkey, + version=version, + execution_address=execution_address, + signed=True, + ) + pre_state = state.copy() + + yield from run_builder_deposit_request_processing(spec, state, builder_deposit_request) + + # Should top up the existing builder (other request fields are ignored) + assert_process_deposit_request( + spec, + state, + pre_state, + deposit_request=builder_deposit_request, + is_builder_deposit=True, + expected_builder_count=pre_builder_count, + expected_builder_balance=pre_balance + amount, + ) + assert state.builders[0].version == pre_state.builders[0].version + assert state.builders[0].execution_address == pre_state.builders[0].execution_address + + +# +# Invalid deposits +# + + +@with_gloas_and_later +@spec_state_test +@always_bls +def test_process_builder_deposit_request__new_builder_invalid_sig(spec, state): + """Test that new builder deposit with invalid signature is dropped.""" + amount = spec.MIN_DEPOSIT_AMOUNT + # Don't sign the deposit + builder_deposit_request = prepare_builder_deposit_request(spec, state, amount, signed=False) + + yield from run_builder_deposit_processing( + spec, state, builder_deposit_request, is_new_builder=True, valid=False + ) + + +@with_gloas_and_later +@spec_state_test +@always_bls +def test_process_builder_deposit_request__top_up_invalid_sig(spec, state): + """Test that top-up deposit with invalid signature still succeeds for existing builders.""" + amount = spec.MIN_DEPOSIT_AMOUNT + pubkey = state.builders[0].pubkey + # Don't sign the deposit + builder_deposit_request = prepare_builder_deposit_request( + spec, state, amount, pubkey=pubkey, signed=False + ) + + # Top-ups don't require signature verification for existing builders + yield from run_builder_deposit_processing( + spec, state, builder_deposit_request, is_new_builder=False + ) + + +# +# Builder slot reuse +# + + +@with_gloas_and_later +@spec_state_test +def test_process_builder_deposit_request__reuses_exited_builder_slot(spec, state): + """Test that new builder can reuse slot of fully exited builder with zero balance.""" + pre_builder_count = len(state.builders) + + # Advance epochs and make builder 0 exited with zero balance + builder_deposit_request = prepare_process_builder_deposit_request( + spec, + state, + amount=spec.MIN_DEPOSIT_AMOUNT, + signed=True, + advance_epochs=1, + builder_modifications={0: {"withdrawable_epoch": "current_epoch-1", "balance": 0}}, + ) + pre_state = state.copy() + + yield from run_builder_deposit_request_processing(spec, state, builder_deposit_request) + + assert_process_deposit_request( + spec, + state, + pre_state, + deposit_request=builder_deposit_request, + is_builder_deposit=True, + slot_reused=True, + expected_builder_count=pre_builder_count, + expected_builder_index=0, + ) + + +@with_gloas_and_later +@spec_state_test +def test_process_builder_deposit_request__reuses_first_of_multiple_exited_slots(spec, state): + """ + Test that first reusable slot is selected when multiple slots are available. + + Input State Configured: + - Builder at index 0: exited with zero balance (reusable) + - Builder at index 1: exited with zero balance (reusable) + - Builder at index 2: active (not reusable) + + Output State Verified: + - First reusable slot (index 0) is used + - Builder count unchanged + """ + pre_builder_count = len(state.builders) + + # Advance epochs and make builders 0 and 1 both reusable (exited with zero balance) + # Builder 2 stays active (default FAR_FUTURE_EPOCH) + builder_deposit_request = prepare_process_builder_deposit_request( + spec, + state, + amount=spec.MIN_DEPOSIT_AMOUNT, + signed=True, + advance_epochs=1, + builder_modifications={ + 0: {"withdrawable_epoch": "current_epoch-1", "balance": 0}, + 1: {"withdrawable_epoch": "current_epoch-1", "balance": 0}, + }, + ) + pre_state = state.copy() + + yield from run_builder_deposit_request_processing(spec, state, builder_deposit_request) + + assert_process_deposit_request( + spec, + state, + pre_state, + deposit_request=builder_deposit_request, + is_builder_deposit=True, + slot_reused=True, + expected_builder_count=pre_builder_count, + expected_builder_index=0, + ) + + +@with_gloas_and_later +@spec_state_test +def test_process_builder_deposit_request__reuses_slot_at_current_epoch(spec, state): + """ + Test slot reuse when withdrawable_epoch == current_epoch. + + Input State Configured: + - Builder at index 0: withdrawable_epoch = current_epoch, balance = 0 + + Output State Verified: + - Slot IS reusable at exact epoch boundary + - New builder placed at index 0 + """ + pre_builder_count = len(state.builders) + + # Advance epochs and make builder 0 reusable exactly at current epoch + builder_deposit_request = prepare_process_builder_deposit_request( + spec, + state, + amount=spec.MIN_DEPOSIT_AMOUNT, + signed=True, + advance_epochs=1, + builder_modifications={0: {"withdrawable_epoch": "current_epoch", "balance": 0}}, + ) + pre_state = state.copy() + + yield from run_builder_deposit_request_processing(spec, state, builder_deposit_request) + + assert_process_deposit_request( + spec, + state, + pre_state, + deposit_request=builder_deposit_request, + is_builder_deposit=True, + slot_reused=True, + expected_builder_count=pre_builder_count, + expected_builder_index=0, + ) + + +@with_gloas_and_later +@spec_state_test +def test_process_builder_deposit_request__no_reuse_future_epoch(spec, state): + """ + Test slot NOT reusable when withdrawable_epoch == current_epoch + 1. + + Input State Configured: + - Builder at index 0: withdrawable_epoch = current_epoch + 1, balance = 0 + + Output State Verified: + - Slot is NOT reusable (epoch in future) + - New builder appended to registry + - Original builders unchanged + """ + pre_builder_count = len(state.builders) + + # Advance epochs and make builder 0 NOT yet reusable (one epoch in future) + builder_deposit_request = prepare_process_builder_deposit_request( + spec, + state, + amount=spec.MIN_DEPOSIT_AMOUNT, + signed=True, + advance_epochs=1, + builder_modifications={0: {"withdrawable_epoch": "current_epoch+1", "balance": 0}}, + ) + pre_state = state.copy() + + yield from run_builder_deposit_request_processing(spec, state, builder_deposit_request) + + # slot_reused=False also verifies original builders are unchanged + assert_process_deposit_request( + spec, + state, + pre_state, + deposit_request=builder_deposit_request, + is_builder_deposit=True, + slot_reused=False, + expected_builder_count=pre_builder_count + 1, + expected_builder_index=pre_builder_count, + ) + + +@with_gloas_and_later +@spec_state_test +def test_process_builder_deposit_request__no_reuse_nonzero_balance(spec, state): + """ + Test slot NOT reusable when balance == 1. + + Input State Configured: + - Builder at index 0: withdrawable_epoch in past, balance = 1 (minimum non-zero) + + Output State Verified: + - Slot is NOT reusable (non-zero balance blocks reuse) + - New builder appended to registry + - Original builders unchanged + """ + pre_builder_count = len(state.builders) + + # Advance epochs and make builder 0 exited but with minimum non-zero balance + builder_deposit_request = prepare_process_builder_deposit_request( + spec, + state, + amount=spec.MIN_DEPOSIT_AMOUNT, + signed=True, + advance_epochs=1, + builder_modifications={0: {"withdrawable_epoch": "current_epoch-1", "balance": 1}}, + ) + pre_state = state.copy() + + yield from run_builder_deposit_request_processing(spec, state, builder_deposit_request) + + # slot_reused=False also verifies original builders are unchanged + assert_process_deposit_request( + spec, + state, + pre_state, + deposit_request=builder_deposit_request, + is_builder_deposit=True, + slot_reused=False, + expected_builder_count=pre_builder_count + 1, + expected_builder_index=pre_builder_count, + ) diff --git a/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_builder_exit_request.py b/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_builder_exit_request.py new file mode 100644 index 00000000000..3379fbc3936 --- /dev/null +++ b/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_builder_exit_request.py @@ -0,0 +1,197 @@ +from eth_consensus_specs.test.context import spec_state_test, with_gloas_and_later +from eth_consensus_specs.test.helpers.keys import builder_pubkeys +from eth_consensus_specs.test.helpers.state import next_slots + + +def advance_past_finalization(spec, state): + """Advance slots and finalize so that genesis-epoch builders become active.""" + epoch = spec.get_current_epoch(state) + next_slots(spec, state, spec.SLOTS_PER_EPOCH * 3) + state.finalized_checkpoint.epoch = epoch + 1 + + +def prepare_builder_exit_request(spec, state, builder_index, source_address=None): + """ + Create a builder exit request for the builder at the given index, + authorized by its execution address unless source_address is provided. + """ + builder = state.builders[builder_index] + if source_address is None: + source_address = builder.execution_address + return spec.BuilderExitRequest( + source_address=source_address, + pubkey=builder.pubkey, + ) + + +def run_builder_exit_request_processing(spec, state, builder_exit_request, valid=True): + """ + Run ``process_builder_exit_request``, yielding: + - pre-state ('pre') + - builder_exit_request ('builder_exit_request') + - post-state ('post'). + + The function never raises. If valid is False, expect the request to be + consumed without changing the state. + """ + pre_state = state.copy() + + yield "pre", state + yield "builder_exit_request", builder_exit_request + + spec.process_builder_exit_request(state, builder_exit_request) + + yield "post", state + + if not valid: + assert state == pre_state + + +@with_gloas_and_later +@spec_state_test +def test_process_builder_exit_request__success(spec, state): + """Test successful builder exit with no pending balance.""" + builder_index = 0 + + advance_past_finalization(spec, state) + assert spec.is_active_builder(state, builder_index) + assert spec.get_pending_balance_to_withdraw_for_builder(state, builder_index) == 0 + + current_epoch = spec.get_current_epoch(state) + builder_exit_request = prepare_builder_exit_request(spec, state, builder_index) + + yield from run_builder_exit_request_processing(spec, state, builder_exit_request) + + assert not spec.is_active_builder(state, builder_index) + expected_withdrawable = current_epoch + spec.config.MIN_BUILDER_WITHDRAWABILITY_DELAY + assert state.builders[builder_index].withdrawable_epoch == expected_withdrawable + + +@with_gloas_and_later +@spec_state_test +def test_process_builder_exit_request__unknown_pubkey(spec, state): + """Test that an exit request for an unknown pubkey is a no-op.""" + advance_past_finalization(spec, state) + + # Use a pubkey that is not in the builder registry + existing_pubkeys = {builder.pubkey for builder in state.builders} + unknown_pubkey = None + for pk in builder_pubkeys: + if pk not in existing_pubkeys: + unknown_pubkey = pk + break + assert unknown_pubkey is not None + + builder_exit_request = spec.BuilderExitRequest( + source_address=spec.ExecutionAddress(b"\x42" * 20), + pubkey=unknown_pubkey, + ) + + yield from run_builder_exit_request_processing(spec, state, builder_exit_request, valid=False) + + +@with_gloas_and_later +@spec_state_test +def test_process_builder_exit_request__inactive_deposit_epoch(spec, state): + """Test that an inactive builder (deposit epoch not finalized) cannot exit.""" + builder_index = 0 + + # Set builder's deposit epoch to a non-finalized epoch + state.builders[builder_index].deposit_epoch = spec.Epoch(1) + + advance_past_finalization(spec, state) + assert state.finalized_checkpoint.epoch == state.builders[builder_index].deposit_epoch + assert not spec.is_active_builder(state, builder_index) + + builder_exit_request = prepare_builder_exit_request(spec, state, builder_index) + + yield from run_builder_exit_request_processing(spec, state, builder_exit_request, valid=False) + + +@with_gloas_and_later +@spec_state_test +def test_process_builder_exit_request__already_exited(spec, state): + """Test that an already-exited builder cannot exit again.""" + builder_index = 0 + + # Set builder's withdrawable epoch which indicates it has initiated an exit + state.builders[builder_index].withdrawable_epoch = spec.get_current_epoch(state) + 10 + + advance_past_finalization(spec, state) + assert not spec.is_active_builder(state, builder_index) + + builder_exit_request = prepare_builder_exit_request(spec, state, builder_index) + + yield from run_builder_exit_request_processing(spec, state, builder_exit_request, valid=False) + + +@with_gloas_and_later +@spec_state_test +def test_process_builder_exit_request__wrong_source_address(spec, state): + """Test that an exit request from the wrong source address is a no-op.""" + builder_index = 0 + + advance_past_finalization(spec, state) + assert spec.is_active_builder(state, builder_index) + + # Use a source address that differs from the builder's execution address + wrong_address = spec.ExecutionAddress(b"\x42" * 20) + assert state.builders[builder_index].execution_address != wrong_address + builder_exit_request = prepare_builder_exit_request( + spec, state, builder_index, source_address=wrong_address + ) + + yield from run_builder_exit_request_processing(spec, state, builder_exit_request, valid=False) + + +@with_gloas_and_later +@spec_state_test +def test_process_builder_exit_request__pending_withdrawal(spec, state): + """Test that a builder cannot exit while having a pending withdrawal.""" + builder_index = 0 + + advance_past_finalization(spec, state) + assert spec.is_active_builder(state, builder_index) + + # Add pending withdrawal for this builder + withdrawal_amount = spec.MIN_ACTIVATION_BALANCE + withdrawal = spec.BuilderPendingWithdrawal( + fee_recipient=spec.ExecutionAddress(b"\x70" * 20), + amount=withdrawal_amount, + builder_index=builder_index, + ) + state.builder_pending_withdrawals.append(withdrawal) + pending_balance = spec.get_pending_balance_to_withdraw_for_builder(state, builder_index) + assert pending_balance == withdrawal_amount + + builder_exit_request = prepare_builder_exit_request(spec, state, builder_index) + + yield from run_builder_exit_request_processing(spec, state, builder_exit_request, valid=False) + + +@with_gloas_and_later +@spec_state_test +def test_process_builder_exit_request__pending_payment(spec, state): + """Test that a builder cannot exit while having a pending payment.""" + builder_index = 0 + + advance_past_finalization(spec, state) + assert spec.is_active_builder(state, builder_index) + + # Add pending payment for this builder + payment_amount = spec.MIN_ACTIVATION_BALANCE + payment = spec.BuilderPendingPayment( + weight=spec.get_builder_payment_quorum_threshold(state) + 1, + withdrawal=spec.BuilderPendingWithdrawal( + fee_recipient=spec.ExecutionAddress(b"\x60" * 20), + amount=payment_amount, + builder_index=builder_index, + ), + ) + state.builder_pending_payments[0] = payment + pending_balance = spec.get_pending_balance_to_withdraw_for_builder(state, builder_index) + assert pending_balance == payment_amount + + builder_exit_request = prepare_builder_exit_request(spec, state, builder_index) + + yield from run_builder_exit_request_processing(spec, state, builder_exit_request, valid=False) diff --git a/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_deposit_request.py b/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_deposit_request.py index 481e1228035..5e97e6252ff 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_deposit_request.py +++ b/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_deposit_request.py @@ -1,267 +1,33 @@ -from eth_consensus_specs.test.context import always_bls, spec_state_test, with_gloas_and_later -from eth_consensus_specs.test.helpers.deposits import ( - make_withdrawal_credentials, - prepare_builder_deposit_request, - prepare_deposit_request, - prepare_pending_deposit, -) -from eth_consensus_specs.test.helpers.keys import ( - builder_pubkey_to_privkey, - pubkeys, -) +from eth_consensus_specs.test.context import spec_state_test, with_gloas_and_later from tests.infra.helpers.deposit_requests import ( assert_process_deposit_request, prepare_process_deposit_request, run_deposit_request_processing, ) - -def run_builder_deposit_request_processing( - spec, state, deposit_request, is_new_builder=True, valid=True -): - """ - Run ``process_deposit_request`` for a builder deposit, yielding: - - pre-state ('pre') - - deposit_request ('deposit_request') - - post-state ('post'). - - Args: - is_new_builder: If True, expect a new builder to be created. - If False, expect a top-up of an existing builder. - valid: If True, expect the deposit to be applied (new builder or top-up). - If False, expect no changes (invalid signature, pubkey already validator, etc). - """ - pre_state = state.copy() - pre_builder_count = len(state.builders) - - yield "pre", state - yield "deposit_request", deposit_request - - spec.process_deposit_request(state, deposit_request) - - yield "post", state - - if not valid: - # Invalid deposit should not change state (builder not created) - assert_process_deposit_request( - spec, - state, - pre_state, - state_unchanged=True, - ) - elif is_new_builder: - # New builder should be added to registry - assert_process_deposit_request( - spec, - state, - pre_state, - deposit_request=deposit_request, - is_builder_deposit=True, - expected_builder_balance=deposit_request.amount, - expected_execution_address=spec.ExecutionAddress( - deposit_request.withdrawal_credentials[12:] - ), - expected_builder_withdrawable_epoch=spec.FAR_FUTURE_EPOCH, - ) - else: - # Top-up should increase balance - assert_process_deposit_request( - spec, - state, - pre_state, - deposit_request=deposit_request, - is_builder_deposit=True, - expected_builder_count=pre_builder_count, - expected_builder_balance_delta=deposit_request.amount, - ) - - -# -# New builder deposits -# - - -@with_gloas_and_later -@spec_state_test -def test_process_deposit_request__new_builder(spec, state): - """Test fresh builder deposit creates a new builder.""" - amount = spec.MIN_DEPOSIT_AMOUNT - deposit_request = prepare_builder_deposit_request(spec, state, amount, signed=True) - - yield from run_builder_deposit_request_processing(spec, state, deposit_request) - - -@with_gloas_and_later -@spec_state_test -def test_process_deposit_request__new_builder_large_amount(spec, state): - """Test fresh builder deposit with a large amount.""" - # 1000 ETH deposit - amount = spec.Gwei(1_000 * spec.ETH_TO_GWEI) - deposit_request = prepare_builder_deposit_request(spec, state, amount, signed=True) - - yield from run_builder_deposit_request_processing(spec, state, deposit_request) - - -@with_gloas_and_later -@spec_state_test -def test_process_deposit_request__new_builder_very_large_amount(spec, state): - """Test fresh builder deposit with a very large amount.""" - # 10k ETH deposit - amount = spec.Gwei(10_000 * spec.ETH_TO_GWEI) - deposit_request = prepare_builder_deposit_request(spec, state, amount, signed=True) - - yield from run_builder_deposit_request_processing(spec, state, deposit_request) - - -@with_gloas_and_later -@spec_state_test -def test_process_deposit_request__new_builder_extra_gwei(spec, state): - """ - Test builder deposit with non-round amount (extra gwei). - - Input State Configured: - - Valid beacon state with existing builders - - Deposit request with amount = MIN_DEPOSIT_AMOUNT + 1 (non-round) - - Output State Verified: - - New builder created - - Builder balance equals exact deposit amount (including extra gwei) - """ - amount = spec.MIN_DEPOSIT_AMOUNT + spec.Gwei(1) - deposit_request = prepare_process_deposit_request( - spec, state, for_builder=True, amount=amount, signed=True - ) - pre_state = state.copy() - - yield from run_deposit_request_processing(spec, state, deposit_request) - - assert_process_deposit_request( - spec, - state, - pre_state, - deposit_request=deposit_request, - is_builder_deposit=True, - expected_builder_balance=amount, - ) - - # -# Top-up deposits for existing builders +# In Gloas, deposit requests never create or top up builders. Builders are +# created and topped up only via builder deposit requests. These tests verify +# that deposits with builder withdrawal credentials, or for pubkeys that are +# already builders, are queued as ordinary pending deposits. # @with_gloas_and_later @spec_state_test -def test_process_deposit_request__builder_top_up(spec, state): +def test_process_deposit_request__builder_credentials_queued(spec, state): """ - Test builder top-up with builder credentials. + Test that a deposit with builder withdrawal credentials is queued. Input State Configured: - - Existing builder pubkey + - New pubkey (not an existing validator or builder) - Builder withdrawal credentials (0x03 prefix) Output State Verified: - - Builder balance increased (top-up) - - No pending deposits added - - Credentials field is ignored for existing builder lookup + - Pending deposit added to the validator queue + - No builder created (builder count unchanged) """ - builder_pubkey = state.builders[0].pubkey - amount = spec.MIN_DEPOSIT_AMOUNT - pre_balance = state.builders[0].balance - pre_builder_count = len(state.builders) - - # Top-up existing builder using its pubkey with builder credentials (default for for_builder) - deposit_request = prepare_process_deposit_request( - spec, state, for_builder=True, pubkey=builder_pubkey, amount=amount, signed=True - ) - pre_state = state.copy() - - yield from run_deposit_request_processing(spec, state, deposit_request) - - assert_process_deposit_request( - spec, - state, - pre_state, - deposit_request=deposit_request, - is_builder_deposit=True, - expected_builder_count=pre_builder_count, - expected_builder_balance=pre_balance + amount, - ) - - -@with_gloas_and_later -@spec_state_test -def test_process_deposit_request__builder_top_up_large(spec, state): - """Test large top-up deposit for an existing builder.""" - # Large top-up (500 ETH) - amount = spec.Gwei(500 * spec.ETH_TO_GWEI) - pubkey = state.builders[0].pubkey - deposit_request = prepare_builder_deposit_request( - spec, state, amount, pubkey=pubkey, signed=True - ) - - yield from run_builder_deposit_request_processing( - spec, state, deposit_request, is_new_builder=False - ) - - -# -# Invalid signature tests -# - - -@with_gloas_and_later -@spec_state_test -@always_bls -def test_process_deposit_request__new_builder_invalid_sig(spec, state): - """Test that new builder deposit with invalid signature is rejected.""" - amount = spec.MIN_DEPOSIT_AMOUNT - # Don't sign the deposit - deposit_request = prepare_builder_deposit_request(spec, state, amount, signed=False) - - yield from run_builder_deposit_request_processing( - spec, state, deposit_request, is_new_builder=True, valid=False - ) - - -@with_gloas_and_later -@spec_state_test -@always_bls -def test_process_deposit_request__builder_top_up_invalid_sig(spec, state): - """Test that top-up deposit with invalid signature still succeeds for existing builders.""" amount = spec.MIN_DEPOSIT_AMOUNT - pubkey = state.builders[0].pubkey - # Don't sign the deposit - deposit_request = prepare_builder_deposit_request( - spec, state, amount, pubkey=pubkey, signed=False - ) - - # Top-ups don't require signature verification for existing builders - yield from run_builder_deposit_request_processing( - spec, state, deposit_request, is_new_builder=False - ) - - -# -# Edge cases -# - - -@with_gloas_and_later -@spec_state_test -def test_process_deposit_request__new_builder_max_minus_one(spec, state): - """ - Test builder deposit with amount = MAX_EFFECTIVE_BALANCE - 1. - - Input State Configured: - - Valid beacon state with existing builders - - Deposit request with amount just below max effective balance - - Output State Verified: - - New builder created - - Builder balance equals MAX_EFFECTIVE_BALANCE - 1 - """ - amount = spec.MAX_EFFECTIVE_BALANCE - 1 deposit_request = prepare_process_deposit_request( spec, state, for_builder=True, amount=amount, signed=True ) @@ -274,234 +40,29 @@ def test_process_deposit_request__new_builder_max_minus_one(spec, state): state, pre_state, deposit_request=deposit_request, - is_builder_deposit=True, - expected_builder_balance=amount, - ) - - -@with_gloas_and_later -@spec_state_test -def test_process_deposit_request__reuses_exited_builder_slot(spec, state): - """Test that new builder can reuse slot of fully exited builder with zero balance.""" - pre_builder_count = len(state.builders) - - # Advance epochs and make builder 0 exited with zero balance - deposit_request = prepare_process_deposit_request( - spec, - state, - for_builder=True, - amount=spec.MIN_DEPOSIT_AMOUNT, - signed=True, - advance_epochs=1, - builder_modifications={0: {"withdrawable_epoch": "current_epoch-1", "balance": 0}}, - ) - pre_state = state.copy() - - yield from run_deposit_request_processing(spec, state, deposit_request) - - assert_process_deposit_request( - spec, - state, - pre_state, - deposit_request=deposit_request, - is_builder_deposit=True, - slot_reused=True, - expected_builder_count=pre_builder_count, - expected_builder_index=0, - ) - - -@with_gloas_and_later -@spec_state_test -def test_process_deposit_request__reuses_first_of_multiple_exited_slots(spec, state): - """ - Test that first reusable slot is selected when multiple slots are available. - - Input State Configured: - - Builder at index 0: exited with zero balance (reusable) - - Builder at index 1: exited with zero balance (reusable) - - Builder at index 2: active (not reusable) - - Output State Verified: - - First reusable slot (index 0) is used - - Builder count unchanged - """ - pre_builder_count = len(state.builders) - - # Advance epochs and make builders 0 and 1 both reusable (exited with zero balance) - # Builder 2 stays active (default FAR_FUTURE_EPOCH) - deposit_request = prepare_process_deposit_request( - spec, - state, - for_builder=True, - amount=spec.MIN_DEPOSIT_AMOUNT, - signed=True, - advance_epochs=1, - builder_modifications={ - 0: {"withdrawable_epoch": "current_epoch-1", "balance": 0}, - 1: {"withdrawable_epoch": "current_epoch-1", "balance": 0}, - }, - ) - pre_state = state.copy() - - yield from run_deposit_request_processing(spec, state, deposit_request) - - assert_process_deposit_request( - spec, - state, - pre_state, - deposit_request=deposit_request, - is_builder_deposit=True, - slot_reused=True, - expected_builder_count=pre_builder_count, - expected_builder_index=0, - ) - - -@with_gloas_and_later -@spec_state_test -def test_process_deposit_request__reuses_slot_at_current_epoch(spec, state): - """ - Test slot reuse when withdrawable_epoch == current_epoch. - - Input State Configured: - - Builder at index 0: withdrawable_epoch = current_epoch, balance = 0 - - Output State Verified: - - Slot IS reusable at exact epoch boundary - - New builder placed at index 0 - """ - pre_builder_count = len(state.builders) - - # Advance epochs and make builder 0 reusable exactly at current epoch - deposit_request = prepare_process_deposit_request( - spec, - state, - for_builder=True, - amount=spec.MIN_DEPOSIT_AMOUNT, - signed=True, - advance_epochs=1, - builder_modifications={0: {"withdrawable_epoch": "current_epoch", "balance": 0}}, - ) - pre_state = state.copy() - - yield from run_deposit_request_processing(spec, state, deposit_request) - - assert_process_deposit_request( - spec, - state, - pre_state, - deposit_request=deposit_request, - is_builder_deposit=True, - slot_reused=True, - expected_builder_count=pre_builder_count, - expected_builder_index=0, - ) - - -@with_gloas_and_later -@spec_state_test -def test_process_deposit_request__no_reuse_future_epoch(spec, state): - """ - Test slot NOT reusable when withdrawable_epoch == current_epoch + 1. - - Input State Configured: - - Builder at index 0: withdrawable_epoch = current_epoch + 1, balance = 0 - - Output State Verified: - - Slot is NOT reusable (epoch in future) - - New builder appended to registry - - Original builders unchanged - """ - pre_builder_count = len(state.builders) - - # Advance epochs and make builder 0 NOT yet reusable (one epoch in future) - deposit_request = prepare_process_deposit_request( - spec, - state, - for_builder=True, - amount=spec.MIN_DEPOSIT_AMOUNT, - signed=True, - advance_epochs=1, - builder_modifications={0: {"withdrawable_epoch": "current_epoch+1", "balance": 0}}, - ) - pre_state = state.copy() - - yield from run_deposit_request_processing(spec, state, deposit_request) - - # slot_reused=False also verifies original builders are unchanged - assert_process_deposit_request( - spec, - state, - pre_state, - deposit_request=deposit_request, - is_builder_deposit=True, - slot_reused=False, - expected_builder_count=pre_builder_count + 1, - expected_builder_index=pre_builder_count, - ) - - -@with_gloas_and_later -@spec_state_test -def test_process_deposit_request__no_reuse_nonzero_balance(spec, state): - """ - Test slot NOT reusable when balance == 1. - - Input State Configured: - - Builder at index 0: withdrawable_epoch in past, balance = 1 (minimum non-zero) - - Output State Verified: - - Slot is NOT reusable (non-zero balance blocks reuse) - - New builder appended to registry - - Original builders unchanged - """ - pre_builder_count = len(state.builders) - - # Advance epochs and make builder 0 exited but with minimum non-zero balance - deposit_request = prepare_process_deposit_request( - spec, - state, - for_builder=True, - amount=spec.MIN_DEPOSIT_AMOUNT, - signed=True, - advance_epochs=1, - builder_modifications={0: {"withdrawable_epoch": "current_epoch-1", "balance": 1}}, - ) - pre_state = state.copy() - - yield from run_deposit_request_processing(spec, state, deposit_request) - - # slot_reused=False also verifies original builders are unchanged - assert_process_deposit_request( - spec, - state, - pre_state, - deposit_request=deposit_request, - is_builder_deposit=True, - slot_reused=False, - expected_builder_count=pre_builder_count + 1, - expected_builder_index=pre_builder_count, + expected_pending_deposit_pubkey=deposit_request.pubkey, + expected_pending_deposit_amount=amount, + expected_pending_deposit_credentials=deposit_request.withdrawal_credentials, ) @with_gloas_and_later @spec_state_test -def test_process_deposit_request__new_builder_empty_registry(spec, state): +def test_process_deposit_request__builder_pubkey_queued(spec, state): """ - Test new builder deposit when state.builders is empty. + Test that a deposit for a pubkey that is already a builder is queued. Input State Configured: - - Empty builders registry + - Existing builder pubkey + - Builder withdrawal credentials (0x03 prefix) Output State Verified: - - New builder appended as index 0 - - Builder has correct fields including withdrawable_epoch = FAR_FUTURE_EPOCH + - Pending deposit added to the validator queue + - Builder unchanged (no top-up) """ amount = spec.MIN_DEPOSIT_AMOUNT - # Clear builders registry via the helper deposit_request = prepare_process_deposit_request( - spec, state, for_builder=True, amount=amount, signed=True, builders=[] + spec, state, builder_index=0, amount=amount, signed=True ) pre_state = state.copy() @@ -512,40 +73,36 @@ def test_process_deposit_request__new_builder_empty_registry(spec, state): state, pre_state, deposit_request=deposit_request, - is_builder_deposit=True, - expected_builder_count=1, - expected_builder_index=0, - expected_builder_balance=amount, - expected_builder_withdrawable_epoch=spec.FAR_FUTURE_EPOCH, + expected_pending_deposit_pubkey=deposit_request.pubkey, + expected_pending_deposit_amount=amount, ) + # The existing builder must be untouched + assert state.builders[0] == pre_state.builders[0] @with_gloas_and_later @spec_state_test -def test_process_deposit_request__builder_top_up_single_builder(spec, state): +def test_process_deposit_request__builder_pubkey_validator_credentials(spec, state): """ - Test top-up with single builder in registry. + Test that a deposit for a builder pubkey with validator credentials is queued. Input State Configured: - - Exactly one builder in registry + - Existing builder pubkey + - ETH1 withdrawal credentials (0x01 prefix) Output State Verified: - - Builder balance increased - - Builder count unchanged (still 1) + - Pending deposit added to the validator queue + - Builder unchanged (no top-up) """ - # Keep only one builder - first_builder = state.builders[0] - pre_balance = first_builder.balance amount = spec.MIN_DEPOSIT_AMOUNT - - # Top-up existing builder at index 0, keeping only one builder + withdrawal_credentials = spec.ETH1_ADDRESS_WITHDRAWAL_PREFIX + b"\x00" * 11 + b"\x59" * 20 deposit_request = prepare_process_deposit_request( spec, state, builder_index=0, amount=amount, signed=True, - builders=[first_builder], + withdrawal_credentials=withdrawal_credentials, ) pre_state = state.copy() @@ -556,725 +113,8 @@ def test_process_deposit_request__builder_top_up_single_builder(spec, state): state, pre_state, deposit_request=deposit_request, - is_builder_deposit=True, - expected_builder_count=1, - expected_builder_balance=pre_balance + amount, - ) - - -@with_gloas_and_later -@spec_state_test -def test_process_deposit_request__builder_top_up_last_index(spec, state): - """ - Test top-up targeting last builder in registry. - - Input State Configured: - - Multiple builders in registry - - Top-up targets last builder - - Output State Verified: - - Last builder's balance increased - - Other builders unchanged (verified by assert_process_deposit_request invariant) - """ - last_index = len(state.builders) - 1 - pubkey = state.builders[last_index].pubkey - pre_balance = state.builders[last_index].balance - pre_builder_count = len(state.builders) - amount = spec.MIN_DEPOSIT_AMOUNT - - # Top-up existing builder at last index using its pubkey - deposit_request = prepare_process_deposit_request( - spec, state, for_builder=True, pubkey=pubkey, amount=amount, signed=True - ) - pre_state = state.copy() - - yield from run_deposit_request_processing(spec, state, deposit_request) - - # assert_process_deposit_request verifies other builders unchanged for top-ups - assert_process_deposit_request( - spec, - state, - pre_state, - deposit_request=deposit_request, - is_builder_deposit=True, - expected_builder_count=pre_builder_count, - expected_builder_index=last_index, - expected_builder_balance=pre_balance + amount, - ) - - -# -# Deposit routing tests -# -# These tests verify the routing logic in process_deposit_request: -# - Existing builder pubkey → builder (regardless of credentials) -# - Existing validator pubkey → validator queue (regardless of credentials) -# - New pubkey + builder credentials → new builder -# - New pubkey + validator credentials → validator queue -# - - -@with_gloas_and_later -@spec_state_test -def test_process_deposit_request__routing__builder_pubkey_validator_credentials(spec, state): - """Test that existing builder pubkey with validator credentials still tops up builder.""" - builder_pubkey = state.builders[0].pubkey - amount = spec.MIN_DEPOSIT_AMOUNT - pre_balance = state.builders[0].balance - pre_builder_count = len(state.builders) - - # Create validator-style withdrawal credentials (BLS prefix) - but use builder pubkey - withdrawal_credentials = spec.BLS_WITHDRAWAL_PREFIX + spec.hash(builder_pubkey)[1:] - - # Use builder_index=0 to use existing builder's pubkey with validator credentials - deposit_request = prepare_process_deposit_request( - spec, - state, - builder_index=0, - amount=amount, - signed=True, - withdrawal_credentials=withdrawal_credentials, - ) - pre_state = state.copy() - - yield from run_deposit_request_processing(spec, state, deposit_request) - - # Should top up existing builder (credentials are ignored for existing builders) - # is_builder_deposit=True because the pubkey belongs to an existing builder - assert_process_deposit_request( - spec, - state, - pre_state, - deposit_request=deposit_request, - is_builder_deposit=True, - expected_builder_count=pre_builder_count, - expected_builder_balance=pre_balance + amount, - ) - - -@with_gloas_and_later -@spec_state_test -def test_process_deposit_request__routing__validator_pubkey_builder_credentials(spec, state): - """Test that existing validator pubkey with builder credentials goes to validator queue.""" - validator_pubkey = state.validators[0].pubkey - amount = spec.MIN_DEPOSIT_AMOUNT - - # Create builder withdrawal credentials - but use existing validator pubkey - withdrawal_credentials = spec.BUILDER_WITHDRAWAL_PREFIX + b"\x00" * 11 + b"\x59" * 20 - - # Use validator_index=0 to use existing validator's pubkey with builder credentials - deposit_request = prepare_process_deposit_request( - spec, - state, - validator_index=0, - amount=amount, - signed=True, - withdrawal_credentials=withdrawal_credentials, - ) - pre_state = state.copy() - - yield from run_deposit_request_processing(spec, state, deposit_request) - - # Should route to validator queue (pubkey lookup finds validator first) - # is_builder_deposit=False because it goes to validator queue - assert_process_deposit_request( - spec, - state, - pre_state, - deposit_request=deposit_request, - is_builder_deposit=False, - expected_pending_deposit_pubkey=validator_pubkey, - expected_pending_deposit_amount=amount, - ) - - -@with_gloas_and_later -@spec_state_test -def test_process_deposit_request__routing__validator_pubkey_validator_credentials(spec, state): - """Test that existing validator pubkey with validator credentials goes to validator queue.""" - validator_pubkey = state.validators[0].pubkey - amount = spec.MIN_DEPOSIT_AMOUNT - - # Create deposit with validator (non-builder) credentials for existing validator - deposit_request = prepare_process_deposit_request( - spec, - state, - validator_index=0, - amount=amount, - signed=True, - ) - pre_state = state.copy() - - yield from run_deposit_request_processing(spec, state, deposit_request) - - # Should route to validator queue - assert_process_deposit_request( - spec, - state, - pre_state, - deposit_request=deposit_request, - is_builder_deposit=False, - expected_pending_deposit_pubkey=validator_pubkey, - expected_pending_deposit_amount=amount, - ) - - -@with_gloas_and_later -@spec_state_test -def test_process_deposit_request__routing__new_pubkey_validator_credentials(spec, state): - """Test that new pubkey with validator credentials goes to validator queue.""" - # Use a pubkey that doesn't exist as validator or builder (new validator index) - new_pubkey = pubkeys[len(state.validators)] - amount = spec.MIN_DEPOSIT_AMOUNT - - # Create deposit with validator (non-builder) credentials for new validator - deposit_request = prepare_process_deposit_request( - spec, - state, - amount=amount, - signed=True, - ) - pre_state = state.copy() - - yield from run_deposit_request_processing(spec, state, deposit_request) - - # Should route to validator queue (for new validator creation) - assert_process_deposit_request( - spec, - state, - pre_state, - deposit_request=deposit_request, - is_builder_deposit=False, - expected_pending_deposit_pubkey=new_pubkey, - expected_pending_deposit_amount=amount, - ) - - -@with_gloas_and_later -@spec_state_test -def test_process_deposit_request__routing__new_pubkey_eth1_credentials(spec, state): - """ - Test that new pubkey with ETH1 credentials (0x01) routes to validator queue. - - Input State Configured: - - New pubkey (not existing validator or builder) - - ETH1_ADDRESS_WITHDRAWAL_PREFIX credentials - - Output State Verified: - - No new builder created (builder count unchanged) - - Pending deposit added to validator queue - """ - new_pubkey = pubkeys[len(state.validators)] - amount = spec.MIN_DEPOSIT_AMOUNT - - # Create ETH1 withdrawal credentials (0x01 prefix) - withdrawal_credentials = ( - spec.ETH1_ADDRESS_WITHDRAWAL_PREFIX + b"\x00" * 11 + b"\x59" * 20 # 20-byte eth1 address - ) - - deposit_request = prepare_process_deposit_request( - spec, - state, - amount=amount, - signed=True, - withdrawal_credentials=withdrawal_credentials, - ) - - pre_state = state.copy() - - yield from run_deposit_request_processing(spec, state, deposit_request) - - assert_process_deposit_request( - spec, - state, - pre_state, - deposit_request=deposit_request, - is_builder_deposit=False, - expected_pending_deposit_pubkey=new_pubkey, + expected_pending_deposit_pubkey=deposit_request.pubkey, expected_pending_deposit_credentials=withdrawal_credentials, ) - - -# -# Pending validator deposit tests -# -# These tests verify that deposits with builder credentials for pubkeys -# that already have a pending deposit with a valid signature go to the -# validator queue instead of becoming builders. -# - - -@with_gloas_and_later -@spec_state_test -def test_process_deposit_request__routing__new_pubkey_compounding_credentials(spec, state): - """ - Test new pubkey with compounding credentials (0x02) routes to validator queue. - - Input State Configured: - - New pubkey (not existing validator or builder) - - COMPOUNDING_WITHDRAWAL_PREFIX credentials - - Output State Verified: - - No new builder created (builder count unchanged) - - Pending deposit added to validator queue - """ - new_pubkey = pubkeys[len(state.validators)] - amount = spec.MIN_DEPOSIT_AMOUNT - - # Create compounding withdrawal credentials (0x02 prefix) - withdrawal_credentials = ( - spec.COMPOUNDING_WITHDRAWAL_PREFIX + b"\x00" * 11 + b"\x59" * 20 # 20-byte eth1 address - ) - - deposit_request = prepare_process_deposit_request( - spec, - state, - amount=amount, - signed=True, - withdrawal_credentials=withdrawal_credentials, - ) - - pre_state = state.copy() - - yield from run_deposit_request_processing(spec, state, deposit_request) - - assert_process_deposit_request( - spec, - state, - pre_state, - deposit_request=deposit_request, - is_builder_deposit=False, - expected_pending_deposit_pubkey=new_pubkey, - expected_pending_deposit_credentials=withdrawal_credentials, - ) - - -@with_gloas_and_later -@spec_state_test -@always_bls -def test_process_deposit_request__routing__pending_deposit_valid_signature(spec, state): - """Test that pubkey with pending deposit with valid signature goes to validator queue.""" - # Use a pubkey that doesn't exist as validator or builder yet - new_validator_index = len(state.validators) - amount = spec.MIN_DEPOSIT_AMOUNT - - # Add a pending deposit with a valid signature for this pubkey - pending_withdrawal_credentials = make_withdrawal_credentials( - spec, spec.ETH1_ADDRESS_WITHDRAWAL_PREFIX, b"\xab" - ) - pending_deposit = prepare_pending_deposit( - spec, - new_validator_index, - amount, - withdrawal_credentials=pending_withdrawal_credentials, - signed=True, - ) - state.pending_deposits.append(pending_deposit) - - # Now create a deposit request with builder credentials for the same pubkey - deposit_request = prepare_deposit_request( - spec, - new_validator_index, - amount, - index=0, - withdrawal_credentials=make_withdrawal_credentials( - spec, spec.BUILDER_WITHDRAWAL_PREFIX, b"\x59" - ), - signed=True, - ) - - pre_state = state.copy() - - yield from run_deposit_request_processing(spec, state, deposit_request) - - # Should NOT create a new builder (pubkey is a pending validator) - assert_process_deposit_request( - spec, - state, - pre_state, - deposit_request=deposit_request, - is_builder_deposit=False, - ) - - -@with_gloas_and_later -@spec_state_test -@always_bls -def test_process_deposit_request__routing__pending_deposit_invalid_signature(spec, state): - """Test that pubkey with pending deposit with invalid signature becomes builder.""" - # Use a pubkey from builder_pubkeys that doesn't exist as validator or builder yet - existing_builder_pubkeys = {builder.pubkey for builder in state.builders} - new_builder_pubkey = None - for pk in builder_pubkey_to_privkey: - if pk not in existing_builder_pubkeys: - new_builder_pubkey = pk - break - assert new_builder_pubkey is not None - privkey = builder_pubkey_to_privkey[new_builder_pubkey] - amount = spec.MIN_DEPOSIT_AMOUNT - - # Add a pending deposit with an invalid signature (won't create a validator) - invalid_pending_deposit = spec.PendingDeposit( - pubkey=new_builder_pubkey, - amount=amount, - withdrawal_credentials=make_withdrawal_credentials( - spec, spec.ETH1_ADDRESS_WITHDRAWAL_PREFIX, b"\xab" - ), - signature=spec.BLSSignature(), # Invalid empty signature - slot=spec.GENESIS_SLOT, - ) - state.pending_deposits.append(invalid_pending_deposit) - - # Now create a deposit request with builder credentials for the same pubkey - deposit_request = prepare_deposit_request( - spec, - 0, - amount, - index=0, - pubkey=new_builder_pubkey, - privkey=privkey, - withdrawal_credentials=make_withdrawal_credentials( - spec, spec.BUILDER_WITHDRAWAL_PREFIX, b"\x59" - ), - signed=True, - ) - - pre_builder_count = len(state.builders) - pre_state = state.copy() - - yield from run_deposit_request_processing(spec, state, deposit_request) - - # SHOULD create a new builder (pending deposit has invalid sig, so is_pending_validator is False) - assert_process_deposit_request( - spec, - state, - pre_state, - deposit_request=deposit_request, - is_builder_deposit=True, - expected_builder_count=pre_builder_count + 1, - expected_builder_balance=amount, - expected_execution_address=spec.ExecutionAddress( - deposit_request.withdrawal_credentials[12:] - ), - ) - - -@with_gloas_and_later -@spec_state_test -def test_process_deposit_request__nonstandard_credential_padding(spec, state): - """ - Test builder deposit with non-zero bytes in credentials[1:12]. - - The Builder container extracts execution_address from credentials[12:]. - This test verifies that non-standard padding (non-zero bytes in [1:12]) - still results in correct address extraction. - - Input State Configured: - - New builder pubkey - - Builder credentials with non-zero padding in bytes 1-11 - - Output State Verified: - - New builder created - - execution_address extracted from credentials[12:] - - Non-zero padding bytes in [1:12] are ignored - """ - amount = spec.MIN_DEPOSIT_AMOUNT - pre_builder_count = len(state.builders) - - # Create credentials with non-zero padding bytes in [1:12] - execution_address = b"\xab" * 20 # 20-byte execution address - nonstandard_padding = b"\xff" * 11 # Non-zero bytes instead of standard 0x00 - - withdrawal_credentials = ( - spec.BUILDER_WITHDRAWAL_PREFIX - + nonstandard_padding # Non-standard: typically b"\x00" * 11 - + execution_address - ) - - deposit_request = prepare_process_deposit_request( - spec, - state, - for_builder=True, - amount=amount, - signed=True, - withdrawal_credentials=withdrawal_credentials, - ) - pre_state = state.copy() - - yield from run_deposit_request_processing(spec, state, deposit_request) - - assert_process_deposit_request( - spec, - state, - pre_state, - deposit_request=deposit_request, - is_builder_deposit=True, - expected_builder_count=pre_builder_count + 1, - expected_builder_balance=amount, - expected_execution_address=spec.ExecutionAddress(execution_address), - ) - - -@with_gloas_and_later -@spec_state_test -@always_bls -def test_process_deposit_request__routing__pending_deposits_invalid_then_valid(spec, state): - """Test that pubkey with invalid pending deposits followed by valid one goes to validator queue.""" - # Use a pubkey that doesn't exist as validator or builder yet - new_validator_index = len(state.validators) - new_pubkey = pubkeys[new_validator_index] - amount = spec.MIN_DEPOSIT_AMOUNT - - # Add multiple pending deposits with invalid signatures first - for _ in range(3): - invalid_pending_deposit = spec.PendingDeposit( - pubkey=new_pubkey, - amount=amount, - withdrawal_credentials=make_withdrawal_credentials( - spec, spec.ETH1_ADDRESS_WITHDRAWAL_PREFIX, b"\xab" - ), - signature=spec.BLSSignature(), # Invalid empty signature - slot=spec.GENESIS_SLOT, - ) - state.pending_deposits.append(invalid_pending_deposit) - - # Add a valid pending deposit after the invalid ones - valid_withdrawal_credentials = make_withdrawal_credentials( - spec, spec.ETH1_ADDRESS_WITHDRAWAL_PREFIX, b"\xcd" - ) - valid_pending_deposit = prepare_pending_deposit( - spec, - new_validator_index, - amount, - withdrawal_credentials=valid_withdrawal_credentials, - signed=True, - ) - state.pending_deposits.append(valid_pending_deposit) - - # Now create a deposit request with builder credentials for the same pubkey - deposit_request = prepare_deposit_request( - spec, - new_validator_index, - amount, - index=0, - withdrawal_credentials=make_withdrawal_credentials( - spec, spec.BUILDER_WITHDRAWAL_PREFIX, b"\x59" - ), - signed=True, - ) - - pre_state = state.copy() - - yield from run_deposit_request_processing(spec, state, deposit_request) - - # Should NOT create a new builder (there's a valid pending deposit in the queue) - assert_process_deposit_request( - spec, - state, - pre_state, - deposit_request=deposit_request, - is_builder_deposit=False, - ) - - -@with_gloas_and_later -@spec_state_test -@always_bls -def test_process_deposit_request__routing__pending_deposit_builder_credentials(spec, state): - """Test that pubkey with pending deposit with builder credentials goes to validator queue.""" - # Use a pubkey that doesn't exist as validator or builder yet - new_validator_index = len(state.validators) - amount = spec.MIN_DEPOSIT_AMOUNT - - # Add a pending deposit with builder credentials and a valid signature - pending_withdrawal_credentials = make_withdrawal_credentials( - spec, spec.BUILDER_WITHDRAWAL_PREFIX, b"\xab" - ) - pending_deposit = prepare_pending_deposit( - spec, - new_validator_index, - amount, - withdrawal_credentials=pending_withdrawal_credentials, - signed=True, - ) - state.pending_deposits.append(pending_deposit) - - # Now create another deposit request with builder credentials for the same pubkey - deposit_request = prepare_deposit_request( - spec, - new_validator_index, - amount, - index=0, - withdrawal_credentials=make_withdrawal_credentials( - spec, spec.BUILDER_WITHDRAWAL_PREFIX, b"\x59" - ), - signed=True, - ) - - pre_state = state.copy() - - yield from run_deposit_request_processing(spec, state, deposit_request) - - # Should NOT create a new builder (pubkey has valid pending deposit) - assert_process_deposit_request( - spec, - state, - pre_state, - deposit_request=deposit_request, - is_builder_deposit=False, - ) - - -@with_gloas_and_later -@spec_state_test -def test_process_deposit_request__routing__pending_deposit_different_pubkeys(spec, state): - """ - Test is_pending_validator skips non-matching pubkeys before finding matching one. - - Exercises the `continue` branch in is_pending_validator when pending deposits - with different pubkeys exist before the matching deposit. - - Input State Configured: - - Pending deposits for two unrelated validator pubkeys - - Pending deposit for the target pubkey - - Builder-credential deposit request for the target pubkey - - Output State Verified: - - Deposit routed to validator queue (not builder) - - is_pending_validator returns True after skipping non-matching deposits - """ - new_validator_index = len(state.validators) - amount = spec.MIN_DEPOSIT_AMOUNT - - # Add pending deposits with different pubkeys (these will be skipped via `continue`) - for i in range(2): - unrelated_index = new_validator_index + 1 + i - unrelated_pending = prepare_pending_deposit( - spec, - unrelated_index, - amount, - signed=True, - ) - state.pending_deposits.append(unrelated_pending) - - # Add a pending deposit with the target pubkey (this will match) - target_pending = prepare_pending_deposit( - spec, - new_validator_index, - amount, - signed=True, - ) - state.pending_deposits.append(target_pending) - - # Create a deposit request with builder credentials for the target pubkey - deposit_request = prepare_deposit_request( - spec, - new_validator_index, - amount, - index=0, - withdrawal_credentials=make_withdrawal_credentials( - spec, spec.BUILDER_WITHDRAWAL_PREFIX, b"\x59" - ), - signed=True, - ) - - pre_state = state.copy() - - yield from run_deposit_request_processing(spec, state, deposit_request) - - # Should NOT create a new builder (is_pending_validator finds matching deposit) - assert_process_deposit_request( - spec, - state, - pre_state, - deposit_request=deposit_request, - is_builder_deposit=False, - ) - - -@with_gloas_and_later -@spec_state_test -def test_process_deposit_request__routing__empty_pending_deposits(spec, state): - """ - Test is_pending_validator with empty pending deposit queue. - - When pending_deposits is empty, is_pending_validator returns False - immediately and the deposit is routed to the builder path. - - Input State Configured: - - Empty pending_deposits queue - - Builder-credential deposit request for a new pubkey - - Output State Verified: - - Deposit routed to builder path (new builder created) - """ - amount = spec.MIN_DEPOSIT_AMOUNT - - # Ensure pending deposits is empty - state.pending_deposits = [] - - deposit_request = prepare_process_deposit_request( - spec, state, for_builder=True, amount=amount, signed=True - ) - pre_state = state.copy() - - yield from run_deposit_request_processing(spec, state, deposit_request) - - # Should create a new builder (no pending deposits, is_pending_validator returns False) - assert_process_deposit_request( - spec, - state, - pre_state, - deposit_request=deposit_request, - is_builder_deposit=True, - expected_builder_balance=amount, - ) - - -@with_gloas_and_later -@spec_state_test -def test_process_deposit_request__routing__pending_deposits_all_different_pubkeys(spec, state): - """ - Test is_pending_validator when all pending deposits have different pubkeys. - - When no pending deposit matches the target pubkey, is_pending_validator - hits `continue` for every deposit and returns False. - - Input State Configured: - - Pending deposits for two unrelated validator pubkeys - - Builder-credential deposit request for a pubkey not in the queue - - Output State Verified: - - Deposit routed to builder path (new builder created) - """ - new_validator_index = len(state.validators) - amount = spec.MIN_DEPOSIT_AMOUNT - - # Add pending deposits with unrelated pubkeys - for i in range(2): - unrelated_index = new_validator_index + 1 + i - unrelated_pending = prepare_pending_deposit( - spec, - unrelated_index, - amount, - signed=True, - ) - state.pending_deposits.append(unrelated_pending) - - # Create a builder deposit request for a pubkey NOT in the pending queue - deposit_request = prepare_process_deposit_request( - spec, state, for_builder=True, amount=amount, signed=True - ) - pre_state = state.copy() - - yield from run_deposit_request_processing(spec, state, deposit_request) - - # Should create a new builder (no matching pubkey, is_pending_validator returns False) - assert_process_deposit_request( - spec, - state, - pre_state, - deposit_request=deposit_request, - is_builder_deposit=True, - expected_builder_balance=amount, - ) + # The existing builder must be untouched + assert state.builders[0] == pre_state.builders[0] diff --git a/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_parent_execution_payload.py b/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_parent_execution_payload.py index 4c1bcd9dbc1..f4bc7ac7303 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_parent_execution_payload.py +++ b/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_parent_execution_payload.py @@ -12,7 +12,7 @@ get_non_empty_execution_requests, ) from eth_consensus_specs.test.helpers.keys import pubkeys -from tests.infra.helpers.deposit_requests import prepare_process_deposit_request +from tests.infra.helpers.deposit_requests import prepare_process_builder_deposit_request from tests.infra.helpers.withdrawals import set_parent_block_full @@ -310,17 +310,16 @@ def test_process_parent_execution_payload__full_parent_with_execution_requests(s @with_gloas_and_later @spec_state_test -def test_process_parent_execution_payload__builder_deposit_after_pending_validator(spec, state): +def test_process_parent_execution_payload__builder_credential_deposits_queued(spec, state): """ - Test that a builder deposit cannot claim a pubkey that is already a pending validator - earlier in the same parent execution requests batch. + Test that deposit requests are queued as pending deposits regardless of + their withdrawal credentials. Deposit requests never create builders. """ new_validator_index = len(state.validators) new_validator_pubkey = pubkeys[new_validator_index] amount = spec.MIN_DEPOSIT_AMOUNT - # First deposit: regular validator credentials with valid signature. - # Since no validator/builder/pending deposit exists for this pubkey, it is queued as a pending validator. + # First deposit: regular validator credentials with valid signature deposit_request_1 = prepare_deposit_request( spec, new_validator_index, @@ -333,10 +332,7 @@ def test_process_parent_execution_payload__builder_deposit_after_pending_validat signed=True, ) - # Second deposit: builder credentials for the same pubkey. - # ``is_pending_validator`` must see the first deposit (just queued) and route this one - # to the pending queue instead of the builder registry, preventing a builder from - # claiming a pubkey already in the validator queue. + # Second deposit: builder credentials for the same pubkey, also queued deposit_request_2 = prepare_deposit_request( spec, new_validator_index, @@ -481,7 +477,7 @@ def test_process_parent_execution_payload__new_builder_does_not_reuse_topped_up_ top_up_amount = spec.MIN_DEPOSIT_AMOUNT new_builder_amount = spec.MIN_DEPOSIT_AMOUNT - deposit_request_1 = prepare_process_deposit_request( + builder_deposit_request_1 = prepare_process_builder_deposit_request( spec, state, builder_index=0, @@ -489,22 +485,17 @@ def test_process_parent_execution_payload__new_builder_does_not_reuse_topped_up_ amount=top_up_amount, signed=True, ) - deposit_request_2 = prepare_process_deposit_request( + builder_deposit_request_2 = prepare_process_builder_deposit_request( spec, state, - for_builder=True, amount=new_builder_amount, signed=True, ) requests = spec.ExecutionRequests( - deposits=spec.List[spec.DepositRequest, spec.MAX_DEPOSIT_REQUESTS_PER_PAYLOAD]( - [deposit_request_1, deposit_request_2] - ), - withdrawals=spec.List[spec.WithdrawalRequest, spec.MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD](), - consolidations=spec.List[ - spec.ConsolidationRequest, spec.MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD - ](), + builder_deposits=spec.List[ + spec.BuilderDepositRequest, spec.MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD + ]([builder_deposit_request_1, builder_deposit_request_2]), ) _commit_parent_requests(spec, state, requests) @@ -524,7 +515,7 @@ def test_process_parent_execution_payload__new_builder_does_not_reuse_topped_up_ new_builder_index = None for i, builder in enumerate(state.builders): - if builder.pubkey == deposit_request_2.pubkey: + if builder.pubkey == builder_deposit_request_2.pubkey: new_builder_index = i break diff --git a/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_voluntary_exit.py b/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_voluntary_exit.py index 2375644c9b6..b898005f700 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_voluntary_exit.py +++ b/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_voluntary_exit.py @@ -1,5 +1,4 @@ from eth_consensus_specs.test.context import ( - always_bls, expect_assertion_error, spec_state_test, with_gloas_and_later, @@ -9,161 +8,25 @@ from eth_consensus_specs.test.helpers.voluntary_exits import sign_voluntary_exit -def advance_past_finalization(spec, state): - """Advance slots and finalize so that genesis-epoch builders become active.""" - epoch = spec.get_current_epoch(state) - next_slots(spec, state, spec.SLOTS_PER_EPOCH * 3) - state.finalized_checkpoint.epoch = epoch + 1 - - -@with_gloas_and_later -@spec_state_test -def test_builder_voluntary_exit__success(spec, state): - """Test successful builder voluntary exit with no pending balance.""" - builder_index = 0 - pubkey = state.builders[builder_index].pubkey - privkey = builder_pubkey_to_privkey[pubkey] - - advance_past_finalization(spec, state) - assert spec.is_active_builder(state, builder_index) - - assert spec.get_pending_balance_to_withdraw_for_builder(state, builder_index) == 0 - - current_epoch = spec.get_current_epoch(state) - - validator_index = spec.convert_builder_index_to_validator_index(builder_index) - voluntary_exit = spec.VoluntaryExit( - epoch=current_epoch, - validator_index=validator_index, - ) - signed_voluntary_exit = sign_voluntary_exit(spec, state, voluntary_exit, privkey) - - yield "pre", state - yield "voluntary_exit", signed_voluntary_exit - spec.process_voluntary_exit(state, signed_voluntary_exit) - yield "post", state - - assert not spec.is_active_builder(state, builder_index) - - expected_withdrawable = current_epoch + spec.config.MIN_BUILDER_WITHDRAWABILITY_DELAY - assert state.builders[builder_index].withdrawable_epoch == expected_withdrawable - - -@with_gloas_and_later -@spec_state_test -def test_builder_voluntary_exit__invalid__inactive_deposit_epoch(spec, state): - """Test that inactive builders cannot exit.""" - builder_index = 0 - pubkey = state.builders[builder_index].pubkey - privkey = builder_pubkey_to_privkey[pubkey] - - # Set builder's deposit epoch to a non-finalized epoch - state.builders[builder_index].deposit_epoch = spec.Epoch(1) - - advance_past_finalization(spec, state) - assert state.finalized_checkpoint.epoch == state.builders[builder_index].deposit_epoch - assert not spec.is_active_builder(state, builder_index) - - validator_index = spec.convert_builder_index_to_validator_index(builder_index) - voluntary_exit = spec.VoluntaryExit( - epoch=spec.get_current_epoch(state), - validator_index=validator_index, - ) - signed_voluntary_exit = sign_voluntary_exit(spec, state, voluntary_exit, privkey) - - yield "pre", state - yield "voluntary_exit", signed_voluntary_exit - expect_assertion_error(lambda: spec.process_voluntary_exit(state, signed_voluntary_exit)) - yield "post", None - - -@with_gloas_and_later -@spec_state_test -def test_builder_voluntary_exit__invalid__inactive_already_exited(spec, state): - """Test that already-exited builders cannot exit again.""" - builder_index = 0 - pubkey = state.builders[builder_index].pubkey - privkey = builder_pubkey_to_privkey[pubkey] - - # Set builder's withdrawable epoch which indicates it has initiated an exit - state.builders[builder_index].withdrawable_epoch = spec.get_current_epoch(state) + 10 - - advance_past_finalization(spec, state) - assert not spec.is_active_builder(state, builder_index) - - validator_index = spec.convert_builder_index_to_validator_index(builder_index) - voluntary_exit = spec.VoluntaryExit( - epoch=spec.get_current_epoch(state), - validator_index=validator_index, - ) - signed_voluntary_exit = sign_voluntary_exit(spec, state, voluntary_exit, privkey) - - yield "pre", state - yield "voluntary_exit", signed_voluntary_exit - expect_assertion_error(lambda: spec.process_voluntary_exit(state, signed_voluntary_exit)) - yield "post", None - - @with_gloas_and_later @spec_state_test -def test_builder_voluntary_exit__invalid__pending_withdrawal(spec, state): - """Test that builder cannot exit while having pending withdrawal.""" - builder_index = 0 - pubkey = state.builders[builder_index].pubkey - privkey = builder_pubkey_to_privkey[pubkey] +def test_voluntary_exit__invalid__builder_index(spec, state): + """ + Test that a voluntary exit targeting a builder index is invalid. - advance_past_finalization(spec, state) - assert spec.is_active_builder(state, builder_index) - - # Add pending withdrawal for this builder - withdrawal_amount = spec.MIN_ACTIVATION_BALANCE - withdrawal = spec.BuilderPendingWithdrawal( - fee_recipient=spec.ExecutionAddress(b"\x70" * 20), - amount=withdrawal_amount, - builder_index=builder_index, - ) - state.builder_pending_withdrawals.append(withdrawal) - pending_balance = spec.get_pending_balance_to_withdraw_for_builder(state, builder_index) - assert pending_balance == withdrawal_amount - - validator_index = spec.convert_builder_index_to_validator_index(builder_index) - voluntary_exit = spec.VoluntaryExit( - epoch=spec.get_current_epoch(state), - validator_index=validator_index, - ) - signed_voluntary_exit = sign_voluntary_exit(spec, state, voluntary_exit, privkey) - - yield "pre", state - yield "voluntary_exit", signed_voluntary_exit - expect_assertion_error(lambda: spec.process_voluntary_exit(state, signed_voluntary_exit)) - yield "post", None - - -@with_gloas_and_later -@spec_state_test -def test_builder_voluntary_exit__invalid__pending_payment(spec, state): - """Test that builder cannot exit while having pending payment.""" + The voluntary exit operation is validator-only. Builders exit via + builder exit requests. + """ builder_index = 0 pubkey = state.builders[builder_index].pubkey privkey = builder_pubkey_to_privkey[pubkey] - advance_past_finalization(spec, state) + # Advance slots and finalize so that genesis-epoch builders become active + epoch = spec.get_current_epoch(state) + next_slots(spec, state, spec.SLOTS_PER_EPOCH * 3) + state.finalized_checkpoint.epoch = epoch + 1 assert spec.is_active_builder(state, builder_index) - # Add pending payment for this builder - payment_amount = spec.MIN_ACTIVATION_BALANCE - payment = spec.BuilderPendingPayment( - weight=spec.get_builder_payment_quorum_threshold(state) + 1, - withdrawal=spec.BuilderPendingWithdrawal( - fee_recipient=spec.ExecutionAddress(b"\x60" * 20), - amount=payment_amount, - builder_index=builder_index, - ), - ) - state.builder_pending_payments[0] = payment - pending_balance = spec.get_pending_balance_to_withdraw_for_builder(state, builder_index) - assert pending_balance == payment_amount - validator_index = spec.convert_builder_index_to_validator_index(builder_index) voluntary_exit = spec.VoluntaryExit( epoch=spec.get_current_epoch(state), @@ -175,27 +38,3 @@ def test_builder_voluntary_exit__invalid__pending_payment(spec, state): yield "voluntary_exit", signed_voluntary_exit expect_assertion_error(lambda: spec.process_voluntary_exit(state, signed_voluntary_exit)) yield "post", None - - -@with_gloas_and_later -@spec_state_test -@always_bls -def test_builder_voluntary_exit__invalid__bad_signature(spec, state): - """Test builder voluntary exit with invalid signature.""" - builder_index = 0 - - advance_past_finalization(spec, state) - assert spec.is_active_builder(state, builder_index) - - validator_index = spec.convert_builder_index_to_validator_index(builder_index) - voluntary_exit = spec.VoluntaryExit( - epoch=spec.get_current_epoch(state), - validator_index=validator_index, - ) - # Use the wrong privkey so the signature is invalid - signed_voluntary_exit = sign_voluntary_exit(spec, state, voluntary_exit, 12345) - - yield "pre", state - yield "voluntary_exit", signed_voluntary_exit - expect_assertion_error(lambda: spec.process_voluntary_exit(state, signed_voluntary_exit)) - yield "post", None diff --git a/tests/core/pyspec/eth_consensus_specs/test/helpers/deposits.py b/tests/core/pyspec/eth_consensus_specs/test/helpers/deposits.py index d0bba8b2716..2c0c94f9869 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/helpers/deposits.py +++ b/tests/core/pyspec/eth_consensus_specs/test/helpers/deposits.py @@ -236,16 +236,33 @@ def prepare_deposit_request( ) +def sign_builder_deposit_request(spec, request, privkey): + """ + Sign the ``BuilderDepositMessage`` corresponding to the given request. + """ + deposit_message = spec.BuilderDepositMessage( + pubkey=request.pubkey, + version=request.version, + execution_address=request.execution_address, + amount=request.amount, + ) + domain = spec.compute_domain(spec.DOMAIN_DEPOSIT) + signing_root = spec.compute_signing_root(deposit_message, domain) + return bls.Sign(privkey, signing_root) + + def prepare_builder_deposit_request( spec, state, amount, pubkey=None, - withdrawal_credentials=None, + privkey=None, + version=None, + execution_address=None, signed=False, ): """ - Create a deposit request for a builder, depositing the given amount. + Create a builder deposit request, depositing the given amount. If pubkey is None, finds an unused keypair from builder_pubkeys for a new builder. If pubkey is provided, creates a top-up deposit for an existing builder. @@ -260,27 +277,26 @@ def prepare_builder_deposit_request( if pubkey is None: raise ValueError("No unused builder pubkeys available") - # Look up privkey from the pubkey->privkey map - privkey = builder_pubkey_to_privkey[pubkey] + if privkey is None: + # Look up privkey from the pubkey->privkey map + privkey = builder_pubkey_to_privkey[pubkey] - # Use builder withdrawal prefix (0x03) - if withdrawal_credentials is None: - withdrawal_credentials = ( - spec.BUILDER_WITHDRAWAL_PREFIX - + b"\x00" * 11 - + spec.hash(pubkey)[12:] # a 20-byte eth1 address derived from pubkey - ) + if version is None: + version = spec.uint8(0) - deposit_data = build_deposit_data( - spec, pubkey, privkey, amount, withdrawal_credentials, signed=signed - ) - return spec.DepositRequest( - pubkey=deposit_data.pubkey, - withdrawal_credentials=deposit_data.withdrawal_credentials, - amount=deposit_data.amount, - signature=deposit_data.signature, - index=spec.uint64(0), + if execution_address is None: + # A 20-byte eth1 address derived from the pubkey + execution_address = spec.ExecutionAddress(spec.hash(pubkey)[12:]) + + request = spec.BuilderDepositRequest( + pubkey=pubkey, + version=version, + execution_address=execution_address, + amount=amount, ) + if signed: + request.signature = sign_builder_deposit_request(spec, request, privkey) + return request def prepare_pending_deposit( diff --git a/tests/infra/helpers/deposit_requests.py b/tests/infra/helpers/deposit_requests.py index ac1755f54d2..cf971c02139 100644 --- a/tests/infra/helpers/deposit_requests.py +++ b/tests/infra/helpers/deposit_requests.py @@ -1,6 +1,9 @@ import pytest -from eth_consensus_specs.test.helpers.deposits import build_deposit_data +from eth_consensus_specs.test.helpers.deposits import ( + build_deposit_data, + sign_builder_deposit_request, +) from eth_consensus_specs.test.helpers.forks import is_post_gloas from eth_consensus_specs.test.helpers.keys import ( builder_pubkey_to_privkey, @@ -33,6 +36,20 @@ def run_deposit_request_processing(spec, state, deposit_request, valid=True): yield "post", None +def run_builder_deposit_request_processing(spec, state, builder_deposit_request): + """ + Run process_builder_deposit_request, yielding pre/post states for test vectors. + + The function never raises. Requests that fail a precondition are consumed + without changing the state. + """ + yield "pre", state + yield "builder_deposit_request", builder_deposit_request + + spec.process_builder_deposit_request(state, builder_deposit_request) + yield "post", state + + def prepare_process_deposit_request( spec, state, @@ -55,9 +72,10 @@ def prepare_process_deposit_request( The process_deposit_request function behavior varies by fork: - Electra/Fulu: Sets deposit_requests_start_index if UNSET, appends PendingDeposit - - Gloas+: For builder deposits (0x03 prefix or existing builder pubkey), applies - deposit immediately via apply_deposit_for_builder. For validator deposits, - appends PendingDeposit (does NOT set deposit_requests_start_index). + - Gloas+: Drops deposits with builder withdrawal credentials (0x03 prefix). + Builders are created and topped up only via BuilderDepositRequest. For + validator deposits, appends PendingDeposit (does NOT set + deposit_requests_start_index). Args: spec: The spec object. @@ -167,19 +185,76 @@ def prepare_process_deposit_request( return deposit_request -def _is_builder_deposit(spec, pre_state, deposit_request): - """Check if request routes to builder path under Gloas+ deposit routing rules.""" - if not is_post_gloas(spec): - return False - builder_pubkeys = {builder.pubkey for builder in pre_state.builders} - validator_pubkeys = {v.pubkey for v in pre_state.validators} - is_builder = deposit_request.pubkey in builder_pubkeys - is_validator = deposit_request.pubkey in validator_pubkeys - return is_builder or ( - spec.is_builder_withdrawal_credential(deposit_request.withdrawal_credentials) - and not is_validator - and not spec.is_pending_validator(pre_state.pending_deposits, deposit_request.pubkey) +def prepare_process_builder_deposit_request( + spec, + state, + advance_epochs=None, + builder_index=None, + pubkey=None, + version=None, + execution_address=None, + amount=None, + signed=False, + builders=None, + builder_modifications=None, +): + """ + Prepare a builder deposit request operation with configurable parameters. + + This helper creates a BuilderDepositRequest object and optionally modifies + state fields related to builder deposit request processing. The parameters + behave as in prepare_process_deposit_request, except that the request is + identified by version and execution_address instead of withdrawal + credentials, and the signature is over the BuilderDepositMessage. + """ + # Phase 1: Advance epochs if requested (before setup) + if advance_epochs is not None: + for _ in range(advance_epochs): + next_epoch(spec, state) + + # Phase 2: Derive effective values + index = builder_index if builder_index is not None else len(state.builders) + effective_pubkey = pubkey if pubkey is not None else builder_pubkeys[index] + effective_privkey = builder_pubkey_to_privkey[effective_pubkey] + effective_amount = amount if amount is not None else spec.MIN_ACTIVATION_BALANCE + effective_version = version if version is not None else spec.uint8(0) + if execution_address is not None: + effective_execution_address = execution_address + else: + # A 20-byte eth1 address derived from the pubkey + effective_execution_address = spec.ExecutionAddress(spec.hash(effective_pubkey)[12:]) + + # Phase 3: Apply state overrides (before creating request) + if builders is not None: + state.builders = builders + + if builder_modifications is not None: + current_epoch = spec.get_current_epoch(state) + for idx, mods in builder_modifications.items(): + if "withdrawable_epoch" in mods: + epoch_value = mods["withdrawable_epoch"] + # Support special string values for relative epochs + if epoch_value == "current_epoch": + epoch_value = current_epoch + elif epoch_value == "current_epoch-1": + epoch_value = current_epoch - 1 + elif epoch_value == "current_epoch+1": + epoch_value = current_epoch + 1 + state.builders[idx].withdrawable_epoch = epoch_value + if "balance" in mods: + state.builders[idx].balance = spec.Gwei(mods["balance"]) + + # Phase 4: Build the request and optionally sign + request = spec.BuilderDepositRequest( + pubkey=effective_pubkey, + version=effective_version, + execution_address=effective_execution_address, + amount=effective_amount, ) + if signed: + request.signature = sign_builder_deposit_request(spec, request, effective_privkey) + + return request def assert_process_deposit_request( @@ -213,7 +288,8 @@ def assert_process_deposit_request( - Validator count unchanged (validators created during epoch processing) - Balances unchanged (balance applied during epoch processing) - INVARIANT CHECKS FOR BUILDER DEPOSITS (Gloas+): + INVARIANT CHECKS FOR BUILDER DEPOSIT REQUESTS (Gloas+, pass + is_builder_deposit=True and a BuilderDepositRequest): - pending_deposits unchanged (builder deposits applied immediately) - Builder balance increases by deposit amount - New builder created if pubkey didn't exist @@ -236,7 +312,7 @@ def assert_process_deposit_request( pre_state: State before deposit request was processed deposit_request: The deposit request that was processed (required for invariant checks) state_unchanged: If True, asserts state equals pre_state (for rejected requests) - is_builder_deposit: If True, use builder deposit assertions. Auto-detected if None. + is_builder_deposit: If True, use builder deposit request assertions. Defaults to False. expected_deposit_requests_start_index: Expected value of deposit_requests_start_index expected_pending_deposit_pubkey: Expected pubkey of new pending deposit expected_pending_deposit_amount: Expected amount of new pending deposit @@ -258,9 +334,10 @@ def assert_process_deposit_request( # Invariant checks require deposit_request assert deposit_request is not None, "deposit_request required when state_unchanged=False" - # Auto-detect builder deposit if not specified + # Deposit requests never route to builders. Builder deposit assertions are + # used only when a BuilderDepositRequest is asserted explicitly. if is_builder_deposit is None: - is_builder_deposit = _is_builder_deposit(spec, pre_state, deposit_request) + is_builder_deposit = False if is_builder_deposit and is_post_gloas(spec): # BUILDER DEPOSIT ASSERTIONS (Gloas+) From be0f3351fddeb59291eb0a61b8a4779cea858b51 Mon Sep 17 00:00:00 2001 From: Justin Traglia Date: Fri, 12 Jun 2026 12:13:55 -0500 Subject: [PATCH 02/14] Go back to using withdrawal_credentials --- specs/gloas/beacon-chain.md | 82 ++++++++----------- specs/gloas/builder.md | 10 +-- specs/gloas/fork.md | 36 +++----- .../test_process_builder_deposit_request.py | 32 ++++---- .../test/helpers/deposits.py | 22 ++--- tests/infra/helpers/deposit_requests.py | 19 ++--- 6 files changed, 84 insertions(+), 117 deletions(-) diff --git a/specs/gloas/beacon-chain.md b/specs/gloas/beacon-chain.md index ad327a87307..ccb33c85ee1 100644 --- a/specs/gloas/beacon-chain.md +++ b/specs/gloas/beacon-chain.md @@ -25,7 +25,6 @@ - [`Builder`](#builder) - [`BuilderPendingPayment`](#builderpendingpayment) - [`BuilderPendingWithdrawal`](#builderpendingwithdrawal) - - [`BuilderDepositMessage`](#builderdepositmessage) - [`BuilderDepositRequest`](#builderdepositrequest) - [`BuilderExitRequest`](#builderexitrequest) - [`PayloadAttestationData`](#payloadattestationdata) @@ -101,9 +100,9 @@ - [Operations](#operations) - [Modified `process_operations`](#modified-process_operations) - [Builder deposit requests](#builder-deposit-requests) - - [New `is_valid_builder_deposit_signature`](#new-is_valid_builder_deposit_signature) - [New `get_index_for_new_builder`](#new-get_index_for_new_builder) - [New `add_builder_to_registry`](#new-add_builder_to_registry) + - [New `apply_deposit_for_builder`](#new-apply_deposit_for_builder) - [New `process_builder_deposit_request`](#new-process_builder_deposit_request) - [Builder exit requests](#builder-exit-requests) - [New `process_builder_exit_request`](#new-process_builder_exit_request) @@ -251,23 +250,12 @@ class BuilderPendingWithdrawal(Container): builder_index: BuilderIndex ``` -#### `BuilderDepositMessage` - -```python -class BuilderDepositMessage(Container): - pubkey: BLSPubkey - version: uint8 - execution_address: ExecutionAddress - amount: Gwei -``` - #### `BuilderDepositRequest` ```python class BuilderDepositRequest(Container): pubkey: BLSPubkey - version: uint8 - execution_address: ExecutionAddress + withdrawal_credentials: Bytes32 amount: Gwei signature: BLSSignature ``` @@ -1575,21 +1563,6 @@ def process_operations(state: BeaconState, body: BeaconBlockBody) -> None: ##### Builder deposit requests -###### New `is_valid_builder_deposit_signature` - -```python -def is_valid_builder_deposit_signature(request: BuilderDepositRequest) -> bool: - deposit_message = BuilderDepositMessage( - pubkey=request.pubkey, - version=request.version, - execution_address=request.execution_address, - amount=request.amount, - ) - domain = compute_domain(DOMAIN_DEPOSIT) - signing_root = compute_signing_root(deposit_message, domain) - return bls.Verify(request.pubkey, signing_root, request.signature) -``` - ###### New `get_index_for_new_builder` ```python @@ -1606,8 +1579,7 @@ def get_index_for_new_builder(state: BeaconState) -> BuilderIndex: def add_builder_to_registry( state: BeaconState, pubkey: BLSPubkey, - version: uint8, - execution_address: ExecutionAddress, + withdrawal_credentials: Bytes32, amount: uint64, slot: Slot, ) -> None: @@ -1616,8 +1588,8 @@ def add_builder_to_registry( get_index_for_new_builder(state), Builder( pubkey=pubkey, - version=version, - execution_address=execution_address, + version=uint8(withdrawal_credentials[0]), + execution_address=ExecutionAddress(withdrawal_credentials[12:]), balance=amount, deposit_epoch=compute_epoch_at_slot(slot), withdrawable_epoch=FAR_FUTURE_EPOCH, @@ -1625,7 +1597,7 @@ def add_builder_to_registry( ) ``` -###### New `process_builder_deposit_request` +###### New `apply_deposit_for_builder` *Note*: Builder indices are reusable. When a builder exits, its index may later be reassigned to a different builder with a new public key. Any deposit sent to @@ -1635,21 +1607,37 @@ may have previously appeared in the builder set. Implementations that rely on caching should account for this behavior. ```python -def process_builder_deposit_request(state: BeaconState, request: BuilderDepositRequest) -> None: +def apply_deposit_for_builder( + state: BeaconState, + pubkey: BLSPubkey, + withdrawal_credentials: Bytes32, + amount: uint64, + signature: BLSSignature, + slot: Slot, +) -> None: builder_pubkeys = [b.pubkey for b in state.builders] - if request.pubkey not in builder_pubkeys: - if is_valid_builder_deposit_signature(request): - add_builder_to_registry( - state, - request.pubkey, - request.version, - request.execution_address, - request.amount, - state.slot, - ) + if pubkey not in builder_pubkeys: + # Verify the deposit signature (proof of possession) which is not checked by the deposit contract + if is_valid_deposit_signature(pubkey, withdrawal_credentials, amount, signature): + add_builder_to_registry(state, pubkey, withdrawal_credentials, amount, slot) else: - builder_index = builder_pubkeys.index(request.pubkey) - state.builders[builder_index].balance += request.amount + # Increase balance by deposit amount + builder_index = builder_pubkeys.index(pubkey) + state.builders[builder_index].balance += amount +``` + +###### New `process_builder_deposit_request` + +```python +def process_builder_deposit_request(state: BeaconState, request: BuilderDepositRequest) -> None: + apply_deposit_for_builder( + state, + request.pubkey, + request.withdrawal_credentials, + request.amount, + request.signature, + state.slot, + ) ``` ##### Builder exit requests diff --git a/specs/gloas/builder.md b/specs/gloas/builder.md index f5bda48e631..d58db0f0bfe 100644 --- a/specs/gloas/builder.md +++ b/specs/gloas/builder.md @@ -41,12 +41,12 @@ deposit contract on the execution layer, as defined in [EIP-8282](https://eips.ethereum.org/EIPS/eip-8282). The request must include: - `pubkey`: The builder's BLS public key. -- `version`: The builder version, `0x00`. -- `execution_address`: The execution-layer address that will receive - withdrawals. +- `withdrawal_credentials`: The withdrawal credentials, where the first byte is + the builder version and the last 20 bytes are the execution-layer address that + will receive withdrawals. - `amount`: At least `MIN_DEPOSIT_AMOUNT` gwei. -- `signature`: BLS proof of possession over the corresponding - `BuilderDepositMessage` under `DOMAIN_DEPOSIT`. +- `signature`: BLS proof of possession over the corresponding `DepositMessage` + under `DOMAIN_DEPOSIT`. *Note*: Builders may be onboarded at the fork by submitting a deposit to the validator deposit contract with a 0x03 withdrawal credential. This must be done diff --git a/specs/gloas/fork.md b/specs/gloas/fork.md index 2095ac53a29..c2b95b84fb0 100644 --- a/specs/gloas/fork.md +++ b/specs/gloas/fork.md @@ -111,10 +111,6 @@ topped up only via `BuilderDepositRequest`. pending deposit signatures and cache the results. The pending deposit queue might be large and verifying many signatures at the fork could be slow. -*Note*: Builders onboarded at the fork are registered with a `version` of -`0x00`, rather than the `BUILDER_WITHDRAWAL_PREFIX` from their deposit's -withdrawal credentials. - ```python def onboard_builders_from_pending_deposits(state: BeaconState) -> None: """ @@ -130,8 +126,9 @@ def onboard_builders_from_pending_deposits(state: BeaconState) -> None: pending_deposits.append(deposit) continue - # Since deposits may add builders to the registry, the list of - # builder pubkeys must be recomputed each iteration. + # Note that the function apply_deposit_for_builder can mutate the + # state and may add a builder to the registry. For this reason, the + # list of builder pubkeys must be recomputed each iteration. builder_pubkeys = [b.pubkey for b in state.builders] # Deposits for non-builders stay in the pending queue. If there is a @@ -144,26 +141,15 @@ def onboard_builders_from_pending_deposits(state: BeaconState) -> None: if is_pending_validator(pending_deposits, deposit.pubkey): pending_deposits.append(deposit) continue - if not is_valid_deposit_signature( - deposit.pubkey, - deposit.withdrawal_credentials, - deposit.amount, - deposit.signature, - ): - continue - add_builder_to_registry( - state, - deposit.pubkey, - uint8(0x00), - ExecutionAddress(deposit.withdrawal_credentials[12:]), - deposit.amount, - deposit.slot, - ) - else: - # Increase balance by deposit amount - builder_index = builder_pubkeys.index(deposit.pubkey) - state.builders[builder_index].balance += deposit.amount + apply_deposit_for_builder( + state, + deposit.pubkey, + deposit.withdrawal_credentials, + deposit.amount, + deposit.signature, + deposit.slot, + ) state.pending_deposits = pending_deposits ``` diff --git a/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_builder_deposit_request.py b/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_builder_deposit_request.py index 9a7a683766e..5c3b0273e9d 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_builder_deposit_request.py +++ b/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_builder_deposit_request.py @@ -45,7 +45,9 @@ def run_builder_deposit_processing( deposit_request=builder_deposit_request, is_builder_deposit=True, expected_builder_balance=builder_deposit_request.amount, - expected_execution_address=builder_deposit_request.execution_address, + expected_execution_address=spec.ExecutionAddress( + builder_deposit_request.withdrawal_credentials[12:] + ), expected_builder_withdrawable_epoch=spec.FAR_FUTURE_EPOCH, ) else: @@ -82,13 +84,15 @@ def test_process_builder_deposit_request__new_builder_nonzero_version(spec, stat """ Test fresh builder deposit with a non-zero version. - The version is not constrained: it is recorded on the builder verbatim and - is committed to by the proof of possession. + The version (the first byte of the withdrawal credentials) is not + constrained: it is recorded on the builder verbatim and is committed to by + the proof of possession. """ amount = spec.MIN_DEPOSIT_AMOUNT version = spec.uint8(7) + withdrawal_credentials = bytes([version]) + b"\x00" * 11 + b"\x42" * 20 builder_deposit_request = prepare_builder_deposit_request( - spec, state, amount, version=version, signed=True + spec, state, amount, withdrawal_credentials=withdrawal_credentials, signed=True ) pre_state = state.copy() @@ -101,7 +105,9 @@ def test_process_builder_deposit_request__new_builder_nonzero_version(spec, stat deposit_request=builder_deposit_request, is_builder_deposit=True, expected_builder_balance=amount, - expected_execution_address=builder_deposit_request.execution_address, + expected_execution_address=spec.ExecutionAddress( + builder_deposit_request.withdrawal_credentials[12:] + ), expected_builder_withdrawable_epoch=spec.FAR_FUTURE_EPOCH, ) @@ -405,27 +411,25 @@ def test_process_builder_deposit_request__top_up_last_index(spec, state): @spec_state_test def test_process_builder_deposit_request__top_up_ignores_request_fields(spec, state): """ - Test that a top-up for an existing builder ignores the supplied version - and execution address. The existing registration is unchanged. + Test that a top-up for an existing builder ignores the supplied withdrawal + credentials. The existing registration is unchanged. """ builder_pubkey = state.builders[0].pubkey amount = spec.MIN_DEPOSIT_AMOUNT pre_balance = state.builders[0].balance pre_builder_count = len(state.builders) - # Use a version and execution address that differ from the registration - version = spec.uint8(7) - execution_address = spec.ExecutionAddress(b"\x42" * 20) - assert state.builders[0].version != version - assert state.builders[0].execution_address != execution_address + # Use withdrawal credentials that differ from the registration + withdrawal_credentials = b"\x07" + b"\x00" * 11 + b"\x42" * 20 + assert state.builders[0].version != spec.uint8(withdrawal_credentials[0]) + assert state.builders[0].execution_address != spec.ExecutionAddress(withdrawal_credentials[12:]) builder_deposit_request = prepare_builder_deposit_request( spec, state, amount, pubkey=builder_pubkey, - version=version, - execution_address=execution_address, + withdrawal_credentials=withdrawal_credentials, signed=True, ) pre_state = state.copy() diff --git a/tests/core/pyspec/eth_consensus_specs/test/helpers/deposits.py b/tests/core/pyspec/eth_consensus_specs/test/helpers/deposits.py index 2c0c94f9869..2b2ade76091 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/helpers/deposits.py +++ b/tests/core/pyspec/eth_consensus_specs/test/helpers/deposits.py @@ -238,12 +238,11 @@ def prepare_deposit_request( def sign_builder_deposit_request(spec, request, privkey): """ - Sign the ``BuilderDepositMessage`` corresponding to the given request. + Sign the ``DepositMessage`` corresponding to the given request. """ - deposit_message = spec.BuilderDepositMessage( + deposit_message = spec.DepositMessage( pubkey=request.pubkey, - version=request.version, - execution_address=request.execution_address, + withdrawal_credentials=request.withdrawal_credentials, amount=request.amount, ) domain = spec.compute_domain(spec.DOMAIN_DEPOSIT) @@ -257,8 +256,7 @@ def prepare_builder_deposit_request( amount, pubkey=None, privkey=None, - version=None, - execution_address=None, + withdrawal_credentials=None, signed=False, ): """ @@ -281,17 +279,13 @@ def prepare_builder_deposit_request( # Look up privkey from the pubkey->privkey map privkey = builder_pubkey_to_privkey[pubkey] - if version is None: - version = spec.uint8(0) - - if execution_address is None: - # A 20-byte eth1 address derived from the pubkey - execution_address = spec.ExecutionAddress(spec.hash(pubkey)[12:]) + if withdrawal_credentials is None: + # Version zero followed by an eth1 address derived from the pubkey + withdrawal_credentials = b"\x00" * 12 + spec.hash(pubkey)[12:] request = spec.BuilderDepositRequest( pubkey=pubkey, - version=version, - execution_address=execution_address, + withdrawal_credentials=withdrawal_credentials, amount=amount, ) if signed: diff --git a/tests/infra/helpers/deposit_requests.py b/tests/infra/helpers/deposit_requests.py index cf971c02139..ae52b6b6126 100644 --- a/tests/infra/helpers/deposit_requests.py +++ b/tests/infra/helpers/deposit_requests.py @@ -191,8 +191,7 @@ def prepare_process_builder_deposit_request( advance_epochs=None, builder_index=None, pubkey=None, - version=None, - execution_address=None, + withdrawal_credentials=None, amount=None, signed=False, builders=None, @@ -203,9 +202,7 @@ def prepare_process_builder_deposit_request( This helper creates a BuilderDepositRequest object and optionally modifies state fields related to builder deposit request processing. The parameters - behave as in prepare_process_deposit_request, except that the request is - identified by version and execution_address instead of withdrawal - credentials, and the signature is over the BuilderDepositMessage. + behave as in prepare_process_deposit_request. """ # Phase 1: Advance epochs if requested (before setup) if advance_epochs is not None: @@ -217,12 +214,11 @@ def prepare_process_builder_deposit_request( effective_pubkey = pubkey if pubkey is not None else builder_pubkeys[index] effective_privkey = builder_pubkey_to_privkey[effective_pubkey] effective_amount = amount if amount is not None else spec.MIN_ACTIVATION_BALANCE - effective_version = version if version is not None else spec.uint8(0) - if execution_address is not None: - effective_execution_address = execution_address + if withdrawal_credentials is not None: + effective_withdrawal_credentials = withdrawal_credentials else: - # A 20-byte eth1 address derived from the pubkey - effective_execution_address = spec.ExecutionAddress(spec.hash(effective_pubkey)[12:]) + # Version zero followed by an eth1 address derived from the pubkey + effective_withdrawal_credentials = b"\x00" * 12 + spec.hash(effective_pubkey)[12:] # Phase 3: Apply state overrides (before creating request) if builders is not None: @@ -247,8 +243,7 @@ def prepare_process_builder_deposit_request( # Phase 4: Build the request and optionally sign request = spec.BuilderDepositRequest( pubkey=effective_pubkey, - version=effective_version, - execution_address=effective_execution_address, + withdrawal_credentials=effective_withdrawal_credentials, amount=effective_amount, ) if signed: From c90980c7fb2644571035ce57d0c3fa00ab9ec970 Mon Sep 17 00:00:00 2001 From: Justin Traglia Date: Fri, 12 Jun 2026 13:52:11 -0500 Subject: [PATCH 03/14] Sign builder deposits with DOMAIN_BUILDER_DEPOSIT --- specs/_features/eip8025/beacon-chain.md | 2 +- specs/gloas/beacon-chain.md | 65 ++++++++++--------- specs/gloas/builder.md | 2 +- specs/gloas/fork.md | 31 +++++---- specs/heze/beacon-chain.md | 2 +- .../test_process_builder_deposit_request.py | 34 +++++++++- .../test_process_pending_deposits.py | 43 ++++++++++++ .../test/helpers/deposits.py | 2 +- 8 files changed, 134 insertions(+), 47 deletions(-) create mode 100644 tests/core/pyspec/eth_consensus_specs/test/gloas/epoch_processing/test_process_pending_deposits.py diff --git a/specs/_features/eip8025/beacon-chain.md b/specs/_features/eip8025/beacon-chain.md index d9222f76bfa..d9e2ae848b9 100644 --- a/specs/_features/eip8025/beacon-chain.md +++ b/specs/_features/eip8025/beacon-chain.md @@ -50,7 +50,7 @@ and imports proof types from [proof-engine.md](./proof-engine.md). | Name | Value | | ------------------------ | -------------------------- | -| `DOMAIN_EXECUTION_PROOF` | `DomainType('0x0E000000')` | +| `DOMAIN_EXECUTION_PROOF` | `DomainType('0x0F000000')` | ## Containers diff --git a/specs/gloas/beacon-chain.md b/specs/gloas/beacon-chain.md index ccb33c85ee1..9df1aa91f2e 100644 --- a/specs/gloas/beacon-chain.md +++ b/specs/gloas/beacon-chain.md @@ -100,9 +100,9 @@ - [Operations](#operations) - [Modified `process_operations`](#modified-process_operations) - [Builder deposit requests](#builder-deposit-requests) + - [New `is_valid_builder_deposit_signature`](#new-is_valid_builder_deposit_signature) - [New `get_index_for_new_builder`](#new-get_index_for_new_builder) - [New `add_builder_to_registry`](#new-add_builder_to_registry) - - [New `apply_deposit_for_builder`](#new-apply_deposit_for_builder) - [New `process_builder_deposit_request`](#new-process_builder_deposit_request) - [Builder exit requests](#builder-exit-requests) - [New `process_builder_exit_request`](#new-process_builder_exit_request) @@ -151,6 +151,7 @@ Gloas is a consensus-layer upgrade containing a number of features. Including: | `DOMAIN_BEACON_BUILDER` | `DomainType('0x0B000000')` | | `DOMAIN_PTC_ATTESTER` | `DomainType('0x0C000000')` | | `DOMAIN_PROPOSER_PREFERENCES` | `DomainType('0x0D000000')` | +| `DOMAIN_BUILDER_DEPOSIT` | `DomainType('0x0E000000')` | ### Misc @@ -1563,6 +1564,25 @@ def process_operations(state: BeaconState, body: BeaconBlockBody) -> None: ##### Builder deposit requests +###### New `is_valid_builder_deposit_signature` + +*Note*: Builder deposits are signed over the `DepositMessage` under +`DOMAIN_BUILDER_DEPOSIT`. The dedicated domain ensures that validator deposit +signatures and builder deposit signatures cannot be replayed against the other +deposit contract. + +```python +def is_valid_builder_deposit_signature(request: BuilderDepositRequest) -> bool: + deposit_message = DepositMessage( + pubkey=request.pubkey, + withdrawal_credentials=request.withdrawal_credentials, + amount=request.amount, + ) + domain = compute_domain(DOMAIN_BUILDER_DEPOSIT) + signing_root = compute_signing_root(deposit_message, domain) + return bls.Verify(request.pubkey, signing_root, request.signature) +``` + ###### New `get_index_for_new_builder` ```python @@ -1597,7 +1617,7 @@ def add_builder_to_registry( ) ``` -###### New `apply_deposit_for_builder` +###### New `process_builder_deposit_request` *Note*: Builder indices are reusable. When a builder exits, its index may later be reassigned to a different builder with a new public key. Any deposit sent to @@ -1607,37 +1627,20 @@ may have previously appeared in the builder set. Implementations that rely on caching should account for this behavior. ```python -def apply_deposit_for_builder( - state: BeaconState, - pubkey: BLSPubkey, - withdrawal_credentials: Bytes32, - amount: uint64, - signature: BLSSignature, - slot: Slot, -) -> None: +def process_builder_deposit_request(state: BeaconState, request: BuilderDepositRequest) -> None: builder_pubkeys = [b.pubkey for b in state.builders] - if pubkey not in builder_pubkeys: - # Verify the deposit signature (proof of possession) which is not checked by the deposit contract - if is_valid_deposit_signature(pubkey, withdrawal_credentials, amount, signature): - add_builder_to_registry(state, pubkey, withdrawal_credentials, amount, slot) + if request.pubkey not in builder_pubkeys: + if is_valid_builder_deposit_signature(request): + add_builder_to_registry( + state, + request.pubkey, + request.withdrawal_credentials, + request.amount, + state.slot, + ) else: - # Increase balance by deposit amount - builder_index = builder_pubkeys.index(pubkey) - state.builders[builder_index].balance += amount -``` - -###### New `process_builder_deposit_request` - -```python -def process_builder_deposit_request(state: BeaconState, request: BuilderDepositRequest) -> None: - apply_deposit_for_builder( - state, - request.pubkey, - request.withdrawal_credentials, - request.amount, - request.signature, - state.slot, - ) + builder_index = builder_pubkeys.index(request.pubkey) + state.builders[builder_index].balance += request.amount ``` ##### Builder exit requests diff --git a/specs/gloas/builder.md b/specs/gloas/builder.md index d58db0f0bfe..6a6eff8147f 100644 --- a/specs/gloas/builder.md +++ b/specs/gloas/builder.md @@ -46,7 +46,7 @@ deposit contract on the execution layer, as defined in will receive withdrawals. - `amount`: At least `MIN_DEPOSIT_AMOUNT` gwei. - `signature`: BLS proof of possession over the corresponding `DepositMessage` - under `DOMAIN_DEPOSIT`. + under `DOMAIN_BUILDER_DEPOSIT`. *Note*: Builders may be onboarded at the fork by submitting a deposit to the validator deposit contract with a 0x03 withdrawal credential. This must be done diff --git a/specs/gloas/fork.md b/specs/gloas/fork.md index c2b95b84fb0..e5cd11cb9cc 100644 --- a/specs/gloas/fork.md +++ b/specs/gloas/fork.md @@ -126,9 +126,9 @@ def onboard_builders_from_pending_deposits(state: BeaconState) -> None: pending_deposits.append(deposit) continue - # Note that the function apply_deposit_for_builder can mutate the - # state and may add a builder to the registry. For this reason, the - # list of builder pubkeys must be recomputed each iteration. + # Note that applying a deposit below can mutate the state and + # may add a builder to the registry. For this reason, the list + # of builder pubkeys must be recomputed each iteration. builder_pubkeys = [b.pubkey for b in state.builders] # Deposits for non-builders stay in the pending queue. If there is a @@ -141,15 +141,24 @@ def onboard_builders_from_pending_deposits(state: BeaconState) -> None: if is_pending_validator(pending_deposits, deposit.pubkey): pending_deposits.append(deposit) continue + if not is_valid_deposit_signature( + deposit.pubkey, + deposit.withdrawal_credentials, + deposit.amount, + deposit.signature, + ): + continue - apply_deposit_for_builder( - state, - deposit.pubkey, - deposit.withdrawal_credentials, - deposit.amount, - deposit.signature, - deposit.slot, - ) + add_builder_to_registry( + state, + deposit.pubkey, + deposit.withdrawal_credentials, + deposit.amount, + deposit.slot, + ) + else: + builder_index = builder_pubkeys.index(deposit.pubkey) + state.builders[builder_index].balance += deposit.amount state.pending_deposits = pending_deposits ``` diff --git a/specs/heze/beacon-chain.md b/specs/heze/beacon-chain.md index decfeb1867f..d85365a1db6 100644 --- a/specs/heze/beacon-chain.md +++ b/specs/heze/beacon-chain.md @@ -38,7 +38,7 @@ Heze is a consensus-layer upgrade containing a number of features. Including: | Name | Value | | --------------------------------- | -------------------------- | -| `DOMAIN_INCLUSION_LIST_COMMITTEE` | `DomainType('0x0E000000')` | +| `DOMAIN_INCLUSION_LIST_COMMITTEE` | `DomainType('0x0F000000')` | ## Preset diff --git a/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_builder_deposit_request.py b/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_builder_deposit_request.py index 5c3b0273e9d..3724e13116d 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_builder_deposit_request.py +++ b/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_builder_deposit_request.py @@ -1,6 +1,7 @@ from eth_consensus_specs.test.context import always_bls, spec_state_test, with_gloas_and_later from eth_consensus_specs.test.helpers.deposits import prepare_builder_deposit_request -from eth_consensus_specs.test.helpers.keys import privkeys, pubkeys +from eth_consensus_specs.test.helpers.keys import builder_pubkey_to_privkey, privkeys, pubkeys +from eth_consensus_specs.utils import bls from tests.infra.helpers.deposit_requests import ( assert_process_deposit_request, prepare_process_builder_deposit_request, @@ -469,6 +470,37 @@ def test_process_builder_deposit_request__new_builder_invalid_sig(spec, state): ) +@with_gloas_and_later +@spec_state_test +@always_bls +def test_process_builder_deposit_request__new_builder_replayed_validator_sig(spec, state): + """ + Test that a builder deposit signed under DOMAIN_DEPOSIT is dropped. + + A signature over the same DepositMessage under DOMAIN_DEPOSIT is a valid + validator deposit signature. It must not be accepted here, otherwise any + validator deposit could be replayed to the builder deposit contract to + register its pubkey as a builder. + """ + amount = spec.MIN_DEPOSIT_AMOUNT + builder_deposit_request = prepare_builder_deposit_request(spec, state, amount, signed=False) + + # Sign over the validator deposit domain instead of DOMAIN_BUILDER_DEPOSIT + deposit_message = spec.DepositMessage( + pubkey=builder_deposit_request.pubkey, + withdrawal_credentials=builder_deposit_request.withdrawal_credentials, + amount=builder_deposit_request.amount, + ) + domain = spec.compute_domain(spec.DOMAIN_DEPOSIT) + signing_root = spec.compute_signing_root(deposit_message, domain) + privkey = builder_pubkey_to_privkey[builder_deposit_request.pubkey] + builder_deposit_request.signature = bls.Sign(privkey, signing_root) + + yield from run_builder_deposit_processing( + spec, state, builder_deposit_request, is_new_builder=True, valid=False + ) + + @with_gloas_and_later @spec_state_test @always_bls diff --git a/tests/core/pyspec/eth_consensus_specs/test/gloas/epoch_processing/test_process_pending_deposits.py b/tests/core/pyspec/eth_consensus_specs/test/gloas/epoch_processing/test_process_pending_deposits.py new file mode 100644 index 00000000000..a4f0a010601 --- /dev/null +++ b/tests/core/pyspec/eth_consensus_specs/test/gloas/epoch_processing/test_process_pending_deposits.py @@ -0,0 +1,43 @@ +from eth_consensus_specs.test.context import always_bls, spec_state_test, with_gloas_and_later +from eth_consensus_specs.test.helpers.deposits import prepare_pending_deposit +from eth_consensus_specs.test.helpers.epoch_processing import run_epoch_processing_with +from eth_consensus_specs.test.helpers.keys import privkeys +from eth_consensus_specs.utils import bls + + +@with_gloas_and_later +@spec_state_test +@always_bls +def test_process_pending_deposits__builder_deposit_domain(spec, state): + """ + Test that a pending deposit signed under DOMAIN_BUILDER_DEPOSIT is dropped. + + A signature over the same DepositMessage under DOMAIN_BUILDER_DEPOSIT is a + valid builder deposit signature. It must not be accepted here, otherwise a + builder deposit could be replayed to the validator deposit contract to + register its pubkey as a validator. + """ + # A new validator, pubkey doesn't exist in the state + validator_index = len(state.validators) + amount = spec.MIN_ACTIVATION_BALANCE + pre_validator_count = len(state.validators) + + deposit = prepare_pending_deposit(spec, validator_index, amount, signed=False) + + # Sign over the builder deposit domain instead of DOMAIN_DEPOSIT + deposit_message = spec.DepositMessage( + pubkey=deposit.pubkey, + withdrawal_credentials=deposit.withdrawal_credentials, + amount=deposit.amount, + ) + domain = spec.compute_domain(spec.DOMAIN_BUILDER_DEPOSIT) + signing_root = spec.compute_signing_root(deposit_message, domain) + deposit.signature = bls.Sign(privkeys[validator_index], signing_root) + + state.pending_deposits.append(deposit) + + yield from run_epoch_processing_with(spec, state, "process_pending_deposits") + + # The deposit was dropped: no validator created and no balance applied + assert len(state.validators) == pre_validator_count + assert state.pending_deposits == [] diff --git a/tests/core/pyspec/eth_consensus_specs/test/helpers/deposits.py b/tests/core/pyspec/eth_consensus_specs/test/helpers/deposits.py index 2b2ade76091..84785e4f6ea 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/helpers/deposits.py +++ b/tests/core/pyspec/eth_consensus_specs/test/helpers/deposits.py @@ -245,7 +245,7 @@ def sign_builder_deposit_request(spec, request, privkey): withdrawal_credentials=request.withdrawal_credentials, amount=request.amount, ) - domain = spec.compute_domain(spec.DOMAIN_DEPOSIT) + domain = spec.compute_domain(spec.DOMAIN_BUILDER_DEPOSIT) signing_root = spec.compute_signing_root(deposit_message, domain) return bls.Sign(privkey, signing_root) From f1e91b96db8eaec1e08f4ab1dc5acc3c58c1e6cd Mon Sep 17 00:00:00 2001 From: Justin Traglia Date: Fri, 12 Jun 2026 13:58:43 -0500 Subject: [PATCH 04/14] Move BUILDER_WITHDRAWAL_PREFIX back to beacon-chain.md --- specs/gloas/beacon-chain.md | 11 +++++++++++ specs/gloas/fork.md | 14 -------------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/specs/gloas/beacon-chain.md b/specs/gloas/beacon-chain.md index 9df1aa91f2e..4e8875374d2 100644 --- a/specs/gloas/beacon-chain.md +++ b/specs/gloas/beacon-chain.md @@ -10,6 +10,7 @@ - [Index flags](#index-flags) - [Domains](#domains) - [Misc](#misc) + - [Withdrawal prefixes](#withdrawal-prefixes) - [Execution-layer triggered requests](#execution-layer-triggered-requests) - [Preset](#preset) - [Misc](#misc-1) @@ -161,6 +162,16 @@ Gloas is a consensus-layer upgrade containing a number of features. Including: | `BUILDER_PAYMENT_THRESHOLD_NUMERATOR` | `uint64(6)` | | | `BUILDER_PAYMENT_THRESHOLD_DENOMINATOR` | `uint64(10)` | | +### Withdrawal prefixes + +*Note*: `BUILDER_WITHDRAWAL_PREFIX` is a temporary constant which is only used +to onboard builders at the fork. It will be deprecated after the upgrade and a +future validator withdrawal prefix may reuse this value. + +| Name | Value | Description | +| --------------------------- | ---------------- | ------------------------------------------ | +| `BUILDER_WITHDRAWAL_PREFIX` | `Bytes1('0x03')` | Withdrawal credential prefix for a builder | + ### Execution-layer triggered requests | Name | Value | diff --git a/specs/gloas/fork.md b/specs/gloas/fork.md index e5cd11cb9cc..49fb2d0a419 100644 --- a/specs/gloas/fork.md +++ b/specs/gloas/fork.md @@ -5,8 +5,6 @@ - [Introduction](#introduction) -- [Constants](#constants) - - [Withdrawal prefixes](#withdrawal-prefixes) - [Configuration](#configuration) - [Helpers](#helpers) - [New `initialize_ptc_window`](#new-initialize_ptc_window) @@ -23,18 +21,6 @@ This document describes the process of the Gloas upgrade. -## Constants - -### Withdrawal prefixes - -*Note*: `BUILDER_WITHDRAWAL_PREFIX` is a temporary constant which is only used -to onboard builders at the fork. It will be deprecated after the upgrade and a -future validator withdrawal prefix may reuse this value. - -| Name | Value | Description | -| --------------------------- | ---------------- | ------------------------------------------ | -| `BUILDER_WITHDRAWAL_PREFIX` | `Bytes1('0x03')` | Withdrawal credential prefix for a builder | - ## Configuration Warning: this configuration is not definitive. From 54836675de3dab6067fa1ae45163d6c16aaf4358 Mon Sep 17 00:00:00 2001 From: Justin Traglia Date: Fri, 12 Jun 2026 14:11:15 -0500 Subject: [PATCH 05/14] Onboarded builders have a version of zero --- specs/gloas/beacon-chain.md | 10 ++++++---- specs/gloas/fork.md | 7 ++++++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/specs/gloas/beacon-chain.md b/specs/gloas/beacon-chain.md index 4e8875374d2..54b91014837 100644 --- a/specs/gloas/beacon-chain.md +++ b/specs/gloas/beacon-chain.md @@ -1610,7 +1610,8 @@ def get_index_for_new_builder(state: BeaconState) -> BuilderIndex: def add_builder_to_registry( state: BeaconState, pubkey: BLSPubkey, - withdrawal_credentials: Bytes32, + version: uint8, + execution_address: ExecutionAddress, amount: uint64, slot: Slot, ) -> None: @@ -1619,8 +1620,8 @@ def add_builder_to_registry( get_index_for_new_builder(state), Builder( pubkey=pubkey, - version=uint8(withdrawal_credentials[0]), - execution_address=ExecutionAddress(withdrawal_credentials[12:]), + version=version, + execution_address=execution_address, balance=amount, deposit_epoch=compute_epoch_at_slot(slot), withdrawable_epoch=FAR_FUTURE_EPOCH, @@ -1645,7 +1646,8 @@ def process_builder_deposit_request(state: BeaconState, request: BuilderDepositR add_builder_to_registry( state, request.pubkey, - request.withdrawal_credentials, + uint8(request.withdrawal_credentials[0]), + ExecutionAddress(request.withdrawal_credentials[12:]), request.amount, state.slot, ) diff --git a/specs/gloas/fork.md b/specs/gloas/fork.md index 49fb2d0a419..0be8a6cd65e 100644 --- a/specs/gloas/fork.md +++ b/specs/gloas/fork.md @@ -97,6 +97,10 @@ topped up only via `BuilderDepositRequest`. pending deposit signatures and cache the results. The pending deposit queue might be large and verifying many signatures at the fork could be slow. +*Note*: Builders onboarded at the fork are registered with a `version` of zero, +rather than the `BUILDER_WITHDRAWAL_PREFIX` from their deposit's withdrawal +credentials. + ```python def onboard_builders_from_pending_deposits(state: BeaconState) -> None: """ @@ -138,7 +142,8 @@ def onboard_builders_from_pending_deposits(state: BeaconState) -> None: add_builder_to_registry( state, deposit.pubkey, - deposit.withdrawal_credentials, + uint8(0), # The version is hardcoded to zero + ExecutionAddress(deposit.withdrawal_credentials[12:]), deposit.amount, deposit.slot, ) From b78e143438c952621ec7a7d08374ba033126ac96 Mon Sep 17 00:00:00 2001 From: Justin Traglia Date: Fri, 12 Jun 2026 14:22:40 -0500 Subject: [PATCH 06/14] Move helper functions back to beacon-chain.md --- specs/gloas/beacon-chain.md | 32 ++++++++++++++++++++++++++++++++ specs/gloas/fork.md | 32 -------------------------------- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/specs/gloas/beacon-chain.md b/specs/gloas/beacon-chain.md index 54b91014837..b70c658b1d2 100644 --- a/specs/gloas/beacon-chain.md +++ b/specs/gloas/beacon-chain.md @@ -48,8 +48,10 @@ - [Predicates](#predicates) - [New `is_builder_index`](#new-is_builder_index) - [New `is_active_builder`](#new-is_active_builder) + - [New `is_builder_withdrawal_credential`](#new-is_builder_withdrawal_credential) - [New `is_attestation_same_slot`](#new-is_attestation_same_slot) - [New `is_valid_indexed_payload_attestation`](#new-is_valid_indexed_payload_attestation) + - [New `is_pending_validator`](#new-is_pending_validator) - [Misc](#misc-2) - [New `convert_builder_index_to_validator_index`](#new-convert_builder_index_to_validator_index) - [New `convert_validator_index_to_builder_index`](#new-convert_validator_index_to_builder_index) @@ -543,6 +545,13 @@ def is_active_builder(state: BeaconState, builder_index: BuilderIndex) -> bool: ) ``` +#### New `is_builder_withdrawal_credential` + +```python +def is_builder_withdrawal_credential(withdrawal_credentials: Bytes32) -> bool: + return withdrawal_credentials[:1] == BUILDER_WITHDRAWAL_PREFIX +``` + #### New `is_attestation_same_slot` ```python @@ -582,6 +591,29 @@ def is_valid_indexed_payload_attestation( return bls.FastAggregateVerify(pubkeys, signing_root, attestation.signature) ``` +#### New `is_pending_validator` + +*Note*: This function naively revalidates deposit signatures on every call. +Implementations SHOULD cache verification results to avoid repeated work. + +```python +def is_pending_validator(pending_deposits: Sequence[PendingDeposit], pubkey: BLSPubkey) -> bool: + """ + Check if a pending deposit with a valid signature is in the queue for the given pubkey. + """ + for pending_deposit in pending_deposits: + if pending_deposit.pubkey != pubkey: + continue + if is_valid_deposit_signature( + pending_deposit.pubkey, + pending_deposit.withdrawal_credentials, + pending_deposit.amount, + pending_deposit.signature, + ): + return True + return False +``` + ### Misc #### New `convert_builder_index_to_validator_index` diff --git a/specs/gloas/fork.md b/specs/gloas/fork.md index 0be8a6cd65e..4f7ba663560 100644 --- a/specs/gloas/fork.md +++ b/specs/gloas/fork.md @@ -8,8 +8,6 @@ - [Configuration](#configuration) - [Helpers](#helpers) - [New `initialize_ptc_window`](#new-initialize_ptc_window) - - [New `is_builder_withdrawal_credential`](#new-is_builder_withdrawal_credential) - - [New `is_pending_validator`](#new-is_pending_validator) - [New `onboard_builders_from_pending_deposits`](#new-onboard_builders_from_pending_deposits) - [Fork to Gloas](#fork-to-gloas) - [Fork trigger](#fork-trigger) @@ -57,36 +55,6 @@ def initialize_ptc_window( return empty_previous_epoch + ptcs ``` -### New `is_builder_withdrawal_credential` - -```python -def is_builder_withdrawal_credential(withdrawal_credentials: Bytes32) -> bool: - return withdrawal_credentials[:1] == BUILDER_WITHDRAWAL_PREFIX -``` - -### New `is_pending_validator` - -*Note*: This function naively revalidates deposit signatures on every call. -Implementations SHOULD cache verification results to avoid repeated work. - -```python -def is_pending_validator(pending_deposits: Sequence[PendingDeposit], pubkey: BLSPubkey) -> bool: - """ - Check if a pending deposit with a valid signature is in the queue for the given pubkey. - """ - for pending_deposit in pending_deposits: - if pending_deposit.pubkey != pubkey: - continue - if is_valid_deposit_signature( - pending_deposit.pubkey, - pending_deposit.withdrawal_credentials, - pending_deposit.amount, - pending_deposit.signature, - ): - return True - return False -``` - ### New `onboard_builders_from_pending_deposits` *Note*: This one-time onboarding is the only path through the validator deposit From a9077f3753f9984c91334382b9be7be0b7dd3b76 Mon Sep 17 00:00:00 2001 From: Justin Traglia Date: Fri, 12 Jun 2026 14:31:29 -0500 Subject: [PATCH 07/14] Keep version from withdrawal credentials for now --- specs/gloas/fork.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/specs/gloas/fork.md b/specs/gloas/fork.md index 4f7ba663560..4abd3e8e7d8 100644 --- a/specs/gloas/fork.md +++ b/specs/gloas/fork.md @@ -65,10 +65,6 @@ topped up only via `BuilderDepositRequest`. pending deposit signatures and cache the results. The pending deposit queue might be large and verifying many signatures at the fork could be slow. -*Note*: Builders onboarded at the fork are registered with a `version` of zero, -rather than the `BUILDER_WITHDRAWAL_PREFIX` from their deposit's withdrawal -credentials. - ```python def onboard_builders_from_pending_deposits(state: BeaconState) -> None: """ @@ -110,7 +106,7 @@ def onboard_builders_from_pending_deposits(state: BeaconState) -> None: add_builder_to_registry( state, deposit.pubkey, - uint8(0), # The version is hardcoded to zero + uint8(deposit.withdrawal_credentials[0]), ExecutionAddress(deposit.withdrawal_credentials[12:]), deposit.amount, deposit.slot, From 4ac82510f6438b6066086d760ebc48288d5fc7a5 Mon Sep 17 00:00:00 2001 From: Justin Traglia Date: Mon, 15 Jun 2026 18:05:18 +0200 Subject: [PATCH 08/14] Set MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD to 256 --- presets/mainnet/gloas.yaml | 4 ++-- presets/minimal/gloas.yaml | 4 ++-- specs/gloas/beacon-chain.md | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/presets/mainnet/gloas.yaml b/presets/mainnet/gloas.yaml index 884b539b8e6..a748aae6d12 100644 --- a/presets/mainnet/gloas.yaml +++ b/presets/mainnet/gloas.yaml @@ -12,8 +12,8 @@ MAX_PAYLOAD_ATTESTATIONS: 4 # Execution # --------------------------------------------------------------- -# 2**4 (= 16) builder deposit requests -MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD: 16 +# 2**8 (= 256) builder deposit requests +MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD: 256 # 2**4 (= 16) builder exit requests MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD: 16 diff --git a/presets/minimal/gloas.yaml b/presets/minimal/gloas.yaml index 1dbbc7c65c1..8b606894149 100644 --- a/presets/minimal/gloas.yaml +++ b/presets/minimal/gloas.yaml @@ -12,8 +12,8 @@ MAX_PAYLOAD_ATTESTATIONS: 4 # Execution # --------------------------------------------------------------- -# 2**4 (= 16) builder deposit requests -MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD: 16 +# 2**8 (= 256) builder deposit requests +MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD: 256 # 2**4 (= 16) builder exit requests MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD: 16 diff --git a/specs/gloas/beacon-chain.md b/specs/gloas/beacon-chain.md index b70c658b1d2..9e20fe81751 100644 --- a/specs/gloas/beacon-chain.md +++ b/specs/gloas/beacon-chain.md @@ -197,10 +197,10 @@ future validator withdrawal prefix may reuse this value. ### Execution -| Name | Value | Description | -| ------------------------------------------ | --------------------- | ---------------------------------------------------------- | -| `MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD` | `uint64(2**4)` (= 16) | Maximum number of builder deposit requests in each payload | -| `MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD` | `uint64(2**4)` (= 16) | Maximum number of builder exit requests in each payload | +| Name | Value | Description | +| ------------------------------------------ | ---------------------- | ---------------------------------------------------------- | +| `MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD` | `uint64(2**8)` (= 256) | Maximum number of builder deposit requests in each payload | +| `MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD` | `uint64(2**4)` (= 16) | Maximum number of builder exit requests in each payload | ### State list lengths From ec21b6e3ae76bb326e4fc5f0a2b481f94df82c11 Mon Sep 17 00:00:00 2001 From: Justin Traglia Date: Thu, 18 Jun 2026 22:01:23 +0200 Subject: [PATCH 09/14] Cast index to BuilderIndex --- specs/gloas/fork.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/gloas/fork.md b/specs/gloas/fork.md index 76bf9b6884a..1083af7ee10 100644 --- a/specs/gloas/fork.md +++ b/specs/gloas/fork.md @@ -112,7 +112,7 @@ def onboard_builders_from_pending_deposits(state: BeaconState) -> None: deposit.slot, ) else: - builder_index = builder_pubkeys.index(deposit.pubkey) + builder_index = BuilderIndex(builder_pubkeys.index(deposit.pubkey)) state.builders[builder_index].balance += deposit.amount state.pending_deposits = pending_deposits From 347f9b76b2e34a8db6b74ce52d419f986b3bfb64 Mon Sep 17 00:00:00 2001 From: Justin Traglia Date: Thu, 18 Jun 2026 22:19:17 +0200 Subject: [PATCH 10/14] Update operations test format --- tests/formats/operations/README.md | 38 ++++++++++++++++-------------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/tests/formats/operations/README.md b/tests/formats/operations/README.md index e783d32ec1f..1b6e413d2f7 100644 --- a/tests/formats/operations/README.md +++ b/tests/formats/operations/README.md @@ -34,24 +34,26 @@ the block-transition. Operations: -| *`operation-name`* | *`operation-object`* | *`input name`* | *`processing call`* | -| -------------------------- | ---------------------------- | ----------------------- | ------------------------------------------------------------------------------ | -| `attestation` | `Attestation` | `attestation` | `process_attestation(state, attestation)` | -| `attester_slashing` | `AttesterSlashing` | `attester_slashing` | `process_attester_slashing(state, attester_slashing)` | -| `block_header` | `BeaconBlock` | **`block`** | `process_block_header(state, block)` | -| `deposit` | `Deposit` | `deposit` | `process_deposit(state, deposit)` (removed in Fulu) | -| `proposer_slashing` | `ProposerSlashing` | `proposer_slashing` | `process_proposer_slashing(state, proposer_slashing)` | -| `voluntary_exit` | `SignedVoluntaryExit` | `voluntary_exit` | `process_voluntary_exit(state, voluntary_exit)` | -| `sync_aggregate` | `SyncAggregate` | `sync_aggregate` | `process_sync_aggregate(state, sync_aggregate)` (new in Altair) | -| `execution_payload` | `BeaconBlockBody` | **`body`** | `process_execution_payload(state, body)` (new in Bellatrix, removed in Gloas) | -| `withdrawals` | `ExecutionPayload` | `execution_payload` | `process_withdrawals(state, execution_payload)` (new in Capella) | -| `bls_to_execution_change` | `SignedBLSToExecutionChange` | `address_change` | `process_bls_to_execution_change(state, address_change)` (new in Capella) | -| `deposit_request` | `DepositRequest` | `deposit_request` | `process_deposit_request(state, deposit_request)` (new in Electra) | -| `withdrawal_request` | `WithdrawalRequest` | `withdrawal_request` | `process_withdrawal_request(state, withdrawal_request)` (new in Electra) | -| `consolidation_request` | `ConsolidationRequest` | `consolidation_request` | `process_consolidation_request(state, consolidation_request)` (new in Electra) | -| `execution_payload_bid` | `SignedExecutionPayloadBid` | `execution_payload_bid` | `process_execution_payload_bid(state, execution_payload_bid)` (new in Gloas) | -| `parent_execution_payload` | `BeaconBlock` | **`block`** | `process_parent_execution_payload(state, block)` (new in Gloas) | -| `payload_attestation` | `PayloadAttestation` | `payload_attestation` | `process_payload_attestation(state, payload_attestation)` (new in Gloas) | +| *`operation-name`* | *`operation-object`* | *`input name`* | *`processing call`* | +| -------------------------- | ---------------------------- | ------------------------- | -------------------------------------------------------------------------------- | +| `attestation` | `Attestation` | `attestation` | `process_attestation(state, attestation)` | +| `attester_slashing` | `AttesterSlashing` | `attester_slashing` | `process_attester_slashing(state, attester_slashing)` | +| `block_header` | `BeaconBlock` | **`block`** | `process_block_header(state, block)` | +| `deposit` | `Deposit` | `deposit` | `process_deposit(state, deposit)` (removed in Fulu) | +| `proposer_slashing` | `ProposerSlashing` | `proposer_slashing` | `process_proposer_slashing(state, proposer_slashing)` | +| `voluntary_exit` | `SignedVoluntaryExit` | `voluntary_exit` | `process_voluntary_exit(state, voluntary_exit)` | +| `sync_aggregate` | `SyncAggregate` | `sync_aggregate` | `process_sync_aggregate(state, sync_aggregate)` (new in Altair) | +| `execution_payload` | `BeaconBlockBody` | **`body`** | `process_execution_payload(state, body)` (new in Bellatrix, removed in Gloas) | +| `withdrawals` | `ExecutionPayload` | `execution_payload` | `process_withdrawals(state, execution_payload)` (new in Capella) | +| `bls_to_execution_change` | `SignedBLSToExecutionChange` | `address_change` | `process_bls_to_execution_change(state, address_change)` (new in Capella) | +| `deposit_request` | `DepositRequest` | `deposit_request` | `process_deposit_request(state, deposit_request)` (new in Electra) | +| `withdrawal_request` | `WithdrawalRequest` | `withdrawal_request` | `process_withdrawal_request(state, withdrawal_request)` (new in Electra) | +| `consolidation_request` | `ConsolidationRequest` | `consolidation_request` | `process_consolidation_request(state, consolidation_request)` (new in Electra) | +| `execution_payload_bid` | `SignedExecutionPayloadBid` | `execution_payload_bid` | `process_execution_payload_bid(state, execution_payload_bid)` (new in Gloas) | +| `parent_execution_payload` | `BeaconBlock` | **`block`** | `process_parent_execution_payload(state, block)` (new in Gloas) | +| `payload_attestation` | `PayloadAttestation` | `payload_attestation` | `process_payload_attestation(state, payload_attestation)` (new in Gloas) | +| `builder_deposit_request` | `BuilderDepositRequest` | `builder_deposit_request` | `process_builder_deposit_request(state, builder_deposit_request)` (new in Gloas) | +| `builder_exit_request` | `BuilderExitRequest` | `builder_exit_request` | `process_builder_exit_request(state, builder_exit_request)` (new in Gloas) | Note that `block_header` is not strictly an operation (and is a full `Block`), but processed in the same manner, and hence included here. From 8a73ad0aedaa3f97004dfa357cbf1440b06e8049 Mon Sep 17 00:00:00 2001 From: Justin Traglia Date: Thu, 18 Jun 2026 22:33:48 +0200 Subject: [PATCH 11/14] Update DOMAIN_INCLUSION_LIST_COMMITTEE --- specs/heze/beacon-chain.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/heze/beacon-chain.md b/specs/heze/beacon-chain.md index 8da5f619145..21bf4f40c85 100644 --- a/specs/heze/beacon-chain.md +++ b/specs/heze/beacon-chain.md @@ -34,7 +34,7 @@ Heze is a consensus-layer upgrade containing a number of features. Including: | Name | Value | | --------------------------------- | -------------------------- | -| `DOMAIN_INCLUSION_LIST_COMMITTEE` | `DomainType('0x0F000000')` | +| `DOMAIN_INCLUSION_LIST_COMMITTEE` | `DomainType('0x10000000')` | ## Preset From c15bd91e3693b3006f050fd1c9048019f47f3a9f Mon Sep 17 00:00:00 2001 From: Justin Traglia Date: Thu, 18 Jun 2026 23:24:48 +0200 Subject: [PATCH 12/14] Improve testing --- .../test_process_builder_deposit_request.py | 96 ++-- .../test_process_deposit_request.py | 21 +- .../test_process_parent_execution_payload.py | 2 +- .../infra/helpers/builder_deposit_requests.py | 263 +++++++++ tests/infra/helpers/deposit_requests.py | 519 +++--------------- tests/infra/helpers/test_deposit_requests.py | 144 +---- 6 files changed, 421 insertions(+), 624 deletions(-) create mode 100644 tests/infra/helpers/builder_deposit_requests.py diff --git a/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_builder_deposit_request.py b/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_builder_deposit_request.py index fc892543ee9..1581219672e 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_builder_deposit_request.py +++ b/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_builder_deposit_request.py @@ -2,8 +2,8 @@ from eth_consensus_specs.test.helpers.deposits import prepare_builder_deposit_request from eth_consensus_specs.test.helpers.keys import builder_pubkey_to_privkey, privkeys, pubkeys from eth_consensus_specs.utils import bls -from tests.infra.helpers.deposit_requests import ( - assert_process_deposit_request, +from tests.infra.helpers.builder_deposit_requests import ( + assert_process_builder_deposit_request, prepare_process_builder_deposit_request, run_builder_deposit_request_processing, ) @@ -31,20 +31,20 @@ def run_builder_deposit_processing( if not valid: # Invalid deposit should not change state (builder not created) - assert_process_deposit_request( + assert_process_builder_deposit_request( spec, state, pre_state, + builder_deposit_request=builder_deposit_request, state_unchanged=True, ) elif is_new_builder: # New builder should be added to registry - assert_process_deposit_request( + assert_process_builder_deposit_request( spec, state, pre_state, - deposit_request=builder_deposit_request, - is_builder_deposit=True, + builder_deposit_request=builder_deposit_request, expected_builder_balance=builder_deposit_request.amount, expected_execution_address=spec.ExecutionAddress( builder_deposit_request.withdrawal_credentials[12:] @@ -53,12 +53,11 @@ def run_builder_deposit_processing( ) else: # Top-up should increase balance - assert_process_deposit_request( + assert_process_builder_deposit_request( spec, state, pre_state, - deposit_request=builder_deposit_request, - is_builder_deposit=True, + builder_deposit_request=builder_deposit_request, expected_builder_count=pre_builder_count, expected_builder_balance_delta=builder_deposit_request.amount, ) @@ -99,12 +98,11 @@ def test_process_builder_deposit_request__new_builder_nonzero_version(spec, stat yield from run_builder_deposit_request_processing(spec, state, builder_deposit_request) - assert_process_deposit_request( + assert_process_builder_deposit_request( spec, state, pre_state, - deposit_request=builder_deposit_request, - is_builder_deposit=True, + builder_deposit_request=builder_deposit_request, expected_builder_balance=amount, expected_execution_address=spec.ExecutionAddress( builder_deposit_request.withdrawal_credentials[12:] @@ -160,12 +158,11 @@ def test_process_builder_deposit_request__new_builder_extra_gwei(spec, state): yield from run_builder_deposit_request_processing(spec, state, builder_deposit_request) - assert_process_deposit_request( + assert_process_builder_deposit_request( spec, state, pre_state, - deposit_request=builder_deposit_request, - is_builder_deposit=True, + builder_deposit_request=builder_deposit_request, expected_builder_balance=amount, ) @@ -192,12 +189,11 @@ def test_process_builder_deposit_request__new_builder_max_minus_one(spec, state) yield from run_builder_deposit_request_processing(spec, state, builder_deposit_request) - assert_process_deposit_request( + assert_process_builder_deposit_request( spec, state, pre_state, - deposit_request=builder_deposit_request, - is_builder_deposit=True, + builder_deposit_request=builder_deposit_request, expected_builder_balance=amount, ) @@ -224,12 +220,11 @@ def test_process_builder_deposit_request__new_builder_empty_registry(spec, state yield from run_builder_deposit_request_processing(spec, state, builder_deposit_request) - assert_process_deposit_request( + assert_process_builder_deposit_request( spec, state, pre_state, - deposit_request=builder_deposit_request, - is_builder_deposit=True, + builder_deposit_request=builder_deposit_request, expected_builder_count=1, expected_builder_index=0, expected_builder_balance=amount, @@ -257,12 +252,11 @@ def test_process_builder_deposit_request__new_builder_pubkey_is_validator(spec, yield from run_builder_deposit_request_processing(spec, state, builder_deposit_request) - assert_process_deposit_request( + assert_process_builder_deposit_request( spec, state, pre_state, - deposit_request=builder_deposit_request, - is_builder_deposit=True, + builder_deposit_request=builder_deposit_request, expected_builder_count=pre_builder_count + 1, expected_builder_balance=amount, ) @@ -299,12 +293,11 @@ def test_process_builder_deposit_request__top_up(spec, state): yield from run_builder_deposit_request_processing(spec, state, builder_deposit_request) - assert_process_deposit_request( + assert_process_builder_deposit_request( spec, state, pre_state, - deposit_request=builder_deposit_request, - is_builder_deposit=True, + builder_deposit_request=builder_deposit_request, expected_builder_count=pre_builder_count, expected_builder_balance=pre_balance + amount, ) @@ -357,12 +350,11 @@ def test_process_builder_deposit_request__top_up_single_builder(spec, state): yield from run_builder_deposit_request_processing(spec, state, builder_deposit_request) - assert_process_deposit_request( + assert_process_builder_deposit_request( spec, state, pre_state, - deposit_request=builder_deposit_request, - is_builder_deposit=True, + builder_deposit_request=builder_deposit_request, expected_builder_count=1, expected_builder_balance=pre_balance + amount, ) @@ -380,7 +372,7 @@ def test_process_builder_deposit_request__top_up_last_index(spec, state): Output State Verified: - Last builder's balance increased - - Other builders unchanged (verified by assert_process_deposit_request invariant) + - Other builders unchanged (verified by assert_process_builder_deposit_request invariant) """ last_index = len(state.builders) - 1 pubkey = state.builders[last_index].pubkey @@ -395,13 +387,12 @@ def test_process_builder_deposit_request__top_up_last_index(spec, state): yield from run_builder_deposit_request_processing(spec, state, builder_deposit_request) - # assert_process_deposit_request verifies other builders unchanged for top-ups - assert_process_deposit_request( + # assert_process_builder_deposit_request verifies other builders unchanged for top-ups + assert_process_builder_deposit_request( spec, state, pre_state, - deposit_request=builder_deposit_request, - is_builder_deposit=True, + builder_deposit_request=builder_deposit_request, expected_builder_count=pre_builder_count, expected_builder_index=last_index, expected_builder_balance=pre_balance + amount, @@ -438,12 +429,11 @@ def test_process_builder_deposit_request__top_up_ignores_request_fields(spec, st yield from run_builder_deposit_request_processing(spec, state, builder_deposit_request) # Should top up the existing builder (other request fields are ignored) - assert_process_deposit_request( + assert_process_builder_deposit_request( spec, state, pre_state, - deposit_request=builder_deposit_request, - is_builder_deposit=True, + builder_deposit_request=builder_deposit_request, expected_builder_count=pre_builder_count, expected_builder_balance=pre_balance + amount, ) @@ -543,12 +533,11 @@ def test_process_builder_deposit_request__reuses_exited_builder_slot(spec, state yield from run_builder_deposit_request_processing(spec, state, builder_deposit_request) - assert_process_deposit_request( + assert_process_builder_deposit_request( spec, state, pre_state, - deposit_request=builder_deposit_request, - is_builder_deposit=True, + builder_deposit_request=builder_deposit_request, slot_reused=True, expected_builder_count=pre_builder_count, expected_builder_index=0, @@ -589,12 +578,11 @@ def test_process_builder_deposit_request__reuses_first_of_multiple_exited_slots( yield from run_builder_deposit_request_processing(spec, state, builder_deposit_request) - assert_process_deposit_request( + assert_process_builder_deposit_request( spec, state, pre_state, - deposit_request=builder_deposit_request, - is_builder_deposit=True, + builder_deposit_request=builder_deposit_request, slot_reused=True, expected_builder_count=pre_builder_count, expected_builder_index=0, @@ -629,12 +617,11 @@ def test_process_builder_deposit_request__reuses_slot_at_current_epoch(spec, sta yield from run_builder_deposit_request_processing(spec, state, builder_deposit_request) - assert_process_deposit_request( + assert_process_builder_deposit_request( spec, state, pre_state, - deposit_request=builder_deposit_request, - is_builder_deposit=True, + builder_deposit_request=builder_deposit_request, slot_reused=True, expected_builder_count=pre_builder_count, expected_builder_index=0, @@ -671,12 +658,11 @@ def test_process_builder_deposit_request__no_reuse_future_epoch(spec, state): yield from run_builder_deposit_request_processing(spec, state, builder_deposit_request) # slot_reused=False also verifies original builders are unchanged - assert_process_deposit_request( + assert_process_builder_deposit_request( spec, state, pre_state, - deposit_request=builder_deposit_request, - is_builder_deposit=True, + builder_deposit_request=builder_deposit_request, slot_reused=False, expected_builder_count=pre_builder_count + 1, expected_builder_index=pre_builder_count, @@ -713,12 +699,11 @@ def test_process_builder_deposit_request__no_reuse_nonzero_balance(spec, state): yield from run_builder_deposit_request_processing(spec, state, builder_deposit_request) # slot_reused=False also verifies original builders are unchanged - assert_process_deposit_request( + assert_process_builder_deposit_request( spec, state, pre_state, - deposit_request=builder_deposit_request, - is_builder_deposit=True, + builder_deposit_request=builder_deposit_request, slot_reused=False, expected_builder_count=pre_builder_count + 1, expected_builder_index=pre_builder_count, @@ -761,12 +746,11 @@ def test_process_builder_deposit_request__exited_builder_top_up(spec, state): yield from run_builder_deposit_request_processing(spec, state, deposit_request) - assert_process_deposit_request( + assert_process_builder_deposit_request( spec, state, pre_state, - deposit_request=deposit_request, - is_builder_deposit=True, + builder_deposit_request=deposit_request, expected_builder_count=pre_builder_count, expected_builder_index=0, expected_builder_balance_delta=amount, diff --git a/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_deposit_request.py b/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_deposit_request.py index 5e97e6252ff..c0573585995 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_deposit_request.py +++ b/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_deposit_request.py @@ -28,8 +28,14 @@ def test_process_deposit_request__builder_credentials_queued(spec, state): - No builder created (builder count unchanged) """ amount = spec.MIN_DEPOSIT_AMOUNT + # Builder withdrawal credentials (0x03 prefix) on an otherwise ordinary deposit + withdrawal_credentials = spec.BUILDER_WITHDRAWAL_PREFIX + b"\x00" * 11 + b"\x59" * 20 deposit_request = prepare_process_deposit_request( - spec, state, for_builder=True, amount=amount, signed=True + spec, + state, + amount=amount, + signed=True, + withdrawal_credentials=withdrawal_credentials, ) pre_state = state.copy() @@ -61,8 +67,16 @@ def test_process_deposit_request__builder_pubkey_queued(spec, state): - Builder unchanged (no top-up) """ amount = spec.MIN_DEPOSIT_AMOUNT + # Deposit for a pubkey that is already a builder, with builder (0x03) credentials + builder_pubkey = state.builders[0].pubkey + withdrawal_credentials = spec.BUILDER_WITHDRAWAL_PREFIX + b"\x00" * 11 + b"\x59" * 20 deposit_request = prepare_process_deposit_request( - spec, state, builder_index=0, amount=amount, signed=True + spec, + state, + pubkey=builder_pubkey, + amount=amount, + signed=True, + withdrawal_credentials=withdrawal_credentials, ) pre_state = state.copy() @@ -95,11 +109,12 @@ def test_process_deposit_request__builder_pubkey_validator_credentials(spec, sta - Builder unchanged (no top-up) """ amount = spec.MIN_DEPOSIT_AMOUNT + builder_pubkey = state.builders[0].pubkey withdrawal_credentials = spec.ETH1_ADDRESS_WITHDRAWAL_PREFIX + b"\x00" * 11 + b"\x59" * 20 deposit_request = prepare_process_deposit_request( spec, state, - builder_index=0, + pubkey=builder_pubkey, amount=amount, signed=True, withdrawal_credentials=withdrawal_credentials, diff --git a/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_parent_execution_payload.py b/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_parent_execution_payload.py index f4bc7ac7303..3bec5ae26e6 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_parent_execution_payload.py +++ b/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_parent_execution_payload.py @@ -12,7 +12,7 @@ get_non_empty_execution_requests, ) from eth_consensus_specs.test.helpers.keys import pubkeys -from tests.infra.helpers.deposit_requests import prepare_process_builder_deposit_request +from tests.infra.helpers.builder_deposit_requests import prepare_process_builder_deposit_request from tests.infra.helpers.withdrawals import set_parent_block_full diff --git a/tests/infra/helpers/builder_deposit_requests.py b/tests/infra/helpers/builder_deposit_requests.py new file mode 100644 index 00000000000..85d3d8bf423 --- /dev/null +++ b/tests/infra/helpers/builder_deposit_requests.py @@ -0,0 +1,263 @@ +from eth_consensus_specs.test.helpers.deposits import sign_builder_deposit_request +from eth_consensus_specs.test.helpers.keys import builder_pubkey_to_privkey, builder_pubkeys +from eth_consensus_specs.test.helpers.state import next_epoch + + +def run_builder_deposit_request_processing(spec, state, builder_deposit_request): + """ + Run process_builder_deposit_request, yielding pre/post states for test vectors. + + The function never raises. Requests that fail a precondition are consumed + without changing the state. + """ + yield "pre", state + yield "builder_deposit_request", builder_deposit_request + + spec.process_builder_deposit_request(state, builder_deposit_request) + yield "post", state + + +def prepare_process_builder_deposit_request( + spec, + state, + advance_epochs=None, + builder_index=None, + pubkey=None, + withdrawal_credentials=None, + amount=None, + signed=False, + builders=None, + builder_modifications=None, +): + """ + Prepare a builder deposit request operation with configurable parameters. + + This helper creates a BuilderDepositRequest object and optionally modifies + state fields related to builder deposit request processing. + + Args: + spec: The spec object. + state: The beacon state to modify. + advance_epochs: Number of epochs to advance before setup. + builder_index: Index for builder pubkey lookup. Default: len(state.builders) + (a new builder). + pubkey: Explicit BLSPubkey. Default: derived from builder_index. + withdrawal_credentials: Explicit Bytes32 credentials. Default: version zero + followed by an eth1 address derived from the pubkey. + amount: Deposit amount in Gwei. Default: MIN_ACTIVATION_BALANCE. + signed: If True, sign with a valid builder deposit signature. + builders: Override state.builders list entirely. Use [] for empty registry. + builder_modifications: Dict mapping builder index to dict of field modifications. + Supported fields: "withdrawable_epoch", "balance". + Example: {0: {"withdrawable_epoch": 5, "balance": 0}} sets builder[0] + to withdrawable at epoch 5 with zero balance. + Use "current_epoch" as a special value to set to current epoch. + Use "current_epoch-1" or "current_epoch+1" for relative epochs. + + Returns: + BuilderDepositRequest: The builder deposit request. + """ + # Phase 1: Advance epochs if requested (before setup) + if advance_epochs is not None: + for _ in range(advance_epochs): + next_epoch(spec, state) + + # Phase 2: Derive effective values + index = builder_index if builder_index is not None else len(state.builders) + effective_pubkey = pubkey if pubkey is not None else builder_pubkeys[index] + effective_privkey = builder_pubkey_to_privkey[effective_pubkey] + effective_amount = amount if amount is not None else spec.MIN_ACTIVATION_BALANCE + if withdrawal_credentials is not None: + effective_withdrawal_credentials = withdrawal_credentials + else: + # Version zero followed by an eth1 address derived from the pubkey + effective_withdrawal_credentials = b"\x00" * 12 + spec.hash(effective_pubkey)[12:] + + # Phase 3: Apply state overrides (before creating request) + if builders is not None: + state.builders = builders + + if builder_modifications is not None: + current_epoch = spec.get_current_epoch(state) + for idx, mods in builder_modifications.items(): + if "withdrawable_epoch" in mods: + epoch_value = mods["withdrawable_epoch"] + # Support special string values for relative epochs + if epoch_value == "current_epoch": + epoch_value = current_epoch + elif epoch_value == "current_epoch-1": + epoch_value = current_epoch - 1 + elif epoch_value == "current_epoch+1": + epoch_value = current_epoch + 1 + state.builders[idx].withdrawable_epoch = epoch_value + if "balance" in mods: + state.builders[idx].balance = spec.Gwei(mods["balance"]) + + # Phase 4: Build the request and optionally sign + request = spec.BuilderDepositRequest( + pubkey=effective_pubkey, + withdrawal_credentials=effective_withdrawal_credentials, + amount=effective_amount, + ) + if signed: + request.signature = sign_builder_deposit_request(spec, request, effective_privkey) + + return request + + +def assert_process_builder_deposit_request( + spec, + state, + pre_state, + builder_deposit_request, + state_unchanged=False, + expected_builder_balance=None, + expected_builder_balance_delta=None, + expected_builder_count=None, + expected_builder_index=None, + expected_execution_address=None, + expected_builder_withdrawable_epoch=None, + slot_reused=None, +): + """ + Assert expected outcomes from process_builder_deposit_request. + + TEST-SPECIFIC CHECKS (controlled by parameters): + - expected_builder_balance: Exact builder balance check + - expected_builder_balance_delta: Builder balance change check + - expected_builder_count: Exact builder count check + - expected_builder_index: Verify builder at specific index + - expected_execution_address: Verify builder's execution_address + - expected_builder_withdrawable_epoch: Expected withdrawable_epoch of the builder + - slot_reused: True = count same (slot reused), False = count +1 (new slot). + When False, also verifies original builders are unchanged. + + Args: + spec: The spec module for the fork being tested + state: State after the builder deposit request was processed + pre_state: State before the builder deposit request was processed + builder_deposit_request: The request that was processed + state_unchanged: If True, asserts state equals pre_state + """ + if state_unchanged: + assert state == pre_state, ( + "Expected state to be unchanged after rejected builder deposit request" + ) + return + + # Find the builder by pubkey + builder_index = None + for i, builder in enumerate(state.builders): + if builder.pubkey == builder_deposit_request.pubkey: + builder_index = i + break + + assert builder_index is not None, ( + f"Builder with pubkey {builder_deposit_request.pubkey[:8]}... should exist after deposit" + ) + + # Check if this was a new builder or top-up + pre_builder_index = None + for i, builder in enumerate(pre_state.builders): + if builder.pubkey == builder_deposit_request.pubkey: + pre_builder_index = i + break + + if pre_builder_index is None: + # New builder was created (could be appended or reused slot) + # The count either increases by 1 (append) or stays the same (slot reuse) + # If slot_reused is specified, that takes precedence for the check + if slot_reused is None: + # Allow either case (append or reuse) if not explicitly specified + assert len(state.builders) >= len(pre_state.builders), ( + "Builder count should not decrease for new builder deposit" + ) + else: + # Top-up of existing builder + assert len(state.builders) == len(pre_state.builders), ( + "Builder count should not change for top-up deposit" + ) + # Balance should increase + pre_balance = pre_state.builders[pre_builder_index].balance + post_balance = state.builders[builder_index].balance + assert post_balance == pre_balance + builder_deposit_request.amount, ( + f"Builder balance should increase by deposit amount: " + f"pre={pre_balance}, post={post_balance}, amount={builder_deposit_request.amount}" + ) + # All other builders should remain unchanged + for i in range(len(pre_state.builders)): + if i != pre_builder_index: + assert state.builders[i] == pre_state.builders[i], ( + f"Builder at index {i} should be unchanged during top-up" + ) + + # Test-specific checks + if expected_builder_balance is not None: + assert state.builders[builder_index].balance == expected_builder_balance + + if expected_builder_balance_delta is not None: + if pre_builder_index is not None: + pre_balance = pre_state.builders[pre_builder_index].balance + else: + pre_balance = 0 + assert state.builders[builder_index].balance == pre_balance + expected_builder_balance_delta + + if expected_builder_count is not None: + assert len(state.builders) == expected_builder_count, ( + f"expected_builder_count: expected={expected_builder_count}, got={len(state.builders)}" + ) + + if expected_builder_index is not None: + assert builder_index == expected_builder_index, ( + f"expected_builder_index: expected={expected_builder_index}, got={builder_index}" + ) + + if expected_execution_address is not None: + assert state.builders[builder_index].execution_address == expected_execution_address, ( + f"expected_execution_address: expected={expected_execution_address}, " + f"got={state.builders[builder_index].execution_address}" + ) + + if expected_builder_withdrawable_epoch is not None: + assert ( + state.builders[builder_index].withdrawable_epoch == expected_builder_withdrawable_epoch + ), ( + f"expected_builder_withdrawable_epoch: expected={expected_builder_withdrawable_epoch}, " + f"got={state.builders[builder_index].withdrawable_epoch}" + ) + + if slot_reused is True: + assert len(state.builders) == len(pre_state.builders), ( + f"slot_reused=True: builder count should be unchanged: " + f"pre={len(pre_state.builders)}, post={len(state.builders)}" + ) + # Verify exactly one original builder was replaced and it met reuse criteria + changed_indices = [ + i for i in range(len(pre_state.builders)) if state.builders[i] != pre_state.builders[i] + ] + assert len(changed_indices) == 1, ( + f"slot_reused=True: exactly one builder should change: " + f"changed_indices={changed_indices}" + ) + reused_idx = changed_indices[0] + pre_builder = pre_state.builders[reused_idx] + current_epoch = spec.get_current_epoch(pre_state) + assert pre_builder.withdrawable_epoch <= current_epoch, ( + f"slot_reused=True: reused builder at index {reused_idx} must have " + f"withdrawable_epoch <= current_epoch: " + f"withdrawable_epoch={pre_builder.withdrawable_epoch}, current_epoch={current_epoch}" + ) + assert pre_builder.balance == 0, ( + f"slot_reused=True: reused builder at index {reused_idx} must have " + f"zero balance: balance={pre_builder.balance}" + ) + elif slot_reused is False: + assert len(state.builders) == len(pre_state.builders) + 1, ( + f"slot_reused=False: builder count should increase by 1: " + f"pre={len(pre_state.builders)}, post={len(state.builders)}" + ) + # Verify original builders are unchanged when new builder is appended + for i in range(len(pre_state.builders)): + assert state.builders[i] == pre_state.builders[i], ( + f"slot_reused=False: original builder at index {i} should be unchanged" + ) diff --git a/tests/infra/helpers/deposit_requests.py b/tests/infra/helpers/deposit_requests.py index ae52b6b6126..f55726fa1da 100644 --- a/tests/infra/helpers/deposit_requests.py +++ b/tests/infra/helpers/deposit_requests.py @@ -1,16 +1,7 @@ import pytest -from eth_consensus_specs.test.helpers.deposits import ( - build_deposit_data, - sign_builder_deposit_request, -) -from eth_consensus_specs.test.helpers.forks import is_post_gloas -from eth_consensus_specs.test.helpers.keys import ( - builder_pubkey_to_privkey, - builder_pubkeys, - privkeys, - pubkeys, -) +from eth_consensus_specs.test.helpers.deposits import build_deposit_data +from eth_consensus_specs.test.helpers.keys import privkeys, pubkeys from eth_consensus_specs.test.helpers.state import next_epoch @@ -36,69 +27,34 @@ def run_deposit_request_processing(spec, state, deposit_request, valid=True): yield "post", None -def run_builder_deposit_request_processing(spec, state, builder_deposit_request): - """ - Run process_builder_deposit_request, yielding pre/post states for test vectors. - - The function never raises. Requests that fail a precondition are consumed - without changing the state. - """ - yield "pre", state - yield "builder_deposit_request", builder_deposit_request - - spec.process_builder_deposit_request(state, builder_deposit_request) - yield "post", state - - def prepare_process_deposit_request( spec, state, advance_epochs=None, validator_index=None, - builder_index=None, pubkey=None, withdrawal_credentials=None, amount=None, signed=False, - for_builder=False, - builders=None, - builder_modifications=None, ): """ - Prepare a deposit request operation with configurable parameters. + Prepare a validator deposit request operation with configurable parameters. - This helper creates a DepositRequest object and optionally modifies - state fields related to deposit request processing. - - The process_deposit_request function behavior varies by fork: - - Electra/Fulu: Sets deposit_requests_start_index if UNSET, appends PendingDeposit - - Gloas+: Drops deposits with builder withdrawal credentials (0x03 prefix). - Builders are created and topped up only via BuilderDepositRequest. For - validator deposits, appends PendingDeposit (does NOT set - deposit_requests_start_index). + This helper creates a DepositRequest object. process_deposit_request sets + deposit_requests_start_index if UNSET (Electra only) and appends a + PendingDeposit. Args: spec: The spec object. state: The beacon state to modify. advance_epochs: Number of epochs to advance before setup. validator_index: Index for pubkey lookup. If None, uses len(state.validators) for new - validator. Ignored if for_builder=True or builder_index is set. - builder_index: Index for builder pubkey lookup. If set, implies for_builder=True. - If None with for_builder=True, uses len(state.builders). - pubkey: Explicit BLSPubkey. Default: derived from validator_index or builder_index. - withdrawal_credentials: Explicit Bytes32 credentials. Default: BLS prefix (0x00) for - validators, Builder prefix (0x03) for builders. + validator. + pubkey: Explicit BLSPubkey. Default: derived from validator_index. + withdrawal_credentials: Explicit Bytes32 credentials. Default: BLS prefix (0x00) + + hash(pubkey)[1:]. amount: Deposit amount in Gwei. Default: MIN_ACTIVATION_BALANCE. signed: If True, sign with valid BLS signature. - for_builder: If True, create a builder deposit using builder keys and - BUILDER_WITHDRAWAL_PREFIX (Gloas+). - builders: Override state.builders list entirely. Use [] for empty registry. - builder_modifications: Dict mapping builder index to dict of field modifications. - Supported fields: "withdrawable_epoch", "balance". - Example: {0: {"withdrawable_epoch": 5, "balance": 0}} sets builder[0] - to withdrawable at epoch 5 with zero balance. - Use "current_epoch" as a special value to set to current epoch. - Use "current_epoch-1" or "current_epoch+1" for relative epochs. Returns: DepositRequest: The deposit request. @@ -108,62 +64,21 @@ def prepare_process_deposit_request( for _ in range(advance_epochs): next_epoch(spec, state) - # Phase 2: Determine if this is a builder deposit - is_builder_deposit = for_builder or builder_index is not None - - # Phase 3: Derive effective values based on deposit type - if is_builder_deposit: - # Builder deposit: use builder keys - index = builder_index if builder_index is not None else len(state.builders) - effective_pubkey = pubkey if pubkey is not None else builder_pubkeys[index] - effective_privkey = builder_pubkey_to_privkey[effective_pubkey] - effective_amount = amount if amount is not None else spec.MIN_ACTIVATION_BALANCE - - # Default withdrawal credentials: Builder prefix (0x03) - if withdrawal_credentials is not None: - effective_withdrawal_credentials = withdrawal_credentials - else: - effective_withdrawal_credentials = ( - spec.BUILDER_WITHDRAWAL_PREFIX - + b"\x00" * 11 - + spec.hash(effective_pubkey)[12:] # 20-byte eth1 address - ) + # Phase 2: Derive effective values + index = validator_index if validator_index is not None else len(state.validators) + effective_pubkey = pubkey if pubkey is not None else pubkeys[index] + effective_privkey = privkeys[index] + effective_amount = amount if amount is not None else spec.MIN_ACTIVATION_BALANCE + + # Default withdrawal credentials: BLS prefix + hash(pubkey)[1:] + if withdrawal_credentials is not None: + effective_withdrawal_credentials = withdrawal_credentials else: - # Validator deposit: use validator keys - index = validator_index if validator_index is not None else len(state.validators) - effective_pubkey = pubkey if pubkey is not None else pubkeys[index] - effective_privkey = privkeys[index] - effective_amount = amount if amount is not None else spec.MIN_ACTIVATION_BALANCE - - # Default withdrawal credentials: BLS prefix + hash(pubkey)[1:] - if withdrawal_credentials is not None: - effective_withdrawal_credentials = withdrawal_credentials - else: - effective_withdrawal_credentials = ( - spec.BLS_WITHDRAWAL_PREFIX + spec.hash(effective_pubkey)[1:] - ) - - # Phase 4: Apply state overrides (before creating request) - if builders is not None: - state.builders = builders - - if builder_modifications is not None: - current_epoch = spec.get_current_epoch(state) - for idx, mods in builder_modifications.items(): - if "withdrawable_epoch" in mods: - epoch_value = mods["withdrawable_epoch"] - # Support special string values for relative epochs - if epoch_value == "current_epoch": - epoch_value = current_epoch - elif epoch_value == "current_epoch-1": - epoch_value = current_epoch - 1 - elif epoch_value == "current_epoch+1": - epoch_value = current_epoch + 1 - state.builders[idx].withdrawable_epoch = epoch_value - if "balance" in mods: - state.builders[idx].balance = spec.Gwei(mods["balance"]) - - # Phase 5: Build deposit data and optionally sign + effective_withdrawal_credentials = ( + spec.BLS_WITHDRAWAL_PREFIX + spec.hash(effective_pubkey)[1:] + ) + + # Phase 3: Build deposit data and optionally sign deposit_data = build_deposit_data( spec, effective_pubkey, @@ -173,7 +88,7 @@ def prepare_process_deposit_request( signed=signed, ) - # Phase 6: Build deposit request object + # Phase 4: Build deposit request object deposit_request = spec.DepositRequest( pubkey=deposit_data.pubkey, withdrawal_credentials=deposit_data.withdrawal_credentials, @@ -185,121 +100,32 @@ def prepare_process_deposit_request( return deposit_request -def prepare_process_builder_deposit_request( - spec, - state, - advance_epochs=None, - builder_index=None, - pubkey=None, - withdrawal_credentials=None, - amount=None, - signed=False, - builders=None, - builder_modifications=None, -): - """ - Prepare a builder deposit request operation with configurable parameters. - - This helper creates a BuilderDepositRequest object and optionally modifies - state fields related to builder deposit request processing. The parameters - behave as in prepare_process_deposit_request. - """ - # Phase 1: Advance epochs if requested (before setup) - if advance_epochs is not None: - for _ in range(advance_epochs): - next_epoch(spec, state) - - # Phase 2: Derive effective values - index = builder_index if builder_index is not None else len(state.builders) - effective_pubkey = pubkey if pubkey is not None else builder_pubkeys[index] - effective_privkey = builder_pubkey_to_privkey[effective_pubkey] - effective_amount = amount if amount is not None else spec.MIN_ACTIVATION_BALANCE - if withdrawal_credentials is not None: - effective_withdrawal_credentials = withdrawal_credentials - else: - # Version zero followed by an eth1 address derived from the pubkey - effective_withdrawal_credentials = b"\x00" * 12 + spec.hash(effective_pubkey)[12:] - - # Phase 3: Apply state overrides (before creating request) - if builders is not None: - state.builders = builders - - if builder_modifications is not None: - current_epoch = spec.get_current_epoch(state) - for idx, mods in builder_modifications.items(): - if "withdrawable_epoch" in mods: - epoch_value = mods["withdrawable_epoch"] - # Support special string values for relative epochs - if epoch_value == "current_epoch": - epoch_value = current_epoch - elif epoch_value == "current_epoch-1": - epoch_value = current_epoch - 1 - elif epoch_value == "current_epoch+1": - epoch_value = current_epoch + 1 - state.builders[idx].withdrawable_epoch = epoch_value - if "balance" in mods: - state.builders[idx].balance = spec.Gwei(mods["balance"]) - - # Phase 4: Build the request and optionally sign - request = spec.BuilderDepositRequest( - pubkey=effective_pubkey, - withdrawal_credentials=effective_withdrawal_credentials, - amount=effective_amount, - ) - if signed: - request.signature = sign_builder_deposit_request(spec, request, effective_privkey) - - return request - - def assert_process_deposit_request( spec, state, pre_state, deposit_request=None, state_unchanged=False, - is_builder_deposit=None, expected_deposit_requests_start_index=None, expected_pending_deposit_pubkey=None, expected_pending_deposit_amount=None, expected_pending_deposit_slot=None, expected_pending_deposit_credentials=None, - expected_builder_balance=None, - expected_builder_balance_delta=None, - expected_builder_count=None, - expected_builder_index=None, - expected_execution_address=None, - expected_builder_withdrawable_epoch=None, - slot_reused=None, ): """ - Assert expected outcomes from process_deposit_request. + Assert expected outcomes from process_deposit_request (validator deposits). - INVARIANT CHECKS FOR VALIDATOR DEPOSITS (always run): + INVARIANT CHECKS (always run): - pending_deposits increases by exactly 1 - New pending deposit has correct pubkey, withdrawal_credentials, amount, signature - New pending deposit slot equals state.slot - - deposit_requests_start_index is set only if previously UNSET (Electra/Fulu only) + - deposit_requests_start_index is set only if previously UNSET (Electra only) - Validator count unchanged (validators created during epoch processing) - Balances unchanged (balance applied during epoch processing) - INVARIANT CHECKS FOR BUILDER DEPOSIT REQUESTS (Gloas+, pass - is_builder_deposit=True and a BuilderDepositRequest): - - pending_deposits unchanged (builder deposits applied immediately) - - Builder balance increases by deposit amount - - New builder created if pubkey didn't exist - TEST-SPECIFIC CHECKS (controlled by parameters): - expected_deposit_requests_start_index: Exact value check - expected_pending_deposit_*: Check specific fields of the new deposit - - expected_builder_balance: Exact builder balance check - - expected_builder_balance_delta: Builder balance change check - - expected_builder_count: Exact builder count check - - expected_builder_index: Verify builder at specific index - - expected_execution_address: Verify builder's execution_address - - expected_builder_withdrawable_epoch: Expected withdrawable_epoch of new builder - - slot_reused: True = count same (slot reused), False = count +1 (new slot). - When False, also verifies original builders are unchanged. Args: spec: The spec module for the fork being tested @@ -307,20 +133,11 @@ def assert_process_deposit_request( pre_state: State before deposit request was processed deposit_request: The deposit request that was processed (required for invariant checks) state_unchanged: If True, asserts state equals pre_state (for rejected requests) - is_builder_deposit: If True, use builder deposit request assertions. Defaults to False. expected_deposit_requests_start_index: Expected value of deposit_requests_start_index expected_pending_deposit_pubkey: Expected pubkey of new pending deposit expected_pending_deposit_amount: Expected amount of new pending deposit expected_pending_deposit_slot: Expected slot of new pending deposit expected_pending_deposit_credentials: Expected withdrawal_credentials of new pending deposit - expected_builder_balance: Expected balance of the builder after deposit - expected_builder_balance_delta: Expected change in builder balance - expected_builder_count: Expected exact count of builders after deposit - expected_builder_index: Expected index of the builder (for verifying slot reuse) - expected_execution_address: Expected execution_address of the new builder - expected_builder_withdrawable_epoch: Expected withdrawable_epoch of the builder - slot_reused: If True, assert builder count unchanged; if False, assert +1 - and verify all original builders are unchanged """ if state_unchanged: assert state == pre_state, "Expected state to be unchanged after rejected deposit request" @@ -329,248 +146,72 @@ def assert_process_deposit_request( # Invariant checks require deposit_request assert deposit_request is not None, "deposit_request required when state_unchanged=False" - # Deposit requests never route to builders. Builder deposit assertions are - # used only when a BuilderDepositRequest is asserted explicitly. - if is_builder_deposit is None: - is_builder_deposit = False - - if is_builder_deposit and is_post_gloas(spec): - # BUILDER DEPOSIT ASSERTIONS (Gloas+) - # Builder deposits are applied immediately, not queued - - # INVARIANT: pending_deposits unchanged for builder deposits - assert len(state.pending_deposits) == len(pre_state.pending_deposits), ( - f"pending_deposits should not change for builder deposits: " - f"pre={len(pre_state.pending_deposits)}, post={len(state.pending_deposits)}" - ) + # INVARIANT: pending_deposits increases by exactly 1 + assert len(state.pending_deposits) == len(pre_state.pending_deposits) + 1, ( + f"pending_deposits should increase by 1: " + f"pre={len(pre_state.pending_deposits)}, post={len(state.pending_deposits)}" + ) - # Find the builder by pubkey - builder_index = None - for i, builder in enumerate(state.builders): - if builder.pubkey == deposit_request.pubkey: - builder_index = i - break + # INVARIANT: New pending deposit matches deposit request fields + # Use len()-1 as SSZ Lists may not support negative indexing + new_pending_deposit = state.pending_deposits[len(state.pending_deposits) - 1] - assert builder_index is not None, ( - f"Builder with pubkey {deposit_request.pubkey[:8]}... should exist after deposit" - ) + assert new_pending_deposit.pubkey == deposit_request.pubkey, ( + "Pending deposit pubkey must match deposit request" + ) + assert new_pending_deposit.withdrawal_credentials == deposit_request.withdrawal_credentials, ( + "Pending deposit withdrawal_credentials must match deposit request" + ) + assert new_pending_deposit.amount == deposit_request.amount, ( + "Pending deposit amount must match deposit request" + ) + assert new_pending_deposit.signature == deposit_request.signature, ( + "Pending deposit signature must match deposit request" + ) - # Check if this was a new builder or top-up - pre_builder_index = None - for i, builder in enumerate(pre_state.builders): - if builder.pubkey == deposit_request.pubkey: - pre_builder_index = i - break - - if pre_builder_index is None: - # New builder was created (could be appended or reused slot) - # The count either increases by 1 (append) or stays the same (slot reuse) - # If slot_reused is specified, that takes precedence for the check - if slot_reused is None: - # Allow either case (append or reuse) if not explicitly specified - assert len(state.builders) >= len(pre_state.builders), ( - "Builder count should not decrease for new builder deposit" - ) - else: - # Top-up of existing builder - assert len(state.builders) == len(pre_state.builders), ( - "Builder count should not change for top-up deposit" - ) - # Balance should increase - pre_balance = pre_state.builders[pre_builder_index].balance - post_balance = state.builders[builder_index].balance - assert post_balance == pre_balance + deposit_request.amount, ( - f"Builder balance should increase by deposit amount: " - f"pre={pre_balance}, post={post_balance}, amount={deposit_request.amount}" - ) - # All other builders should remain unchanged - for i in range(len(pre_state.builders)): - if i != pre_builder_index: - assert state.builders[i] == pre_state.builders[i], ( - f"Builder at index {i} should be unchanged during top-up" - ) - - # INVARIANT: Validator count unchanged - assert len(state.validators) == len(pre_state.validators), ( - "Validator count should not change during builder deposit processing" - ) + # INVARIANT: New pending deposit slot equals state.slot (at time of processing) + assert new_pending_deposit.slot == state.slot, ( + f"Pending deposit slot must equal state.slot: " + f"deposit.slot={new_pending_deposit.slot}, state.slot={state.slot}" + ) - # INVARIANT: Validator balances unchanged - assert list(state.balances) == list(pre_state.balances), ( - "Validator balances should not change during builder deposit processing" + # INVARIANT: deposit_requests_start_index is set only if it was previously UNSET + was_unset = pre_state.deposit_requests_start_index == spec.UNSET_DEPOSIT_REQUESTS_START_INDEX + if was_unset: + # Should be set to deposit_request.index + assert state.deposit_requests_start_index == deposit_request.index, ( + f"deposit_requests_start_index should be set to request index: " + f"expected={deposit_request.index}, got={state.deposit_requests_start_index}" ) - - # Test-specific checks for builders - if expected_builder_balance is not None: - assert state.builders[builder_index].balance == expected_builder_balance - - if expected_builder_balance_delta is not None: - if pre_builder_index is not None: - pre_balance = pre_state.builders[pre_builder_index].balance - else: - pre_balance = 0 - assert ( - state.builders[builder_index].balance - == pre_balance + expected_builder_balance_delta - ) - - if expected_builder_count is not None: - assert len(state.builders) == expected_builder_count, ( - f"expected_builder_count: expected={expected_builder_count}, " - f"got={len(state.builders)}" - ) - - if expected_builder_index is not None: - assert builder_index == expected_builder_index, ( - f"expected_builder_index: expected={expected_builder_index}, got={builder_index}" - ) - - if expected_execution_address is not None: - assert state.builders[builder_index].execution_address == expected_execution_address, ( - f"expected_execution_address: expected={expected_execution_address}, " - f"got={state.builders[builder_index].execution_address}" - ) - - if expected_builder_withdrawable_epoch is not None: - assert ( - state.builders[builder_index].withdrawable_epoch - == expected_builder_withdrawable_epoch - ), ( - f"expected_builder_withdrawable_epoch: expected={expected_builder_withdrawable_epoch}, " - f"got={state.builders[builder_index].withdrawable_epoch}" - ) - - if slot_reused is True: - assert len(state.builders) == len(pre_state.builders), ( - f"slot_reused=True: builder count should be unchanged: " - f"pre={len(pre_state.builders)}, post={len(state.builders)}" - ) - # Verify exactly one original builder was replaced and it met reuse criteria - changed_indices = [ - i - for i in range(len(pre_state.builders)) - if state.builders[i] != pre_state.builders[i] - ] - assert len(changed_indices) == 1, ( - f"slot_reused=True: exactly one builder should change: " - f"changed_indices={changed_indices}" - ) - reused_idx = changed_indices[0] - pre_builder = pre_state.builders[reused_idx] - current_epoch = spec.get_current_epoch(pre_state) - assert pre_builder.withdrawable_epoch <= current_epoch, ( - f"slot_reused=True: reused builder at index {reused_idx} must have " - f"withdrawable_epoch <= current_epoch: " - f"withdrawable_epoch={pre_builder.withdrawable_epoch}, current_epoch={current_epoch}" - ) - assert pre_builder.balance == 0, ( - f"slot_reused=True: reused builder at index {reused_idx} must have " - f"zero balance: balance={pre_builder.balance}" - ) - elif slot_reused is False: - assert len(state.builders) == len(pre_state.builders) + 1, ( - f"slot_reused=False: builder count should increase by 1: " - f"pre={len(pre_state.builders)}, post={len(state.builders)}" - ) - # Verify original builders are unchanged when new builder is appended - for i in range(len(pre_state.builders)): - assert state.builders[i] == pre_state.builders[i], ( - f"slot_reused=False: original builder at index {i} should be unchanged" - ) - else: - # VALIDATOR DEPOSIT ASSERTIONS - - # Guard: expected_builder_count is meaningless for validator deposits - # (builder count is an invariant that should never change). - assert expected_builder_count is None, ( - "expected_builder_count must not be used with validator deposits: " - "builder count is invariant for validator deposits" + # Should remain unchanged + assert state.deposit_requests_start_index == pre_state.deposit_requests_start_index, ( + "deposit_requests_start_index should not change when already set" ) - # INVARIANT: pending_deposits increases by exactly 1 - assert len(state.pending_deposits) == len(pre_state.pending_deposits) + 1, ( - f"pending_deposits should increase by 1: " - f"pre={len(pre_state.pending_deposits)}, post={len(state.pending_deposits)}" - ) - - # INVARIANT: New pending deposit matches deposit request fields - # Use len()-1 as SSZ Lists may not support negative indexing - new_pending_deposit = state.pending_deposits[len(state.pending_deposits) - 1] - - assert new_pending_deposit.pubkey == deposit_request.pubkey, ( - "Pending deposit pubkey must match deposit request" - ) - assert ( - new_pending_deposit.withdrawal_credentials == deposit_request.withdrawal_credentials - ), "Pending deposit withdrawal_credentials must match deposit request" - assert new_pending_deposit.amount == deposit_request.amount, ( - "Pending deposit amount must match deposit request" - ) - assert new_pending_deposit.signature == deposit_request.signature, ( - "Pending deposit signature must match deposit request" - ) - - # INVARIANT: New pending deposit slot equals state.slot (at time of processing) - assert new_pending_deposit.slot == state.slot, ( - f"Pending deposit slot must equal state.slot: " - f"deposit.slot={new_pending_deposit.slot}, state.slot={state.slot}" - ) - - # INVARIANT: deposit_requests_start_index logic (Electra/Fulu only, removed in Gloas) - if not is_post_gloas(spec): - was_unset = ( - pre_state.deposit_requests_start_index == spec.UNSET_DEPOSIT_REQUESTS_START_INDEX - ) - if was_unset: - # Should be set to deposit_request.index - assert state.deposit_requests_start_index == deposit_request.index, ( - f"deposit_requests_start_index should be set to request index: " - f"expected={deposit_request.index}, got={state.deposit_requests_start_index}" - ) - else: - # Should remain unchanged - assert ( - state.deposit_requests_start_index == pre_state.deposit_requests_start_index - ), "deposit_requests_start_index should not change when already set" - else: - # In Gloas+, deposit_requests_start_index is not modified by process_deposit_request - assert state.deposit_requests_start_index == pre_state.deposit_requests_start_index, ( - "deposit_requests_start_index should not change in Gloas+" - ) - - # INVARIANT: Builder count unchanged (Gloas+ only, builders don't exist pre-Gloas) - if is_post_gloas(spec): - assert len(state.builders) == len(pre_state.builders), ( - "Builder count should not change during validator deposit processing" - ) - - # INVARIANT: Validator count unchanged (new validators created during epoch processing) - assert len(state.validators) == len(pre_state.validators), ( - "Validator count should not change during deposit request processing" - ) + # INVARIANT: Validator count unchanged (new validators created during epoch processing) + assert len(state.validators) == len(pre_state.validators), ( + "Validator count should not change during deposit request processing" + ) - # INVARIANT: Balances unchanged (balance applied during epoch processing) - assert list(state.balances) == list(pre_state.balances), ( - "Balances should not change during deposit request processing" - ) + # INVARIANT: Balances unchanged (balance applied during epoch processing) + assert list(state.balances) == list(pre_state.balances), ( + "Balances should not change during deposit request processing" + ) - # Test-specific checks for validators - if expected_pending_deposit_pubkey is not None: - assert new_pending_deposit.pubkey == expected_pending_deposit_pubkey + # Test-specific checks + if expected_pending_deposit_pubkey is not None: + assert new_pending_deposit.pubkey == expected_pending_deposit_pubkey - if expected_pending_deposit_amount is not None: - assert new_pending_deposit.amount == expected_pending_deposit_amount + if expected_pending_deposit_amount is not None: + assert new_pending_deposit.amount == expected_pending_deposit_amount - if expected_pending_deposit_slot is not None: - assert new_pending_deposit.slot == expected_pending_deposit_slot + if expected_pending_deposit_slot is not None: + assert new_pending_deposit.slot == expected_pending_deposit_slot - if expected_pending_deposit_credentials is not None: - assert ( - new_pending_deposit.withdrawal_credentials == expected_pending_deposit_credentials - ) + if expected_pending_deposit_credentials is not None: + assert new_pending_deposit.withdrawal_credentials == expected_pending_deposit_credentials - # Common test-specific checks if expected_deposit_requests_start_index is not None: assert state.deposit_requests_start_index == expected_deposit_requests_start_index, ( f"deposit_requests_start_index: expected={expected_deposit_requests_start_index}, " diff --git a/tests/infra/helpers/test_deposit_requests.py b/tests/infra/helpers/test_deposit_requests.py index 6fb4ae4ce53..4a10c0650f0 100644 --- a/tests/infra/helpers/test_deposit_requests.py +++ b/tests/infra/helpers/test_deposit_requests.py @@ -1,13 +1,10 @@ -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock from eth_consensus_specs.test.context import ( spec_state_test, - with_all_phases_from_to, with_electra_and_later, - with_gloas_and_later, ) -from eth_consensus_specs.test.helpers.constants import ELECTRA, GLOAS -from eth_consensus_specs.test.helpers.keys import builder_pubkeys, pubkeys +from eth_consensus_specs.test.helpers.keys import pubkeys from eth_consensus_specs.utils import bls from tests.infra.helpers.deposit_requests import ( assert_process_deposit_request, @@ -119,13 +116,12 @@ def test_assert_validator_deposit(): state.deposit_requests_start_index = deposit_request.index state.slot = slot - with patch("tests.infra.helpers.deposit_requests.is_post_gloas", return_value=False): - assert_process_deposit_request( - spec, - state, - pre_state, - deposit_request=deposit_request, - ) + assert_process_deposit_request( + spec, + state, + pre_state, + deposit_request=deposit_request, + ) def test_assert_process_deposit_request_start_index_set(): @@ -164,14 +160,13 @@ def test_assert_process_deposit_request_start_index_set(): state.deposit_requests_start_index = 0 state.slot = slot - with patch("tests.infra.helpers.deposit_requests.is_post_gloas", return_value=False): - assert_process_deposit_request( - spec, - state, - pre_state, - deposit_request=deposit_request, - expected_deposit_requests_start_index=0, - ) + assert_process_deposit_request( + spec, + state, + pre_state, + deposit_request=deposit_request, + expected_deposit_requests_start_index=0, + ) def test_assert_process_deposit_request_start_index_unchanged(): @@ -211,111 +206,10 @@ def test_assert_process_deposit_request_start_index_unchanged(): state.deposit_requests_start_index = initial_start_index state.slot = slot - with patch("tests.infra.helpers.deposit_requests.is_post_gloas", return_value=False): - assert_process_deposit_request( - spec, - state, - pre_state, - deposit_request=deposit_request, - expected_deposit_requests_start_index=initial_start_index, - ) - - -# ============================================================================ -# Builder deposit tests (Gloas+) -# ============================================================================ - - -@with_gloas_and_later -@spec_state_test -def test_prepare_process_deposit_request_for_builder(spec, state): - """Test prepare_process_deposit_request with for_builder=True.""" - builder_index = len(state.builders) - deposit_request = prepare_process_deposit_request(spec, state, for_builder=True) - - # Should use builder pubkey - assert deposit_request.pubkey == builder_pubkeys[builder_index] - - # Should use BUILDER_WITHDRAWAL_PREFIX (0x03) - assert deposit_request.withdrawal_credentials[0:1] == spec.BUILDER_WITHDRAWAL_PREFIX - - # Default amount should be MIN_ACTIVATION_BALANCE - assert deposit_request.amount == spec.MIN_ACTIVATION_BALANCE - - -@with_gloas_and_later -@spec_state_test -def test_prepare_process_deposit_request_builder_custom_amount(spec, state): - """Test prepare_process_deposit_request for builder with custom amount.""" - custom_amount = spec.Gwei(100_000_000_000) # 100 ETH - deposit_request = prepare_process_deposit_request( - spec, state, for_builder=True, amount=custom_amount - ) - - assert deposit_request.amount == custom_amount - - -def test_assert_new_builder_deposit(): - """Test assert_process_deposit_request passes for a new builder deposit.""" - spec = MagicMock() - - builder_pubkey = b"\x03" * 48 - deposit_request = MagicMock() - deposit_request.pubkey = builder_pubkey - deposit_request.withdrawal_credentials = b"\x03" + b"\x00" * 11 + b"\x59" * 20 - deposit_request.amount = 32_000_000_000 - deposit_request.signature = b"\x02" * 96 - deposit_request.index = 0 - - # Pre state: no builders - pre_state = MagicMock() - pre_state.pending_deposits = [] - pre_state.validators = [MagicMock()] - pre_state.balances = [32_000_000_000] - pre_state.builders = [] - - # Post state: one new builder with matching pubkey - new_builder = MagicMock() - new_builder.pubkey = builder_pubkey - new_builder.balance = deposit_request.amount - - state = MagicMock() - state.pending_deposits = [] - state.validators = [MagicMock()] - state.balances = [32_000_000_000] - state.builders = [new_builder] - - with patch("tests.infra.helpers.deposit_requests.is_post_gloas", return_value=True): - assert_process_deposit_request( - spec, - state, - pre_state, - deposit_request=deposit_request, - is_builder_deposit=True, - ) - - -@with_all_phases_from_to(ELECTRA, GLOAS) -@spec_state_test -def test_prepare_process_deposit_request_builder_credentials_before_gloas(spec, state): - """Test prepare_process_deposit_request with builder credentials (0x03) before Gloas.""" - amount = spec.MIN_ACTIVATION_BALANCE - - # Builder withdrawal credentials (0x03 prefix) - no special meaning before Gloas - withdrawal_credentials = b"\x03" + b"\x00" * 11 + b"\x59" * 20 - - deposit_request = prepare_process_deposit_request( + assert_process_deposit_request( spec, state, - amount=amount, - signed=True, - withdrawal_credentials=withdrawal_credentials, + pre_state, + deposit_request=deposit_request, + expected_deposit_requests_start_index=initial_start_index, ) - - # Should create a valid deposit request with the builder credentials preserved - assert deposit_request.withdrawal_credentials == withdrawal_credentials - assert deposit_request.amount == amount - - # Should use validator keys (not builder keys) since for_builder is False - validator_index = len(state.validators) - assert deposit_request.pubkey == pubkeys[validator_index] From ed960f1f083cb6a9ca17c613b46382704b679790 Mon Sep 17 00:00:00 2001 From: Justin Traglia Date: Fri, 19 Jun 2026 11:45:59 +0200 Subject: [PATCH 13/14] Apply feedback from nflaig --- specs/gloas/builder.md | 24 ++++++++++++++++++++---- specs/gloas/validator.md | 6 ++---- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/specs/gloas/builder.md b/specs/gloas/builder.md index 958bf5cfa8e..d033fbfdb29 100644 --- a/specs/gloas/builder.md +++ b/specs/gloas/builder.md @@ -10,6 +10,7 @@ - [Process deposit](#process-deposit) - [Builder index](#builder-index) - [Activation](#activation) + - [Exiting](#exiting) - [Builder activities](#builder-activities) - [Constructing the `SignedExecutionPayloadBid`](#constructing-the-signedexecutionpayloadbid) - [Constructing the `DataColumnSidecar`s](#constructing-the-datacolumnsidecars) @@ -49,10 +50,12 @@ deposit contract on the execution layer, as defined in under `DOMAIN_BUILDER_DEPOSIT`. *Note*: Builders may be onboarded at the fork by submitting a deposit to the -validator deposit contract with a 0x03 withdrawal credential. This must be done -late enough that the deposit is still pending at the fork. Such a deposit signs -over `DepositMessage` under `DOMAIN_DEPOSIT`, with withdrawal credentials of the -form `BUILDER_WITHDRAWAL_PREFIX + b"\x00" * 11 + execution_address`. +validator deposit contract with a `0x03` withdrawal credential. This must be +done late enough that the deposit is still pending at the fork, but early enough +that the slot in which the deposit is added to the pending deposit queue is +finalized so that the builder is considered active. Such a deposit signs over +`DepositMessage` under `DOMAIN_DEPOSIT`, with withdrawal credentials of the form +`BUILDER_WITHDRAWAL_PREFIX + b"\x00" * 11 + execution_address`. ### Process deposit @@ -78,6 +81,19 @@ epoch of the pending deposit, not the fork epoch. Therefore, if that epoch is finalized at the fork, the builder will be immediately active. See `onboard_builders_from_pending_deposits` for details. +### Exiting + +A builder exits by submitting a builder exit request to the builder exit +contract on the execution layer, as defined in +[EIP-8282](https://eips.ethereum.org/EIPS/eip-8282). The request contains the +builder's `pubkey` and is authorized by the builder's `execution_address` (the +transaction sender), not the BLS key. + +The consensus layer initiates the exit only if the builder is active, the +request's `source_address` matches the builder's `execution_address`, and the +builder has no pending balance to withdraw. Otherwise the request is consumed +without effect and must be resubmitted once those conditions hold. + ## Builder activities Builders have two optional activities: submitting bids and submitting payloads. diff --git a/specs/gloas/validator.md b/specs/gloas/validator.md index 4c5c60cb252..9ec9159fe1b 100644 --- a/specs/gloas/validator.md +++ b/specs/gloas/validator.md @@ -252,10 +252,8 @@ parent's execution payload. The proposer constructs this field as follows: ##### Execution requests -*Note*: In Gloas, execution requests are carried in the -`ExecutionPayloadEnvelope` rather than the `BeaconBlockBody`. The function -`get_execution_requests` is modified to parse the builder deposit and builder -exit request types, and is used by the builder when constructing the envelope. +*Note*: The function `get_execution_requests` is modified to parse the builder +deposit requests and builder exit requests. ```python def get_execution_requests(execution_requests_list: Sequence[bytes]) -> ExecutionRequests: From c84f2e4a71d67ac55a818c27ebc77b216058e789 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Fri, 19 Jun 2026 13:49:48 +0100 Subject: [PATCH 14/14] add test for builder exit request --- .../test_process_parent_execution_payload.py | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_parent_execution_payload.py b/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_parent_execution_payload.py index 3bec5ae26e6..094012036c7 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_parent_execution_payload.py +++ b/tests/core/pyspec/eth_consensus_specs/test/gloas/block_processing/test_process_parent_execution_payload.py @@ -522,3 +522,47 @@ def test_process_parent_execution_payload__new_builder_does_not_reuse_topped_up_ assert new_builder_index is not None assert new_builder_index != 0 assert state.builders[new_builder_index].balance == new_builder_amount + + +@with_gloas_and_later +@spec_state_test +def test_process_parent_execution_payload__builder_exit_request(spec, state): + """ + Test that a builder exit request in the parent's execution requests exits an + active builder authorized by its execution address. + """ + builder_index = 0 + + # Finalize the builder's deposit epoch so that it is active + state.finalized_checkpoint.epoch = state.builders[builder_index].deposit_epoch + 1 + assert spec.is_active_builder(state, builder_index) + assert spec.get_pending_balance_to_withdraw_for_builder(state, builder_index) == 0 + + builder = state.builders[builder_index] + requests = spec.ExecutionRequests( + builder_exits=spec.List[ + spec.BuilderExitRequest, spec.MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD + ]( + [ + spec.BuilderExitRequest( + source_address=builder.execution_address, + pubkey=builder.pubkey, + ) + ] + ), + ) + + _commit_parent_requests(spec, state, requests) + + block = build_empty_block_for_next_slot(spec, state) + block.body.parent_execution_requests = requests + + spec.process_slots(state, block.slot) + current_epoch = spec.get_current_epoch(state) + + yield from run_parent_execution_payload_processing(spec, state, block) + + # The builder exit was initiated + assert not spec.is_active_builder(state, builder_index) + expected_withdrawable = current_epoch + spec.config.MIN_BUILDER_WITHDRAWABILITY_DELAY + assert state.builders[builder_index].withdrawable_epoch == expected_withdrawable