From 1f072a11b9f978481ec9e1e148c6c883a8e82c6c Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Fri, 29 May 2026 15:23:07 +0100 Subject: [PATCH 1/5] feat: implement EIP-8045 exclude slashed validators from proposing --- .../src/epoch/processProposerLookahead.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/state-transition/src/epoch/processProposerLookahead.ts b/packages/state-transition/src/epoch/processProposerLookahead.ts index 4d92afcf45cc..6ab87c190405 100644 --- a/packages/state-transition/src/epoch/processProposerLookahead.ts +++ b/packages/state-transition/src/epoch/processProposerLookahead.ts @@ -1,6 +1,7 @@ import {ForkSeq, MIN_SEED_LOOKAHEAD, SLOTS_PER_EPOCH} from "@lodestar/params"; import {ssz} from "@lodestar/types"; import {CachedBeaconStateFulu, EpochTransitionCache} from "../types.js"; +import {FLAG_UNSLASHED} from "../util/attesterStatus.js"; import {computeEpochShuffling} from "../util/epochShuffling.js"; import {computeProposerIndices} from "../util/seed.js"; @@ -26,7 +27,13 @@ export function processProposerLookahead( // Save shuffling to cache so afterProcessEpoch can reuse it instead of recomputing cache.nextShuffling = shuffling; - const lastEpochProposerLookahead = computeProposerIndices(fork, state, shuffling, epoch); + const activeIndices = + fork >= ForkSeq.gloas + ? // Exclude slashed validators from proposing (EIP-8045) + shuffling.activeIndices.filter((index) => (cache.flags[index] & FLAG_UNSLASHED) !== 0) + : shuffling.activeIndices; + + const lastEpochProposerLookahead = computeProposerIndices(fork, state, {activeIndices}, epoch); state.proposerLookahead = ssz.fulu.ProposerLookahead.toViewDU([ ...remainingProposerLookahead, From fafebf527d247a675af6ed07910ffb87cf48a322 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Tue, 2 Jun 2026 13:00:03 +0100 Subject: [PATCH 2/5] bump spec to v1.7.0-alpha.9 --- .../test/spec/presets/fork_choice.test.ts | 13 +- .../test/spec/utils/specTestIterator.ts | 5 + spec-tests-version.json | 2 +- specrefs/.ethspecify.yml | 26 +- specrefs/constants.yml | 7 + specrefs/containers.yml | 9 - specrefs/dataclasses.yml | 191 ++- specrefs/functions.yml | 1295 ++++++++++++----- 8 files changed, 1070 insertions(+), 478 deletions(-) diff --git a/packages/beacon-node/test/spec/presets/fork_choice.test.ts b/packages/beacon-node/test/spec/presets/fork_choice.test.ts index 8651ef3ed396..74b99efa031a 100644 --- a/packages/beacon-node/test/spec/presets/fork_choice.test.ts +++ b/packages/beacon-node/test/spec/presets/fork_choice.test.ts @@ -632,7 +632,18 @@ const forkChoiceTest = name.includes("include_votes_another_empty_chain_without_enough_ffg_votes_current_epoch"))) || // TODO GLOAS: Spec test fixture bug in v1.7.0-alpha.5: wrong_withdrawals envelope SSZ data is // byte-for-byte identical to the valid envelope, making it impossible to reject - name.endsWith("on_execution_payload_envelope__wrong_withdrawals"), + name.endsWith("on_execution_payload_envelope__wrong_withdrawals") || + // TODO: re-enable after "apply proposer boost if dependent roots match" (consensus-specs #5306) + // is implemented. New behavior in v1.7.0-alpha.9; Lodestar still applies the boost + // unconditionally. Only the altair vectors exercise the changed condition (other forks pass). + name.endsWith("altair/fork_choice/on_block/pyspec_tests/justified_update_always_if_better") || + name.endsWith("altair/fork_choice/on_block/pyspec_tests/justified_update_not_realized_finality") || + name.endsWith("altair/fork_choice/get_head/pyspec_tests/voting_source_beyond_two_epoch") || + // TODO GLOAS: re-enable after the validate_on_attestation payload-status updates + // (consensus-specs #5275) are implemented. New gloas on_attestation vectors in v1.7.0-alpha.9. + name.endsWith("validate_on_attestation_beacon_root_payload_check") || + name.endsWith("validate_on_attestation_payload_invalid_index") || + name.endsWith("validate_on_attestation_same_slot_full_vote_rejected"), }, }; }; diff --git a/packages/beacon-node/test/spec/utils/specTestIterator.ts b/packages/beacon-node/test/spec/utils/specTestIterator.ts index c0ee63af2bcb..a5b67f998bca 100644 --- a/packages/beacon-node/test/spec/utils/specTestIterator.ts +++ b/packages/beacon-node/test/spec/utils/specTestIterator.ts @@ -84,6 +84,11 @@ export const defaultSkipOpts: SkipOpts = { // New test suite added in v1.7.0-alpha.8 (consensus-specs #5206); gloas PTC fork choice // handling is not yet implemented in Lodestar. /^gloas\/fork_choice\/on_payload_attestation_message\/.*$/, + // TODO-FULU: re-enable after the Fulu deposit-mechanism removal (consensus-specs #4704) is + // implemented. v1.7.0-alpha.9 regenerated these transition vectors so eth1-bridge deposits are + // no longer processed across the Electra to Fulu boundary; Lodestar still applies the Electra + // deposit flow, diverging the post-fork state root. + /^fulu\/transition\/.*/, ], skippedTests: [ // TODO-GLOAS: re-enable after gloas light client is implemented diff --git a/spec-tests-version.json b/spec-tests-version.json index 2e5a2ffdfe92..306c3252b52c 100644 --- a/spec-tests-version.json +++ b/spec-tests-version.json @@ -1,6 +1,6 @@ { "ethereumConsensusSpecsTests": { - "specVersion": "v1.7.0-alpha.8", + "specVersion": "v1.7.0-alpha.9", "specTestsRepoUrl": "https://github.com/ethereum/consensus-specs", "outputDirBase": "spec-tests", "testsToDownload": [ diff --git a/specrefs/.ethspecify.yml b/specrefs/.ethspecify.yml index 1dcdf1a78e23..2dd0246922d9 100644 --- a/specrefs/.ethspecify.yml +++ b/specrefs/.ethspecify.yml @@ -1,4 +1,4 @@ -version: v1.7.0-alpha.8 +version: v1.7.0-alpha.9 style: full specrefs: @@ -25,6 +25,7 @@ exceptions: - UINT64_MAX_SQRT#phase0 # bellatrix + - EMPTY_BLOCK_HASH#bellatrix - PAYLOAD_STATUS_INVALIDATED#bellatrix - PAYLOAD_STATUS_NOT_VALIDATED#bellatrix - PAYLOAD_STATUS_VALID#bellatrix @@ -61,7 +62,6 @@ exceptions: containers: # gloas - - ForkChoiceNode#gloas - LightClientHeader#gloas - PartialDataColumnGroupID#gloas - PartialDataColumnHeader#gloas @@ -81,6 +81,7 @@ exceptions: dataclasses: # phase0 + - ForkChoiceNode#phase0 - LatestMessage#phase0 - Seen#phase0 @@ -99,6 +100,9 @@ exceptions: # electra - Seen#electra + # fulu + - Seen#fulu + # capella - ExpectedWithdrawals#capella @@ -107,7 +111,9 @@ exceptions: # gloas - ExpectedWithdrawals#gloas + - ForkChoiceNode#gloas - LatestMessage#gloas + - Seen#gloas - Store#gloas # phase0 fast confirmation / not implemented @@ -146,10 +152,14 @@ exceptions: - get_base_reward#phase0 - get_checkpoint_block#phase0 - get_current_store_epoch#phase0 + - get_dependent_root#phase0 - get_eligible_validator_indices#phase0 - get_eth1_vote#phase0 - get_filtered_block_tree#phase0 - get_forkchoice_store#phase0 + - get_node_children#phase0 + - get_node_for_root#phase0 + - get_supported_node#phase0 - get_head_deltas#phase0 - get_inactivity_penalty_deltas#phase0 - get_inclusion_delay_deltas#phase0 @@ -210,6 +220,7 @@ exceptions: - compute_fork_version#bellatrix - get_execution_payload#bellatrix - get_pow_block_at_terminal_total_difficulty#bellatrix + - get_safe_execution_block_hash#bellatrix - get_terminal_pow_block#bellatrix - is_execution_block#bellatrix - is_merge_transition_block#bellatrix @@ -319,6 +330,9 @@ exceptions: - verify_cell_kzg_proof_batch_impl#fulu - verify_partial_data_column_header_inclusion_proof#fulu - verify_partial_data_column_sidecar_kzg_proofs#fulu + - validate_beacon_block_gossip#fulu + - validate_data_column_sidecar_gossip#fulu + - validate_partial_data_column_sidecar_gossip#fulu # gloas - add_builder_to_registry#gloas @@ -329,9 +343,11 @@ exceptions: - get_ancestor#gloas - get_attestation_participation_flag_indices#gloas - get_attestation_score#gloas + - get_beacon_proposer_indices#gloas - get_builder_withdrawals#gloas - get_builders_sweep_withdrawals#gloas - get_checkpoint_block#gloas + - get_dependent_root#gloas - get_execution_payload_bid_signature#gloas - get_execution_payload_envelope_signature#gloas - get_forkchoice_store#gloas @@ -339,6 +355,7 @@ exceptions: - get_index_for_new_builder#gloas - get_next_sync_committee_indices#gloas - get_node_children#gloas + - get_node_for_root#gloas - get_parent_payload_status#gloas - get_payload_attestation_due_ms#gloas - get_payload_attestation_message_signature#gloas @@ -346,14 +363,17 @@ exceptions: - get_pending_balance_to_withdraw#gloas - get_proposer_preferences_signature#gloas - get_ptc_assignment#gloas + - get_safe_execution_block_hash#gloas + - get_supported_node#gloas - get_upcoming_proposal_slots#gloas - get_weight#gloas - has_compounding_withdrawal_credential#gloas + - is_ancestor#gloas - is_head_late#gloas - is_head_weak#gloas - is_parent_node_full#gloas - is_parent_strong#gloas - - is_supporting_vote#gloas + - is_previous_slot_payload_decision#gloas - is_valid_proposal_slot#gloas - notify_ptc_messages#gloas - on_block#gloas diff --git a/specrefs/constants.yml b/specrefs/constants.yml index 0dd86d9bc4e9..fc08509f0329 100644 --- a/specrefs/constants.yml +++ b/specrefs/constants.yml @@ -292,6 +292,13 @@ DOMAIN_VOLUNTARY_EXIT: DomainType = '0x04000000' +- name: EMPTY_BLOCK_HASH#bellatrix + sources: [] + spec: | + + EMPTY_BLOCK_HASH: Hash32 = + + - name: ENDIANNESS#phase0 sources: [] spec: | diff --git a/specrefs/containers.yml b/specrefs/containers.yml index 79ec57900654..c1ab68597329 100644 --- a/specrefs/containers.yml +++ b/specrefs/containers.yml @@ -1175,15 +1175,6 @@ epoch: Epoch -- name: ForkChoiceNode#gloas - sources: [] - spec: | - - class ForkChoiceNode(Container): - root: Root - payload_status: PayloadStatus # One of PAYLOAD_STATUS_* values - - - name: ForkData#phase0 sources: - file: packages/types/src/phase0/sszTypes.ts diff --git a/specrefs/dataclasses.yml b/specrefs/dataclasses.yml index 2e91e918d831..7916aeeea911 100644 --- a/specrefs/dataclasses.yml +++ b/specrefs/dataclasses.yml @@ -14,8 +14,8 @@ - file: packages/types/src/deneb/sszTypes.ts search: export const BlobsBundle = spec: | - - class BlobsBundle(object): + + class BlobsBundle: commitments: List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK] proofs: List[KZGProof, MAX_BLOB_COMMITMENTS_PER_BLOCK] blobs: List[Blob, MAX_BLOB_COMMITMENTS_PER_BLOCK] @@ -26,8 +26,8 @@ - file: packages/types/src/fulu/sszTypes.ts search: export const BlobsBundle = spec: | - - class BlobsBundle(object): + + class BlobsBundle: commitments: List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK] # [Modified in Fulu:EIP7594] proofs: List[KZGProof, FIELD_ELEMENTS_PER_EXT_BLOB * MAX_BLOB_COMMITMENTS_PER_BLOCK] @@ -37,8 +37,8 @@ - name: ExpectedWithdrawals#capella sources: [] spec: | - - class ExpectedWithdrawals(object): + + class ExpectedWithdrawals: withdrawals: Sequence[Withdrawal] processed_sweep_withdrawals_count: uint64 @@ -46,8 +46,8 @@ - name: ExpectedWithdrawals#electra sources: [] spec: | - - class ExpectedWithdrawals(object): + + class ExpectedWithdrawals: withdrawals: Sequence[Withdrawal] # [New in Electra:EIP7251] processed_partial_withdrawals_count: uint64 @@ -57,8 +57,8 @@ - name: ExpectedWithdrawals#gloas sources: [] spec: | - - class ExpectedWithdrawals(object): + + class ExpectedWithdrawals: withdrawals: Sequence[Withdrawal] # [New in Gloas:EIP7732] processed_builder_withdrawals_count: uint64 @@ -71,8 +71,8 @@ - name: FastConfirmationStore#phase0 sources: [] spec: | - - class FastConfirmationStore(object): + + class FastConfirmationStore: store: Store confirmed_root: Root previous_epoch_observed_justified_checkpoint: Checkpoint @@ -82,11 +82,31 @@ current_slot_head: Root +- name: ForkChoiceNode#phase0 + sources: [] + spec: | + + @dataclass(eq=True, frozen=True) + class ForkChoiceNode: + root: Root + + +- name: ForkChoiceNode#gloas + sources: [] + spec: | + + @dataclass(eq=True, frozen=True) + class ForkChoiceNode: + root: Root + # [New in Gloas:EIP7732] + payload_status: PayloadStatus # One of PAYLOAD_STATUS_* values + + - name: GetInclusionListResponse#heze sources: [] spec: | - - class GetInclusionListResponse(object): + + class GetInclusionListResponse: inclusion_list_transactions: Sequence[Transaction] @@ -97,8 +117,8 @@ - file: packages/beacon-node/src/execution/engine/interface.ts search: "getPayload(" spec: | - - class GetPayloadResponse(object): + + class GetPayloadResponse: execution_payload: ExecutionPayload @@ -109,8 +129,8 @@ - file: packages/beacon-node/src/execution/engine/interface.ts search: "getPayload(" spec: | - - class GetPayloadResponse(object): + + class GetPayloadResponse: execution_payload: ExecutionPayload block_value: uint256 @@ -122,8 +142,8 @@ - file: packages/beacon-node/src/execution/engine/interface.ts search: "getPayload(" spec: | - - class GetPayloadResponse(object): + + class GetPayloadResponse: execution_payload: ExecutionPayload block_value: uint256 # [New in Deneb:EIP4844] @@ -137,8 +157,8 @@ - file: packages/beacon-node/src/execution/engine/interface.ts search: "getPayload(" spec: | - - class GetPayloadResponse(object): + + class GetPayloadResponse: execution_payload: ExecutionPayload block_value: uint256 blobs_bundle: BlobsBundle @@ -153,8 +173,8 @@ - file: packages/beacon-node/src/execution/engine/interface.ts search: "getPayload(" spec: | - - class GetPayloadResponse(object): + + class GetPayloadResponse: execution_payload: ExecutionPayload block_value: uint256 # [Modified in Fulu:EIP7594] @@ -165,8 +185,8 @@ - name: InclusionListStore#heze sources: [] spec: | - - class InclusionListStore(object): + + class InclusionListStore: inclusion_lists: DefaultDict[Tuple[Slot, Root], Dict[Root, InclusionList]] = field( default_factory=lambda: defaultdict(dict) ) @@ -179,9 +199,9 @@ - name: LatestMessage#phase0 sources: [] spec: | - + @dataclass(eq=True, frozen=True) - class LatestMessage(object): + class LatestMessage: epoch: Epoch root: Root @@ -189,9 +209,9 @@ - name: LatestMessage#gloas sources: [] spec: | - + @dataclass(eq=True, frozen=True) - class LatestMessage(object): + class LatestMessage: slot: Slot root: Root payload_present: boolean @@ -202,8 +222,8 @@ - file: packages/types/src/altair/sszTypes.ts search: export const LightClientStore = spec: | - - class LightClientStore(object): + + class LightClientStore: # Header that is finalized finalized_header: LightClientHeader # Sync committees corresponding to the finalized header @@ -223,8 +243,8 @@ - file: packages/types/src/capella/sszTypes.ts search: export const LightClientStore = spec: | - - class LightClientStore(object): + + class LightClientStore: # [Modified in Capella] finalized_header: LightClientHeader current_sync_committee: SyncCommittee @@ -242,8 +262,8 @@ - file: packages/beacon-node/src/execution/engine/interface.ts search: "notifyNewPayload(" spec: | - - class NewPayloadRequest(object): + + class NewPayloadRequest: execution_payload: ExecutionPayload @@ -252,8 +272,8 @@ - file: packages/beacon-node/src/execution/engine/interface.ts search: "notifyNewPayload(" spec: | - - class NewPayloadRequest(object): + + class NewPayloadRequest: execution_payload: ExecutionPayload versioned_hashes: Sequence[VersionedHash] parent_beacon_block_root: Root @@ -264,8 +284,8 @@ - file: packages/beacon-node/src/execution/engine/interface.ts search: "notifyNewPayload(" spec: | - - class NewPayloadRequest(object): + + class NewPayloadRequest: execution_payload: ExecutionPayload versioned_hashes: Sequence[VersionedHash] parent_beacon_block_root: Root @@ -289,8 +309,8 @@ - file: packages/types/src/bellatrix/sszTypes.ts search: export const PayloadAttributes = spec: | - - class PayloadAttributes(object): + + class PayloadAttributes: timestamp: uint64 prev_randao: Bytes32 suggested_fee_recipient: ExecutionAddress @@ -301,8 +321,8 @@ - file: packages/types/src/capella/sszTypes.ts search: export const PayloadAttributes = spec: | - - class PayloadAttributes(object): + + class PayloadAttributes: timestamp: uint64 prev_randao: Bytes32 suggested_fee_recipient: ExecutionAddress @@ -315,8 +335,8 @@ - file: packages/types/src/deneb/sszTypes.ts search: export const PayloadAttributes = spec: | - - class PayloadAttributes(object): + + class PayloadAttributes: timestamp: uint64 prev_randao: Bytes32 suggested_fee_recipient: ExecutionAddress @@ -330,8 +350,8 @@ - file: packages/types/src/gloas/sszTypes.ts search: export const PayloadAttributes = spec: | - - class PayloadAttributes(object): + + class PayloadAttributes: timestamp: uint64 prev_randao: Bytes32 suggested_fee_recipient: ExecutionAddress @@ -346,8 +366,8 @@ - name: PayloadAttributes#heze sources: [] spec: | - - class PayloadAttributes(object): + + class PayloadAttributes: timestamp: uint64 prev_randao: Bytes32 suggested_fee_recipient: ExecutionAddress @@ -362,8 +382,8 @@ - name: Seen#altair sources: [] spec: | - - class Seen(object): + + class Seen: proposer_slots: Set[Tuple[ValidatorIndex, Slot]] aggregator_epochs: Set[Tuple[ValidatorIndex, Epoch]] aggregate_data_roots: Dict[Root, Set[Tuple[boolean, ...]]] @@ -382,8 +402,8 @@ - name: Seen#capella sources: [] spec: | - - class Seen(object): + + class Seen: proposer_slots: Set[Tuple[ValidatorIndex, Slot]] aggregator_epochs: Set[Tuple[ValidatorIndex, Epoch]] aggregate_data_roots: Dict[Root, Set[Tuple[boolean, ...]]] @@ -401,8 +421,8 @@ - name: Seen#deneb sources: [] spec: | - - class Seen(object): + + class Seen: proposer_slots: Set[Tuple[ValidatorIndex, Slot]] aggregator_epochs: Set[Tuple[ValidatorIndex, Epoch]] aggregate_data_roots: Dict[Root, Set[Tuple[boolean, ...]]] @@ -421,8 +441,8 @@ - name: Seen#electra sources: [] spec: | - - class Seen(object): + + class Seen: proposer_slots: Set[Tuple[ValidatorIndex, Slot]] aggregator_epochs: Set[Tuple[ValidatorIndex, Epoch]] # [Modified in Electra:EIP7549] @@ -438,13 +458,56 @@ blob_sidecar_tuples: Set[Tuple[Slot, ValidatorIndex, BlobIndex]] +- name: Seen#fulu + sources: [] + spec: | + + class Seen: + proposer_slots: Set[Tuple[ValidatorIndex, Slot]] + aggregator_epochs: Set[Tuple[ValidatorIndex, Epoch]] + aggregate_data_roots: Dict[Tuple[Root, CommitteeIndex], Set[Tuple[boolean, ...]]] + voluntary_exit_indices: Set[ValidatorIndex] + proposer_slashing_indices: Set[ValidatorIndex] + attester_slashing_indices: Set[ValidatorIndex] + attestation_validator_epochs: Set[Tuple[ValidatorIndex, Epoch]] + sync_contribution_aggregator_slots: Set[Tuple[ValidatorIndex, Slot, uint64]] + sync_contribution_data: Dict[Tuple[Slot, Root, uint64], Set[Tuple[boolean, ...]]] + sync_message_validator_slots: Set[Tuple[Slot, ValidatorIndex, uint64]] + bls_to_execution_change_indices: Set[ValidatorIndex] + # [Modified in Fulu:EIP7594] + # Removed `blob_sidecar_tuples` + # [New in Fulu:EIP7594] + data_column_sidecar_tuples: Set[Tuple[Slot, ValidatorIndex, ColumnIndex]] + # [New in Fulu] + partial_data_column_headers: Dict[Root, PartialDataColumnHeader] + + +- name: Seen#gloas + sources: [] + spec: | + + class Seen: + proposer_slots: Set[Tuple[ValidatorIndex, Slot]] + aggregator_epochs: Set[Tuple[ValidatorIndex, Epoch]] + aggregate_data_roots: Dict[Tuple[Root, CommitteeIndex], Set[Tuple[boolean, ...]]] + voluntary_exit_indices: Set[ValidatorIndex] + proposer_slashing_indices: Set[ValidatorIndex] + attester_slashing_indices: Set[ValidatorIndex] + attestation_validator_epochs: Set[Tuple[ValidatorIndex, Epoch]] + sync_contribution_aggregator_slots: Set[Tuple[ValidatorIndex, Slot, uint64]] + sync_contribution_data: Dict[Tuple[Slot, Root, uint64], Set[Tuple[boolean, ...]]] + sync_message_validator_slots: Set[Tuple[Slot, ValidatorIndex, uint64]] + bls_to_execution_change_indices: Set[ValidatorIndex] + data_column_sidecar_tuples: Set[Tuple[Slot, ValidatorIndex, ColumnIndex]] + + - name: Store#phase0 sources: - file: packages/fork-choice/src/forkChoice/store.ts search: export interface IForkChoiceStore spec: | - - class Store(object): + + class Store: time: uint64 genesis_time: uint64 justified_checkpoint: Checkpoint @@ -464,8 +527,8 @@ - name: Store#gloas sources: [] spec: | - - class Store(object): + + class Store: time: uint64 genesis_time: uint64 justified_checkpoint: Checkpoint @@ -494,8 +557,8 @@ - name: Store#heze sources: [] spec: | - - class Store(object): + + class Store: time: uint64 genesis_time: uint64 justified_checkpoint: Checkpoint diff --git a/specrefs/functions.yml b/specrefs/functions.yml index ed47d7825511..e63529bfd7a3 100644 --- a/specrefs/functions.yml +++ b/specrefs/functions.yml @@ -916,8 +916,8 @@ - name: compute_fork_version#phase0 sources: [] spec: | - - def compute_fork_version(epoch: Epoch) -> Version: + + def compute_fork_version(epoch: Epoch) -> Version: # noqa: ARG001 """ Return the fork version at the given ``epoch``. """ @@ -1129,7 +1129,7 @@ - file: packages/beacon-node/src/util/dataColumns.ts search: export async function getCellsAndProofs( spec: | - + def compute_matrix(blobs: Sequence[Blob]) -> Sequence[MatrixEntry]: """ Return the full, flattened sequence of matrix entries. @@ -1140,7 +1140,7 @@ matrix = [] for blob_index, blob in enumerate(blobs): cells, proofs = compute_cells_and_kzg_proofs(blob) - for cell_index, (cell, proof) in enumerate(zip(cells, proofs)): + for cell_index, (cell, proof) in enumerate(zip(cells, proofs, strict=True)): matrix.append( MatrixEntry( cell=cell, @@ -1232,7 +1232,7 @@ - name: compute_on_chain_aggregate#electra sources: [] spec: | - + def compute_on_chain_aggregate(network_aggregates: Sequence[Attestation]) -> Attestation: aggregates = sorted( network_aggregates, key=lambda a: get_committee_indices(a.committee_bits)[0] @@ -1247,7 +1247,7 @@ signature = bls.Aggregate([a.signature for a in aggregates]) committee_indices = [get_committee_indices(a.committee_bits)[0] for a in aggregates] - committee_flags = [(index in committee_indices) for index in range(0, MAX_COMMITTEES_PER_SLOT)] + committee_flags = [(index in committee_indices) for index in range(MAX_COMMITTEES_PER_SLOT)] committee_bits = Bitvector[MAX_COMMITTEES_PER_SLOT](committee_flags) return Attestation( @@ -1587,7 +1587,7 @@ - name: compute_subnets_for_sync_committee#altair sources: [] spec: | - + def compute_subnets_for_sync_committee( state: BeaconState, validator_index: ValidatorIndex ) -> Set[SubnetID]: @@ -1603,12 +1603,10 @@ sync_committee_indices = [ index for index, pubkey in enumerate(sync_committee.pubkeys) if pubkey == target_pubkey ] - return set( - [ - SubnetID(index // (SYNC_COMMITTEE_SIZE // SYNC_COMMITTEE_SUBNET_COUNT)) - for index in sync_committee_indices - ] - ) + return { + SubnetID(index // (SYNC_COMMITTEE_SIZE // SYNC_COMMITTEE_SUBNET_COUNT)) + for index in sync_committee_indices + } - name: compute_subscribed_subnet#phase0 @@ -1678,7 +1676,7 @@ - file: packages/state-transition/src/util/weakSubjectivity.ts search: export function computeWeakSubjectivityPeriod( spec: | - + def compute_weak_subjectivity_period(state: BeaconState) -> uint64: """ Returns the weak subjectivity period for the current ``state``. @@ -1696,7 +1694,7 @@ Delta = MAX_DEPOSITS * SLOTS_PER_EPOCH D = SAFETY_DECAY - if T * (200 + 3 * D) < t * (200 + 12 * D): + if t * (200 + 12 * D) > T * (200 + 3 * D): epochs_for_validator_set_churn = ( N * (t * (200 + 12 * D) - T * (200 + 3 * D)) // (600 * delta * (2 * t + T)) ) @@ -2012,12 +2010,10 @@ search: '^\s+nodeIsViableForHead\(node:' regex: true spec: | - + def filter_block_tree(store: Store, block_root: Root, blocks: Dict[Root, BeaconBlock]) -> bool: block = store.blocks[block_root] - children = [ - root for root in store.blocks.keys() if store.blocks[root].parent_root == block_root - ] + children = [root for root in store.blocks if store.blocks[root].parent_root == block_root] # If any children branches contain expected finalized/justified checkpoints, # add to filtered block-tree and signal viability to parent. @@ -2083,7 +2079,7 @@ - name: find_latest_confirmed_descendant#phase0 sources: [] spec: | - + def find_latest_confirmed_descendant( fcr_store: FastConfirmationStore, latest_confirmed_root: Root ) -> Root: @@ -2092,7 +2088,7 @@ starting from ``latest_confirmed_root``. """ store = fcr_store.store - head = get_head(store) + head = get_head(store).root current_epoch = get_current_store_epoch(store) confirmed_root = latest_confirmed_root @@ -2127,7 +2123,11 @@ # The algorithm can only rely on the previous head # if it is a descendant of the block that is attempted to be confirmed - if not is_ancestor(store, fcr_store.previous_slot_head, block_root): + if not is_ancestor( + store, + get_node_for_root(fcr_store.previous_slot_head), + get_node_for_root(block_root), + ): break if not is_one_confirmed(store, get_current_balance_source(fcr_store), block_root): @@ -2309,37 +2309,29 @@ search: '^\s+getAncestor\(' regex: true spec: | - - def get_ancestor(store: Store, root: Root, slot: Slot) -> Root: - block = store.blocks[root] + + def get_ancestor(store: Store, node: ForkChoiceNode, slot: Slot) -> ForkChoiceNode: + block = store.blocks[node.root] if block.slot > slot: - return get_ancestor(store, block.parent_root, slot) - return root + parent = ForkChoiceNode(root=block.parent_root) + return get_ancestor(store, parent, slot) + return node - name: get_ancestor#gloas sources: [] spec: | - - def get_ancestor(store: Store, root: Root, slot: Slot) -> ForkChoiceNode: - """ - Returns the beacon block root and the payload status of the ancestor of the beacon block - with ``root`` at ``slot``. If the beacon block with ``root`` is already at ``slot`` or we are - requesting an ancestor "in the future", it returns ``PAYLOAD_STATUS_PENDING``. - """ - block = store.blocks[root] - if block.slot <= slot: - return ForkChoiceNode(root=root, payload_status=PAYLOAD_STATUS_PENDING) - - parent = store.blocks[block.parent_root] - while parent.slot > slot: - block = parent - parent = store.blocks[block.parent_root] - - return ForkChoiceNode( - root=block.parent_root, - payload_status=get_parent_payload_status(store, block), - ) + + def get_ancestor(store: Store, node: ForkChoiceNode, slot: Slot) -> ForkChoiceNode: + block = store.blocks[node.root] + if block.slot > slot: + # [Modified in Gloas:EIP7732] + parent = ForkChoiceNode( + root=block.parent_root, + payload_status=get_parent_payload_status(store, block), + ) + return get_ancestor(store, parent, slot) + return node - name: get_ancestor_roots#phase0 @@ -2582,8 +2574,8 @@ - name: get_attestation_score#phase0 sources: [] spec: | - - def get_attestation_score(store: Store, root: Root, state: BeaconState) -> Gwei: + + def get_attestation_score(store: Store, node: ForkChoiceNode, state: BeaconState) -> Gwei: unslashed_and_active_indices = [ i for i in get_active_validator_indices(state, get_current_epoch(state)) @@ -2596,8 +2588,7 @@ if ( i in store.latest_messages and i not in store.equivocating_indices - and get_ancestor(store, store.latest_messages[i].root, store.blocks[root].slot) - == root + and is_ancestor(store, get_supported_node(store, store.latest_messages[i]), node) ) ) ) @@ -2606,15 +2597,8 @@ - name: get_attestation_score#gloas sources: [] spec: | - - def get_attestation_score( - store: Store, - # [Modified in Gloas:EIP7732] - # Removed `root` - # [New in Gloas:EIP7732] - node: ForkChoiceNode, - state: BeaconState, - ) -> Gwei: + + def get_attestation_score(store: Store, node: ForkChoiceNode, state: BeaconState) -> Gwei: unslashed_and_active_indices = [ i for i in get_active_validator_indices(state, get_current_epoch(state)) @@ -2627,8 +2611,7 @@ if ( i in store.latest_messages and i not in store.equivocating_indices - # [Modified in Gloas:EIP7732] - and is_supporting_vote(store, node, store.latest_messages[i]) + and is_ancestor(store, get_supported_node(store, store.latest_messages[i]), node) ) ) ) @@ -2665,13 +2648,13 @@ - file: packages/state-transition/src/cache/epochCache.ts search: "getAttestingIndices(fork: ForkSeq, attestation: Attestation)" spec: | - + def get_attesting_indices(state: BeaconState, attestation: Attestation) -> Set[ValidatorIndex]: """ Return the set of attesting indices corresponding to ``data`` and ``bits``. """ committee = get_beacon_committee(state, attestation.data.slot, attestation.data.index) - return set(index for i, index in enumerate(committee) if attestation.aggregation_bits[i]) + return {index for i, index in enumerate(committee) if attestation.aggregation_bits[i]} - name: get_attesting_indices#electra @@ -2679,7 +2662,7 @@ - file: packages/state-transition/src/cache/epochCache.ts search: "getAttestingIndices(fork: ForkSeq, attestation: Attestation)" spec: | - + def get_attesting_indices(state: BeaconState, attestation: Attestation) -> Set[ValidatorIndex]: """ Return the set of attesting indices corresponding to ``aggregation_bits`` and ``committee_bits``. @@ -2689,11 +2672,11 @@ committee_offset = 0 for committee_index in committee_indices: committee = get_beacon_committee(state, attestation.data.slot, committee_index) - committee_attesters = set( + committee_attesters = { attester_index for i, attester_index in enumerate(committee) if attestation.aggregation_bits[committee_offset + i] - ) + } output = output.union(committee_attesters) committee_offset += len(committee) @@ -2841,6 +2824,26 @@ return compute_proposer_indices(state, epoch, seed, indices) +- name: get_beacon_proposer_indices#gloas + sources: [] + spec: | + + def get_beacon_proposer_indices( + state: BeaconState, epoch: Epoch + ) -> Vector[ValidatorIndex, SLOTS_PER_EPOCH]: + """ + Return the proposer indices for the given ``epoch``. + """ + # [Modified in Gloas:EIP8045] + indices = [ + index + for index in get_active_validator_indices(state, epoch) + if not state.validators[index].slashed + ] + seed = get_seed(state, epoch, DOMAIN_BEACON_PROPOSER) + return compute_proposer_indices(state, epoch, seed, indices) + + - name: get_blob_parameters#fulu sources: - file: packages/config/src/forkConfig/index.ts @@ -3084,25 +3087,28 @@ - name: get_checkpoint_block#phase0 sources: [] spec: | - + def get_checkpoint_block(store: Store, root: Root, epoch: Epoch) -> Root: """ Compute the checkpoint block for epoch ``epoch`` in the chain of block ``root`` """ epoch_first_slot = compute_start_slot_at_epoch(epoch) - return get_ancestor(store, root, epoch_first_slot) + node = ForkChoiceNode(root=root) + return get_ancestor(store, node, epoch_first_slot).root - name: get_checkpoint_block#gloas sources: [] spec: | - + def get_checkpoint_block(store: Store, root: Root, epoch: Epoch) -> Root: """ Compute the checkpoint block for epoch ``epoch`` in the chain of block ``root`` """ epoch_first_slot = compute_start_slot_at_epoch(epoch) - return get_ancestor(store, root, epoch_first_slot).root + # [Modified in Gloas:EIP7732] + node = ForkChoiceNode(root=root, payload_status=PAYLOAD_STATUS_PENDING) + return get_ancestor(store, node, epoch_first_slot).root - name: get_checkpoint_for_block#phase0 @@ -3307,12 +3313,12 @@ - name: get_current_target#phase0 sources: [] spec: | - + def get_current_target(store: Store) -> Checkpoint: """ Return current epoch target. """ - head = get_head(store) + head = get_head(store).root current_epoch = get_current_store_epoch(store) return get_checkpoint_for_block(store, head, current_epoch) @@ -3570,6 +3576,40 @@ ) +- name: get_dependent_root#phase0 + sources: [] + spec: | + + def get_dependent_root(store: Store, root: Root) -> Root: + epoch = get_current_store_epoch(store) + if epoch <= MIN_SEED_LOOKAHEAD: + # Genesis block parent + return Root() + + node = ForkChoiceNode(root=root) + dependent_slot = Slot(compute_start_slot_at_epoch(epoch - MIN_SEED_LOOKAHEAD) - 1) + return get_ancestor(store, node, dependent_slot).root + + +- name: get_dependent_root#gloas + sources: [] + spec: | + + def get_dependent_root(store: Store, root: Root) -> Root: + epoch = get_current_store_epoch(store) + if epoch <= MIN_SEED_LOOKAHEAD: + # Genesis block parent + return Root() + + # [Modified in Gloas:EIP7732] + node = ForkChoiceNode( + root=root, + payload_status=PAYLOAD_STATUS_PENDING, + ) + dependent_slot = Slot(compute_start_slot_at_epoch(epoch - MIN_SEED_LOOKAHEAD) - 1) + return get_ancestor(store, node, dependent_slot).root + + - name: get_domain#phase0 sources: - file: packages/config/src/genesisConfig/index.ts @@ -4141,31 +4181,32 @@ search: '^\s+getHead\(\):' regex: true spec: | - - def get_head(store: Store) -> Root: + + def get_head(store: Store) -> ForkChoiceNode: # Get filtered block tree that only includes viable branches blocks = get_filtered_block_tree(store) # Execute the LMD-GHOST fork choice - head = store.justified_checkpoint.root + head = ForkChoiceNode(root=store.justified_checkpoint.root) while True: - children = [root for root in blocks.keys() if blocks[root].parent_root == head] + children = get_node_children(store, blocks, head) if len(children) == 0: return head # Sort by latest attesting balance with ties broken lexicographically # Ties broken by favoring block with lexicographically higher root - head = max(children, key=lambda root: (get_weight(store, root), root)) + head = max(children, key=lambda child: (get_weight(store, child), child.root)) - name: get_head#gloas sources: [] spec: | - + def get_head(store: Store) -> ForkChoiceNode: # Get filtered block tree that only includes viable branches blocks = get_filtered_block_tree(store) # Execute the LMD-GHOST fork-choice head = ForkChoiceNode( root=store.justified_checkpoint.root, + # [New in Gloas:EIP7732] payload_status=PAYLOAD_STATUS_PENDING, ) @@ -4179,6 +4220,7 @@ key=lambda child: ( get_weight(store, child), child.root, + # [New in Gloas:EIP7732] get_payload_status_tiebreaker(store, child), ), ) @@ -4344,7 +4386,7 @@ - name: get_inclusion_list_committee#heze sources: [] spec: | - + def get_inclusion_list_committee( state: BeaconState, slot: Slot ) -> Vector[ValidatorIndex, INCLUSION_LIST_COMMITTEE_SIZE]: @@ -4358,9 +4400,9 @@ for i in range(committees_per_slot): committee = get_beacon_committee(state, slot, CommitteeIndex(i)) indices.extend(committee) - return Vector[ValidatorIndex, INCLUSION_LIST_COMMITTEE_SIZE]( - [indices[i % len(indices)] for i in range(INCLUSION_LIST_COMMITTEE_SIZE)] - ) + return Vector[ValidatorIndex, INCLUSION_LIST_COMMITTEE_SIZE]([ + indices[i % len(indices)] for i in range(INCLUSION_LIST_COMMITTEE_SIZE) + ]) - name: get_inclusion_list_committee_assignment#heze @@ -4515,7 +4557,7 @@ - name: get_latest_confirmed#phase0 sources: [] spec: | - + def get_latest_confirmed(fcr_store: FastConfirmationStore) -> Root: """ Return the most recent confirmed block by executing the FCR algorithm. @@ -4529,10 +4571,10 @@ # 2) the latest confirmed block does not belong to the canonical chain, # 3) the confirmed chain starting from the current epoch observed justified checkpoint # cannot be re-confirmed at the start of the current epoch. - head = get_head(store) + head = get_head(store).root if ( get_block_epoch(store, confirmed_root) + 1 < current_epoch - or not is_ancestor(store, head, confirmed_root) + or not is_ancestor(store, get_node_for_root(head), get_node_for_root(confirmed_root)) or ( is_start_slot_at_epoch(get_current_slot(store)) and not is_confirmed_chain_safe(fcr_store, confirmed_root) @@ -4867,10 +4909,22 @@ ) +- name: get_node_children#phase0 + sources: [] + spec: | + + def get_node_children( + store: Store, # noqa: ARG001 + blocks: Dict[Root, BeaconBlock], + node: ForkChoiceNode, + ) -> Sequence[ForkChoiceNode]: + return [ForkChoiceNode(root=root) for root in blocks if blocks[root].parent_root == node.root] + + - name: get_node_children#gloas sources: [] spec: | - + def get_node_children( store: Store, blocks: Dict[Root, BeaconBlock], node: ForkChoiceNode ) -> Sequence[ForkChoiceNode]: @@ -4882,7 +4936,7 @@ else: return [ ForkChoiceNode(root=root, payload_status=PAYLOAD_STATUS_PENDING) - for root in blocks.keys() + for root in blocks if ( blocks[root].parent_root == node.root and node.payload_status == get_parent_payload_status(store, blocks[root]) @@ -4890,6 +4944,23 @@ ] +- name: get_node_for_root#phase0 + sources: [] + spec: | + + def get_node_for_root(block_root: Root) -> ForkChoiceNode: + return ForkChoiceNode(root=block_root) + + +- name: get_node_for_root#gloas + sources: [] + spec: | + + def get_node_for_root(block_root: Root) -> ForkChoiceNode: + # [Modified in Gloas:EIP7732] + return ForkChoiceNode(root=block_root, payload_status=PAYLOAD_STATUS_PENDING) + + - name: get_parent_payload_status#gloas sources: [] spec: | @@ -4934,19 +5005,18 @@ - name: get_payload_status_tiebreaker#gloas sources: [] spec: | - + def get_payload_status_tiebreaker(store: Store, node: ForkChoiceNode) -> uint8: - if node.payload_status == PAYLOAD_STATUS_PENDING or store.blocks[ - node.root - ].slot + 1 != get_current_slot(store): - return node.payload_status - else: + if is_previous_slot_payload_decision(store, node): # To decide on a payload from the previous slot, choose # between FULL and EMPTY based on `should_extend_payload` if node.payload_status == PAYLOAD_STATUS_EMPTY: return 1 - else: - return 2 if should_extend_payload(store, node.root) else 0 + if should_extend_payload(store, node.root): + return 2 + return 0 + else: + return node.payload_status - name: get_pending_balance_to_withdraw#electra @@ -5103,7 +5173,7 @@ - file: packages/fork-choice/src/forkChoice/forkChoice.ts search: "* Same as https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.4/specs/phase0/fork-choice.md#get_proposer_head" spec: | - + def get_proposer_head(store: Store, head_root: Root, slot: Slot) -> Root: head_block = store.blocks[head_root] parent_root = head_block.parent_root @@ -5139,18 +5209,16 @@ # Re-org more aggressively if there is a proposer equivocation in the previous slot. proposer_equivocation = is_proposer_equivocation(store, head_root) - if all( - [ - head_late, - shuffling_stable, - ffg_competitive, - finalization_ok, - proposing_on_time, - single_slot_reorg, - head_weak, - parent_strong, - ] - ): + if all([ + head_late, + shuffling_stable, + ffg_competitive, + finalization_ok, + proposing_on_time, + single_slot_reorg, + head_weak, + parent_strong, + ]): # We can re-org the current head by building upon its parent block. return parent_root elif all([head_weak, current_time_ok, proposer_equivocation]): @@ -5246,12 +5314,12 @@ - name: get_pulled_up_head_state#phase0 sources: [] spec: | - + def get_pulled_up_head_state(store: Store) -> BeaconState: """ Return the state of the head pulled up to the current epoch if needed. """ - head = get_head(store) + head = get_head(store).root head_state = store.block_states[head] if get_current_epoch(head_state) < get_current_store_epoch(store): pulled_up_state = copy(head_state) @@ -5274,6 +5342,25 @@ return state.randao_mixes[epoch % EPOCHS_PER_HISTORICAL_VECTOR] +- name: get_safe_execution_block_hash#bellatrix + sources: [] + spec: | + + def get_safe_execution_block_hash(fcr_store: FastConfirmationStore) -> Hash32: + safe_block = fcr_store.store.blocks[fcr_store.confirmed_root] + return safe_block.body.execution_payload.block_hash + + +- name: get_safe_execution_block_hash#gloas + sources: [] + spec: | + + def get_safe_execution_block_hash(fcr_store: FastConfirmationStore) -> Hash32: + safe_block = fcr_store.store.blocks[fcr_store.confirmed_root] + # [Modified in Gloas:EIP7732] + return safe_block.body.signed_execution_payload_bid.message.parent_block_hash + + - name: get_safety_threshold#altair sources: - file: packages/state-transition/src/lightClient/spec/utils.ts @@ -5309,12 +5396,12 @@ - name: get_slot_committee#phase0 sources: [] spec: | - + def get_slot_committee(store: Store, slot: Slot) -> Set[ValidatorIndex]: """ Return participants of all committees in ``slot``. """ - head = get_head(store) + head = get_head(store).root shuffling_source = store.block_states[head] committees_count = get_committee_count_per_slot(shuffling_source, compute_epoch_at_slot(slot)) participants: Set[ValidatorIndex] = set() @@ -5391,6 +5478,42 @@ return compute_empty_slot_support_discount(store, balance_source, block_root) +- name: get_supported_node#phase0 + sources: [] + spec: | + + def get_supported_node( + store: Store, # noqa: ARG001 + message: LatestMessage, + ) -> ForkChoiceNode: + """ + Return a node supported by the ``message``. + """ + return ForkChoiceNode(root=message.root) + + +- name: get_supported_node#gloas + sources: [] + spec: | + + def get_supported_node(store: Store, message: LatestMessage) -> ForkChoiceNode: + """ + Return a node supported by the ``message``. + """ + # [New in Gloas:EIP7732] + block = store.blocks[message.root] + if block.slot < message.slot: + if message.payload_present: + payload_status = PAYLOAD_STATUS_FULL + else: + payload_status = PAYLOAD_STATUS_EMPTY + else: + payload_status = PAYLOAD_STATUS_PENDING + + # [Modified in Gloas:EIP7732] + return ForkChoiceNode(root=message.root, payload_status=payload_status) + + - name: get_sync_committee_message#altair sources: - file: packages/validator/src/services/validatorStore.ts @@ -5492,9 +5615,9 @@ - name: get_terminal_pow_block#bellatrix sources: [] spec: | - + def get_terminal_pow_block(pow_chain: Dict[Hash32, PowBlock]) -> Optional[PowBlock]: - if TERMINAL_BLOCK_HASH != Hash32(): + if TERMINAL_BLOCK_HASH != EMPTY_BLOCK_HASH: # Terminal block hash override takes precedence over terminal total difficulty if TERMINAL_BLOCK_HASH in pow_chain: return pow_chain[TERMINAL_BLOCK_HASH] @@ -5822,58 +5945,51 @@ - name: get_weight#phase0 sources: [] spec: | - - def get_weight(store: Store, root: Root) -> Gwei: + + def get_weight(store: Store, node: ForkChoiceNode) -> Gwei: state = store.checkpoint_states[store.justified_checkpoint] - attestation_score = get_attestation_score(store, root, state) + attestation_score = get_attestation_score(store, node, state) if store.proposer_boost_root == Root(): # Return only attestation score if ``proposer_boost_root`` is not set return attestation_score # Calculate proposer score if ``proposer_boost_root`` is set proposer_score = Gwei(0) - # Boost is applied if ``root`` is an ancestor of ``proposer_boost_root`` - if get_ancestor(store, store.proposer_boost_root, store.blocks[root].slot) == root: + proposer_boost_node = ForkChoiceNode(root=store.proposer_boost_root) + # Boost is applied if ``node`` is an ancestor of ``proposer_boost_node`` + if is_ancestor(store, proposer_boost_node, node): proposer_score = get_proposer_score(store) + return attestation_score + proposer_score - name: get_weight#gloas sources: [] spec: | - - def get_weight( - store: Store, + + def get_weight(store: Store, node: ForkChoiceNode) -> Gwei: + # [New in Gloas:EIP7732] + if is_previous_slot_payload_decision(store, node): + return Gwei(0) + + state = store.checkpoint_states[store.justified_checkpoint] + attestation_score = get_attestation_score(store, node, state) # [Modified in Gloas:EIP7732] - node: ForkChoiceNode, - ) -> Gwei: - if node.payload_status == PAYLOAD_STATUS_PENDING or store.blocks[ - node.root - ].slot + 1 != get_current_slot(store): - state = store.checkpoint_states[store.justified_checkpoint] - attestation_score = get_attestation_score(store, node, state) - if not should_apply_proposer_boost(store): - # Return only attestation score if - # proposer boost should not apply - return attestation_score - - # Calculate proposer score if `proposer_boost_root` is set - proposer_score = Gwei(0) - - # `proposer_boost_root` is treated as a vote for the - # proposer's block in the current slot. Proposer boost - # is applied accordingly to all ancestors - message = LatestMessage( - slot=get_current_slot(store), - root=store.proposer_boost_root, - payload_present=False, - ) - if is_supporting_vote(store, node, message): - proposer_score = get_proposer_score(store) + if not should_apply_proposer_boost(store): + # Return only attestation score if proposer boost should not apply + return attestation_score - return attestation_score + proposer_score - else: - return Gwei(0) + # Calculate proposer score if proposer boost should apply + proposer_score = Gwei(0) + # [Modified in Gloas:EIP7732] + proposer_boost_node = ForkChoiceNode( + root=store.proposer_boost_root, payload_status=PAYLOAD_STATUS_PENDING + ) + # Boost is applied if ``node`` is an ancestor of ``proposer_boost_node`` + if is_ancestor(store, proposer_boost_node, node): + proposer_score = get_proposer_score(store) + + return attestation_score + proposer_score - name: has_compounding_withdrawal_credential#electra @@ -5961,7 +6077,7 @@ - file: packages/state-transition/src/util/genesis.ts search: export function initializeBeaconStateFromEth1( spec: | - + def initialize_beacon_state_from_eth1( eth1_block_hash: Hash32, eth1_timestamp: uint64, deposits: Sequence[Deposit] ) -> BeaconState: @@ -5980,7 +6096,7 @@ ) # Process deposits - leaves = list(map(lambda deposit: deposit.data, deposits)) + leaves = [deposit.data for deposit in deposits] for index, deposit in enumerate(deposits): deposit_data_list = List[DepositData, 2**DEPOSIT_CONTRACT_TREE_DEPTH](*leaves[: index + 1]) state.eth1_data.deposit_root = hash_tree_root(deposit_data_list) @@ -6214,12 +6330,25 @@ - name: is_ancestor#phase0 sources: [] spec: | - - def is_ancestor(store: Store, block_root: Root, ancestor_root: Root) -> bool: - """ - Return ``True`` if ``ancestor_root`` is an ancestor of ``block_root``. - """ - return get_ancestor(store, block_root, store.blocks[ancestor_root].slot) == ancestor_root + + def is_ancestor(store: Store, node: ForkChoiceNode, ancestor: ForkChoiceNode) -> bool: + return get_ancestor(store, node, store.blocks[ancestor.root].slot) == ancestor + + +- name: is_ancestor#gloas + sources: [] + spec: | + + def is_ancestor(store: Store, node: ForkChoiceNode, ancestor: ForkChoiceNode) -> bool: + node_ancestor = get_ancestor(store, node, store.blocks[ancestor.root].slot) + if node_ancestor.root != ancestor.root: + return False + + # [New in Gloas:EIP7732] + return ( + node_ancestor.payload_status == ancestor.payload_status + or ancestor.payload_status == PAYLOAD_STATUS_PENDING + ) - name: is_assigned_to_sync_committee#altair @@ -6365,16 +6494,16 @@ - name: is_confirmed_chain_safe#phase0 sources: [] spec: | - + def is_confirmed_chain_safe(fcr_store: FastConfirmationStore, confirmed_root: Root) -> bool: """ Return ``True`` if and only if all blocks of the confirmed chain starting from current_epoch_observed_justified_checkpoint are LMD-GHOST safe. """ store = fcr_store.store - # Check if the confirmed_root is descendant of current_epoch_observed_justified_checkpoint - if not is_ancestor( - store, confirmed_root, fcr_store.current_epoch_observed_justified_checkpoint.root + # Check if the confirmed_root has current_epoch_observed_justified_checkpoint in its chain + if fcr_store.current_epoch_observed_justified_checkpoint != get_checkpoint_for_block( + store, confirmed_root, fcr_store.current_epoch_observed_justified_checkpoint.epoch ): return False @@ -6387,8 +6516,10 @@ # Limit reconfirmation to the first block of the previous epoch # as if it is successful, reconfirmation of the ancestors is implied. ancestor_at_previous_epoch_start = get_ancestor( - store, confirmed_root, compute_start_slot_at_epoch(Epoch(current_epoch - 1)) - ) + store, + get_node_for_root(confirmed_root), + compute_start_slot_at_epoch(Epoch(current_epoch - 1)), + ).root if get_block_epoch(store, ancestor_at_previous_epoch_start) + 1 == current_epoch: # The parent of the first block of the previous epoch start_root_exclusive = store.blocks[ancestor_at_previous_epoch_start].parent_root @@ -6691,11 +6822,11 @@ - file: packages/fork-choice/src/forkChoice/forkChoice.ts search: "// https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.4/specs/phase0/fork-choice.md#is_head_weak" spec: | - + def is_head_weak(store: Store, head_root: Root) -> bool: justified_state = store.checkpoint_states[store.justified_checkpoint] reorg_threshold = calculate_committee_fraction(justified_state, REORG_HEAD_WEIGHT_THRESHOLD) - head_weight = get_weight(store, head_root) + head_weight = get_weight(store, ForkChoiceNode(root=head_root)) return head_weight < reorg_threshold @@ -6740,7 +6871,7 @@ - name: is_inclusion_list_bits_inclusive#heze sources: [] spec: | - + def is_inclusion_list_bits_inclusive( store: InclusionListStore, state: BeaconState, @@ -6757,7 +6888,7 @@ return all( inclusion_bit or not local_inclusion_bit for inclusion_bit, local_inclusion_bit in zip( - inclusion_list_bits, local_inclusion_list_bits + inclusion_list_bits, local_inclusion_list_bits, strict=True ) ) @@ -6791,7 +6922,7 @@ - name: is_non_strict_superset#phase0 sources: [] spec: | - + def is_non_strict_superset( seen_bits_set: Set[Tuple[boolean, ...]], new_bits: Tuple[boolean, ...], @@ -6802,7 +6933,7 @@ """ for prior_bits in seen_bits_set: is_superset = True - for prior_bit, new_bit in zip(prior_bits, new_bits): + for prior_bit, new_bit in zip(prior_bits, new_bits, strict=True): if new_bit and not prior_bit: is_superset = False break @@ -6814,12 +6945,12 @@ - name: is_one_confirmed#phase0 sources: [] spec: | - + def is_one_confirmed(store: Store, balance_source: BeaconState, block_root: Root) -> bool: """ Return ``True`` if and only if the block is LMD-GHOST safe. """ - support = get_attestation_score(store, block_root, balance_source) + support = get_attestation_score(store, get_node_for_root(block_root), balance_source) safety_threshold = compute_safety_threshold(store, block_root, balance_source) return support > safety_threshold @@ -6861,12 +6992,12 @@ - file: packages/fork-choice/src/forkChoice/forkChoice.ts search: "// https://github.com/ethereum/consensus-specs/blob/v1.6.1/specs/phase0/fork-choice.md#is_parent_strong" spec: | - + def is_parent_strong(store: Store, root: Root) -> bool: justified_state = store.checkpoint_states[store.justified_checkpoint] parent_threshold = calculate_committee_fraction(justified_state, REORG_PARENT_WEIGHT_THRESHOLD) parent_root = store.blocks[root].parent_root - parent_weight = get_weight(store, parent_root) + parent_weight = get_weight(store, ForkChoiceNode(root=parent_root)) return parent_weight > parent_threshold @@ -6978,6 +7109,16 @@ return False +- name: is_previous_slot_payload_decision#gloas + sources: [] + spec: | + + def is_previous_slot_payload_decision(store: Store, node: ForkChoiceNode) -> bool: + is_previous_slot = store.blocks[node.root].slot + 1 == get_current_slot(store) + is_payload_decision = node.payload_status in [PAYLOAD_STATUS_EMPTY, PAYLOAD_STATUS_FULL] + return is_previous_slot and is_payload_decision + + - name: is_proposer#phase0 sources: [] spec: | @@ -7071,34 +7212,6 @@ return compute_slots_since_epoch_start(slot) == 0 -- name: is_supporting_vote#gloas - sources: [] - spec: | - - def is_supporting_vote(store: Store, node: ForkChoiceNode, message: LatestMessage) -> bool: - """ - Returns whether the vote ``message`` supports the chain containing the - forkchoice node ``node``. - """ - block = store.blocks[node.root] - if node.root == message.root: - if node.payload_status == PAYLOAD_STATUS_PENDING: - return True - assert message.slot >= block.slot - if message.slot == block.slot: - return False - if message.payload_present: - return node.payload_status == PAYLOAD_STATUS_FULL - else: - return node.payload_status == PAYLOAD_STATUS_EMPTY - else: - ancestor = get_ancestor(store, message.root, block.slot) - return node.root == ancestor.root and ( - node.payload_status == PAYLOAD_STATUS_PENDING - or node.payload_status == ancestor.payload_status - ) - - - name: is_sync_committee_aggregator#altair sources: - file: packages/state-transition/src/util/aggregator.ts @@ -7182,7 +7295,7 @@ - file: packages/state-transition/src/block/isValidIndexedAttestation.ts search: export function isValidIndexedAttestation( spec: | - + def is_valid_indexed_attestation( state: BeaconState, indexed_attestation: IndexedAttestation ) -> bool: @@ -7191,7 +7304,7 @@ """ # Verify indices are sorted and unique indices = indexed_attestation.attesting_indices - if len(indices) == 0 or not indices == sorted(set(indices)): + if len(indices) == 0 or indices != sorted(set(indices)): return False # Verify aggregate signature pubkeys = [state.validators[i].pubkey for i in indices] @@ -7205,7 +7318,7 @@ - file: packages/state-transition/src/block/isValidIndexedPayloadAttestation.ts search: export function isValidIndexedPayloadAttestation( spec: | - + def is_valid_indexed_payload_attestation( state: BeaconState, attestation: IndexedPayloadAttestation ) -> bool: @@ -7215,7 +7328,7 @@ """ # Verify indices are non-empty and sorted indices = attestation.attesting_indices - if len(indices) == 0 or not indices == sorted(indices): + if len(indices) == 0 or indices != sorted(indices): return False # Verify aggregate signature @@ -7630,7 +7743,7 @@ search: '^\s+onBlock\(' regex: true spec: | - + def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: block = signed_block.message # Parent block must be known @@ -7654,14 +7767,17 @@ # Check the block is valid and compute the post-state state = pre_state.copy() block_root = hash_tree_root(block) - state_transition(state, signed_block, True) + state_transition(state, signed_block, validate_result=True) + + # Compute head before applying the block + head = get_head(store) # Add new block to the store store.blocks[block_root] = block # Add new state for this block to the store store.block_states[block_root] = state record_block_timeliness(store, block_root) - update_proposer_boost_root(store, block_root) + update_proposer_boost_root(store, head.root, block_root) # Update checkpoints in store if necessary update_checkpoints(store, state.current_justified_checkpoint, state.finalized_checkpoint) @@ -7676,7 +7792,7 @@ search: '^\s+onBlock\(' regex: true spec: | - + def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: """ Run ``on_block`` upon receiving a new block. @@ -7706,19 +7822,21 @@ # Check the block is valid and compute the post-state state = pre_state.copy() block_root = hash_tree_root(block) - state_transition(state, signed_block, True) + state_transition(state, signed_block, validate_result=True) # [New in Bellatrix] if is_merge_transition_block(pre_state, block.body): validate_merge_block(block) + # Compute head before applying the block + head = get_head(store) # Add new block to the store store.blocks[block_root] = block # Add new state for this block to the store store.block_states[block_root] = state record_block_timeliness(store, block_root) - update_proposer_boost_root(store, block_root) + update_proposer_boost_root(store, head.root, block_root) # Update checkpoints in store if necessary update_checkpoints(store, state.current_justified_checkpoint, state.finalized_checkpoint) @@ -7733,7 +7851,7 @@ search: '^\s+onBlock\(' regex: true spec: | - + def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: """ Run ``on_block`` upon receiving a new block. @@ -7759,15 +7877,17 @@ # Make a copy of the state to avoid mutability issues state = copy(store.block_states[block.parent_root]) block_root = hash_tree_root(block) - state_transition(state, signed_block, True) + state_transition(state, signed_block, validate_result=True) + # Compute head before applying the block + head = get_head(store) # Add new block to the store store.blocks[block_root] = block # Add new state for this block to the store store.block_states[block_root] = state record_block_timeliness(store, block_root) - update_proposer_boost_root(store, block_root) + update_proposer_boost_root(store, head.root, block_root) # Update checkpoints in store if necessary update_checkpoints(store, state.current_justified_checkpoint, state.finalized_checkpoint) @@ -7782,7 +7902,7 @@ search: '^\s+onBlock\(' regex: true spec: | - + def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: """ Run ``on_block`` upon receiving a new block. @@ -7813,15 +7933,17 @@ # Make a copy of the state to avoid mutability issues state = copy(store.block_states[block.parent_root]) block_root = hash_tree_root(block) - state_transition(state, signed_block, True) + state_transition(state, signed_block, validate_result=True) + # Compute head before applying the block + head = get_head(store) # Add new block to the store store.blocks[block_root] = block # Add new state for this block to the store store.block_states[block_root] = state record_block_timeliness(store, block_root) - update_proposer_boost_root(store, block_root) + update_proposer_boost_root(store, head.root, block_root) # Update checkpoints in store if necessary update_checkpoints(store, state.current_justified_checkpoint, state.finalized_checkpoint) @@ -7836,7 +7958,7 @@ search: '^\s+onBlock\(' regex: true spec: | - + def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: """ Run ``on_block`` upon receiving a new block. @@ -7867,15 +7989,17 @@ # Check the block is valid and compute the post-state block_root = hash_tree_root(block) - state_transition(state, signed_block, True) + state_transition(state, signed_block, validate_result=True) + # Compute head before applying the block + head = get_head(store) # Add new block to the store store.blocks[block_root] = block # Add new state for this block to the store store.block_states[block_root] = state record_block_timeliness(store, block_root) - update_proposer_boost_root(store, block_root) + update_proposer_boost_root(store, head.root, block_root) # Update checkpoints in store if necessary update_checkpoints(store, state.current_justified_checkpoint, state.finalized_checkpoint) @@ -7887,7 +8011,7 @@ - name: on_block#gloas sources: [] spec: | - + def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: """ Run ``on_block`` upon receiving a new block. @@ -7921,8 +8045,10 @@ # Check the block is valid and compute the post-state block_root = hash_tree_root(block) - state_transition(state, signed_block, True) + state_transition(state, signed_block, validate_result=True) + # Compute head before applying the block + head = get_head(store) # Add new block to the store store.blocks[block_root] = block # Add new state for this block to the store @@ -7935,7 +8061,7 @@ notify_ptc_messages(store, state, block.body.payload_attestations) record_block_timeliness(store, block_root) - update_proposer_boost_root(store, block_root) + update_proposer_boost_root(store, head.root, block_root) # Update checkpoints in store if necessary update_checkpoints(store, state.current_justified_checkpoint, state.finalized_checkpoint) @@ -8227,7 +8353,7 @@ - file: packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts search: export async function prepareExecutionPayload( spec: | - + def prepare_execution_payload( state: BeaconState, safe_block_hash: Hash32, @@ -8238,7 +8364,7 @@ ) -> Optional[PayloadId]: if not is_merge_transition_complete(state): assert pow_chain is not None - is_terminal_block_hash_set = TERMINAL_BLOCK_HASH != Hash32() + is_terminal_block_hash_set = TERMINAL_BLOCK_HASH != EMPTY_BLOCK_HASH is_activation_epoch_reached = ( get_current_epoch(state) >= TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH ) @@ -8607,7 +8733,7 @@ - file: packages/state-transition/src/block/processAttestationsAltair.ts search: export function processAttestationsAltair( spec: | - + def process_attestation(state: BeaconState, attestation: Attestation) -> None: data = attestation.data assert data.target.epoch in (get_previous_epoch(state), get_current_epoch(state)) @@ -8621,11 +8747,11 @@ for committee_index in committee_indices: assert committee_index < get_committee_count_per_slot(state, data.target.epoch) committee = get_beacon_committee(state, data.slot, committee_index) - committee_attesters = set( + committee_attesters = { attester_index for i, attester_index in enumerate(committee) if attestation.aggregation_bits[committee_offset + i] - ) + } assert len(committee_attesters) > 0 committee_offset += len(committee) @@ -8666,7 +8792,7 @@ - name: process_attestation#gloas sources: [] spec: | - + def process_attestation(state: BeaconState, attestation: Attestation) -> None: data = attestation.data assert data.target.epoch in (get_previous_epoch(state), get_current_epoch(state)) @@ -8680,11 +8806,11 @@ for committee_index in committee_indices: assert committee_index < get_committee_count_per_slot(state, data.target.epoch) committee = get_beacon_committee(state, data.slot, committee_index) - committee_attesters = set( + committee_attesters = { attester_index for i, attester_index in enumerate(committee) if attestation.aggregation_bits[committee_offset + i] - ) + } assert len(committee_attesters) > 0 committee_offset += len(committee) @@ -8858,6 +8984,23 @@ process_sync_aggregate(state, block.body.sync_aggregate) +- name: process_block#fulu + sources: + - file: packages/state-transition/src/block/index.ts + search: export function processBlock( + spec: | + + def process_block(state: BeaconState, block: BeaconBlock) -> None: + process_block_header(state, block) + process_withdrawals(state, block.body.execution_payload) + process_execution_payload(state, block.body, EXECUTION_ENGINE) + process_randao(state, block.body) + process_eth1_data(state, block.body) + # [Modified in Fulu] + process_operations(state, block.body) + process_sync_aggregate(state, block.body.sync_aggregate) + + - name: process_block#gloas sources: [] spec: | @@ -9103,13 +9246,30 @@ - file: packages/state-transition/src/block/processDepositRequest.ts search: export function processDepositRequest( spec: | - + def process_deposit_request(state: BeaconState, deposit_request: DepositRequest) -> None: # Set deposit request start index if state.deposit_requests_start_index == UNSET_DEPOSIT_REQUESTS_START_INDEX: state.deposit_requests_start_index = deposit_request.index - # Create pending deposit + state.pending_deposits.append( + PendingDeposit( + pubkey=deposit_request.pubkey, + withdrawal_credentials=deposit_request.withdrawal_credentials, + amount=deposit_request.amount, + signature=deposit_request.signature, + slot=state.slot, + ) + ) + + +- name: process_deposit_request#fulu + sources: + - file: packages/state-transition/src/block/processDepositRequest.ts + search: export function processDepositRequest( + spec: | + + def process_deposit_request(state: BeaconState, deposit_request: DepositRequest) -> None: state.pending_deposits.append( PendingDeposit( pubkey=deposit_request.pubkey, @@ -10114,21 +10274,38 @@ for_ops(body.execution_requests.consolidations, process_consolidation_request) +- name: process_operations#fulu + sources: + - file: packages/state-transition/src/block/processOperations.ts + search: export function processOperations( + spec: | + + def process_operations(state: BeaconState, body: BeaconBlockBody) -> None: + # [Modified in Fulu] + assert len(body.deposits) == 0 + + def for_ops(operations: Sequence[Any], fn: Callable[[BeaconState, Any], None]) -> None: + for operation in operations: + fn(state, operation) + + for_ops(body.proposer_slashings, process_proposer_slashing) + for_ops(body.attester_slashings, process_attester_slashing) + for_ops(body.attestations, process_attestation) + # [Modified in Fulu] + # Removed `process_deposit` + for_ops(body.voluntary_exits, process_voluntary_exit) + for_ops(body.bls_to_execution_changes, process_bls_to_execution_change) + for_ops(body.execution_requests.deposits, process_deposit_request) + for_ops(body.execution_requests.withdrawals, process_withdrawal_request) + for_ops(body.execution_requests.consolidations, process_consolidation_request) + + - name: process_operations#gloas sources: [] spec: | - + def process_operations(state: BeaconState, body: BeaconBlockBody) -> None: - # Disable former deposit mechanism once all prior deposits are processed - eth1_deposit_index_limit = min( - state.eth1_data.deposit_count, state.deposit_requests_start_index - ) - if state.eth1_deposit_index < eth1_deposit_index_limit: - assert len(body.deposits) == min( - MAX_DEPOSITS, eth1_deposit_index_limit - state.eth1_deposit_index - ) - else: - assert len(body.deposits) == 0 + assert len(body.deposits) == 0 def for_ops(operations: Sequence[Any], fn: Callable[[BeaconState, Any], None]) -> None: for operation in operations: @@ -10139,7 +10316,6 @@ for_ops(body.attester_slashings, process_attester_slashing) # [Modified in Gloas:EIP7732] for_ops(body.attestations, process_attestation) - for_ops(body.deposits, process_deposit) # [Modified in Gloas:EIP7732] for_ops(body.voluntary_exits, process_voluntary_exit) for_ops(body.bls_to_execution_changes, process_bls_to_execution_change) @@ -10322,17 +10498,17 @@ state.deposit_balance_to_consume = Gwei(0) -- name: process_pending_deposits#gloas +- name: process_pending_deposits#fulu sources: - file: packages/state-transition/src/epoch/processPendingDeposits.ts search: export function processPendingDeposits( spec: | - + def process_pending_deposits(state: BeaconState) -> None: next_epoch = Epoch(get_current_epoch(state) + 1) - # [Modified in Gloas:EIP8061] - # Deposits still consume the activation-only churn budget in Gloas. - available_for_processing = state.deposit_balance_to_consume + get_activation_churn_limit(state) + available_for_processing = state.deposit_balance_to_consume + get_activation_exit_churn_limit( + state + ) processed_amount = 0 next_deposit_index = 0 deposits_to_postpone = [] @@ -10340,16 +10516,6 @@ finalized_slot = compute_start_slot_at_epoch(state.finalized_checkpoint.epoch) for deposit in state.pending_deposits: - # Do not process deposit requests if Eth1 bridge deposits are not yet applied. - if ( - # Is deposit request - deposit.slot > GENESIS_SLOT - and - # There are pending Eth1 bridge deposits - state.eth1_deposit_index < state.deposit_requests_start_index - ): - break - # Check if deposit has been finalized, otherwise, stop processing. if deposit.slot > finalized_slot: break @@ -10395,11 +10561,74 @@ state.deposit_balance_to_consume = Gwei(0) -- name: process_proposer_lookahead#fulu +- name: process_pending_deposits#gloas sources: - - file: packages/state-transition/src/epoch/processProposerLookahead.ts - search: export function processProposerLookahead( - spec: | + - file: packages/state-transition/src/epoch/processPendingDeposits.ts + search: export function processPendingDeposits( + spec: | + + def process_pending_deposits(state: BeaconState) -> None: + next_epoch = Epoch(get_current_epoch(state) + 1) + # [Modified in Gloas:EIP8061] + # Deposits still consume the activation-only churn budget in Gloas. + available_for_processing = state.deposit_balance_to_consume + get_activation_churn_limit(state) + processed_amount = 0 + next_deposit_index = 0 + deposits_to_postpone = [] + is_churn_limit_reached = False + finalized_slot = compute_start_slot_at_epoch(state.finalized_checkpoint.epoch) + + for deposit in state.pending_deposits: + # Check if deposit has been finalized, otherwise, stop processing. + if deposit.slot > finalized_slot: + break + + # Check if number of processed deposits has not reached the limit, otherwise, stop processing. + if next_deposit_index >= MAX_PENDING_DEPOSITS_PER_EPOCH: + break + + # Read validator state + is_validator_exited = False + is_validator_withdrawn = False + validator_pubkeys = [v.pubkey for v in state.validators] + if deposit.pubkey in validator_pubkeys: + validator = state.validators[ValidatorIndex(validator_pubkeys.index(deposit.pubkey))] + is_validator_exited = validator.exit_epoch < FAR_FUTURE_EPOCH + is_validator_withdrawn = validator.withdrawable_epoch < next_epoch + + if is_validator_withdrawn: + # Deposited balance will never become active. Increase balance but do not consume churn + apply_pending_deposit(state, deposit) + elif is_validator_exited: + # Validator is exiting, postpone the deposit until after withdrawable epoch + deposits_to_postpone.append(deposit) + else: + # Check if deposit fits in the churn, otherwise, do no more deposit processing in this epoch. + is_churn_limit_reached = processed_amount + deposit.amount > available_for_processing + if is_churn_limit_reached: + break + + # Consume churn and apply deposit. + processed_amount += deposit.amount + apply_pending_deposit(state, deposit) + + # Regardless of how the deposit was handled, we move on in the queue. + next_deposit_index += 1 + + state.pending_deposits = state.pending_deposits[next_deposit_index:] + deposits_to_postpone + + # Accumulate churn only if the churn limit has been hit. + if is_churn_limit_reached: + state.deposit_balance_to_consume = available_for_processing - processed_amount + else: + state.deposit_balance_to_consume = Gwei(0) + + +- name: process_proposer_lookahead#fulu + sources: + - file: packages/state-transition/src/epoch/processProposerLookahead.ts + search: export function processProposerLookahead( + spec: | def process_proposer_lookahead(state: BeaconState) -> None: last_epoch_start = len(state.proposer_lookahead) - SLOTS_PER_EPOCH @@ -10844,7 +11073,7 @@ - file: packages/state-transition/src/block/processSyncCommittee.ts search: export function processSyncAggregate( spec: | - + def process_sync_aggregate(state: BeaconState, sync_aggregate: SyncAggregate) -> None: # Verify sync committee aggregate signature signing over the previous slot block root committee_pubkeys = state.current_sync_committee.pubkeys @@ -10856,7 +11085,7 @@ # More than half participated - subtract non-participant keys. # First determine nonparticipating members non_participant_pubkeys = [ - pubkey for pubkey, bit in zip(committee_pubkeys, committee_bits) if not bit + pubkey for pubkey, bit in zip(committee_pubkeys, committee_bits, strict=True) if not bit ] # Compute aggregate of non-participants non_participant_aggregate = eth_aggregate_pubkeys(non_participant_pubkeys) @@ -10871,7 +11100,9 @@ # Less than half participated - aggregate participant keys participant_pubkeys = [ pubkey - for pubkey, bit in zip(committee_pubkeys, sync_aggregate.sync_committee_bits) + for pubkey, bit in zip( + committee_pubkeys, sync_aggregate.sync_committee_bits, strict=True + ) if bit ] previous_slot = max(state.slot, Slot(1)) - Slot(1) @@ -10899,7 +11130,7 @@ ValidatorIndex(all_pubkeys.index(pubkey)) for pubkey in state.current_sync_committee.pubkeys ] for participant_index, participation_bit in zip( - committee_indices, sync_aggregate.sync_committee_bits + committee_indices, sync_aggregate.sync_committee_bits, strict=True ): if participation_bit: increase_balance(state, participant_index, participant_reward) @@ -11305,7 +11536,7 @@ - file: packages/beacon-node/src/util/blobs.ts search: export async function dataColumnMatrixRecovery( spec: | - + def recover_matrix( partial_matrix: Sequence[MatrixEntry], blob_count: uint64 ) -> Sequence[MatrixEntry]: @@ -11320,7 +11551,9 @@ cell_indices = [e.column_index for e in partial_matrix if e.row_index == blob_index] cells = [e.cell for e in partial_matrix if e.row_index == blob_index] recovered_cells, recovered_proofs = recover_cells_and_kzg_proofs(cell_indices, cells) - for cell_index, (cell, proof) in enumerate(zip(recovered_cells, recovered_proofs)): + for cell_index, (cell, proof) in enumerate( + zip(recovered_cells, recovered_proofs, strict=True) + ): matrix.append( MatrixEntry( cell=cell, @@ -11411,12 +11644,18 @@ - name: should_build_on_full#gloas sources: [] spec: | - + def should_build_on_full(store: Store, head: ForkChoiceNode) -> bool: assert head.payload_status != PAYLOAD_STATUS_PENDING if head.payload_status == PAYLOAD_STATUS_EMPTY: return False - return not payload_data_availability(store, head.root, available=False) + if store.blocks[head.root].slot + 1 != get_current_slot(store): + return True + if payload_data_availability(store, head.root, available=False): + return False + if payload_timeliness(store, head.root, timely=False): + return False + return True - name: should_extend_payload#gloas @@ -11458,71 +11697,6 @@ ) -- name: should_override_forkchoice_update#bellatrix - sources: - - file: packages/fork-choice/src/forkChoice/forkChoice.ts - search: "// See https://github.com/ethereum/consensus-specs/blob/v1.5.0/specs/bellatrix/fork-choice.md#should_override_forkchoice_update" - spec: | - - def should_override_forkchoice_update(store: Store, head_root: Root) -> bool: - head_block = store.blocks[head_root] - parent_root = head_block.parent_root - parent_block = store.blocks[parent_root] - current_slot = get_current_slot(store) - proposal_slot = head_block.slot + Slot(1) - - # Only re-org the head_block block if it arrived later than the attestation deadline. - head_late = is_head_late(store, head_root) - - # Shuffling stable. - shuffling_stable = is_shuffling_stable(proposal_slot) - - # FFG information of the new head_block will be competitive with the current head. - ffg_competitive = is_ffg_competitive(store, head_root, parent_root) - - # Do not re-org if the chain is not finalizing with acceptable frequency. - finalization_ok = is_finalization_ok(store, proposal_slot) - - # Only suppress the fork choice update if we are confident that we will propose the next block. - parent_state_advanced = store.block_states[parent_root].copy() - process_slots(parent_state_advanced, proposal_slot) - proposer_index = get_beacon_proposer_index(parent_state_advanced) - proposing_reorg_slot = validator_is_connected(proposer_index) - - # Single slot re-org. - parent_slot_ok = parent_block.slot + 1 == head_block.slot - proposing_on_time = is_proposing_on_time(store) - - # Note that this condition is different from `get_proposer_head` - current_time_ok = head_block.slot == current_slot or ( - proposal_slot == current_slot and proposing_on_time - ) - single_slot_reorg = parent_slot_ok and current_time_ok - - # Check the head weight only if the attestations from the head slot have already been applied. - # Implementations may want to do this in different ways, e.g. by advancing - # `store.time` early, or by counting queued attestations during the head block's slot. - if current_slot > head_block.slot: - head_weak = is_head_weak(store, head_root) - parent_strong = is_parent_strong(store, head_root) - else: - head_weak = True - parent_strong = True - - return all( - [ - head_late, - shuffling_stable, - ffg_competitive, - finalization_ok, - proposing_reorg_slot, - single_slot_reorg, - head_weak, - parent_strong, - ] - ) - - - name: slash_validator#phase0 sources: - file: packages/state-transition/src/block/slashValidator.ts @@ -11780,12 +11954,12 @@ - name: update_fast_confirmation_variables#phase0 sources: [] spec: | - + def update_fast_confirmation_variables(fcr_store: FastConfirmationStore) -> None: # Update prev and curr slot head store = fcr_store.store fcr_store.previous_slot_head = fcr_store.current_slot_head - fcr_store.current_slot_head = get_head(store) + fcr_store.current_slot_head = get_head(store).root # Update greatest unrealized justified checkpoint at the last slot of an epoch if is_start_slot_at_epoch(Slot(get_current_slot(store) + 1)): @@ -11916,44 +12090,32 @@ - name: update_proposer_boost_root#phase0 sources: [] spec: | - - def update_proposer_boost_root(store: Store, root: Root) -> None: + + def update_proposer_boost_root(store: Store, head: Root, root: Root) -> None: is_first_block = store.proposer_boost_root == Root() is_timely = store.block_timeliness[root] + is_same_dependent_root = get_dependent_root(store, root) == get_dependent_root(store, head) # Add proposer score boost if the block is timely, not conflicting with an - # existing block, with the same the proposer as the canonical chain. - if is_timely and is_first_block: - head_state = copy(store.block_states[get_head(store)]) - slot = get_current_slot(store) - if head_state.slot < slot: - process_slots(head_state, slot) - block = store.blocks[root] - # Only update if the proposer is the same as on the canonical chain - if block.proposer_index == get_beacon_proposer_index(head_state): - store.proposer_boost_root = root + # existing block, with the same dependent root as the canonical chain head. + if is_timely and is_first_block and is_same_dependent_root: + store.proposer_boost_root = root - name: update_proposer_boost_root#gloas sources: [] spec: | - - def update_proposer_boost_root(store: Store, root: Root) -> None: + + def update_proposer_boost_root(store: Store, head: Root, root: Root) -> None: is_first_block = store.proposer_boost_root == Root() # [Modified in Gloas:EIP7732] is_timely = store.block_timeliness[root][ATTESTATION_TIMELINESS_INDEX] + is_same_dependent_root = get_dependent_root(store, root) == get_dependent_root(store, head) - # Add proposer score boost if the block is the first timely block - # for this slot, with the same proposer as the canonical chain. - if is_timely and is_first_block: - head_state = copy(store.block_states[get_head(store).root]) - slot = get_current_slot(store) - if head_state.slot < slot: - process_slots(head_state, slot) - block = store.blocks[root] - # Only update if the proposer is the same as on the canonical chain - if block.proposer_index == get_beacon_proposer_index(head_state): - store.proposer_boost_root = root + # Add proposer score boost if the block is timely, not conflicting with an + # existing block, with the same dependent root as the canonical chain head. + if is_timely and is_first_block and is_same_dependent_root: + store.proposer_boost_root = root - name: update_unrealized_checkpoints#phase0 @@ -12682,15 +12844,14 @@ - file: packages/state-transition/src/slot/upgradeStateToElectra.ts search: export function upgradeStateToElectra( spec: | - + def upgrade_to_electra(pre: deneb.BeaconState) -> BeaconState: epoch = deneb.get_current_epoch(pre) earliest_exit_epoch = compute_activation_exit_epoch(get_current_epoch(pre)) for validator in pre.validators: if validator.exit_epoch != FAR_FUTURE_EPOCH: - if validator.exit_epoch > earliest_exit_epoch: - earliest_exit_epoch = validator.exit_epoch + earliest_exit_epoch = max(earliest_exit_epoch, validator.exit_epoch) earliest_exit_epoch += Epoch(1) post = BeaconState( @@ -13286,14 +13447,14 @@ - name: validate_beacon_attestation_gossip#deneb sources: [] spec: | - + def validate_beacon_attestation_gossip( seen: Seen, store: Store, state: BeaconState, attestation: Attestation, - subnet_id: uint64, current_time_ms: uint64, + subnet_id: SubnetID, ) -> None: """ Validate an Attestation for gossip propagation on a subnet. @@ -13393,15 +13554,15 @@ - name: validate_beacon_attestation_gossip#electra sources: [] spec: | - + def validate_beacon_attestation_gossip( seen: Seen, store: Store, state: BeaconState, # [Modified in Electra:EIP7549] attestation: SingleAttestation, - subnet_id: uint64, current_time_ms: uint64, + subnet_id: SubnetID, ) -> None: """ Validate a SingleAttestation for gossip propagation on a subnet. @@ -13504,7 +13665,7 @@ - name: validate_beacon_block_gossip#bellatrix sources: [] spec: | - + def validate_beacon_block_gossip( seen: Seen, store: Store, @@ -13512,7 +13673,7 @@ signed_beacon_block: SignedBeaconBlock, current_time_ms: uint64, # [New in Bellatrix] - block_payload_statuses: Dict[Root, PayloadValidationStatus] = {}, + block_payload_statuses: Dict[Root, PayloadValidationStatus], ) -> None: """ Validate a SignedBeaconBlock for gossip propagation. @@ -13574,11 +13735,11 @@ # [IGNORE] The block's parent's execution payload passes validation if parent_payload_status == PAYLOAD_STATUS_INVALIDATED: raise GossipIgnore("block's parent is valid and EL result is invalid") - else: - # [REJECT] The block's parent passes validation - if block.parent_root not in store.block_states: - # [Modified in Bellatrix] - raise GossipReject("block's parent is invalid and execution is not enabled") + + # [REJECT] The block's parent passes validation + elif block.parent_root not in store.block_states: + # [Modified in Bellatrix] + raise GossipReject("block's parent is invalid and execution is not enabled") # [REJECT] The block is from a higher slot than its parent if block.slot <= store.blocks[block.parent_root].slot: @@ -13606,14 +13767,14 @@ - name: validate_beacon_block_gossip#capella sources: [] spec: | - + def validate_beacon_block_gossip( seen: Seen, store: Store, state: BeaconState, signed_beacon_block: SignedBeaconBlock, current_time_ms: uint64, - block_payload_statuses: Dict[Root, PayloadValidationStatus] = {}, + block_payload_statuses: Dict[Root, PayloadValidationStatus], ) -> None: """ Validate a SignedBeaconBlock for gossip propagation. @@ -13700,14 +13861,14 @@ - name: validate_beacon_block_gossip#deneb sources: [] spec: | - + def validate_beacon_block_gossip( seen: Seen, store: Store, state: BeaconState, signed_beacon_block: SignedBeaconBlock, current_time_ms: uint64, - block_payload_statuses: Dict[Root, PayloadValidationStatus] = {}, + block_payload_statuses: Dict[Root, PayloadValidationStatus], ) -> None: """ Validate a SignedBeaconBlock for gossip propagation. @@ -13799,14 +13960,14 @@ - name: validate_beacon_block_gossip#electra sources: [] spec: | - + def validate_beacon_block_gossip( seen: Seen, store: Store, state: BeaconState, signed_beacon_block: SignedBeaconBlock, current_time_ms: uint64, - block_payload_statuses: Dict[Root, PayloadValidationStatus] = {}, + block_payload_statuses: Dict[Root, PayloadValidationStatus], ) -> None: """ Validate a SignedBeaconBlock for gossip propagation. @@ -13895,17 +14056,119 @@ seen.proposer_slots.add((block.proposer_index, block.slot)) +- name: validate_beacon_block_gossip#fulu + sources: [] + spec: | + + def validate_beacon_block_gossip( + seen: Seen, + store: Store, + state: BeaconState, + signed_beacon_block: SignedBeaconBlock, + current_time_ms: uint64, + block_payload_statuses: Optional[Dict[Root, PayloadValidationStatus]] = None, + ) -> None: + """ + Validate a SignedBeaconBlock for gossip propagation. + Raises GossipIgnore or GossipReject on validation failure. + """ + if block_payload_statuses is None: + block_payload_statuses = {} + block = signed_beacon_block.message + execution_payload = block.body.execution_payload + + # [IGNORE] The block is not from a future slot + # (MAY be queued for processing at the appropriate slot) + if not is_not_from_future_slot(state, block.slot, current_time_ms): + raise GossipIgnore("block is from a future slot") + + # [IGNORE] The block is from a slot greater than the latest finalized slot + # (MAY choose to validate and store such blocks for additional purposes + # -- e.g. slashing detection, archive nodes, etc) + finalized_slot = compute_start_slot_at_epoch(store.finalized_checkpoint.epoch) + if block.slot <= finalized_slot: + raise GossipIgnore("block is not from a slot greater than the latest finalized slot") + + # [IGNORE] The block is the first block with valid signature received for the proposer for the slot + if (block.proposer_index, block.slot) in seen.proposer_slots: + raise GossipIgnore("block is not the first valid block for this proposer and slot") + + # [REJECT] The proposer index is a valid validator index + if block.proposer_index >= len(state.validators): + raise GossipReject("proposer index out of range") + + # [REJECT] The proposer signature is valid + proposer = state.validators[block.proposer_index] + domain = get_domain(state, DOMAIN_BEACON_PROPOSER, compute_epoch_at_slot(block.slot)) + signing_root = compute_signing_root(block, domain) + if not bls.Verify(proposer.pubkey, signing_root, signed_beacon_block.signature): + raise GossipReject("invalid proposer signature") + + # [IGNORE] The block's parent has been seen (via gossip or non-gossip sources) + # (MAY be queued until parent is retrieved) + if block.parent_root not in store.blocks: + raise GossipIgnore("block's parent has not been seen") + + # [REJECT] The block's execution payload timestamp is correct with respect to the slot + if execution_payload.timestamp != compute_time_at_slot(state, block.slot): + raise GossipReject("incorrect execution payload timestamp") + + parent_payload_status = PAYLOAD_STATUS_NOT_VALIDATED + if block.parent_root in block_payload_statuses: + parent_payload_status = block_payload_statuses[block.parent_root] + + if block.parent_root not in store.block_states: + if parent_payload_status == PAYLOAD_STATUS_NOT_VALIDATED: + # [REJECT] The block's parent passes validation + raise GossipReject("block's parent is invalid and EL result is unknown") + + # [IGNORE] The block's parent passes validation + raise GossipIgnore("block's parent is invalid and EL result is known") + + # [IGNORE] The block's parent's execution payload passes validation + if parent_payload_status == PAYLOAD_STATUS_INVALIDATED: + raise GossipIgnore("block's parent is valid and EL result is invalid") + + # [REJECT] The block is from a higher slot than its parent + if block.slot <= store.blocks[block.parent_root].slot: + raise GossipReject("block is not from a higher slot than its parent") + + # [REJECT] The current finalized checkpoint is an ancestor of the block + checkpoint_block = get_checkpoint_block( + store, block.parent_root, store.finalized_checkpoint.epoch + ) + if checkpoint_block != store.finalized_checkpoint.root: + raise GossipReject("finalized checkpoint is not an ancestor of block") + + # [Modified in Fulu:EIP7892] + # [REJECT] The length of KZG commitments is less than or equal to the limit + max_blobs = get_blob_parameters(get_current_epoch(state)).max_blobs_per_block + if len(block.body.blob_kzg_commitments) > max_blobs: + raise GossipReject("too many blob kzg commitments") + + # [REJECT] The block is proposed by the expected proposer for the slot + # (if shuffling is not available, IGNORE instead and MAY be queued for later) + parent_state = store.block_states[block.parent_root].copy() + process_slots(parent_state, block.slot) + expected_proposer = get_beacon_proposer_index(parent_state) + if block.proposer_index != expected_proposer: + raise GossipReject("block proposer_index does not match expected proposer") + + # Mark this block as seen + seen.proposer_slots.add((block.proposer_index, block.slot)) + + - name: validate_blob_sidecar_gossip#deneb sources: [] spec: | - + def validate_blob_sidecar_gossip( seen: Seen, store: Store, state: BeaconState, blob_sidecar: BlobSidecar, - subnet_id: SubnetID, current_time_ms: uint64, + subnet_id: SubnetID, ) -> None: """ Validate a BlobSidecar for gossip propagation on a subnet. @@ -13993,14 +14256,14 @@ - name: validate_blob_sidecar_gossip#electra sources: [] spec: | - + def validate_blob_sidecar_gossip( seen: Seen, store: Store, state: BeaconState, blob_sidecar: BlobSidecar, - subnet_id: SubnetID, current_time_ms: uint64, + subnet_id: SubnetID, ) -> None: """ Validate a BlobSidecar for gossip propagation on a subnet. @@ -14145,12 +14408,105 @@ seen.bls_to_execution_change_indices.add(validator_index) +- name: validate_data_column_sidecar_gossip#fulu + sources: [] + spec: | + + def validate_data_column_sidecar_gossip( + seen: Seen, + store: Store, + state: BeaconState, + sidecar: DataColumnSidecar, + current_time_ms: uint64, + subnet_id: SubnetID, + ) -> None: + """ + Validate a DataColumnSidecar for gossip propagation on a subnet. + Raises GossipIgnore or GossipReject on validation failure. + """ + block_header = sidecar.signed_block_header.message + + # [IGNORE] The sidecar is the first sidecar for the tuple + # (block_header.slot, block_header.proposer_index, sidecar.index) + sidecar_tuple = (block_header.slot, block_header.proposer_index, sidecar.index) + if sidecar_tuple in seen.data_column_sidecar_tuples: + raise GossipIgnore("already seen sidecar from this proposer for this slot and index") + + # [REJECT] The sidecar is valid as verified by verify_data_column_sidecar + if not verify_data_column_sidecar(sidecar): + raise GossipReject("invalid sidecar") + + # [REJECT] The sidecar is for the correct subnet + if compute_subnet_for_data_column_sidecar(sidecar.index) != subnet_id: + raise GossipReject("sidecar is for wrong subnet") + + # [IGNORE] The sidecar is not from a future slot + # (MAY be queued for processing at the appropriate slot) + if not is_not_from_future_slot(state, block_header.slot, current_time_ms): + raise GossipIgnore("sidecar is from a future slot") + + # [IGNORE] The sidecar is from a slot greater than the latest finalized slot + finalized_slot = compute_start_slot_at_epoch(store.finalized_checkpoint.epoch) + if block_header.slot <= finalized_slot: + raise GossipIgnore("sidecar is not from a slot greater than the latest finalized slot") + + # [REJECT] The proposer index is a valid validator index + if block_header.proposer_index >= len(state.validators): + raise GossipReject("proposer index out of range") + + # [REJECT] The proposer signature of sidecar.signed_block_header is valid + proposer = state.validators[block_header.proposer_index] + domain = get_domain(state, DOMAIN_BEACON_PROPOSER, compute_epoch_at_slot(block_header.slot)) + signing_root = compute_signing_root(block_header, domain) + if not bls.Verify(proposer.pubkey, signing_root, sidecar.signed_block_header.signature): + raise GossipReject("invalid proposer signature on sidecar block header") + + # [IGNORE] The sidecar's block's parent has been seen + # (MAY be queued for processing once the parent block is retrieved) + if block_header.parent_root not in store.blocks: + raise GossipIgnore("sidecar's parent has not been seen") + + # [REJECT] The sidecar's block's parent passes validation + if block_header.parent_root not in store.block_states: + raise GossipReject("sidecar's parent failed validation") + + # [REJECT] The sidecar is from a higher slot than the sidecar's block's parent + if block_header.slot <= store.blocks[block_header.parent_root].slot: + raise GossipReject("sidecar is not from a higher slot than its parent") + + # [REJECT] The current finalized_checkpoint is an ancestor of the sidecar's block + checkpoint_block = get_checkpoint_block( + store, block_header.parent_root, store.finalized_checkpoint.epoch + ) + if checkpoint_block != store.finalized_checkpoint.root: + raise GossipReject("finalized checkpoint is not an ancestor of sidecar's block") + + # [REJECT] The sidecar is valid as verified by verify_data_column_sidecar_inclusion_proof + if not verify_data_column_sidecar_inclusion_proof(sidecar): + raise GossipReject("invalid sidecar inclusion proof") + + # [REJECT] The sidecar is valid as verified by verify_data_column_sidecar_kzg_proofs + if not verify_data_column_sidecar_kzg_proofs(sidecar): + raise GossipReject("invalid sidecar kzg proofs") + + # [REJECT] The sidecar is proposed by the expected proposer_index + # (if shuffling is not available, IGNORE instead and MAY be queued for later) + parent_state = store.block_states[block_header.parent_root].copy() + process_slots(parent_state, block_header.slot) + expected_proposer = get_beacon_proposer_index(parent_state) + if block_header.proposer_index != expected_proposer: + raise GossipReject("sidecar proposer_index does not match expected proposer") + + # Mark this data column sidecar as seen + seen.data_column_sidecar_tuples.add(sidecar_tuple) + + - name: validate_light_client_update#altair sources: - file: packages/state-transition/src/lightClient/spec/validateLightClientUpdate.ts search: export function validateLightClientUpdate( spec: | - + def validate_light_client_update( store: LightClientStore, update: LightClientUpdate, @@ -14222,7 +14578,9 @@ sync_committee = store.next_sync_committee participant_pubkeys = [ pubkey - for (bit, pubkey) in zip(sync_aggregate.sync_committee_bits, sync_committee.pubkeys) + for (bit, pubkey) in zip( + sync_aggregate.sync_committee_bits, sync_committee.pubkeys, strict=True + ) if bit ] fork_version_slot = max(update.signature_slot, Slot(1)) - Slot(1) @@ -14237,7 +14595,7 @@ - name: validate_merge_block#bellatrix sources: [] spec: | - + def validate_merge_block(block: BeaconBlock) -> None: """ Check the parent PoW block of execution payload is a valid terminal PoW block. @@ -14246,7 +14604,7 @@ and a client software MAY delay a call to ``validate_merge_block`` until the PoW block(s) become available. """ - if TERMINAL_BLOCK_HASH != Hash32(): + if TERMINAL_BLOCK_HASH != EMPTY_BLOCK_HASH: # If `TERMINAL_BLOCK_HASH` is used as an override, the activation epoch must be reached. assert compute_epoch_at_slot(block.slot) >= TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH assert block.body.execution_payload.parent_hash == TERMINAL_BLOCK_HASH @@ -14265,7 +14623,7 @@ - name: validate_merge_block#gloas sources: [] spec: | - + def validate_merge_block(block: BeaconBlock) -> None: """ Check the parent PoW block of execution payload is a valid terminal PoW block. @@ -14274,7 +14632,7 @@ and a client software MAY delay a call to ``validate_merge_block`` until the PoW block(s) become available. """ - if TERMINAL_BLOCK_HASH != Hash32(): + if TERMINAL_BLOCK_HASH != EMPTY_BLOCK_HASH: # If `TERMINAL_BLOCK_HASH` is used as an override, the activation epoch must be reached. assert compute_epoch_at_slot(block.slot) >= TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH assert block.body.execution_payload.parent_hash == TERMINAL_BLOCK_HASH @@ -14371,6 +14729,143 @@ assert get_current_slot(store) >= attestation.data.slot + 1 +- name: validate_partial_data_column_sidecar_gossip#fulu + sources: [] + spec: | + + def validate_partial_data_column_sidecar_gossip( + seen: Seen, + store: Store, + state: BeaconState, + sidecar: PartialDataColumnSidecar, + block_root: Root, + column_index: ColumnIndex, + current_time_ms: uint64, + ) -> None: + """ + Validate a PartialDataColumnSidecar for gossip propagation on a subnet. + Raises GossipIgnore or GossipReject on validation failure. + """ + has_header = len(sidecar.header) == 1 + num_cells_present = sum(1 for b in sidecar.cells_present_bitmap if b) + has_cells = num_cells_present > 0 + + # [REJECT] A header and/or cells are present in the message + if not (has_header or has_cells): + raise GossipReject("partial message is semantically empty") + + # [REJECT] The cell count equals the number of bits set in cells_present_bitmap + if len(sidecar.partial_column) != num_cells_present: + raise GossipReject("number of cells does not match number of set bits") + + # [REJECT] The proof count equals the number of bits set in cells_present_bitmap + if len(sidecar.kzg_proofs) != num_cells_present: + raise GossipReject("number of proofs does not match number of set bits") + + if has_header: + header = sidecar.header[0] + block_header = header.signed_block_header.message + + # [REJECT] The received header MUST equal any previously validated header for this block + prior_header = seen.partial_data_column_headers.get(block_root) + if prior_header is not None and prior_header != header: + raise GossipReject("header differs from previously validated header") + + # [REJECT] The signed_block_header hash matches the partial message's group id + if hash_tree_root(block_header) != block_root: + raise GossipReject("header's block root does not match partial message group id") + + # [REJECT] The header's kzg_commitments list is non-empty + if len(header.kzg_commitments) == 0: + raise GossipReject("header's kzg_commitments is empty") + + # [IGNORE] The header is not from a future slot + # (MAY be queued for processing at the appropriate slot) + if not is_not_from_future_slot(state, block_header.slot, current_time_ms): + raise GossipIgnore("header is from a future slot") + + # [IGNORE] The header is from a slot greater than the latest finalized slot + finalized_slot = compute_start_slot_at_epoch(store.finalized_checkpoint.epoch) + if block_header.slot <= finalized_slot: + raise GossipIgnore("header is not from a slot greater than the latest finalized slot") + + # [REJECT] The proposer index is a valid validator index + if block_header.proposer_index >= len(state.validators): + raise GossipReject("proposer index out of range") + + # [REJECT] The proposer signature of signed_block_header is valid + proposer = state.validators[block_header.proposer_index] + domain = get_domain(state, DOMAIN_BEACON_PROPOSER, compute_epoch_at_slot(block_header.slot)) + signing_root = compute_signing_root(block_header, domain) + if not bls.Verify(proposer.pubkey, signing_root, header.signed_block_header.signature): + raise GossipReject("invalid proposer signature on header") + + # [IGNORE] The header's block's parent has been seen + # (MAY be queued for processing once the parent block is retrieved) + if block_header.parent_root not in store.blocks: + raise GossipIgnore("header's parent has not been seen") + + # [REJECT] The header's block's parent passes validation + if block_header.parent_root not in store.block_states: + raise GossipReject("header's parent failed validation") + + # [REJECT] The header is from a higher slot than the header's block's parent + if block_header.slot <= store.blocks[block_header.parent_root].slot: + raise GossipReject("header is not from a higher slot than its parent") + + # [REJECT] The current finalized_checkpoint is an ancestor of the header's block + checkpoint_block = get_checkpoint_block( + store, block_header.parent_root, store.finalized_checkpoint.epoch + ) + if checkpoint_block != store.finalized_checkpoint.root: + raise GossipReject("finalized checkpoint is not an ancestor of header's block") + + # [REJECT] The header's kzg_commitments inclusion proof is valid + if not verify_partial_data_column_header_inclusion_proof(header): + raise GossipReject("invalid header inclusion proof") + + # [REJECT] The header is proposed by the expected proposer_index + # (if shuffling is not available, IGNORE instead and MAY be queued for later) + parent_state = store.block_states[block_header.parent_root].copy() + process_slots(parent_state, block_header.slot) + expected_proposer = get_beacon_proposer_index(parent_state) + if block_header.proposer_index != expected_proposer: + raise GossipReject("header proposer_index does not match expected proposer") + + # Mark this header as seen + seen.partial_data_column_headers[block_root] = header + + if has_cells: + # [IGNORE] A valid corresponding PartialDataColumnHeader has been seen + header = seen.partial_data_column_headers.get(block_root) + if header is None: + raise GossipIgnore("valid corresponding header has not been seen") + + block_header = header.signed_block_header.message + + # [IGNORE] The corresponding header is not from a future slot + # (MAY be queued for processing at the appropriate slot) + if not is_not_from_future_slot(state, block_header.slot, current_time_ms): + raise GossipIgnore("corresponding header is from a future slot") + + # [IGNORE] The corresponding header is from a slot greater than the latest finalized slot + finalized_slot = compute_start_slot_at_epoch(store.finalized_checkpoint.epoch) + if block_header.slot <= finalized_slot: + raise GossipIgnore( + "corresponding header is not from a slot greater than the latest finalized slot" + ) + + # [REJECT] The cells_present_bitmap length equals the number of header kzg_commitments + if len(sidecar.cells_present_bitmap) != len(header.kzg_commitments): + raise GossipReject("bitmap length does not match commitments length") + + # [REJECT] The sidecar's cell and proof data is valid + if not verify_partial_data_column_sidecar_kzg_proofs( + sidecar, header.kzg_commitments, column_index + ): + raise GossipReject("invalid sidecar kzg proofs") + + - name: validate_sync_committee_contribution_and_proof_gossip#altair sources: [] spec: | @@ -14482,13 +14977,13 @@ - name: validate_sync_committee_message_gossip#altair sources: [] spec: | - + def validate_sync_committee_message_gossip( seen: Seen, state: BeaconState, sync_committee_message: SyncCommitteeMessage, - subnet_id: uint64, current_time_ms: uint64, + subnet_id: SubnetID, ) -> None: """ Validate a SyncCommitteeMessage for gossip propagation on a subnet. @@ -14954,10 +15449,10 @@ - file: packages/utils/src/bytes/browser.ts search: export function xor( spec: | - + def xor(bytes_1: Bytes32, bytes_2: Bytes32) -> Bytes32: """ Return the exclusive-or of two 32-byte strings. """ - return Bytes32(a ^ b for a, b in zip(bytes_1, bytes_2)) + return Bytes32(a ^ b for a, b in zip(bytes_1, bytes_2, strict=True)) From 6251c12d253a76f8e0fdaaf616b2e3cdd73df77a Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Tue, 2 Jun 2026 13:10:50 +0100 Subject: [PATCH 3/5] remove ForkChoiceNode skip --- packages/beacon-node/test/spec/utils/specTestIterator.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/beacon-node/test/spec/utils/specTestIterator.ts b/packages/beacon-node/test/spec/utils/specTestIterator.ts index a5b67f998bca..50d1a69c30c4 100644 --- a/packages/beacon-node/test/spec/utils/specTestIterator.ts +++ b/packages/beacon-node/test/spec/utils/specTestIterator.ts @@ -72,7 +72,6 @@ export const defaultSkipOpts: SkipOpts = { /^electra\/light_client\/single_merkle_proof\/BeaconBlockBody.*/, /^fulu\/light_client\/single_merkle_proof\/BeaconBlockBody.*/, /^.+\/light_client\/data_collection\/.*/, - /^gloas\/ssz_static\/ForkChoiceNode.*$/, // Ignore the partial data column container additions for now. Unskip them when // cell level DAS is ready /^fulu\/ssz_static\/PartialDataColumn(Header|PartsMetadata|Sidecar)\/.*$/, From feb4e4ec7cf16a526719db689ba97a842bfaa1b7 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Tue, 2 Jun 2026 13:12:35 +0100 Subject: [PATCH 4/5] remove wrong withdrawals skip --- packages/beacon-node/test/spec/presets/fork_choice.test.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/beacon-node/test/spec/presets/fork_choice.test.ts b/packages/beacon-node/test/spec/presets/fork_choice.test.ts index 74b99efa031a..728e5044ef76 100644 --- a/packages/beacon-node/test/spec/presets/fork_choice.test.ts +++ b/packages/beacon-node/test/spec/presets/fork_choice.test.ts @@ -630,9 +630,6 @@ const forkChoiceTest = name.includes("include_votes_another_empty_chain_with_enough_ffg_votes_current_epoch") || name.includes("include_votes_another_empty_chain_with_enough_ffg_votes_previous_epoch") || name.includes("include_votes_another_empty_chain_without_enough_ffg_votes_current_epoch"))) || - // TODO GLOAS: Spec test fixture bug in v1.7.0-alpha.5: wrong_withdrawals envelope SSZ data is - // byte-for-byte identical to the valid envelope, making it impossible to reject - name.endsWith("on_execution_payload_envelope__wrong_withdrawals") || // TODO: re-enable after "apply proposer boost if dependent roots match" (consensus-specs #5306) // is implemented. New behavior in v1.7.0-alpha.9; Lodestar still applies the boost // unconditionally. Only the altair vectors exercise the changed condition (other forks pass). From c1f629bfdeae9189bb72ccbb0ce759ccc0db1e9b Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Tue, 2 Jun 2026 15:09:00 +0100 Subject: [PATCH 5/5] ignore EMPTY_BLOCK_HASH for now --- packages/beacon-node/test/e2e/api/impl/config.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/beacon-node/test/e2e/api/impl/config.test.ts b/packages/beacon-node/test/e2e/api/impl/config.test.ts index 454cdaf8d068..369f3eb138f5 100644 --- a/packages/beacon-node/test/e2e/api/impl/config.test.ts +++ b/packages/beacon-node/test/e2e/api/impl/config.test.ts @@ -13,6 +13,10 @@ const CONSTANT_NAMES_SKIP_LIST = new Set([ "PAYLOAD_STATUS_VALID", "PAYLOAD_STATUS_INVALIDATED", "PAYLOAD_STATUS_NOT_VALIDATED", + // TODO: decide whether to expose EMPTY_BLOCK_HASH in the config API. New bellatrix constant in + // v1.7.0-alpha.9; a zero Hash32 only used in the terminal-block fork-choice check, so skipped for + // now like the PAYLOAD_STATUS_* constants above. + "EMPTY_BLOCK_HASH", ]); describe("api / impl / config", () => {