diff --git a/Makefile b/Makefile index c1fe0b20c..a6f86d7a0 100644 --- a/Makefile +++ b/Makefile @@ -130,9 +130,11 @@ codespell: codespell . --skip ./.git -I .codespell-whitelist # TODO: add future protocol upgrade patch packages to linting. +# NOTE: we use `pylint` just for catching unused arguments in spec code lint: pyspec . venv/bin/activate; cd $(PY_SPEC_DIR); \ flake8 --config $(LINTER_CONFIG_FILE) ./eth2spec \ + && pylint --disable=all --enable unused-argument ./eth2spec/phase0 ./eth2spec/altair ./eth2spec/bellatrix \ && mypy --config-file $(LINTER_CONFIG_FILE) -p eth2spec.phase0 -p eth2spec.altair -p eth2spec.bellatrix -p eth2spec.capella lint_generators: pyspec diff --git a/README.md b/README.md index 738d43a05..fb58edf8d 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ The current features are: * [Honest Validator guide changes](specs/altair/validator.md) * [P2P Networking](specs/altair/p2p-interface.md) -### Bellatrix (as known as The Merge) +### Bellatrix (also known as The Merge) The Bellatrix protocol upgrade is still actively in development. The exact specification has not been formally accepted as final and details are still subject to change. @@ -45,7 +45,6 @@ The Bellatrix protocol upgrade is still actively in development. The exact speci * [Bellatrix fork](specs/bellatrix/fork.md) * [Fork Choice changes](specs/bellatrix/fork-choice.md) * [Validator additions](specs/bellatrix/validator.md) - * [Client settings](specs/bellatrix/client-settings.md) * [P2P Networking](specs/bellatrix/p2p-interface.md) ### Sharding diff --git a/configs/README.md b/configs/README.md index f470b932d..6ef081e4c 100644 --- a/configs/README.md +++ b/configs/README.md @@ -9,7 +9,7 @@ Standard configs: - [`minimal.yaml`](./minimal.yaml): Minimal configuration, used in spec-testing along with the [`minimal`](../presets/minimal) preset. Not all network configurations are in scope for the specification, -see [`github.com/eth2-clients/eth2-networks`](https://github.com/eth2-clients/eth2-networks) for common networks, +see [`github.com/eth-clients/eth2-networks`](https://github.com/eth-clients/eth2-networks) for common networks, and additional testnet assets. ## Forking diff --git a/configs/mainnet.yaml b/configs/mainnet.yaml index 1493dbaa5..5b2046654 100644 --- a/configs/mainnet.yaml +++ b/configs/mainnet.yaml @@ -3,6 +3,13 @@ # Extends the mainnet preset PRESET_BASE: 'mainnet' +# Free-form short name of the network that this configuration applies to - known +# canonical network names include: +# * 'mainnet' - there can be only one +# * 'prater' - testnet +# Must match the regex: [a-z0-9\-] +CONFIG_NAME: 'mainnet' + # Transition # --------------------------------------------------------------- # TBD, 2**256-2**10 is a placeholder diff --git a/configs/minimal.yaml b/configs/minimal.yaml index 35ac1a7af..0910a1430 100644 --- a/configs/minimal.yaml +++ b/configs/minimal.yaml @@ -3,6 +3,13 @@ # Extends the minimal preset PRESET_BASE: 'minimal' +# Free-form short name of the network that this configuration applies to - known +# canonical network names include: +# * 'mainnet' - there can be only one +# * 'prater' - testnet +# Must match the regex: [a-z0-9\-] +CONFIG_NAME: 'minimal' + # Transition # --------------------------------------------------------------- # TBD, 2**256-2**10 is a placeholder diff --git a/setup.py b/setup.py index ec2523dfd..f2bc1d711 100644 --- a/setup.py +++ b/setup.py @@ -516,7 +516,7 @@ def get_pow_block(hash: Bytes32) -> Optional[PowBlock]: return PowBlock(block_hash=hash, parent_hash=Bytes32(), total_difficulty=uint256(0)) -def get_execution_state(execution_state_root: Bytes32) -> ExecutionState: +def get_execution_state(_execution_state_root: Bytes32) -> ExecutionState: pass @@ -526,7 +526,7 @@ def get_pow_chain_head() -> PowBlock: class NoopExecutionEngine(ExecutionEngine): - def execute_payload(self: ExecutionEngine, execution_payload: ExecutionPayload) -> bool: + def notify_new_payload(self: ExecutionEngine, execution_payload: ExecutionPayload) -> bool: return True def notify_forkchoice_updated(self: ExecutionEngine, @@ -767,7 +767,7 @@ def parse_config_vars(conf: Dict[str, str]) -> Dict[str, str]: """ out: Dict[str, str] = dict() for k, v in conf.items(): - if isinstance(v, str) and (v.startswith("0x") or k == 'PRESET_BASE'): + if isinstance(v, str) and (v.startswith("0x") or k == 'PRESET_BASE' or k == 'CONFIG_NAME'): # Represent byte data with string, to avoid misinterpretation as big-endian int. # Everything is either byte data or an integer, with PRESET_BASE as one exception. out[k] = f"'{v}'" @@ -883,6 +883,7 @@ class PySpecCommand(Command): specs/bellatrix/fork.md specs/bellatrix/fork-choice.md specs/bellatrix/validator.md + sync/optimistic.md """ if self.spec_fork == CAPELLA: self.md_doc_paths += """ @@ -1030,7 +1031,7 @@ setup( python_requires=">=3.8, <4", extras_require={ "test": ["pytest>=4.4", "pytest-cov", "pytest-xdist"], - "lint": ["flake8==3.7.7", "mypy==0.812"], + "lint": ["flake8==3.7.7", "mypy==0.812", "pylint==2.12.2"], "generator": ["python-snappy==0.5.4"], }, install_requires=[ diff --git a/specs/altair/beacon-chain.md b/specs/altair/beacon-chain.md index 3c7177a1b..a14ddb4f6 100644 --- a/specs/altair/beacon-chain.md +++ b/specs/altair/beacon-chain.md @@ -126,7 +126,7 @@ This patch updates a few configuration values to move penalty parameters closer | Name | Value | Unit | Duration | | - | - | - | - | -| `SYNC_COMMITTEE_SIZE` | `uint64(2**9)` (= 512) | Validators | | +| `SYNC_COMMITTEE_SIZE` | `uint64(2**9)` (= 512) | validators | | | `EPOCHS_PER_SYNC_COMMITTEE_PERIOD` | `uint64(2**8)` (= 256) | epochs | ~27 hours | ## Configuration @@ -521,7 +521,7 @@ def process_deposit(state: BeaconState, deposit: Deposit) -> None: signing_root = compute_signing_root(deposit_message, domain) # Initialize validator if the deposit signature is valid if bls.Verify(pubkey, signing_root, deposit.data.signature): - state.validators.append(get_validator_from_deposit(state, deposit)) + state.validators.append(get_validator_from_deposit(deposit)) state.balances.append(amount) state.previous_epoch_participation.append(ParticipationFlags(0b0000_0000)) state.current_epoch_participation.append(ParticipationFlags(0b0000_0000)) diff --git a/specs/altair/sync-protocol.md b/specs/altair/sync-protocol.md index c8c7c3d4d..36b634742 100644 --- a/specs/altair/sync-protocol.md +++ b/specs/altair/sync-protocol.md @@ -42,17 +42,17 @@ uses sync committees introduced in [this beacon chain extension](./beacon-chain. | Name | Value | | - | - | -| `FINALIZED_ROOT_INDEX` | `get_generalized_index(BeaconState, 'finalized_checkpoint', 'root')` | -| `NEXT_SYNC_COMMITTEE_INDEX` | `get_generalized_index(BeaconState, 'next_sync_committee')` | +| `FINALIZED_ROOT_INDEX` | `get_generalized_index(BeaconState, 'finalized_checkpoint', 'root')` (= 105) | +| `NEXT_SYNC_COMMITTEE_INDEX` | `get_generalized_index(BeaconState, 'next_sync_committee')` (= 55) | ## Preset ### Misc -| Name | Value | Notes | -| - | - | - | -| `MIN_SYNC_COMMITTEE_PARTICIPANTS` | `1` | | -| `UPDATE_TIMEOUT` | `SLOTS_PER_EPOCH * EPOCHS_PER_SYNC_COMMITTEE_PERIOD` | ~27.3 hours | +| Name | Value | Unit | Duration | +| - | - | - | - | +| `MIN_SYNC_COMMITTEE_PARTICIPANTS` | `1` | validators | +| `UPDATE_TIMEOUT` | `SLOTS_PER_EPOCH * EPOCHS_PER_SYNC_COMMITTEE_PERIOD` | epochs | ~27.3 hours | ## Containers @@ -69,7 +69,7 @@ class LightClientUpdate(Container): finalized_header: BeaconBlockHeader finality_branch: Vector[Bytes32, floorlog2(FINALIZED_ROOT_INDEX)] # Sync committee aggregate signature - sync_committee_aggregate: SyncAggregate + sync_aggregate: SyncAggregate # Fork version for the aggregate signature fork_version: Version ``` @@ -127,7 +127,7 @@ def get_safety_threshold(store: LightClientStore) -> uint64: ## Light client state updates -A light client maintains its state in a `store` object of type `LightClientStore` and receives `update` objects of type `LightClientUpdate`. Every `update` triggers `process_light_client_update(store, update, current_slot)` where `current_slot` is the current slot based on some local clock. `process_slot_for_light_client_store` is processed every time the current slot increments. +A light client maintains its state in a `store` object of type `LightClientStore` and receives `update` objects of type `LightClientUpdate`. Every `update` triggers `process_light_client_update(store, update, current_slot, genesis_validators_root)` where `current_slot` is the current slot based on a local clock. `process_slot_for_light_client_store` is triggered every time the current slot increments. #### `process_slot_for_light_client_store` @@ -157,8 +157,8 @@ def validate_light_client_update(store: LightClientStore, assert current_slot >= active_header.slot > store.finalized_header.slot # Verify update does not skip a sync committee period - finalized_period = compute_epoch_at_slot(store.finalized_header.slot) // EPOCHS_PER_SYNC_COMMITTEE_PERIOD - update_period = compute_epoch_at_slot(active_header.slot) // EPOCHS_PER_SYNC_COMMITTEE_PERIOD + finalized_period = compute_sync_committee_period(compute_epoch_at_slot(store.finalized_header.slot)) + update_period = compute_sync_committee_period(compute_epoch_at_slot(active_header.slot)) assert update_period in (finalized_period, finalized_period + 1) # Verify that the `finalized_header`, if present, actually is the finalized header saved in the @@ -187,8 +187,8 @@ def validate_light_client_update(store: LightClientStore, index=get_subtree_index(NEXT_SYNC_COMMITTEE_INDEX), root=active_header.state_root, ) - - sync_aggregate = update.sync_committee_aggregate + + sync_aggregate = update.sync_aggregate # Verify sync committee has sufficient participants assert sum(sync_aggregate.sync_committee_bits) >= MIN_SYNC_COMMITTEE_PARTICIPANTS @@ -208,12 +208,14 @@ def validate_light_client_update(store: LightClientStore, ```python def apply_light_client_update(store: LightClientStore, update: LightClientUpdate) -> None: active_header = get_active_header(update) - finalized_period = compute_epoch_at_slot(store.finalized_header.slot) // EPOCHS_PER_SYNC_COMMITTEE_PERIOD - update_period = compute_epoch_at_slot(active_header.slot) // EPOCHS_PER_SYNC_COMMITTEE_PERIOD + finalized_period = compute_sync_committee_period(compute_epoch_at_slot(store.finalized_header.slot)) + update_period = compute_sync_committee_period(compute_epoch_at_slot(active_header.slot)) if update_period == finalized_period + 1: store.current_sync_committee = store.next_sync_committee store.next_sync_committee = update.next_sync_committee store.finalized_header = active_header + if store.finalized_header.slot > store.optimistic_header.slot: + store.optimistic_header = store.finalized_header ``` #### `process_light_client_update` @@ -225,12 +227,12 @@ def process_light_client_update(store: LightClientStore, genesis_validators_root: Root) -> None: validate_light_client_update(store, update, current_slot, genesis_validators_root) - sync_committee_bits = update.sync_committee_aggregate.sync_committee_bits + sync_committee_bits = update.sync_aggregate.sync_committee_bits # Update the best update in case we have to force-update to it if the timeout elapses if ( store.best_valid_update is None - or sum(sync_committee_bits) > sum(store.best_valid_update.sync_committee_aggregate.sync_committee_bits) + or sum(sync_committee_bits) > sum(store.best_valid_update.sync_aggregate.sync_committee_bits) ): store.best_valid_update = update diff --git a/specs/altair/validator.md b/specs/altair/validator.md index 461c9a70d..c59aa29f7 100644 --- a/specs/altair/validator.md +++ b/specs/altair/validator.md @@ -268,7 +268,7 @@ This process occurs each slot. If a validator is in the current sync committee (i.e. `is_assigned_to_sync_committee()` above returns `True`), then for every `slot` in the current sync committee period, the validator should prepare a `SyncCommitteeMessage` for the previous slot (`slot - 1`) according to the logic in `get_sync_committee_message` as soon as they have determined the head block of `slot - 1`. This means that when assigned to `slot` a `SyncCommitteeMessage` is prepared and broadcast in `slot-1 ` instead of `slot`. This logic is triggered upon the same conditions as when producing an attestation. -Meaning, a sync committee member should produce and broadcast a `SyncCommitteeMessage` either when (a) the validator has received a valid block from the expected block proposer for the current `slot` or (b) one-third of the slot has transpired (`SECONDS_PER_SLOT / 3` seconds after the start of the slot) -- whichever comes first. +Meaning, a sync committee member should produce and broadcast a `SyncCommitteeMessage` either when (a) the validator has received a valid block from the expected block proposer for the current `slot` or (b) one-third of the slot has transpired (`SECONDS_PER_SLOT / INTERVALS_PER_SLOT` seconds after the start of the slot) -- whichever comes first. `get_sync_committee_message(state, block_root, validator_index, privkey)` assumes the parameter `state` is the head state corresponding to processing the block up to the current slot as determined by the fork choice (including any empty slots up to the current slot processed with `process_slots` on top of the latest block), `block_root` is the root of the head block, `validator_index` is the index of the validator in the registry `state.validators` controlled by `privkey`, and `privkey` is the BLS private key for the validator. @@ -385,7 +385,7 @@ The collection of input signatures should include one signature per validator wh ##### Broadcast sync committee contribution -If the validator is selected to aggregate (`is_sync_committee_aggregator()`), then they broadcast their best aggregate as a `SignedContributionAndProof` to the global aggregate channel (`sync_committee_contribution_and_proof` topic) two-thirds of the way through the `slot`-that is, `SECONDS_PER_SLOT * 2 / 3` seconds after the start of `slot`. +If the validator is selected to aggregate (`is_sync_committee_aggregator()`), then they broadcast their best aggregate as a `SignedContributionAndProof` to the global aggregate channel (`sync_committee_contribution_and_proof` topic) two-thirds of the way through the `slot`-that is, `SECONDS_PER_SLOT * 2 / INTERVALS_PER_SLOT` seconds after the start of `slot`. Selection proofs are provided in `ContributionAndProof` to prove to the gossip channel that the validator has been selected as an aggregator. diff --git a/specs/bellatrix/beacon-chain.md b/specs/bellatrix/beacon-chain.md index 738f03556..b37a8ab71 100644 --- a/specs/bellatrix/beacon-chain.md +++ b/specs/bellatrix/beacon-chain.md @@ -35,7 +35,7 @@ - [Modified `slash_validator`](#modified-slash_validator) - [Beacon chain state transition function](#beacon-chain-state-transition-function) - [Execution engine](#execution-engine) - - [`execute_payload`](#execute_payload) + - [`notify_new_payload`](#notify_new_payload) - [Block processing](#block-processing) - [Execution payload](#execution-payload) - [`process_execution_payload`](#process_execution_payload) @@ -169,9 +169,9 @@ class ExecutionPayload(Container): parent_hash: Hash32 fee_recipient: ExecutionAddress # 'beneficiary' in the yellow paper state_root: Bytes32 - receipt_root: Bytes32 # 'receipts root' in the yellow paper + receipts_root: Bytes32 logs_bloom: ByteVector[BYTES_PER_LOGS_BLOOM] - random: Bytes32 # 'difficulty' in the yellow paper + prev_randao: Bytes32 # 'difficulty' in the yellow paper block_number: uint64 # 'number' in the yellow paper gas_limit: uint64 gas_used: uint64 @@ -191,9 +191,9 @@ class ExecutionPayloadHeader(Container): parent_hash: Hash32 fee_recipient: ExecutionAddress state_root: Bytes32 - receipt_root: Bytes32 + receipts_root: Bytes32 logs_bloom: ByteVector[BYTES_PER_LOGS_BLOOM] - random: Bytes32 + prev_randao: Bytes32 block_number: uint64 gas_limit: uint64 gas_used: uint64 @@ -307,17 +307,17 @@ def slash_validator(state: BeaconState, The implementation-dependent `ExecutionEngine` protocol encapsulates the execution sub-system logic via: * a state object `self.execution_state` of type `ExecutionState` -* a state transition function `self.execute_payload` which applies changes to the `self.execution_state` +* a notification function `self.notify_new_payload` which may apply changes to the `self.execution_state` -*Note*: `execute_payload` is a function accessed through the `EXECUTION_ENGINE` module which instantiates the `ExecutionEngine` protocol. +*Note*: `notify_new_payload` is a function accessed through the `EXECUTION_ENGINE` module which instantiates the `ExecutionEngine` protocol. The body of this function is implementation dependent. The Engine API may be used to implement this and similarly defined functions via an external execution engine. -#### `execute_payload` +#### `notify_new_payload` ```python -def execute_payload(self: ExecutionEngine, execution_payload: ExecutionPayload) -> bool: +def notify_new_payload(self: ExecutionEngine, execution_payload: ExecutionPayload) -> bool: """ Return ``True`` if and only if ``execution_payload`` is valid with respect to ``self.execution_state``. """ @@ -348,20 +348,20 @@ def process_execution_payload(state: BeaconState, payload: ExecutionPayload, exe # Verify consistency of the parent hash with respect to the previous execution payload header if is_merge_transition_complete(state): assert payload.parent_hash == state.latest_execution_payload_header.block_hash - # Verify random - assert payload.random == get_randao_mix(state, get_current_epoch(state)) + # Verify prev_randao + assert payload.prev_randao == get_randao_mix(state, get_current_epoch(state)) # Verify timestamp assert payload.timestamp == compute_timestamp_at_slot(state, state.slot) # Verify the execution payload is valid - assert execution_engine.execute_payload(payload) + assert execution_engine.notify_new_payload(payload) # Cache execution payload header state.latest_execution_payload_header = ExecutionPayloadHeader( parent_hash=payload.parent_hash, fee_recipient=payload.fee_recipient, state_root=payload.state_root, - receipt_root=payload.receipt_root, + receipts_root=payload.receipts_root, logs_bloom=payload.logs_bloom, - random=payload.random, + prev_randao=payload.prev_randao, block_number=payload.block_number, gas_limit=payload.gas_limit, gas_used=payload.gas_used, diff --git a/specs/bellatrix/fork-choice.md b/specs/bellatrix/fork-choice.md index 91fda88bc..0f5a7b646 100644 --- a/specs/bellatrix/fork-choice.md +++ b/specs/bellatrix/fork-choice.md @@ -80,7 +80,7 @@ Used to signal to initiate the payload build process via `notify_forkchoice_upda @dataclass class PayloadAttributes(object): timestamp: uint64 - random: Bytes32 + prev_randao: Bytes32 suggested_fee_recipient: ExecutionAddress ``` diff --git a/specs/bellatrix/p2p-interface.md b/specs/bellatrix/p2p-interface.md index aefbb54c7..60a9be774 100644 --- a/specs/bellatrix/p2p-interface.md +++ b/specs/bellatrix/p2p-interface.md @@ -29,6 +29,7 @@ Readers should understand the Phase 0 and Altair documents and use them as a bas - [Why was the max gossip message size increased at Bellatrix?](#why-was-the-max-gossip-message-size-increased-at-bellatrix) - [Req/Resp](#reqresp) - [Why was the max chunk response size increased at Bellatrix?](#why-was-the-max-chunk-response-size-increased-at-bellatrix) + - [Why allow invalid payloads on the P2P network?](#why-allow-invalid-payloads-on-the-p2p-network) @@ -85,13 +86,26 @@ The *type* of the payload of this topic changes to the (modified) `SignedBeaconB Specifically, this type changes with the addition of `execution_payload` to the inner `BeaconBlockBody`. See Bellatrix [state transition document](./beacon-chain.md#beaconblockbody) for further details. +Blocks with execution enabled will be permitted to propagate regardless of the +validity of the execution payload. This prevents network segregation between +[optimistic](/sync/optimistic.md) and non-optimistic nodes. + In addition to the gossip validations for this topic from prior specifications, the following validations MUST pass before forwarding the `signed_beacon_block` on the network. Alias `block = signed_beacon_block.message`, `execution_payload = block.body.execution_payload`. - If the execution is enabled for the block -- i.e. `is_execution_enabled(state, block.body)` then validate the following: - - _[REJECT]_ The block's execution payload timestamp is correct with respect to the slot - -- i.e. `execution_payload.timestamp == compute_timestamp_at_slot(state, block.slot)`. + - _[REJECT]_ The block's execution payload timestamp is correct with respect to the slot + -- i.e. `execution_payload.timestamp == compute_timestamp_at_slot(state, block.slot)`. + - If `exection_payload` verification of block's parent by an execution node is *not* complete: + - [REJECT] The block's parent (defined by `block.parent_root`) passes all + validation (excluding execution node verification of the `block.body.execution_payload`). + - otherwise: + - [IGNORE] The block's parent (defined by `block.parent_root`) passes all + validation (including execution node verification of the `block.body.execution_payload`). + +The following gossip validation from prior specifications MUST NOT be applied if the execution is enabled for the block -- i.e. `is_execution_enabled(state, block.body)`: + - [REJECT] The block's parent (defined by `block.parent_root`) passes validation. ### Transitioning the gossip @@ -100,6 +114,14 @@ details on how to handle transitioning gossip topics for Bellatrix. ## The Req/Resp domain +Non-faulty, [optimistic](/sync/optimistic.md) nodes may send blocks which +result in an INVALID response from an execution engine. To prevent network +segregation between optimistic and non-optimistic nodes, transmission of an +INVALID execution payload via the Req/Resp domain SHOULD NOT cause a node to be +down-scored or disconnected. Transmission of a block which is invalid due to +any consensus layer rules (i.e., *not* execution layer rules) MAY result in +down-scoring or disconnection. + ### Messages #### BeaconBlocksByRange v2 @@ -181,3 +203,26 @@ valid block sizes in the range of gas limits expected in the medium term. As with both gossip and req/rsp maximum values, type-specific limits should always by simultaneously respected. + +### Why allow invalid payloads on the P2P network? + +The specification allows blocks with invalid execution payloads to propagate across +gossip and via RPC calls. The reasoning for this is as follows: + +1. Optimistic nodes must listen to block gossip to obtain a view of the head of + the chain. +2. Therefore, optimistic nodes must propagate gossip blocks. Otherwise, they'd + be censoring. +3. If optimistic nodes will propagate blocks via gossip, then they must respond + to requests for the parent via RPC. +4. Therefore, optimistic nodes must send optimistic blocks via RPC. + +So, to prevent network segregation from optimistic nodes inadvertently sending +invalid execution payloads, nodes should never downscore/disconnect nodes due to such invalid +payloads. This does open the network to some DoS attacks from invalid execution +payloads, but the scope of actors is limited to validators who can put those +payloads in valid (and slashable) beacon blocks. Therefore, it is argued that +the DoS risk introduced in tolerable. + +More complicated schemes are possible that could restrict invalid payloads from +RPC. However, it's not clear that complexity is warranted. diff --git a/specs/bellatrix/validator.md b/specs/bellatrix/validator.md index 7e0369a39..47de49ba6 100644 --- a/specs/bellatrix/validator.md +++ b/specs/bellatrix/validator.md @@ -143,7 +143,7 @@ def prepare_execution_payload(state: BeaconState, # Set the forkchoice head and initiate the payload build process payload_attributes = PayloadAttributes( timestamp=compute_timestamp_at_slot(state, state.slot), - random=get_randao_mix(state, get_current_epoch(state)), + prev_randao=get_randao_mix(state, get_current_epoch(state)), suggested_fee_recipient=suggested_fee_recipient, ) return execution_engine.notify_forkchoice_updated(parent_hash, finalized_block_hash, payload_attributes) diff --git a/specs/custody_game/beacon-chain.md b/specs/custody_game/beacon-chain.md index 6f9f61cf9..fd00dedda 100644 --- a/specs/custody_game/beacon-chain.md +++ b/specs/custody_game/beacon-chain.md @@ -11,7 +11,8 @@ - [Introduction](#introduction) - [Constants](#constants) - [Misc](#misc) -- [Configuration](#configuration) + - [Domain types](#domain-types) +- [Preset](#preset) - [Time parameters](#time-parameters) - [Max operations per block](#max-operations-per-block) - [Size parameters](#size-parameters) diff --git a/specs/das/p2p-interface.md b/specs/das/p2p-interface.md index b4a0a9d2c..b1d96c718 100644 --- a/specs/das/p2p-interface.md +++ b/specs/das/p2p-interface.md @@ -196,11 +196,6 @@ This builds on top of the protocol identification and encoding spec which was in Note that DAS networking uses a different protocol prefix: `/eth2/das/req` -The result codes are extended with: -- 3: **ResourceUnavailable** -- when the request was valid but cannot be served at this point in time. - -TODO: unify with phase0? Lighthoue already defined this in their response codes enum. - ### Messages #### DASQuery diff --git a/specs/phase0/beacon-chain.md b/specs/phase0/beacon-chain.md index 7172307c6..618f39637 100644 --- a/specs/phase0/beacon-chain.md +++ b/specs/phase0/beacon-chain.md @@ -1832,7 +1832,7 @@ def process_attestation(state: BeaconState, attestation: Attestation) -> None: ##### Deposits ```python -def get_validator_from_deposit(state: BeaconState, deposit: Deposit) -> Validator: +def get_validator_from_deposit(deposit: Deposit) -> Validator: amount = deposit.data.amount effective_balance = min(amount - amount % EFFECTIVE_BALANCE_INCREMENT, MAX_EFFECTIVE_BALANCE) @@ -1877,7 +1877,7 @@ def process_deposit(state: BeaconState, deposit: Deposit) -> None: return # Add validator and balance entries - state.validators.append(get_validator_from_deposit(state, deposit)) + state.validators.append(get_validator_from_deposit(deposit)) state.balances.append(amount) else: # Increase balance by deposit amount diff --git a/specs/phase0/p2p-interface.md b/specs/phase0/p2p-interface.md index f5c138f99..4532feac8 100644 --- a/specs/phase0/p2p-interface.md +++ b/specs/phase0/p2p-interface.md @@ -569,11 +569,11 @@ The response code can have one of the following values, encoded as a single unsi The response payload adheres to the `ErrorMessage` schema (described below). - 3: **ResourceUnavailable** -- the responder does not have requested resource. The response payload adheres to the `ErrorMessage` schema (described below). - *Note*: This response code is only valid as a response to `BlocksByRange`. + *Note*: This response code is only valid as a response where specified. Clients MAY use response codes above `128` to indicate alternative, erroneous request-specific responses. -The range `[3, 127]` is RESERVED for future usages, and should be treated as error if not recognized expressly. +The range `[4, 127]` is RESERVED for future usages, and should be treated as error if not recognized expressly. The `ErrorMessage` schema is: diff --git a/sync/optimistic.md b/sync/optimistic.md new file mode 100644 index 000000000..8e6acc442 --- /dev/null +++ b/sync/optimistic.md @@ -0,0 +1,376 @@ +# Optimistic Sync + +## Introduction + +In order to provide a syncing execution engine with a partial view of the head +of the chain, it may be desirable for a consensus engine to import beacon +blocks without verifying the execution payloads. This partial sync is called an +*optimistic sync*. + +Optimistic sync is designed to be opt-in and backwards compatible (i.e., +non-optimistic nodes can tolerate optimistic nodes on the network and vice +versa). Optimistic sync is not a fundamental requirement for consensus nodes. +Rather, it's a stop-gap measure to allow execution nodes to sync via +established methods until future Ethereum roadmap items are implemented (e.g., +statelessness). + +## Constants + +|Name|Value|Unit +|---|---|---| +|`SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY`| `128` | slots + +*Note: the `SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY` must be user-configurable. See +[Fork Choice Poisoning](#fork-choice-poisoning).* + +## Helpers + +For brevity, we define two aliases for values of the `status` field on +`PayloadStatusV1`: + +- Alias `NOT_VALIDATED` to: + - `SYNCING` + - `ACCEPTED` +- Alias `INVALIDATED` to: + - `INVALID` + - `INVALID_BLOCK_HASH` + - `INVALID_TERMINAL_BLOCK` + +Let `head: BeaconBlock` be the result of calling of the fork choice +algorithm at the time of block production. Let `head_block_root: Root` be the +root of that block. + +Let `blocks: Dict[Root, BeaconBlock]` and `block_states: Dict[Root, +BeaconState]` be the blocks (and accompanying states) that have been verified +either completely or optimistically. + +Let `optimistic_roots: Set[Root]` be the set of `hash_tree_root(block)` for all +optimistically imported blocks which have only received a `NOT_VALIDATED` designation +from an execution engine (i.e., they are not known to be `INVALIDATED` or `VALID`). + +Let `current_slot: Slot` be `(time - genesis_time) // SECONDS_PER_SLOT` where +`time` is the UNIX time according to the local system clock. + +```python +@dataclass +class OptimisticStore(object): + optimistic_roots: Set[Root] + head_block_root: Root + blocks: Dict[Root, BeaconBlock] + block_states: Dict[Root, BeaconState] +``` + +```python +def is_optimistic(opt_store: OptimisticStore, block: BeaconBlock) -> bool: + return hash_tree_root(block) in opt_store.optimistic_roots +``` + +```python +def latest_verified_ancestor(opt_store: OptimisticStore, block: BeaconBlock) -> BeaconBlock: + # It is assumed that the `block` parameter is never an INVALIDATED block. + while True: + if not is_optimistic(opt_store, block) or block.parent_root == Root(): + return block + block = opt_store.blocks[block.parent_root] +``` + +```python +def is_execution_block(block: BeaconBlock) -> bool: + return block.body.execution_payload != ExecutionPayload() +``` + +```python +def is_optimistic_candidate_block(opt_store: OptimisticStore, current_slot: Slot, block: BeaconBlock) -> bool: + justified_root = opt_store.block_states[opt_store.head_block_root].current_justified_checkpoint.root + justified_is_execution_block = is_execution_block(opt_store.blocks[justified_root]) + block_is_deep = block.slot + SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY <= current_slot + return justified_is_execution_block or block_is_deep +``` + +Let only a node which returns `is_optimistic(opt_store, head) is True` be an *optimistic +node*. Let only a validator on an optimistic node be an *optimistic validator*. + +When this specification only defines behaviour for an optimistic +node/validator, but *not* for the non-optimistic case, assume default +behaviours without regard for optimistic sync. + +## Mechanisms + +### When to optimistically import blocks + +A block MAY be optimistically imported when +`is_optimistic_candidate_block(opt_store, current_slot, block)` returns +`True`. This ensures that blocks are only optimistically imported if either: + +1. The justified checkpoint has execution enabled. +1. The current slot (as per the system clock) is at least + `SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY` ahead of the slot of the block being + imported. + +*See [Fork Choice Poisoning](#fork-choice-poisoning) for the motivations behind +these conditions.* + +### How to optimistically import blocks + +To optimistically import a block: + +- The [`notify_new_payload`](../specs/bellatrix/beacon-chain.md#notify_new_payload) function MUST return `True` if the execution + engine returns `NOT_VALIDATED` or `VALID`. An `INVALIDATED` response MUST return `False`. +- The [`validate_merge_block`](../specs/bellatrix/fork-choice.md#validate_merge_block) + function MUST NOT raise an assertion if both the +`pow_block` and `pow_parent` are unknown to the execution engine. + - All other assertions in [`validate_merge_block`](../specs/bellatrix/fork-choice.md#validate_merge_block) + (e.g., `TERMINAL_BLOCK_HASH`) MUST prevent an optimistic import. +- The parent of the block MUST NOT have an `INVALIDATED` execution payload. + +In addition to this change in validation, the consensus engine MUST track which +blocks returned `NOT_VALIDATED` and which returned `VALID` for subsequent processing. + +Optimistically imported blocks MUST pass all verifications included in +`process_block` (withstanding the modifications to `notify_new_payload`). + +A consensus engine MUST be able to retrospectively (i.e., after import) modify +the status of `NOT_VALIDATED` blocks to be either `VALID` or `INVALIDATED` based upon responses +from an execution engine. I.e., perform the following transitions: + +- `NOT_VALIDATED` -> `VALID` +- `NOT_VALIDATED` -> `INVALIDATED` + +When a block transitions from `NOT_VALIDATED` -> `VALID`, all *ancestors* of the +block MUST also transition from `NOT_VALIDATED` -> `VALID`. Such a block and any previously `NOT_VALIDATED` ancestors are no longer +considered "optimistically imported". + +When a block transitions from `NOT_VALIDATED` -> `INVALIDATED`, all *descendants* of the +block MUST also transition from `NOT_VALIDATED` -> `INVALIDATED`. + +When a block transitions from the `NOT_VALIDATED` state, it is removed from the set of +`opt_store.optimistic_roots`. + +When a "merge block" (i.e. the first block which enables execution in a chain) is declared to be +`VALID` by an execution engine (either directly or indirectly), the full +[`validate_merge_block`](../specs/bellatrix/fork-choice.md#validate_merge_block) +MUST be run against the merge block. If the block +fails [`validate_merge_block`](../specs/bellatrix/fork-choice.md#validate_merge_block), +the merge block MUST be treated the same as +an `INVALIDATED` block (i.e., it and all its descendants are invalidated and +removed from the block tree). + +### Execution Engine Errors + +When an execution engine returns an error or fails to respond to a payload +validity request for some block, a consensus engine: + +- MUST NOT optimistically import the block. +- MUST NOT apply the block to the fork choice store. +- MAY queue the block for later processing. + +### Assumptions about Execution Engine Behaviour + +This specification assumes execution engines will only return `NOT_VALIDATED` when +there is insufficient information available to make a `VALID` or `INVALIDATED` +determination on the given `ExecutionPayload` (e.g., the parent payload is +unknown). Specifically, `NOT_VALIDATED` responses should be fork-specific, in that +the search for a block on one chain MUST NOT trigger a `NOT_VALIDATED` response for +another chain. + +### Re-Orgs + +The consensus engine MUST support any chain reorganisation which does *not* +affect the justified checkpoint. + +If the justified checkpoint transitions from `NOT_VALIDATED` -> `INVALIDATED`, a +consensus engine MAY choose to alert the user and force the application to +exit. + +## Fork Choice + +Consensus engines MUST support removing blocks from fork choice that transition +from `NOT_VALIDATED` to `INVALIDATED`. Specifically, a block deemed `INVALIDATED` at any +point MUST NOT be included in the canonical chain and the weights from those +`INVALIDATED` blocks MUST NOT be applied to any `VALID` or `NOT_VALIDATED` ancestors. + +### Fork Choice Poisoning + +During the merge transition it is possible for an attacker to craft a +`BeaconBlock` with an execution payload that references an +eternally-unavailable `body.execution_payload.parent_hash` (i.e., the parent +hash is random bytes). In rare circumstances, it is possible that an attacker +can build atop such a block to trigger justification. If an optimistic node +imports this malicious chain, that node will have a "poisoned" fork choice +store, such that the node is unable to produce a block that descends from the +head (due to the invalid chain of payloads) and the node is unable to produce a +block that forks around the head (due to the justification of the malicious +chain). + +If an honest chain exists which justifies a higher epoch than the malicious +chain, that chain will take precedence and revive any poisoned store. Such a +chain, if imported before the malicious chain, will prevent the store from +being poisoned. Therefore, the poisoning attack is temporary if >= 2/3rds of +the network is honest and non-faulty. + +The `SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY` parameter assumes that the network +will justify a honest chain within some number of slots. With this assumption, +it is acceptable to optimistically import transition blocks during the sync +process. Since there is an assumption that an honest chain with a higher +justified checkpoint exists, any fork choice poisoning will be short-lived and +resolved before that node is required to produce a block. + +However, the assumption that the honest, canonical chain will always justify +within `SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY` slots is dubious. Therefore, +clients MUST provide the following command line flag to assist with manual +disaster recovery: + +- `--safe-slots-to-import-optimistically`: modifies the + `SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY`. + +## Checkpoint Sync (Weak Subjectivity Sync) + +A consensus engine MAY assume that the `ExecutionPayload` of a block used as an +anchor for checkpoint sync is `VALID` without necessarily providing that +payload to an execution engine. + +## Validator assignments + +An optimistic node is *not* a full node. It is unable to produce blocks, since +an execution engine cannot produce a payload upon an unknown parent. It cannot +faithfully attest to the head block of the chain, since it has not fully +verified that block. + +### Block Production + +An optimistic validator MUST NOT produce a block (i.e., sign across the +`DOMAIN_BEACON_PROPOSER` domain). + +### Attesting + +An optimistic validator MUST NOT participate in attestation (i.e., sign across the +`DOMAIN_BEACON_ATTESTER`, `DOMAIN_SELECTION_PROOF` or +`DOMAIN_AGGREGATE_AND_PROOF` domains). + +### Participating in Sync Committees + +An optimistic validator MUST NOT participate in sync committees (i.e., sign across the +`DOMAIN_SYNC_COMMITTEE`, `DOMAIN_SYNC_COMMITTEE_SELECTION_PROOF` or +`DOMAIN_CONTRIBUTION_AND_PROOF` domains). + +## Ethereum Beacon APIs + +Consensus engines which provide an implementation of the [Ethereum Beacon +APIs](https://github.com/ethereum/beacon-APIs) must take care to avoid +presenting optimistic blocks as fully-verified blocks. + +### Helpers + +Let the following response types be defined as any response with the +corresponding HTTP status code: + +- "Success" Response: Status Codes 200-299. +- "Not Found" Response: Status Code 404. +- "Syncing" Response: Status Code 503. + +### Requests for Optimistic Blocks + +When information about an optimistic block is requested, the consensus engine: + +- MUST NOT respond with success. +- MAY respond with not found. +- MAY respond with syncing. + +### Requests for an Optimistic Head + +When `is_optimistic(opt_store, head) is True`, the consensus engine: + +- MUST NOT return an optimistic `head`. +- MAY substitute the head block with `latest_verified_ancestor(block)`. +- MAY return syncing. + +### Requests to Validators Endpoints + +When `is_optimistic(opt_store, head) is True`, the consensus engine MUST return syncing to +all endpoints which match the following pattern: + +- `eth/*/validator/*` + +## Design Decision Rationale + +### Why `SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY`? + +Nodes can only import an optimistic block if their justified checkpoint is +verified or the block is older than `SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY`. + +These restraints are applied in order to mitigate an attack where a block which +enables execution (a *transition block*) can reference a junk parent hash. This +makes it impossible for honest nodes to build atop that block. If an attacker +exploits a nuance in fork choice `filter_block_tree`, they can, in some rare +cases, produce a junk block that out-competes all locally produced blocks for +the head. This prevents a node from producing a chain of blocks, therefore +breaking liveness. + +Thankfully, if 2/3rds of validators are not poisoned, they can justify an +honest chain which will un-poison all other nodes. + +Notably, this attack only exists for optimistic nodes. Nodes which fully verify +the transition block will reject a block with a junk parent hash. Therefore, +liveness is unaffected if a vast majority of nodes have fully synced execution +and consensus clients before and during the transition. + +Given all of this, we can say two things: + +1. **BNs which are following the head during the transition shouldn't + optimistically import the transition block.** If 1/3rd of validators + optimistically import the poison block, there will be no remaining nodes to + justify an honest chain. +2. **BNs which are syncing can optimistically import transition blocks.** In + this case a justified chain already exists blocks. The poison block would be + quickly reverted and would have no affect on liveness. + +Astute readers will notice that (2) contains a glaring assumption about network +liveness. This is necessary because a node cannot feasibly ascertain that the +transition block is justified without importing that block and risking +poisoning. Therefore, we use `SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY` to say +something along the lines of: *"if the transition block is sufficiently old +enough, then we can just assume that block is honest or there exists an honest +justified chain to out-compete it."* + +Note the use of "feasibly" in the previous paragraph. One can imagine +mechanisms to check that a block is justified before importing it. For example, +just keep processing blocks without adding them to fork choice. However, there +are still edge-cases here (e.g., when to halt and declare there was no +justification?) and how to mitigate implementation complexity. At this point, +it's important to reflect on the attack and how likely it is to happen. It +requires some rather contrived circumstances and it seems very unlikely to +occur. Therefore, we need to consider if adding complexity to avoid an +unlikely attack increases or decreases our total risk. Presently, it appears +that `SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY` sits in a sweet spot for this +trade-off. + +### Transitioning from VALID -> INVALIDATED or INVALIDATED -> VALID + +These operations are purposefully omitted. It is outside of the scope of the +specification since it's only possible with a faulty EE. + +Such a scenario requires manual intervention. + +## What about Light Clients? + +An alternative to optimistic sync is to run a light client inside/alongside +beacon nodes that mitigates the need for optimistic sync by providing +tip-of-chain blocks to the execution engine. However, light clients comes with +their own set of complexities. Relying on light clients may also restrict nodes +from syncing from genesis, if they so desire. + +A notable thing about optimistic sync is that it's *optional*. Should an +implementation decide to go the light-client route, then they can just ignore +optimistic sync all together. + +## What if `TERMINAL_BLOCK_HASH` is used? + +If the terminal block hash override is used (i.e., `TERMINAL_BLOCK_HASH != +Hash32()`), the [`validate_merge_block`](../specs/bellatrix/fork-choice.md#validate_merge_block) +function will deterministically +return `True` or `False`. Whilst it's not *technically* required +retrospectively call [`validate_merge_block`](../specs/bellatrix/fork-choice.md#validate_merge_block) +on a transition block that +matches `TERMINAL_BLOCK_HASH` after an optimistic sync, doing so will have no +effect. For simplicity, the optimistic sync specification does not define +edge-case behaviour for when `TERMINAL_BLOCK_HASH` is used. diff --git a/tests/core/pyspec/eth2spec/VERSION.txt b/tests/core/pyspec/eth2spec/VERSION.txt index db1527897..a5e428299 100644 --- a/tests/core/pyspec/eth2spec/VERSION.txt +++ b/tests/core/pyspec/eth2spec/VERSION.txt @@ -1 +1 @@ -1.1.8 \ No newline at end of file +1.1.9 \ No newline at end of file diff --git a/tests/core/pyspec/eth2spec/config/config_util.py b/tests/core/pyspec/eth2spec/config/config_util.py index 0d06428ea..8fe2d8344 100644 --- a/tests/core/pyspec/eth2spec/config/config_util.py +++ b/tests/core/pyspec/eth2spec/config/config_util.py @@ -14,7 +14,7 @@ def parse_config_vars(conf: Dict[str, Any]) -> Dict[str, Any]: out[k] = [int(item) if item.isdigit() else item for item in v] elif isinstance(v, str) and v.startswith("0x"): out[k] = bytes.fromhex(v[2:]) - elif k != 'PRESET_BASE': + elif k != 'PRESET_BASE' and k != 'CONFIG_NAME': out[k] = int(v) else: out[k] = v diff --git a/tests/core/pyspec/eth2spec/test/altair/epoch_processing/test_process_sync_committee_updates.py b/tests/core/pyspec/eth2spec/test/altair/epoch_processing/test_process_sync_committee_updates.py index 1667a12b2..4b0cb70d5 100644 --- a/tests/core/pyspec/eth2spec/test/altair/epoch_processing/test_process_sync_committee_updates.py +++ b/tests/core/pyspec/eth2spec/test/altair/epoch_processing/test_process_sync_committee_updates.py @@ -24,7 +24,7 @@ def run_sync_committees_progress_test(spec, state): first_sync_committee = state.current_sync_committee.copy() second_sync_committee = state.next_sync_committee.copy() - current_period = spec.get_current_epoch(state) // spec.EPOCHS_PER_SYNC_COMMITTEE_PERIOD + current_period = spec.compute_sync_committee_period(spec.get_current_epoch(state)) next_period = current_period + 1 next_period_start_epoch = next_period * spec.EPOCHS_PER_SYNC_COMMITTEE_PERIOD next_period_start_slot = next_period_start_epoch * spec.SLOTS_PER_EPOCH diff --git a/tests/core/pyspec/eth2spec/test/altair/unittests/test_sync_protocol.py b/tests/core/pyspec/eth2spec/test/altair/unittests/test_sync_protocol.py index 30444c4ce..04553b3f8 100644 --- a/tests/core/pyspec/eth2spec/test/altair/unittests/test_sync_protocol.py +++ b/tests/core/pyspec/eth2spec/test/altair/unittests/test_sync_protocol.py @@ -5,38 +5,29 @@ from eth2spec.test.context import ( with_presets, with_altair_and_later, ) -from eth2spec.test.helpers.attestations import next_epoch_with_attestations +from eth2spec.test.helpers.attestations import ( + next_epoch_with_attestations, +) from eth2spec.test.helpers.block import ( build_empty_block, build_empty_block_for_next_slot, ) from eth2spec.test.helpers.constants import MINIMAL +from eth2spec.test.helpers.light_client import ( + get_sync_aggregate, + initialize_light_client_store, +) from eth2spec.test.helpers.state import ( next_slots, state_transition_and_sign_block, ) -from eth2spec.test.helpers.sync_committee import ( - compute_aggregate_sync_committee_signature, -) from eth2spec.test.helpers.merkle import build_proof -def _initialize_light_client_store(spec, state): - return spec.LightClientStore( - finalized_header=spec.BeaconBlockHeader(), - current_sync_committee=state.current_sync_committee, - next_sync_committee=state.next_sync_committee, - best_valid_update=None, - optimistic_header=spec.BeaconBlockHeader(), - previous_max_active_participants=0, - current_max_active_participants=0, - ) - - @with_altair_and_later @spec_state_test def test_process_light_client_update_not_timeout(spec, state): - store = _initialize_light_client_store(spec, state) + store = initialize_light_client_store(spec, state) # Block at slot 1 doesn't increase sync committee period, so it won't force update store.finalized_header block = build_empty_block_for_next_slot(spec, state) @@ -49,19 +40,7 @@ def test_process_light_client_update_not_timeout(spec, state): body_root=signed_block.message.body.hash_tree_root(), ) # Sync committee signing the header - all_pubkeys = [v.pubkey for v in state.validators] - committee = [all_pubkeys.index(pubkey) for pubkey in state.current_sync_committee.pubkeys] - sync_committee_bits = [True] * len(committee) - sync_committee_signature = compute_aggregate_sync_committee_signature( - spec, - state, - block_header.slot, - committee, - ) - sync_committee_aggregate = spec.SyncAggregate( - sync_committee_bits=sync_committee_bits, - sync_committee_signature=sync_committee_signature, - ) + sync_aggregate = get_sync_aggregate(spec, state, block_header, block_root=None) next_sync_committee_branch = [spec.Bytes32() for _ in range(spec.floorlog2(spec.NEXT_SYNC_COMMITTEE_INDEX))] # Ensure that finality checkpoint is genesis @@ -76,7 +55,7 @@ def test_process_light_client_update_not_timeout(spec, state): next_sync_committee_branch=next_sync_committee_branch, finalized_header=finality_header, finality_branch=finality_branch, - sync_committee_aggregate=sync_committee_aggregate, + sync_aggregate=sync_aggregate, fork_version=state.fork.current_version, ) @@ -94,12 +73,12 @@ def test_process_light_client_update_not_timeout(spec, state): @spec_state_test @with_presets([MINIMAL], reason="too slow") def test_process_light_client_update_timeout(spec, state): - store = _initialize_light_client_store(spec, state) + store = initialize_light_client_store(spec, state) # Forward to next sync committee period next_slots(spec, state, spec.UPDATE_TIMEOUT) - snapshot_period = spec.compute_epoch_at_slot(store.optimistic_header.slot) // spec.EPOCHS_PER_SYNC_COMMITTEE_PERIOD - update_period = spec.compute_epoch_at_slot(state.slot) // spec.EPOCHS_PER_SYNC_COMMITTEE_PERIOD + snapshot_period = spec.compute_sync_committee_period(spec.compute_epoch_at_slot(store.optimistic_header.slot)) + update_period = spec.compute_sync_committee_period(spec.compute_epoch_at_slot(state.slot)) assert snapshot_period + 1 == update_period block = build_empty_block_for_next_slot(spec, state) @@ -113,20 +92,8 @@ def test_process_light_client_update_timeout(spec, state): ) # Sync committee signing the finalized_block_header - all_pubkeys = [v.pubkey for v in state.validators] - committee = [all_pubkeys.index(pubkey) for pubkey in state.current_sync_committee.pubkeys] - sync_committee_bits = [True] * len(committee) - sync_committee_signature = compute_aggregate_sync_committee_signature( - spec, - state, - block_header.slot, - committee, - block_root=spec.Root(block_header.hash_tree_root()), - ) - sync_committee_aggregate = spec.SyncAggregate( - sync_committee_bits=sync_committee_bits, - sync_committee_signature=sync_committee_signature, - ) + sync_aggregate = get_sync_aggregate( + spec, state, block_header, block_root=spec.Root(block_header.hash_tree_root())) # Sync committee is updated next_sync_committee_branch = build_proof(state.get_backing(), spec.NEXT_SYNC_COMMITTEE_INDEX) @@ -140,7 +107,7 @@ def test_process_light_client_update_timeout(spec, state): next_sync_committee_branch=next_sync_committee_branch, finalized_header=finality_header, finality_branch=finality_branch, - sync_committee_aggregate=sync_committee_aggregate, + sync_aggregate=sync_aggregate, fork_version=state.fork.current_version, ) @@ -158,7 +125,7 @@ def test_process_light_client_update_timeout(spec, state): @spec_state_test @with_presets([MINIMAL], reason="too slow") def test_process_light_client_update_finality_updated(spec, state): - store = _initialize_light_client_store(spec, state) + store = initialize_light_client_store(spec, state) # Change finality blocks = [] @@ -169,8 +136,8 @@ def test_process_light_client_update_finality_updated(spec, state): # Ensure that finality checkpoint has changed assert state.finalized_checkpoint.epoch == 3 # Ensure that it's same period - snapshot_period = spec.compute_epoch_at_slot(store.optimistic_header.slot) // spec.EPOCHS_PER_SYNC_COMMITTEE_PERIOD - update_period = spec.compute_epoch_at_slot(state.slot) // spec.EPOCHS_PER_SYNC_COMMITTEE_PERIOD + snapshot_period = spec.compute_sync_committee_period(spec.compute_epoch_at_slot(store.optimistic_header.slot)) + update_period = spec.compute_sync_committee_period(spec.compute_epoch_at_slot(state.slot)) assert snapshot_period == update_period # Updated sync_committee and finality @@ -191,20 +158,8 @@ def test_process_light_client_update_finality_updated(spec, state): ) # Sync committee signing the finalized_block_header - all_pubkeys = [v.pubkey for v in state.validators] - committee = [all_pubkeys.index(pubkey) for pubkey in state.current_sync_committee.pubkeys] - sync_committee_bits = [True] * len(committee) - sync_committee_signature = compute_aggregate_sync_committee_signature( - spec, - state, - block_header.slot, - committee, - block_root=spec.Root(block_header.hash_tree_root()), - ) - sync_committee_aggregate = spec.SyncAggregate( - sync_committee_bits=sync_committee_bits, - sync_committee_signature=sync_committee_signature, - ) + sync_aggregate = get_sync_aggregate( + spec, state, block_header, block_root=spec.Root(block_header.hash_tree_root())) update = spec.LightClientUpdate( attested_header=block_header, @@ -212,7 +167,7 @@ def test_process_light_client_update_finality_updated(spec, state): next_sync_committee_branch=next_sync_committee_branch, finalized_header=finalized_block_header, finality_branch=finality_branch, - sync_committee_aggregate=sync_committee_aggregate, + sync_aggregate=sync_aggregate, fork_version=state.fork.current_version, ) diff --git a/tests/core/pyspec/eth2spec/test/bellatrix/block_processing/test_process_execution_payload.py b/tests/core/pyspec/eth2spec/test/bellatrix/block_processing/test_process_execution_payload.py index b8c5f55c3..cd7a74259 100644 --- a/tests/core/pyspec/eth2spec/test/bellatrix/block_processing/test_process_execution_payload.py +++ b/tests/core/pyspec/eth2spec/test/bellatrix/block_processing/test_process_execution_payload.py @@ -25,7 +25,7 @@ def run_execution_payload_processing(spec, state, execution_payload, valid=True, called_new_block = False class TestEngine(spec.NoopExecutionEngine): - def execute_payload(self, payload) -> bool: + def notify_new_payload(self, payload) -> bool: nonlocal called_new_block, execution_valid called_new_block = True assert payload == execution_payload @@ -153,7 +153,7 @@ def test_bad_random_first_payload(spec, state): # execution payload execution_payload = build_empty_execution_payload(spec, state) - execution_payload.random = b'\x42' * 32 + execution_payload.prev_randao = b'\x42' * 32 yield from run_execution_payload_processing(spec, state, execution_payload, valid=False) @@ -167,7 +167,7 @@ def test_bad_random_regular_payload(spec, state): # execution payload execution_payload = build_empty_execution_payload(spec, state) - execution_payload.random = b'\x04' * 32 + execution_payload.prev_randao = b'\x04' * 32 yield from run_execution_payload_processing(spec, state, execution_payload, valid=False) @@ -182,7 +182,7 @@ def test_bad_everything_regular_payload(spec, state): # execution payload execution_payload = build_empty_execution_payload(spec, state) execution_payload.parent_hash = spec.Hash32() - execution_payload.random = spec.Bytes32() + execution_payload.prev_randao = spec.Bytes32() execution_payload.timestamp = 0 yield from run_execution_payload_processing(spec, state, execution_payload, valid=False) @@ -214,3 +214,35 @@ def test_bad_timestamp_regular_payload(spec, state): execution_payload.timestamp = execution_payload.timestamp + 1 yield from run_execution_payload_processing(spec, state, execution_payload, valid=False) + + +@with_bellatrix_and_later +@spec_state_test +def test_non_empty_extra_data_first_payload(spec, state): + # pre-state + state = build_state_with_incomplete_transition(spec, state) + next_slot(spec, state) + + # execution payload + execution_payload = build_empty_execution_payload(spec, state) + execution_payload.extra_data = b'\x45' * 12 + + yield from run_execution_payload_processing(spec, state, execution_payload) + + assert state.latest_execution_payload_header.extra_data == execution_payload.extra_data + + +@with_bellatrix_and_later +@spec_state_test +def test_non_empty_extra_data_regular_payload(spec, state): + # pre-state + state = build_state_with_complete_transition(spec, state) + next_slot(spec, state) + + # execution payload + execution_payload = build_empty_execution_payload(spec, state) + execution_payload.extra_data = b'\x45' * 12 + + yield from run_execution_payload_processing(spec, state, execution_payload) + + assert state.latest_execution_payload_header.extra_data == execution_payload.extra_data diff --git a/tests/core/pyspec/eth2spec/test/bellatrix/random/__init__.py b/tests/core/pyspec/eth2spec/test/bellatrix/random/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/core/pyspec/eth2spec/test/bellatrix/random/test_random.py b/tests/core/pyspec/eth2spec/test/bellatrix/random/test_random.py new file mode 100644 index 000000000..2dccaf949 --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/bellatrix/random/test_random.py @@ -0,0 +1,438 @@ +""" +This module is generated from the ``random`` test generator. +Please do not edit this file manually. +See the README for that generator for more information. +""" + +from eth2spec.test.helpers.constants import BELLATRIX +from eth2spec.test.context import ( + misc_balances_in_default_range_with_many_validators, + with_phases, + zero_activation_threshold, + only_generator, +) +from eth2spec.test.context import ( + always_bls, + spec_test, + with_custom_state, + single_phase, +) +from eth2spec.test.utils.randomized_block_tests import ( + run_generated_randomized_test, +) + + +@only_generator("randomized test for broad coverage, not point-to-point CI") +@with_phases([BELLATRIX]) +@with_custom_state( + balances_fn=misc_balances_in_default_range_with_many_validators, + threshold_fn=zero_activation_threshold +) +@spec_test +@single_phase +@always_bls +def test_randomized_0(spec, state): + # scenario as high-level, informal text: + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:random_block_bellatrix + # epochs:1,slots:0,with-block:no_block + # epochs:0,slots:random_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block_bellatrix + scenario = {'transitions': [{'validation': 'validate_is_not_leaking', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block'}, {'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 0, 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_bellatrix', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}, {'epochs_to_skip': 1, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'random_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_bellatrix', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}], 'state_randomizer': 'randomize_state_bellatrix'} # noqa: E501 + yield from run_generated_randomized_test( + spec, + state, + scenario, + ) + + +@only_generator("randomized test for broad coverage, not point-to-point CI") +@with_phases([BELLATRIX]) +@with_custom_state( + balances_fn=misc_balances_in_default_range_with_many_validators, + threshold_fn=zero_activation_threshold +) +@spec_test +@single_phase +@always_bls +def test_randomized_1(spec, state): + # scenario as high-level, informal text: + # epochs:0,slots:0,with-block:no_block + # epochs:1,slots:0,with-block:no_block + # epochs:0,slots:random_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block_bellatrix + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:random_block_bellatrix + scenario = {'transitions': [{'validation': 'validate_is_not_leaking', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block'}, {'epochs_to_skip': 1, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'random_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_bellatrix', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}, {'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 0, 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_bellatrix', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}], 'state_randomizer': 'randomize_state_bellatrix'} # noqa: E501 + yield from run_generated_randomized_test( + spec, + state, + scenario, + ) + + +@only_generator("randomized test for broad coverage, not point-to-point CI") +@with_phases([BELLATRIX]) +@with_custom_state( + balances_fn=misc_balances_in_default_range_with_many_validators, + threshold_fn=zero_activation_threshold +) +@spec_test +@single_phase +@always_bls +def test_randomized_2(spec, state): + # scenario as high-level, informal text: + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:penultimate_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block_bellatrix + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:last_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block_bellatrix + scenario = {'transitions': [{'validation': 'validate_is_not_leaking', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block'}, {'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'penultimate_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_bellatrix', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}, {'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'last_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_bellatrix', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}], 'state_randomizer': 'randomize_state_bellatrix'} # noqa: E501 + yield from run_generated_randomized_test( + spec, + state, + scenario, + ) + + +@only_generator("randomized test for broad coverage, not point-to-point CI") +@with_phases([BELLATRIX]) +@with_custom_state( + balances_fn=misc_balances_in_default_range_with_many_validators, + threshold_fn=zero_activation_threshold +) +@spec_test +@single_phase +@always_bls +def test_randomized_3(spec, state): + # scenario as high-level, informal text: + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:last_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block_bellatrix + # epochs:1,slots:0,with-block:no_block + # epochs:0,slots:last_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block_bellatrix + scenario = {'transitions': [{'validation': 'validate_is_not_leaking', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block'}, {'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'last_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_bellatrix', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}, {'epochs_to_skip': 1, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'last_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_bellatrix', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}], 'state_randomizer': 'randomize_state_bellatrix'} # noqa: E501 + yield from run_generated_randomized_test( + spec, + state, + scenario, + ) + + +@only_generator("randomized test for broad coverage, not point-to-point CI") +@with_phases([BELLATRIX]) +@with_custom_state( + balances_fn=misc_balances_in_default_range_with_many_validators, + threshold_fn=zero_activation_threshold +) +@spec_test +@single_phase +@always_bls +def test_randomized_4(spec, state): + # scenario as high-level, informal text: + # epochs:0,slots:0,with-block:no_block + # epochs:1,slots:0,with-block:no_block + # epochs:0,slots:last_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block_bellatrix + # epochs:1,slots:0,with-block:no_block + # epochs:0,slots:penultimate_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block_bellatrix + scenario = {'transitions': [{'validation': 'validate_is_not_leaking', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block'}, {'epochs_to_skip': 1, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'last_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_bellatrix', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}, {'epochs_to_skip': 1, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'penultimate_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_bellatrix', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}], 'state_randomizer': 'randomize_state_bellatrix'} # noqa: E501 + yield from run_generated_randomized_test( + spec, + state, + scenario, + ) + + +@only_generator("randomized test for broad coverage, not point-to-point CI") +@with_phases([BELLATRIX]) +@with_custom_state( + balances_fn=misc_balances_in_default_range_with_many_validators, + threshold_fn=zero_activation_threshold +) +@spec_test +@single_phase +@always_bls +def test_randomized_5(spec, state): + # scenario as high-level, informal text: + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:random_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block_bellatrix + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:random_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block_bellatrix + scenario = {'transitions': [{'validation': 'validate_is_not_leaking', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block'}, {'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'random_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_bellatrix', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}, {'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'random_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_bellatrix', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}], 'state_randomizer': 'randomize_state_bellatrix'} # noqa: E501 + yield from run_generated_randomized_test( + spec, + state, + scenario, + ) + + +@only_generator("randomized test for broad coverage, not point-to-point CI") +@with_phases([BELLATRIX]) +@with_custom_state( + balances_fn=misc_balances_in_default_range_with_many_validators, + threshold_fn=zero_activation_threshold +) +@spec_test +@single_phase +@always_bls +def test_randomized_6(spec, state): + # scenario as high-level, informal text: + # epochs:0,slots:0,with-block:no_block + # epochs:1,slots:0,with-block:no_block + # epochs:0,slots:penultimate_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block_bellatrix + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:penultimate_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block_bellatrix + scenario = {'transitions': [{'validation': 'validate_is_not_leaking', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block'}, {'epochs_to_skip': 1, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'penultimate_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_bellatrix', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}, {'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'penultimate_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_bellatrix', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}], 'state_randomizer': 'randomize_state_bellatrix'} # noqa: E501 + yield from run_generated_randomized_test( + spec, + state, + scenario, + ) + + +@only_generator("randomized test for broad coverage, not point-to-point CI") +@with_phases([BELLATRIX]) +@with_custom_state( + balances_fn=misc_balances_in_default_range_with_many_validators, + threshold_fn=zero_activation_threshold +) +@spec_test +@single_phase +@always_bls +def test_randomized_7(spec, state): + # scenario as high-level, informal text: + # epochs:0,slots:0,with-block:no_block + # epochs:1,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:random_block_bellatrix + # epochs:1,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:random_block_bellatrix + scenario = {'transitions': [{'validation': 'validate_is_not_leaking', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block'}, {'epochs_to_skip': 1, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 0, 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_bellatrix', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}, {'epochs_to_skip': 1, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 0, 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_bellatrix', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}], 'state_randomizer': 'randomize_state_bellatrix'} # noqa: E501 + yield from run_generated_randomized_test( + spec, + state, + scenario, + ) + + +@only_generator("randomized test for broad coverage, not point-to-point CI") +@with_phases([BELLATRIX]) +@with_custom_state( + balances_fn=misc_balances_in_default_range_with_many_validators, + threshold_fn=zero_activation_threshold +) +@spec_test +@single_phase +@always_bls +def test_randomized_8(spec, state): + # scenario as high-level, informal text: + # epochs:epochs_until_leak,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:random_block_bellatrix + # epochs:1,slots:0,with-block:no_block + # epochs:0,slots:random_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block_bellatrix + scenario = {'transitions': [{'epochs_to_skip': 'epochs_until_leak', 'validation': 'validate_is_leaking', 'slots_to_skip': 0, 'block_producer': 'no_block'}, {'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 0, 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_bellatrix', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}, {'epochs_to_skip': 1, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'random_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_bellatrix', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}], 'state_randomizer': 'randomize_state_bellatrix'} # noqa: E501 + yield from run_generated_randomized_test( + spec, + state, + scenario, + ) + + +@only_generator("randomized test for broad coverage, not point-to-point CI") +@with_phases([BELLATRIX]) +@with_custom_state( + balances_fn=misc_balances_in_default_range_with_many_validators, + threshold_fn=zero_activation_threshold +) +@spec_test +@single_phase +@always_bls +def test_randomized_9(spec, state): + # scenario as high-level, informal text: + # epochs:epochs_until_leak,slots:0,with-block:no_block + # epochs:1,slots:0,with-block:no_block + # epochs:0,slots:random_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block_bellatrix + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:random_block_bellatrix + scenario = {'transitions': [{'epochs_to_skip': 'epochs_until_leak', 'validation': 'validate_is_leaking', 'slots_to_skip': 0, 'block_producer': 'no_block'}, {'epochs_to_skip': 1, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'random_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_bellatrix', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}, {'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 0, 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_bellatrix', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}], 'state_randomizer': 'randomize_state_bellatrix'} # noqa: E501 + yield from run_generated_randomized_test( + spec, + state, + scenario, + ) + + +@only_generator("randomized test for broad coverage, not point-to-point CI") +@with_phases([BELLATRIX]) +@with_custom_state( + balances_fn=misc_balances_in_default_range_with_many_validators, + threshold_fn=zero_activation_threshold +) +@spec_test +@single_phase +@always_bls +def test_randomized_10(spec, state): + # scenario as high-level, informal text: + # epochs:epochs_until_leak,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:penultimate_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block_bellatrix + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:last_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block_bellatrix + scenario = {'transitions': [{'epochs_to_skip': 'epochs_until_leak', 'validation': 'validate_is_leaking', 'slots_to_skip': 0, 'block_producer': 'no_block'}, {'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'penultimate_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_bellatrix', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}, {'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'last_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_bellatrix', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}], 'state_randomizer': 'randomize_state_bellatrix'} # noqa: E501 + yield from run_generated_randomized_test( + spec, + state, + scenario, + ) + + +@only_generator("randomized test for broad coverage, not point-to-point CI") +@with_phases([BELLATRIX]) +@with_custom_state( + balances_fn=misc_balances_in_default_range_with_many_validators, + threshold_fn=zero_activation_threshold +) +@spec_test +@single_phase +@always_bls +def test_randomized_11(spec, state): + # scenario as high-level, informal text: + # epochs:epochs_until_leak,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:last_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block_bellatrix + # epochs:1,slots:0,with-block:no_block + # epochs:0,slots:last_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block_bellatrix + scenario = {'transitions': [{'epochs_to_skip': 'epochs_until_leak', 'validation': 'validate_is_leaking', 'slots_to_skip': 0, 'block_producer': 'no_block'}, {'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'last_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_bellatrix', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}, {'epochs_to_skip': 1, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'last_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_bellatrix', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}], 'state_randomizer': 'randomize_state_bellatrix'} # noqa: E501 + yield from run_generated_randomized_test( + spec, + state, + scenario, + ) + + +@only_generator("randomized test for broad coverage, not point-to-point CI") +@with_phases([BELLATRIX]) +@with_custom_state( + balances_fn=misc_balances_in_default_range_with_many_validators, + threshold_fn=zero_activation_threshold +) +@spec_test +@single_phase +@always_bls +def test_randomized_12(spec, state): + # scenario as high-level, informal text: + # epochs:epochs_until_leak,slots:0,with-block:no_block + # epochs:1,slots:0,with-block:no_block + # epochs:0,slots:last_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block_bellatrix + # epochs:1,slots:0,with-block:no_block + # epochs:0,slots:penultimate_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block_bellatrix + scenario = {'transitions': [{'epochs_to_skip': 'epochs_until_leak', 'validation': 'validate_is_leaking', 'slots_to_skip': 0, 'block_producer': 'no_block'}, {'epochs_to_skip': 1, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'last_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_bellatrix', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}, {'epochs_to_skip': 1, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'penultimate_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_bellatrix', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}], 'state_randomizer': 'randomize_state_bellatrix'} # noqa: E501 + yield from run_generated_randomized_test( + spec, + state, + scenario, + ) + + +@only_generator("randomized test for broad coverage, not point-to-point CI") +@with_phases([BELLATRIX]) +@with_custom_state( + balances_fn=misc_balances_in_default_range_with_many_validators, + threshold_fn=zero_activation_threshold +) +@spec_test +@single_phase +@always_bls +def test_randomized_13(spec, state): + # scenario as high-level, informal text: + # epochs:epochs_until_leak,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:random_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block_bellatrix + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:random_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block_bellatrix + scenario = {'transitions': [{'epochs_to_skip': 'epochs_until_leak', 'validation': 'validate_is_leaking', 'slots_to_skip': 0, 'block_producer': 'no_block'}, {'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'random_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_bellatrix', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}, {'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'random_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_bellatrix', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}], 'state_randomizer': 'randomize_state_bellatrix'} # noqa: E501 + yield from run_generated_randomized_test( + spec, + state, + scenario, + ) + + +@only_generator("randomized test for broad coverage, not point-to-point CI") +@with_phases([BELLATRIX]) +@with_custom_state( + balances_fn=misc_balances_in_default_range_with_many_validators, + threshold_fn=zero_activation_threshold +) +@spec_test +@single_phase +@always_bls +def test_randomized_14(spec, state): + # scenario as high-level, informal text: + # epochs:epochs_until_leak,slots:0,with-block:no_block + # epochs:1,slots:0,with-block:no_block + # epochs:0,slots:penultimate_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block_bellatrix + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:penultimate_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block_bellatrix + scenario = {'transitions': [{'epochs_to_skip': 'epochs_until_leak', 'validation': 'validate_is_leaking', 'slots_to_skip': 0, 'block_producer': 'no_block'}, {'epochs_to_skip': 1, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'penultimate_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_bellatrix', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}, {'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'penultimate_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_bellatrix', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}], 'state_randomizer': 'randomize_state_bellatrix'} # noqa: E501 + yield from run_generated_randomized_test( + spec, + state, + scenario, + ) + + +@only_generator("randomized test for broad coverage, not point-to-point CI") +@with_phases([BELLATRIX]) +@with_custom_state( + balances_fn=misc_balances_in_default_range_with_many_validators, + threshold_fn=zero_activation_threshold +) +@spec_test +@single_phase +@always_bls +def test_randomized_15(spec, state): + # scenario as high-level, informal text: + # epochs:epochs_until_leak,slots:0,with-block:no_block + # epochs:1,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:random_block_bellatrix + # epochs:1,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:random_block_bellatrix + scenario = {'transitions': [{'epochs_to_skip': 'epochs_until_leak', 'validation': 'validate_is_leaking', 'slots_to_skip': 0, 'block_producer': 'no_block'}, {'epochs_to_skip': 1, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 0, 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_bellatrix', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}, {'epochs_to_skip': 1, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 0, 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_bellatrix', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}], 'state_randomizer': 'randomize_state_bellatrix'} # noqa: E501 + yield from run_generated_randomized_test( + spec, + state, + scenario, + ) diff --git a/tests/core/pyspec/eth2spec/test/bellatrix/unittests/validator/test_validator.py b/tests/core/pyspec/eth2spec/test/bellatrix/unittests/validator/test_validator.py index 5e767d2cd..05a941dfe 100644 --- a/tests/core/pyspec/eth2spec/test/bellatrix/unittests/validator/test_validator.py +++ b/tests/core/pyspec/eth2spec/test/bellatrix/unittests/validator/test_validator.py @@ -103,7 +103,7 @@ def test_prepare_execution_payload(spec, state): # 1. Handle `is_merge_complete` if is_merge_complete: - state.latest_execution_payload_header = spec.ExecutionPayloadHeader(random=b'\x12' * 32) + state.latest_execution_payload_header = spec.ExecutionPayloadHeader(prev_randao=b'\x12' * 32) else: state.latest_execution_payload_header = spec.ExecutionPayloadHeader() diff --git a/tests/core/pyspec/eth2spec/test/helpers/execution_payload.py b/tests/core/pyspec/eth2spec/test/helpers/execution_payload.py index 9c9663584..14602a2cd 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/execution_payload.py +++ b/tests/core/pyspec/eth2spec/test/helpers/execution_payload.py @@ -13,10 +13,10 @@ def build_empty_execution_payload(spec, state, randao_mix=None): parent_hash=latest.block_hash, fee_recipient=spec.ExecutionAddress(), state_root=latest.state_root, # no changes to the state - receipt_root=b"no receipts here" + b"\x00" * 16, # TODO: root of empty MPT may be better. + receipts_root=b"no receipts here" + b"\x00" * 16, # TODO: root of empty MPT may be better. logs_bloom=spec.ByteVector[spec.BYTES_PER_LOGS_BLOOM](), # TODO: zeroed logs bloom for empty logs ok? block_number=latest.block_number + 1, - random=randao_mix, + prev_randao=randao_mix, gas_limit=latest.gas_limit, # retain same limit gas_used=0, # empty block, 0 gas timestamp=timestamp, @@ -36,9 +36,9 @@ def get_execution_payload_header(spec, execution_payload): parent_hash=execution_payload.parent_hash, fee_recipient=execution_payload.fee_recipient, state_root=execution_payload.state_root, - receipt_root=execution_payload.receipt_root, + receipts_root=execution_payload.receipts_root, logs_bloom=execution_payload.logs_bloom, - random=execution_payload.random, + prev_randao=execution_payload.prev_randao, block_number=execution_payload.block_number, gas_limit=execution_payload.gas_limit, gas_used=execution_payload.gas_used, diff --git a/tests/core/pyspec/eth2spec/test/helpers/genesis.py b/tests/core/pyspec/eth2spec/test/helpers/genesis.py index 13556b933..e92d3ead9 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/genesis.py +++ b/tests/core/pyspec/eth2spec/test/helpers/genesis.py @@ -33,9 +33,9 @@ def get_sample_genesis_execution_payload_header(spec, parent_hash=b'\x30' * 32, fee_recipient=b'\x42' * 20, state_root=b'\x20' * 32, - receipt_root=b'\x20' * 32, + receipts_root=b'\x20' * 32, logs_bloom=b'\x35' * spec.BYTES_PER_LOGS_BLOOM, - random=eth1_block_hash, + prev_randao=eth1_block_hash, block_number=0, gas_limit=30000000, base_fee_per_gas=1000000000, diff --git a/tests/core/pyspec/eth2spec/test/helpers/light_client.py b/tests/core/pyspec/eth2spec/test/helpers/light_client.py new file mode 100644 index 000000000..15c764fc4 --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/helpers/light_client.py @@ -0,0 +1,35 @@ +from eth2spec.test.helpers.sync_committee import ( + compute_aggregate_sync_committee_signature, +) + + +def initialize_light_client_store(spec, state): + return spec.LightClientStore( + finalized_header=spec.BeaconBlockHeader(), + current_sync_committee=state.current_sync_committee, + next_sync_committee=state.next_sync_committee, + best_valid_update=None, + optimistic_header=spec.BeaconBlockHeader(), + previous_max_active_participants=0, + current_max_active_participants=0, + ) + + +def get_sync_aggregate(spec, state, block_header, block_root=None, signature_slot=None): + if signature_slot is None: + signature_slot = block_header.slot + + all_pubkeys = [v.pubkey for v in state.validators] + committee = [all_pubkeys.index(pubkey) for pubkey in state.current_sync_committee.pubkeys] + sync_committee_bits = [True] * len(committee) + sync_committee_signature = compute_aggregate_sync_committee_signature( + spec, + state, + block_header.slot, + committee, + block_root=block_root, + ) + return spec.SyncAggregate( + sync_committee_bits=sync_committee_bits, + sync_committee_signature=sync_committee_signature, + ) diff --git a/tests/core/pyspec/eth2spec/test/utils/randomized_block_tests.py b/tests/core/pyspec/eth2spec/test/utils/randomized_block_tests.py index 02a5464f7..386b4c8b2 100644 --- a/tests/core/pyspec/eth2spec/test/utils/randomized_block_tests.py +++ b/tests/core/pyspec/eth2spec/test/utils/randomized_block_tests.py @@ -55,12 +55,22 @@ def randomize_state(spec, state, stats, exit_fraction=0.1, slash_fraction=0.1): return scenario_state -def randomize_state_altair(spec, state, stats): - scenario_state = randomize_state(spec, state, stats, exit_fraction=0.1, slash_fraction=0.1) +def randomize_state_altair(spec, state, stats, exit_fraction=0.1, slash_fraction=0.1): + scenario_state = randomize_state(spec, state, stats, exit_fraction=exit_fraction, slash_fraction=slash_fraction) randomize_inactivity_scores(spec, state) return scenario_state +def randomize_state_bellatrix(spec, state, stats, exit_fraction=0.1, slash_fraction=0.1): + scenario_state = randomize_state_altair(spec, + state, + stats, + exit_fraction=exit_fraction, + slash_fraction=slash_fraction) + # TODO: randomize execution payload, merge status, etc. + return scenario_state + + # epochs def epochs_until_leak(spec): @@ -179,6 +189,12 @@ def random_block_altair_with_cycling_sync_committee_participation(spec, return block +def random_block_bellatrix(spec, state, signed_blocks, scenario_state): + block = random_block_altair_with_cycling_sync_committee_participation(spec, state, signed_blocks, scenario_state) + # TODO: return randomized execution payload + return block + + # validations def no_op_validation(_spec, _state): diff --git a/tests/generators/random/Makefile b/tests/generators/random/Makefile index 1b518bfde..607d69a47 100644 --- a/tests/generators/random/Makefile +++ b/tests/generators/random/Makefile @@ -4,5 +4,7 @@ all: pip3 install -r requirements.txt rm -f ../../core/pyspec/eth2spec/test/phase0/random/test_random.py rm -f ../../core/pyspec/eth2spec/test/altair/random/test_random.py + rm -f ../../core/pyspec/eth2spec/test/bellatrix/random/test_random.py python3 generate.py phase0 > ../../core/pyspec/eth2spec/test/phase0/random/test_random.py python3 generate.py altair > ../../core/pyspec/eth2spec/test/altair/random/test_random.py + python3 generate.py bellatrix > ../../core/pyspec/eth2spec/test/bellatrix/random/test_random.py diff --git a/tests/generators/random/generate.py b/tests/generators/random/generate.py index b880cb12b..f96f05a75 100644 --- a/tests/generators/random/generate.py +++ b/tests/generators/random/generate.py @@ -19,8 +19,10 @@ from eth2spec.test.utils.randomized_block_tests import ( no_op_validation, randomize_state, randomize_state_altair, + randomize_state_bellatrix, random_block, random_block_altair_with_cycling_sync_committee_participation, + random_block_bellatrix, last_slot_in_epoch, random_slot_in_epoch, penultimate_slot_in_epoch, @@ -30,7 +32,7 @@ from eth2spec.test.utils.randomized_block_tests import ( transition_to_leaking, transition_without_leak, ) -from eth2spec.test.helpers.constants import PHASE0, ALTAIR +from eth2spec.test.helpers.constants import PHASE0, ALTAIR, BELLATRIX # Ensure this many blocks are present in *each* randomized scenario @@ -254,5 +256,12 @@ if __name__ == "__main__": state_randomizer=randomize_state_altair, block_randomizer=random_block_altair_with_cycling_sync_committee_participation, ) + if BELLATRIX in sys.argv: + did_generate = True + run_generate_tests_to_std_out( + BELLATRIX, + state_randomizer=randomize_state_bellatrix, + block_randomizer=random_block_bellatrix, + ) if not did_generate: warnings.warn("no phase given for test generation") diff --git a/tests/generators/random/main.py b/tests/generators/random/main.py index f6f1b1847..1791da83b 100644 --- a/tests/generators/random/main.py +++ b/tests/generators/random/main.py @@ -1,4 +1,4 @@ -from eth2spec.test.helpers.constants import PHASE0, ALTAIR +from eth2spec.test.helpers.constants import PHASE0, ALTAIR, BELLATRIX from eth2spec.gen_helpers.gen_from_tests.gen import run_state_test_generators @@ -9,10 +9,14 @@ if __name__ == "__main__": altair_mods = {key: 'eth2spec.test.altair.random.test_' + key for key in [ 'random', ]} + bellatrix_mods = {key: 'eth2spec.test.bellatrix.random.test_' + key for key in [ + 'random', + ]} all_mods = { PHASE0: phase_0_mods, ALTAIR: altair_mods, + BELLATRIX: bellatrix_mods, } run_state_test_generators(runner_name="random", all_mods=all_mods) diff --git a/tests/generators/ssz_generic/ssz_container.py b/tests/generators/ssz_generic/ssz_container.py index 8f3a59ce3..1b30d687a 100644 --- a/tests/generators/ssz_generic/ssz_container.py +++ b/tests/generators/ssz_generic/ssz_container.py @@ -104,17 +104,33 @@ def invalid_cases(): RandomizationMode.mode_nil_count, RandomizationMode.mode_one_count, RandomizationMode.mode_max_count]: - if len(offsets) != 0: - for offset_index in offsets: - yield f'{name}_{mode.to_name()}_offset_{offset_index}_plus_one', \ - invalid_test_case(lambda: mod_offset( - b=serialize(container_case_fn(rng, mode, typ)), - offset_index=offset_index, - change=lambda x: x + 1 - )) - yield f'{name}_{mode.to_name()}_offset_{offset_index}_zeroed', \ - invalid_test_case(lambda: mod_offset( - b=serialize(container_case_fn(rng, mode, typ)), - offset_index=offset_index, - change=lambda x: 0 - )) + for index, offset_index in enumerate(offsets): + yield f'{name}_{mode.to_name()}_offset_{offset_index}_plus_one', \ + invalid_test_case(lambda: mod_offset( + b=serialize(container_case_fn(rng, mode, typ)), + offset_index=offset_index, + change=lambda x: x + 1 + )) + yield f'{name}_{mode.to_name()}_offset_{offset_index}_zeroed', \ + invalid_test_case(lambda: mod_offset( + b=serialize(container_case_fn(rng, mode, typ)), + offset_index=offset_index, + change=lambda x: 0 + )) + if index == 0: + yield f'{name}_{mode.to_name()}_offset_{offset_index}_minus_one', \ + invalid_test_case(lambda: mod_offset( + b=serialize(container_case_fn(rng, mode, typ)), + offset_index=offset_index, + change=lambda x: x - 1 + )) + if mode == RandomizationMode.mode_max_count: + serialized = serialize(container_case_fn(rng, mode, typ)) + serialized = serialized + serialized[:2] + yield f'{name}_{mode.to_name()}_last_offset_{offset_index}_overflow', \ + invalid_test_case(lambda: serialized) + if mode == RandomizationMode.mode_one_count: + serialized = serialize(container_case_fn(rng, mode, typ)) + serialized = serialized + serialized[:1] + yield f'{name}_{mode.to_name()}_last_offset_{offset_index}_wrong_byte_length', \ + invalid_test_case(lambda: serialized)