diff --git a/packages/api/test/unit/beacon/testData/events.ts b/packages/api/test/unit/beacon/testData/events.ts index 81da01b8552b..990b9615130b 100644 --- a/packages/api/test/unit/beacon/testData/events.ts +++ b/packages/api/test/unit/beacon/testData/events.ts @@ -299,7 +299,7 @@ export const eventTestData: EventData = { proposal_slot: "10", validator_index: "42", fee_recipient: "0x0000000000000000000000000000000000000000", - gas_limit: "30000000", + target_gas_limit: "30000000", }, signature: "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505", diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index ecc3eac51b1c..c2eca49d9200 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -1067,7 +1067,15 @@ export function getValidatorApi( const blockIsForSlot = block.slot === slot; const payloadInput = chain.seenPayloadEnvelopeInputCache.get(block.blockRoot); - const payloadPresent = blockIsForSlot && (payloadInput?.hasPayloadEnvelope() ?? false); + // Spec: set payload_present only if the envelope was seen before get_payload_due_ms() + // into the slot. Use the envelope's own arrival time (getPayloadEnvelopeSource), not + // the input's creation time. + const payloadDueSec = config.getPayloadDueMs() / 1000; + const payloadPresent = + blockIsForSlot && + payloadInput !== undefined && + payloadInput.hasPayloadEnvelope() && + chain.clock.secFromSlot(slot, payloadInput.getPayloadEnvelopeSource().seenTimestampSec) < payloadDueSec; const blobDataAvailable = blockIsForSlot && (payloadInput?.hasAllData() ?? false); return { diff --git a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts index f335e81f2468..ec853bdb6e2b 100644 --- a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts +++ b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts @@ -18,6 +18,8 @@ import { G2_POINT_AT_INFINITY, IBeaconStateView, type IBeaconStateViewBellatrix, + type IBeaconStateViewGloas, + computeEpochAtSlot, computeTimeAtSlot, isStatePostBellatrix, isStatePostCapella, @@ -58,10 +60,12 @@ import { PayloadId, getExpectedGasLimit, } from "../../execution/index.js"; +import {getShufflingDependentRoot} from "../../util/dependentRoot.js"; import {fromGraffitiBytes} from "../../util/graffiti.js"; import {kzg} from "../../util/kzg.js"; import type {BeaconChain} from "../chain.js"; import {CommonBlockBody} from "../interface.js"; +import {ProposerPreferencesPool} from "../opPools/index.js"; import {validateBlobsAndKzgCommitments, validateCellsAndKzgCommitments} from "./validateBlobsAndKzgCommitments.js"; // Time to provide the EL to generate a payload from new payload id @@ -204,6 +208,9 @@ export async function produceBlockBody( // this into a completely separate function and have pre/post gloas more separated const safeBlockHash = getSafeExecutionBlockHash(this.forkChoice); const finalizedBlockHash = this.forkChoice.getFinalizedBlock().executionPayloadBlockHash ?? ZERO_HASH_HEX; + // TODO GLOAS: post-Gloas, proposer feeRecipient is also carried (signed) in + // ProposerPreferencesPool. Consider using this unified cache instead + // see https://github.com/ChainSafe/lodestar/issues/9379 const feeRecipient = requestedFeeRecipient ?? this.beaconProposerCache.getOrDefault(proposerIndex); const endExecutionPayload = this.metrics?.executionBlockProductionTimeSteps.startTimer(); @@ -633,6 +640,8 @@ export async function prepareExecutionPayload( chain: { executionEngine: IExecutionEngine; config: ChainForkConfig; + forkChoice: IForkChoice; + proposerPreferencesPool: ProposerPreferencesPool; }, logger: Logger, fork: ForkPostBellatrix, @@ -733,6 +742,7 @@ export function getPayloadAttributesForSSE( chain: { config: ChainForkConfig; forkChoice: IForkChoice; + proposerPreferencesPool: ProposerPreferencesPool; }, { prepareState, @@ -789,6 +799,8 @@ function preparePayloadAttributes( fork: ForkPostBellatrix, chain: { config: ChainForkConfig; + forkChoice: IForkChoice; + proposerPreferencesPool: ProposerPreferencesPool; }, { prepareState, @@ -851,12 +863,59 @@ function preparePayloadAttributes( } if (ForkSeq[fork] >= ForkSeq.gloas) { + if (!isStatePostGloas(prepareState)) { + throw new Error("Expected Gloas state for Gloas payload attributes"); + } (payloadAttributes as gloas.SSEPayloadAttributes["payloadAttributes"]).slotNumber = prepareSlot; + (payloadAttributes as gloas.SSEPayloadAttributes["payloadAttributes"]).targetGasLimit = getProposerTargetGasLimit( + chain, + prepareState, + prepareSlot, + parentBlockRoot + ); } return payloadAttributes; } +/** + * Resolve the proposer's preferred (target) gas limit for the Gloas `PayloadAttributesV4` + * `targetGasLimit` field (consensus-specs#5235, execution-apis#796). + * + * Sourced from the `SignedProposerPreferences` the proposer's VC submitted to the pool + * (same `(slot, dependent_root)` lookup as gossip bid validation). When no matching + * preferences are pooled, target the parent payload's gas limit so the gas limit stays + * unchanged (`is_gas_limit_target_compatible` then requires `gas_limit == parent_gas_limit`). + */ +function getProposerTargetGasLimit( + chain: {forkChoice: IForkChoice; proposerPreferencesPool: ProposerPreferencesPool}, + state: IBeaconStateViewGloas, + prepareSlot: Slot, + parentBlockRoot: Root +): number { + const parentBlock = chain.forkChoice.getBlockHexDefaultStatus(toRootHex(parentBlockRoot)); + const dependentRootHex = (() => { + if (parentBlock === null) { + return null; + } + try { + return getShufflingDependentRoot( + chain.forkChoice, + computeEpochAtSlot(prepareSlot), + computeEpochAtSlot(parentBlock.slot), + parentBlock + ); + } catch { + return null; + } + })(); + + const pref = dependentRootHex !== null ? chain.proposerPreferencesPool.get(prepareSlot, dependentRootHex) : null; + // TODO GLOAS: state.latestExecutionPayloadBid is the latest *bid*, not the latest *executed* + // payload — for EMPTY parents this drifts. Consider having a default value like Prysm's DefaultBuilderGasLimit. + return Number(pref ? pref.message.targetGasLimit : state.latestExecutionPayloadBid.gasLimit); +} + export async function produceCommonBlockBody( this: BeaconChain, blockType: T, diff --git a/packages/beacon-node/src/chain/validation/executionPayloadBid.ts b/packages/beacon-node/src/chain/validation/executionPayloadBid.ts index 9e293cf2adaa..c3f1a1e009c1 100644 --- a/packages/beacon-node/src/chain/validation/executionPayloadBid.ts +++ b/packages/beacon-node/src/chain/validation/executionPayloadBid.ts @@ -128,14 +128,14 @@ async function validateExecutionPayloadBid( }); } - // [REJECT] `bid.gas_limit == proposer_preferences.gas_limit`. + // [REJECT] `bid.gas_limit == proposer_preferences.target_gas_limit`. const bidGasLimit = Number(bid.gasLimit); - if (bidGasLimit !== proposerPreferences.message.gasLimit) { + if (bidGasLimit !== proposerPreferences.message.targetGasLimit) { throw new ExecutionPayloadBidError(GossipAction.REJECT, { code: ExecutionPayloadBidErrorCode.PROPOSER_PREFERENCES_GAS_LIMIT_MISMATCH, builderIndex: bid.builderIndex, bidGasLimit, - expectedGasLimit: proposerPreferences.message.gasLimit, + expectedGasLimit: proposerPreferences.message.targetGasLimit, }); } diff --git a/packages/beacon-node/src/execution/engine/interface.ts b/packages/beacon-node/src/execution/engine/interface.ts index c8b7cdba6816..d5aeabacdcd9 100644 --- a/packages/beacon-node/src/execution/engine/interface.ts +++ b/packages/beacon-node/src/execution/engine/interface.ts @@ -88,6 +88,7 @@ export type PayloadAttributes = { withdrawals?: capella.Withdrawal[]; parentBeaconBlockRoot?: Uint8Array; slotNumber?: number; // EIP-7843 + targetGasLimit?: number; // GLOAS (PayloadAttributesV4, execution-apis#796) }; export type VersionedHashes = Uint8Array[]; diff --git a/packages/beacon-node/src/execution/engine/types.ts b/packages/beacon-node/src/execution/engine/types.ts index 36e6737f1e34..896a9766cbb2 100644 --- a/packages/beacon-node/src/execution/engine/types.ts +++ b/packages/beacon-node/src/execution/engine/types.ts @@ -245,6 +245,8 @@ export type PayloadAttributesRpc = { parentBeaconBlockRoot?: DATA; /** QUANTITY, 64 Bits - value for the slot number field of the new payload (EIP-7843) */ slotNumber?: QUANTITY; + /** QUANTITY, 64 Bits - target value for the gasLimit field of the new payload (GLOAS, execution-apis#796) */ + targetGasLimit?: QUANTITY; }; export type ClientVersionRpc = { @@ -425,6 +427,7 @@ export function serializePayloadAttributes(data: PayloadAttributes): PayloadAttr withdrawals: data.withdrawals?.map(serializeWithdrawal), parentBeaconBlockRoot: data.parentBeaconBlockRoot ? bytesToData(data.parentBeaconBlockRoot) : undefined, slotNumber: data.slotNumber !== undefined ? numToQuantity(data.slotNumber) : undefined, + targetGasLimit: data.targetGasLimit !== undefined ? numToQuantity(data.targetGasLimit) : undefined, }; } @@ -442,6 +445,7 @@ export function deserializePayloadAttributes(data: PayloadAttributesRpc): Payloa withdrawals: data.withdrawals?.map((withdrawal) => deserializeWithdrawal(withdrawal)), parentBeaconBlockRoot: data.parentBeaconBlockRoot ? dataToBytes(data.parentBeaconBlockRoot, 32) : undefined, slotNumber: data.slotNumber !== undefined ? quantityToNum(data.slotNumber) : undefined, + targetGasLimit: data.targetGasLimit !== undefined ? quantityToNum(data.targetGasLimit) : undefined, }; } diff --git a/packages/beacon-node/test/spec/utils/specTestIterator.ts b/packages/beacon-node/test/spec/utils/specTestIterator.ts index fc3cb7a48e1f..c0ee63af2bcb 100644 --- a/packages/beacon-node/test/spec/utils/specTestIterator.ts +++ b/packages/beacon-node/test/spec/utils/specTestIterator.ts @@ -80,13 +80,14 @@ export const defaultSkipOpts: SkipOpts = { // TODO-GLOAS: re-enable after Gloas light client is implemented /^gloas\/light_client\/.*/, /^gloas\/ssz_static\/LightClient(Bootstrap|FinalityUpdate|Header|OptimisticUpdate|Update)\/.*/, + // TODO-GLOAS: re-enable after on_payload_attestation_message (PTC) fork choice is implemented. + // 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\/.*$/, ], skippedTests: [ // TODO-GLOAS: re-enable after gloas light client is implemented /\/gloas_fork$/, - // TODOGLOAS: re-enable after upgrading to v1.7.0-alpha.8 - // (which includes #5254). https://github.com/ethereum/consensus-specs/pull/5254 - /^gloas\/fork\/fork\/pyspec_tests\/fork_invalid_validator_deposit_followed_by_builder_credentials$/, ], // TODO GLOAS: Investigate why networking tests are failing since alpha.5 skippedRunners: ["fast_confirmation", "networking"], diff --git a/packages/beacon-node/test/unit/api/impl/validator/produceAttestationData.test.ts b/packages/beacon-node/test/unit/api/impl/validator/produceAttestationData.test.ts index 85602c877d9d..791dddd868df 100644 --- a/packages/beacon-node/test/unit/api/impl/validator/produceAttestationData.test.ts +++ b/packages/beacon-node/test/unit/api/impl/validator/produceAttestationData.test.ts @@ -4,7 +4,10 @@ import {ProtoBlock} from "@lodestar/fork-choice"; import {toRootHex} from "@lodestar/utils"; import {getValidatorApi} from "../../../../../src/api/impl/validator/index.js"; import {defaultApiOptions} from "../../../../../src/api/options.js"; -import {PayloadEnvelopeInput} from "../../../../../src/chain/blocks/payloadEnvelopeInput/index.js"; +import { + PayloadEnvelopeInput, + PayloadEnvelopeInputSource, +} from "../../../../../src/chain/blocks/payloadEnvelopeInput/index.js"; import {ZERO_HASH_HEX} from "../../../../../src/constants/index.js"; import {SyncState} from "../../../../../src/sync/interface.js"; import {ApiTestModules, getApiTestModules} from "../../../../utils/api.js"; @@ -66,8 +69,10 @@ describe("api - validator - produceAttestationData", () => { slot: 0, blockRoot: ZERO_HASH_HEX, } as ProtoBlock); + vi.mocked(modules.chain.clock.secFromSlot).mockReturnValue(0); vi.mocked(modules.chain.seenPayloadEnvelopeInputCache.get).mockReturnValue({ hasPayloadEnvelope: () => true, + getPayloadEnvelopeSource: () => ({source: PayloadEnvelopeInputSource.gossip, seenTimestampSec: 0}), hasAllData: () => true, } as PayloadEnvelopeInput); diff --git a/packages/beacon-node/test/unit/chain/opPools/proposerPreferencesPool.test.ts b/packages/beacon-node/test/unit/chain/opPools/proposerPreferencesPool.test.ts index ca3ebc92feb1..1b2c4a6728f0 100644 --- a/packages/beacon-node/test/unit/chain/opPools/proposerPreferencesPool.test.ts +++ b/packages/beacon-node/test/unit/chain/opPools/proposerPreferencesPool.test.ts @@ -14,7 +14,7 @@ describe("chain / opPools / ProposerPreferencesPool", () => { proposalSlot, validatorIndex, feeRecipient: Buffer.alloc(20, 0xab), - gasLimit: 30_000_000, + targetGasLimit: 30_000_000, }, signature: Buffer.alloc(96, 0), }); diff --git a/packages/config/src/chainConfig/configs/mainnet.ts b/packages/config/src/chainConfig/configs/mainnet.ts index 547ce201358a..7788121f4881 100644 --- a/packages/config/src/chainConfig/configs/mainnet.ts +++ b/packages/config/src/chainConfig/configs/mainnet.ts @@ -70,8 +70,8 @@ export const chainConfig: ChainConfig = { SECONDS_PER_ETH1_BLOCK: 14, // 2**8 (= 256) epochs ~27 hours MIN_VALIDATOR_WITHDRAWABILITY_DELAY: 256, - // 2**6 (= 64) epochs - MIN_BUILDER_WITHDRAWABILITY_DELAY: 64, + // 2**13 (= 8,192) epochs ~36 days + MIN_BUILDER_WITHDRAWABILITY_DELAY: 8192, // 2**8 (= 256) epochs ~27 hours SHARD_COMMITTEE_PERIOD: 256, // 2**11 (= 2,048) Eth1 blocks ~8 hours @@ -99,6 +99,8 @@ export const chainConfig: ChainConfig = { CONTRIBUTION_DUE_BPS_GLOAS: 5000, // 75% of SLOT_DURATION_MS PAYLOAD_ATTESTATION_DUE_BPS: 7500, + // 75% of SLOT_DURATION_MS + PAYLOAD_DUE_BPS: 7500, // Validator cycle // --------------------------------------------------------------- diff --git a/packages/config/src/chainConfig/configs/minimal.ts b/packages/config/src/chainConfig/configs/minimal.ts index da2d9d99bef6..6624eaccd802 100644 --- a/packages/config/src/chainConfig/configs/minimal.ts +++ b/packages/config/src/chainConfig/configs/minimal.ts @@ -93,6 +93,8 @@ export const chainConfig: ChainConfig = { CONTRIBUTION_DUE_BPS_GLOAS: 5000, // 75% of SLOT_DURATION_MS PAYLOAD_ATTESTATION_DUE_BPS: 7500, + // 75% of SLOT_DURATION_MS + PAYLOAD_DUE_BPS: 7500, // Validator cycle // --------------------------------------------------------------- diff --git a/packages/config/src/chainConfig/types.ts b/packages/config/src/chainConfig/types.ts index 6c986ce7f72f..9066a2afc4f2 100644 --- a/packages/config/src/chainConfig/types.ts +++ b/packages/config/src/chainConfig/types.ts @@ -72,6 +72,7 @@ export type ChainConfig = { SYNC_MESSAGE_DUE_BPS_GLOAS: number; CONTRIBUTION_DUE_BPS_GLOAS: number; PAYLOAD_ATTESTATION_DUE_BPS: number; + PAYLOAD_DUE_BPS: number; // Validator cycle INACTIVITY_SCORE_BIAS: number; @@ -191,6 +192,7 @@ export const chainConfigTypes: SpecTypes = { SYNC_MESSAGE_DUE_BPS_GLOAS: "number", CONTRIBUTION_DUE_BPS_GLOAS: "number", PAYLOAD_ATTESTATION_DUE_BPS: "number", + PAYLOAD_DUE_BPS: "number", // Validator cycle INACTIVITY_SCORE_BIAS: "number", diff --git a/packages/config/src/forkConfig/index.ts b/packages/config/src/forkConfig/index.ts index 3f45a2895cdd..47bd518a3f55 100644 --- a/packages/config/src/forkConfig/index.ts +++ b/packages/config/src/forkConfig/index.ts @@ -228,6 +228,9 @@ export function createForkConfig(config: ChainConfig): ForkConfig { getProposerReorgCutoffMs(_fork: ForkName): number { return this.getSlotComponentDurationMs(config.PROPOSER_REORG_CUTOFF_BPS); }, + getPayloadDueMs(): number { + return this.getSlotComponentDurationMs(config.PAYLOAD_DUE_BPS); + }, getSlotComponentDurationMs(basisPoints: number): number { return Math.round((basisPoints * config.SLOT_DURATION_MS) / BASIS_POINTS); diff --git a/packages/config/src/forkConfig/types.ts b/packages/config/src/forkConfig/types.ts index 9bdc7f16d8d3..841e0212ddc9 100644 --- a/packages/config/src/forkConfig/types.ts +++ b/packages/config/src/forkConfig/types.ts @@ -62,6 +62,7 @@ export type ForkConfig = { getSyncMessageDueMs(fork: ForkName): number; getSyncContributionDueMs(fork: ForkName): number; getProposerReorgCutoffMs(fork: ForkName): number; + getPayloadDueMs(): number; /** Convert basis points to milliseconds into the slot */ getSlotComponentDurationMs(basisPoints: number): number; diff --git a/packages/state-transition/src/slot/upgradeStateToGloas.ts b/packages/state-transition/src/slot/upgradeStateToGloas.ts index 6b76bb7b0e14..786f2b76a8e0 100644 --- a/packages/state-transition/src/slot/upgradeStateToGloas.ts +++ b/packages/state-transition/src/slot/upgradeStateToGloas.ts @@ -48,6 +48,7 @@ export function upgradeStateToGloas(stateFulu: CachedBeaconStateFulu): CachedBea stateGloasView.currentSyncCommittee = stateGloasCloned.currentSyncCommittee; stateGloasView.nextSyncCommittee = stateGloasCloned.nextSyncCommittee; stateGloasView.latestExecutionPayloadBid.blockHash = stateFulu.latestExecutionPayloadHeader.blockHash; + stateGloasView.latestExecutionPayloadBid.gasLimit = BigInt(stateFulu.latestExecutionPayloadHeader.gasLimit); stateGloasView.latestExecutionPayloadBid.executionRequestsRoot = ssz.electra.ExecutionRequests.hashTreeRoot( ssz.electra.ExecutionRequests.defaultValue() ); @@ -86,7 +87,7 @@ export function upgradeStateToGloas(stateFulu: CachedBeaconStateFulu): CachedBea /** * Applies any pending deposits for builders to onboard builders during the fork transition - * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.2/specs/gloas/fork.md#new-onboard_builders_from_pending_deposits + * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.8/specs/gloas/fork.md#new-onboard_builders_from_pending_deposits */ function onboardBuildersFromPendingDeposits(state: CachedBeaconStateGloas): void { // Track pubkeys of new builders added when applying deposits diff --git a/packages/types/src/gloas/sszTypes.ts b/packages/types/src/gloas/sszTypes.ts index d0ddc160fe4b..f6661f25aa43 100644 --- a/packages/types/src/gloas/sszTypes.ts +++ b/packages/types/src/gloas/sszTypes.ts @@ -126,7 +126,7 @@ export const ProposerPreferences = new ContainerType( proposalSlot: Slot, validatorIndex: ValidatorIndex, feeRecipient: ExecutionAddress, - gasLimit: UintNum64, + targetGasLimit: UintNum64, }, {typeName: "ProposerPreferences", jsonCase: "eth2"} ); @@ -324,6 +324,7 @@ export const PayloadAttributes = new ContainerType( { ...denebSsz.PayloadAttributes.fields, slotNumber: Slot, + targetGasLimit: UintNum64, }, {typeName: "PayloadAttributes", jsonCase: "eth2"} ); diff --git a/packages/validator/src/services/validatorStore.ts b/packages/validator/src/services/validatorStore.ts index c7fd4108cd92..e37c2e5047e5 100644 --- a/packages/validator/src/services/validatorStore.ts +++ b/packages/validator/src/services/validatorStore.ts @@ -723,7 +723,7 @@ export class ValidatorStore { proposalSlot: duty.slot, validatorIndex: duty.validatorIndex, feeRecipient: fromHex(feeRecipient), - gasLimit, + targetGasLimit: gasLimit, }; const signingSlot = duty.slot; diff --git a/packages/validator/src/util/params.ts b/packages/validator/src/util/params.ts index 57d928a9617c..072ce8265be1 100644 --- a/packages/validator/src/util/params.ts +++ b/packages/validator/src/util/params.ts @@ -321,6 +321,7 @@ function getSpecCriticalParams(localConfig: ChainConfig): Record - MIN_BUILDER_WITHDRAWABILITY_DELAY: uint64 = 64 + + MIN_BUILDER_WITHDRAWABILITY_DELAY: uint64 = 8192 - name: MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS#deneb @@ -601,6 +601,15 @@ PAYLOAD_ATTESTATION_DUE_BPS: uint64 = 7500 +- name: PAYLOAD_DUE_BPS#gloas + sources: + - file: packages/config/src/chainConfig/configs/mainnet.ts + search: "PAYLOAD_DUE_BPS:" + spec: | + + PAYLOAD_DUE_BPS: uint64 = 7500 + + - name: PROPOSER_REORG_CUTOFF_BPS#phase0 sources: - file: packages/config/src/chainConfig/configs/mainnet.ts diff --git a/specrefs/containers.yml b/specrefs/containers.yml index 150d77bc4cc5..79ec57900654 100644 --- a/specrefs/containers.yml +++ b/specrefs/containers.yml @@ -1567,13 +1567,13 @@ - file: packages/types/src/gloas/sszTypes.ts search: export const ProposerPreferences = spec: | - + class ProposerPreferences(Container): dependent_root: Root proposal_slot: Slot validator_index: ValidatorIndex fee_recipient: ExecutionAddress - gas_limit: uint64 + target_gas_limit: uint64 - name: ProposerSlashing#phase0 diff --git a/specrefs/dataclasses.yml b/specrefs/dataclasses.yml index 8fb8cb1d837c..2e91e918d831 100644 --- a/specrefs/dataclasses.yml +++ b/specrefs/dataclasses.yml @@ -330,7 +330,7 @@ - file: packages/types/src/gloas/sszTypes.ts search: export const PayloadAttributes = spec: | - + class PayloadAttributes(object): timestamp: uint64 prev_randao: Bytes32 @@ -339,12 +339,14 @@ parent_beacon_block_root: Root # [New in Gloas:EIP7843] slot_number: uint64 + # [New in Gloas] + target_gas_limit: uint64 - name: PayloadAttributes#heze sources: [] spec: | - + class PayloadAttributes(object): timestamp: uint64 prev_randao: Bytes32 @@ -352,6 +354,7 @@ withdrawals: Sequence[Withdrawal] parent_beacon_block_root: Root slot_number: uint64 + target_gas_limit: uint64 # [New in Heze:EIP7805] inclusion_list_transactions: Sequence[Transaction] @@ -395,6 +398,46 @@ bls_to_execution_change_indices: Set[ValidatorIndex] +- name: Seen#deneb + sources: [] + spec: | + + class Seen(object): + proposer_slots: Set[Tuple[ValidatorIndex, Slot]] + aggregator_epochs: Set[Tuple[ValidatorIndex, Epoch]] + aggregate_data_roots: Dict[Root, 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] + # [New in Deneb] + blob_sidecar_tuples: Set[Tuple[Slot, ValidatorIndex, BlobIndex]] + + +- name: Seen#electra + sources: [] + spec: | + + class Seen(object): + proposer_slots: Set[Tuple[ValidatorIndex, Slot]] + aggregator_epochs: Set[Tuple[ValidatorIndex, Epoch]] + # [Modified in Electra:EIP7549] + 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] + blob_sidecar_tuples: Set[Tuple[Slot, ValidatorIndex, BlobIndex]] + + - name: Store#phase0 sources: - file: packages/fork-choice/src/forkChoice/store.ts @@ -421,7 +464,7 @@ - name: Store#gloas sources: [] spec: | - + class Store(object): time: uint64 genesis_time: uint64 @@ -433,20 +476,17 @@ equivocating_indices: Set[ValidatorIndex] blocks: Dict[Root, BeaconBlock] = field(default_factory=dict) block_states: Dict[Root, BeaconState] = field(default_factory=dict) - block_timeliness: Dict[Root, Vector[boolean, NUM_BLOCK_TIMELINESS_DEADLINES]] = field( - default_factory=dict - ) + # [Modified in Gloas:EIP7732] + block_timeliness: Dict[Root, list[boolean]] = field(default_factory=dict) checkpoint_states: Dict[Checkpoint, BeaconState] = field(default_factory=dict) latest_messages: Dict[ValidatorIndex, LatestMessage] = field(default_factory=dict) unrealized_justifications: Dict[Root, Checkpoint] = field(default_factory=dict) # [New in Gloas:EIP7732] payloads: Dict[Root, ExecutionPayloadEnvelope] = field(default_factory=dict) # [New in Gloas:EIP7732] - payload_timeliness_vote: Dict[Root, Vector[Optional[boolean], PTC_SIZE]] = field( - default_factory=dict - ) + payload_timeliness_vote: Dict[Root, list[Optional[boolean]]] = field(default_factory=dict) # [New in Gloas:EIP7732] - payload_data_availability_vote: Dict[Root, Vector[Optional[boolean], PTC_SIZE]] = field( + payload_data_availability_vote: Dict[Root, list[Optional[boolean]]] = field( default_factory=dict ) @@ -454,7 +494,7 @@ - name: Store#heze sources: [] spec: | - + class Store(object): time: uint64 genesis_time: uint64 @@ -466,15 +506,13 @@ equivocating_indices: Set[ValidatorIndex] blocks: Dict[Root, BeaconBlock] = field(default_factory=dict) block_states: Dict[Root, BeaconState] = field(default_factory=dict) - block_timeliness: Dict[Root, Vector[boolean, NUM_BLOCK_TIMELINESS_DEADLINES]] = field( - default_factory=dict - ) + block_timeliness: Dict[Root, list[boolean]] = field(default_factory=dict) checkpoint_states: Dict[Checkpoint, BeaconState] = field(default_factory=dict) latest_messages: Dict[ValidatorIndex, LatestMessage] = field(default_factory=dict) unrealized_justifications: Dict[Root, Checkpoint] = field(default_factory=dict) payloads: Dict[Root, ExecutionPayloadEnvelope] = field(default_factory=dict) - payload_timeliness_vote: Dict[Root, Vector[boolean, PTC_SIZE]] = field(default_factory=dict) - payload_data_availability_vote: Dict[Root, Vector[boolean, PTC_SIZE]] = field( + payload_timeliness_vote: Dict[Root, list[Optional[boolean]]] = field(default_factory=dict) + payload_data_availability_vote: Dict[Root, list[Optional[boolean]]] = field( default_factory=dict ) # [New in Heze:EIP7805] diff --git a/specrefs/functions.yml b/specrefs/functions.yml index 0dd71919dfba..9ea25245a989 100644 --- a/specrefs/functions.yml +++ b/specrefs/functions.yml @@ -596,7 +596,7 @@ - name: compute_balance_weighted_selection#gloas sources: [] spec: | - + def compute_balance_weighted_selection( state: BeaconState, indices: Sequence[ValidatorIndex], @@ -608,7 +608,7 @@ Return ``size`` indices sampled by effective balance, using ``indices`` as candidates. If ``shuffle_indices`` is ``True``, candidate indices are themselves sampled from ``indices`` by shuffling it, otherwise - ``indices`` is traversed in order. + ``indices`` is traversed in order. The returned list can contain duplicates. """ MAX_RANDOM_VALUE = 2**16 - 1 total = uint64(len(indices)) @@ -1363,10 +1363,10 @@ - file: packages/state-transition/src/util/seed.ts search: export function computePayloadTimelinessCommitteeForSlot( spec: | - + def compute_ptc(state: BeaconState, slot: Slot) -> Vector[ValidatorIndex, PTC_SIZE]: """ - Get the payload timeliness committee for the given ``slot``. + Get the payload timeliness committee, with possible duplicates, for the given ``slot``. """ epoch = compute_epoch_at_slot(slot) seed = hash(get_seed(state, epoch, DOMAIN_PTC_ATTESTER) + uint_to_bytes(slot)) @@ -4921,6 +4921,16 @@ return bls.Sign(privkey, signing_root) +- name: get_payload_due_ms#gloas + sources: + - file: packages/config/src/forkConfig/index.ts + search: "getPayloadDueMs(): number {" + spec: | + + def get_payload_due_ms() -> uint64: + return get_slot_component_duration_ms(PAYLOAD_DUE_BPS) + + - name: get_payload_status_tiebreaker#gloas sources: [] spec: | @@ -5078,12 +5088,14 @@ - name: get_proposer_dependent_root#gloas sources: [] spec: | - + def get_proposer_dependent_root(state: BeaconState, epoch: Epoch) -> Root: """ Return the dependent root for the proposer lookahead at ``epoch``. """ - return get_block_root_at_slot(state, Slot(compute_start_slot_at_epoch(Epoch(epoch - 1)) - 1)) + return get_block_root_at_slot( + state, Slot(compute_start_slot_at_epoch(Epoch(epoch - MIN_SEED_LOOKAHEAD)) - 1) + ) - name: get_proposer_head#phase0 @@ -5564,13 +5576,13 @@ - name: get_upcoming_proposal_slots#gloas sources: [] spec: | - + def get_upcoming_proposal_slots( state: BeaconState, validator_index: ValidatorIndex ) -> Sequence[Slot]: """ - Get the future slots in the current epoch and the slots in the next - epoch for which ``validator_index`` is proposing. + Get the future slots within the proposer lookahead for which + ``validator_index`` is proposing. """ current_epoch_start_slot = compute_start_slot_at_epoch(get_current_epoch(state)) upcoming_proposal_slots = [] @@ -6632,6 +6644,28 @@ ) +- name: is_gas_limit_target_compatible#gloas + sources: [] + spec: | + + def is_gas_limit_target_compatible( + parent_gas_limit: uint64, gas_limit: uint64, target_gas_limit: uint64 + ) -> bool: + """ + Check if ``gas_limit`` is compatible with ``target_gas_limit`` under the + EIP-1559 transition rule from ``parent_gas_limit``. + """ + max_gas_limit_difference = max(parent_gas_limit // 1024, 1) - 1 + min_gas_limit = parent_gas_limit - max_gas_limit_difference + max_gas_limit = parent_gas_limit + max_gas_limit_difference + + if target_gas_limit >= min_gas_limit and target_gas_limit <= max_gas_limit: + return gas_limit == target_gas_limit + if target_gas_limit > max_gas_limit: + return gas_limit == max_gas_limit + return gas_limit == min_gas_limit + + - name: is_head_late#phase0 sources: - file: packages/fork-choice/src/forkChoice/forkChoice.ts @@ -6886,27 +6920,6 @@ ) -- name: is_payload_data_available#gloas - sources: [] - spec: | - - def is_payload_data_available(store: Store, root: Root) -> bool: - """ - Return whether the blob data for the beacon block with root ``root`` - was voted as present by the PTC, and was locally determined to be available. - """ - # The beacon block root must be known - assert root in store.payload_data_availability_vote - - # If the payload is not locally available, the blob data - # is not considered available regardless of the PTC vote - if not is_payload_verified(store, root): - return False - - votes = store.payload_data_availability_vote[root] - return sum(vote is True for vote in votes) > DATA_AVAILABILITY_TIMELY_THRESHOLD - - - name: is_payload_inclusion_list_satisfied#heze sources: [] spec: | @@ -6927,27 +6940,6 @@ return store.payload_inclusion_list_satisfaction[root] -- name: is_payload_timely#gloas - sources: [] - spec: | - - def is_payload_timely(store: Store, root: Root) -> bool: - """ - Return whether the execution payload for the beacon block with root ``root`` - was voted as present by the PTC, and was locally determined to be available. - """ - # The beacon block root must be known - assert root in store.payload_timeliness_vote - - # If the payload is not locally available, the payload - # is not considered available regardless of the PTC vote - if not is_payload_verified(store, root): - return False - - votes = store.payload_timeliness_vote[root] - return sum(vote is True for vote in votes) > PAYLOAD_TIMELY_THRESHOLD - - - name: is_payload_verified#gloas sources: [] spec: | @@ -6966,12 +6958,12 @@ - file: packages/state-transition/src/util/pendingDepositsLookup.ts search: "hasPendingValidator(config: BeaconConfig, pubkeyHex: PubkeyHex): boolean {" spec: | - - def is_pending_validator(state: BeaconState, pubkey: BLSPubkey) -> bool: + + def is_pending_validator(pending_deposits: Sequence[PendingDeposit], pubkey: BLSPubkey) -> bool: """ Check if a pending deposit with a valid signature is in the queue for the given pubkey. """ - for pending_deposit in state.pending_deposits: + for pending_deposit in pending_deposits: if pending_deposit.pubkey != pubkey: continue if is_valid_deposit_signature( @@ -7370,17 +7362,17 @@ - name: is_valid_proposal_slot#gloas sources: [] spec: | - + def is_valid_proposal_slot(state: BeaconState, preferences: ProposerPreferences) -> bool: """ - Check if the validator is the proposer for the given slot in the current or - next epoch. + Check if the validator is the proposer for the given slot within the + proposer lookahead. """ current_epoch = get_current_epoch(state) proposal_epoch = compute_epoch_at_slot(preferences.proposal_slot) if proposal_epoch < current_epoch: return False - if proposal_epoch > current_epoch + Epoch(1): + if proposal_epoch > current_epoch + Epoch(MIN_SEED_LOOKAHEAD): return False index = (proposal_epoch - current_epoch) * SLOTS_PER_EPOCH @@ -8041,7 +8033,7 @@ - name: on_payload_attestation_message#gloas sources: [] spec: | - + def on_payload_attestation_message( store: Store, ptc_message: PayloadAttestationMessage, is_from_block: bool = False ) -> None: @@ -8049,17 +8041,25 @@ Run ``on_payload_attestation_message`` upon receiving a new ``ptc_message`` from either within a block or directly on the wire. """ - # The beacon block root must be known data = ptc_message.data + # PTC attestation must be for a known block. If block is unknown, delay consideration until the block is found assert data.beacon_block_root in store.block_states state = store.block_states[data.beacon_block_root] - ptc = get_ptc(state, data.slot) + # PTC votes can only change the vote for their assigned beacon block, return early otherwise if data.slot != state.slot: return + + # Get all positions of the attester in the PTC + ptc_indices = [] + ptc = get_ptc(state, data.slot) + for ptc_index, validator_index in enumerate(ptc): + if validator_index == ptc_message.validator_index: + ptc_indices.append(ptc_index) + # Check that the attester is from the PTC - assert ptc_message.validator_index in ptc + assert len(ptc_indices) > 0 # Verify the signature and check that its for the current slot if it is coming from the wire if not is_from_block: @@ -8074,12 +8074,13 @@ signature=ptc_message.signature, ), ) + # Update the votes for the block - ptc_index = ptc.index(ptc_message.validator_index) payload_timeliness_vote = store.payload_timeliness_vote[data.beacon_block_root] - payload_timeliness_vote[ptc_index] = data.payload_present payload_data_availability_vote = store.payload_data_availability_vote[data.beacon_block_root] - payload_data_availability_vote[ptc_index] = data.blob_data_available + for ptc_index in ptc_indices: + payload_timeliness_vote[ptc_index] = data.payload_present + payload_data_availability_vote[ptc_index] = data.blob_data_available - name: on_tick#phase0 @@ -8132,7 +8133,7 @@ - file: packages/state-transition/src/slot/upgradeStateToGloas.ts search: function onboardBuildersFromPendingDeposits( spec: | - + def onboard_builders_from_pending_deposits(state: BeaconState) -> None: """ Applies any pending deposit for builders, effectively @@ -8142,45 +8143,83 @@ pending_deposits = [] for deposit in state.pending_deposits: - # Deposits for existing validators stay in pending queue + # Deposits for existing validators stay in the pending queue if deposit.pubkey in validator_pubkeys: pending_deposits.append(deposit) continue - # If the pubkey is associated with a builder that was created in a - # previous iteration or it is a builder deposit, try to apply the - # deposit to the new/existing builder. Note that the function - # apply_deposit_for_builder can mutate the state and may add a builder - # to the registry. For this reason, the list of builder pubkeys must - # be recomputed each iteration. + # Note that the function apply_deposit_for_builder can mutate the + # state and may add a builder to the registry. For this reason, the + # list of builder pubkeys must be recomputed each iteration. builder_pubkeys = [b.pubkey for b in state.builders] - is_existing_builder = deposit.pubkey in builder_pubkeys - has_builder_credentials = is_builder_withdrawal_credential(deposit.withdrawal_credentials) - if is_existing_builder or has_builder_credentials: - apply_deposit_for_builder( - state, - deposit.pubkey, - deposit.withdrawal_credentials, - deposit.amount, - deposit.signature, - deposit.slot, - ) - continue - # If there is a pending deposit for a new validator that has a valid - # signature, track the pubkey so that subsequent builder deposits for - # the same pubkey stay in pending (applied to the validator later) - # rather than creating a builder. Deposits with invalid signatures are - # dropped here since they would fail in apply_pending_deposit anyway. - if is_valid_deposit_signature( - deposit.pubkey, deposit.withdrawal_credentials, deposit.amount, deposit.signature - ): - validator_pubkeys.append(deposit.pubkey) - pending_deposits.append(deposit) + # Deposits for non-builders stay in the pending queue. If there is a + # valid pending deposit for a new validator with this pubkey, keep this + # deposit in the pending queue to be applied to that validator later. + if deposit.pubkey not in builder_pubkeys: + if not is_builder_withdrawal_credential(deposit.withdrawal_credentials): + pending_deposits.append(deposit) + continue + if is_pending_validator(pending_deposits, deposit.pubkey): + pending_deposits.append(deposit) + continue + + apply_deposit_for_builder( + state, + deposit.pubkey, + deposit.withdrawal_credentials, + deposit.amount, + deposit.signature, + deposit.slot, + ) state.pending_deposits = pending_deposits +- name: payload_data_availability#gloas + sources: [] + spec: | + + def payload_data_availability(store: Store, root: Root, available: bool) -> bool: + """ + Return whether the blob data for the beacon block with root ``root`` is + considered ``available`` (or not, when ``available`` is ``False``), taking into + consideration local availability and PTC votes. + """ + # The beacon block root must be known + assert root in store.payload_data_availability_vote + + # If the payload is not locally available, the blob data + # is not considered available regardless of the PTC vote + if not is_payload_verified(store, root): + return not available + + votes = store.payload_data_availability_vote[root] + return sum(vote is available for vote in votes) > DATA_AVAILABILITY_TIMELY_THRESHOLD + + +- name: payload_timeliness#gloas + sources: [] + spec: | + + def payload_timeliness(store: Store, root: Root, timely: bool) -> bool: + """ + Return whether the execution payload for the beacon block with root ``root`` + is considered ``timely`` (or not, when ``timely`` is ``False``), taking into + consideration local availability and PTC votes. + """ + # The beacon block root must be known + assert root in store.payload_timeliness_vote + + # If the payload is not locally available, the payload + # is not considered available regardless of the PTC vote + if not is_payload_verified(store, root): + return not timely + + votes = store.payload_timeliness_vote[root] + return sum(vote is timely for vote in votes) > PAYLOAD_TIMELY_THRESHOLD + + - name: prepare_execution_payload#bellatrix sources: - file: packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts @@ -8333,21 +8372,24 @@ - name: prepare_execution_payload#gloas sources: [] spec: | - + def prepare_execution_payload( # [New in Gloas:EIP7732] store: Store, + # [New in Gloas:EIP7732] + head: ForkChoiceNode, state: BeaconState, safe_block_hash: Hash32, finalized_block_hash: Hash32, suggested_fee_recipient: ExecutionAddress, + # [New in Gloas] + target_gas_limit: uint64, execution_engine: ExecutionEngine, ) -> Optional[PayloadId]: # [New in Gloas:EIP7732] parent_bid = state.latest_execution_payload_bid - parent_root = hash_tree_root(state.latest_block_header) - if should_extend_payload(store, parent_root): - envelope = store.payloads[parent_root] + if should_build_on_full(store, head): + envelope = store.payloads[head.root] # Make a copy of the state to avoid mutability issues state = copy(state) # Apply parent payload before computing withdrawals @@ -8368,6 +8410,8 @@ parent_beacon_block_root=hash_tree_root(state.latest_block_header), # [New in Gloas:EIP7843] slot_number=state.slot, + # [New in Gloas] + target_gas_limit=target_gas_limit, ) return execution_engine.notify_forkchoice_updated( # [Modified in Gloas:EIP7732] @@ -8381,19 +8425,20 @@ - name: prepare_execution_payload#heze sources: [] spec: | - + def prepare_execution_payload( store: Store, + head: ForkChoiceNode, state: BeaconState, safe_block_hash: Hash32, finalized_block_hash: Hash32, suggested_fee_recipient: ExecutionAddress, + target_gas_limit: uint64, execution_engine: ExecutionEngine, ) -> Optional[PayloadId]: parent_bid = state.latest_execution_payload_bid - parent_root = hash_tree_root(state.latest_block_header) - if should_extend_payload(store, parent_root): - envelope = store.payloads[parent_root] + if should_build_on_full(store, head): + envelope = store.payloads[head.root] # Make a copy of the state to avoid mutability issues state = copy(state) # Apply parent payload before computing withdrawals @@ -8412,6 +8457,7 @@ withdrawals=withdrawals, parent_beacon_block_root=hash_tree_root(state.latest_block_header), slot_number=state.slot, + target_gas_limit=target_gas_limit, # [New in Heze:EIP7805] inclusion_list_transactions=get_inclusion_list_transactions( get_inclusion_list_store(), state, Slot(state.slot - 1), only_timely=False @@ -9078,7 +9124,7 @@ - file: packages/state-transition/src/block/processDepositRequest.ts search: export function processDepositRequest( spec: | - + def process_deposit_request(state: BeaconState, deposit_request: DepositRequest) -> None: # [New in Gloas:EIP7732] builder_pubkeys = [b.pubkey for b in state.builders] @@ -9092,7 +9138,7 @@ if is_builder or ( is_builder_withdrawal_credential(deposit_request.withdrawal_credentials) and not is_validator - and not is_pending_validator(state, deposit_request.pubkey) + and not is_pending_validator(state.pending_deposits, deposit_request.pubkey) ): # Apply builder deposits immediately apply_deposit_for_builder( @@ -11360,16 +11406,29 @@ return len(equivocations) == 0 +- 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) + + - name: should_extend_payload#gloas sources: [] spec: | - + def should_extend_payload(store: Store, root: Root) -> bool: if not is_payload_verified(store, root): return False proposer_root = store.proposer_boost_root + payload_is_timely = payload_timeliness(store, root, timely=True) + payload_data_is_available = payload_data_availability(store, root, available=True) return ( - (is_payload_timely(store, root) and is_payload_data_available(store, root)) + (payload_is_timely and payload_data_is_available) or proposer_root == Root() or store.blocks[proposer_root].parent_root != root or is_parent_node_full(store, store.blocks[proposer_root]) @@ -11379,7 +11438,7 @@ - name: should_extend_payload#heze sources: [] spec: | - + def should_extend_payload(store: Store, root: Root) -> bool: if not is_payload_verified(store, root): return False @@ -11387,8 +11446,10 @@ if not is_payload_inclusion_list_satisfied(store, root): return False proposer_root = store.proposer_boost_root + payload_is_timely = payload_timeliness(store, root, timely=True) + payload_data_is_available = payload_data_availability(store, root, available=True) return ( - (is_payload_timely(store, root) and is_payload_data_available(store, root)) + (payload_is_timely and payload_data_is_available) or proposer_root == Root() or store.blocks[proposer_root].parent_root != root or is_parent_node_full(store, store.blocks[proposer_root]) @@ -12787,7 +12848,7 @@ - file: packages/state-transition/src/slot/upgradeStateToGloas.ts search: export function upgradeStateToGloas( spec: | - + def upgrade_to_gloas(pre: fulu.BeaconState) -> BeaconState: epoch = fulu.get_current_epoch(pre) @@ -12851,6 +12912,7 @@ # [New in Gloas:EIP7732] latest_execution_payload_bid=ExecutionPayloadBid( block_hash=pre.latest_execution_payload_header.block_hash, + gas_limit=pre.latest_execution_payload_header.gas_limit, execution_requests_root=hash_tree_root(ExecutionRequests()), ), # [New in Gloas:EIP7732] @@ -12946,10 +13008,501 @@ return post +- name: validate_beacon_aggregate_and_proof_gossip#deneb + sources: [] + spec: | + + def validate_beacon_aggregate_and_proof_gossip( + seen: Seen, + store: Store, + state: BeaconState, + signed_aggregate_and_proof: SignedAggregateAndProof, + current_time_ms: uint64, + ) -> None: + """ + Validate a SignedAggregateAndProof for gossip propagation. + Raises GossipIgnore or GossipReject on validation failure. + """ + aggregate_and_proof = signed_aggregate_and_proof.message + aggregate = aggregate_and_proof.aggregate + index = aggregate.data.index + aggregation_bits = aggregate.aggregation_bits + + # [REJECT] The committee index is within the expected range + committee_count = get_committee_count_per_slot(state, aggregate.data.target.epoch) + if index >= committee_count: + raise GossipReject("committee index out of range") + + # [New in Deneb:EIP7045] + # [IGNORE] The aggregate attestation's slot is not from a future slot + # (MAY be queued for processing at the appropriate slot) + if not is_not_from_future_slot(state, aggregate.data.slot, current_time_ms): + raise GossipIgnore("aggregate slot is from a future slot") + + # [Modified in Deneb:EIP7045] + # [IGNORE] The aggregate attestation's epoch is either the current or previous epoch + attestation_epoch = compute_epoch_at_slot(aggregate.data.slot) + is_previous_epoch_attestation = is_within_slot_range( + state, + compute_start_slot_at_epoch(Epoch(attestation_epoch + 1)), + SLOTS_PER_EPOCH - 1, + current_time_ms, + ) + is_current_epoch_attestation = is_within_slot_range( + state, + compute_start_slot_at_epoch(attestation_epoch), + SLOTS_PER_EPOCH - 1, + current_time_ms, + ) + if not (is_previous_epoch_attestation or is_current_epoch_attestation): + raise GossipIgnore("aggregate epoch is not previous or current epoch") + + # [REJECT] The aggregate attestation's epoch matches its target + if aggregate.data.target.epoch != compute_epoch_at_slot(aggregate.data.slot): + raise GossipReject("attestation epoch does not match target epoch") + + # [REJECT] The number of aggregation bits matches the committee size + committee = get_beacon_committee(state, aggregate.data.slot, index) + if len(aggregation_bits) != len(committee): + raise GossipReject("aggregation bits length does not match committee size") + + # [REJECT] The aggregate attestation has participants + attesting_indices = get_attesting_indices(state, aggregate) + if len(attesting_indices) < 1: + raise GossipReject("aggregate has no participants") + + # [IGNORE] A valid aggregate with a superset of aggregation bits has not already been seen + aggregate_data_root = hash_tree_root(aggregate.data) + aggregate_bits = tuple(bool(bit) for bit in aggregation_bits) + seen_bits = seen.aggregate_data_roots.get(aggregate_data_root, set()) + if is_non_strict_superset(seen_bits, aggregate_bits): + raise GossipIgnore("already seen aggregate for this data") + + # [IGNORE] This is the first valid aggregate for this aggregator in this epoch + aggregator_index = aggregate_and_proof.aggregator_index + target_epoch = aggregate.data.target.epoch + if (aggregator_index, target_epoch) in seen.aggregator_epochs: + raise GossipIgnore("already seen aggregate from this aggregator for this epoch") + + # [REJECT] The selection proof selects the validator as an aggregator + if not is_aggregator(state, aggregate.data.slot, index, aggregate_and_proof.selection_proof): + raise GossipReject("validator is not selected as aggregator") + + # [REJECT] The aggregator's validator index is within the committee + if aggregator_index not in committee: + raise GossipReject("aggregator index not in committee") + + # [REJECT] The selection proof signature is valid + aggregator = state.validators[aggregator_index] + domain = get_domain(state, DOMAIN_SELECTION_PROOF, target_epoch) + signing_root = compute_signing_root(aggregate.data.slot, domain) + if not bls.Verify(aggregator.pubkey, signing_root, aggregate_and_proof.selection_proof): + raise GossipReject("invalid selection proof signature") + + # [REJECT] The aggregator signature is valid + domain = get_domain(state, DOMAIN_AGGREGATE_AND_PROOF, target_epoch) + signing_root = compute_signing_root(aggregate_and_proof, domain) + if not bls.Verify(aggregator.pubkey, signing_root, signed_aggregate_and_proof.signature): + raise GossipReject("invalid aggregator signature") + + # [REJECT] The aggregate signature is valid + if not is_valid_indexed_attestation(state, get_indexed_attestation(state, aggregate)): + raise GossipReject("invalid aggregate signature") + + # [IGNORE] The block being voted for has been seen (via gossip or non-gossip sources) + # (MAY be queued until block is retrieved) + if aggregate.data.beacon_block_root not in store.blocks: + raise GossipIgnore("block being voted for has not been seen") + + # [REJECT] The block being voted for passes validation + if aggregate.data.beacon_block_root not in store.block_states: + raise GossipReject("block being voted for failed validation") + + # [REJECT] The target block is an ancestor of the LMD vote block + checkpoint_block = get_checkpoint_block( + store, aggregate.data.beacon_block_root, aggregate.data.target.epoch + ) + if checkpoint_block != aggregate.data.target.root: + raise GossipReject("target block is not an ancestor of LMD vote block") + + # [IGNORE] The finalized checkpoint is an ancestor of the block + finalized_checkpoint_block = get_checkpoint_block( + store, aggregate.data.beacon_block_root, store.finalized_checkpoint.epoch + ) + if finalized_checkpoint_block != store.finalized_checkpoint.root: + raise GossipIgnore("finalized checkpoint is not an ancestor of block") + + # Mark this aggregate as seen + seen.aggregator_epochs.add((aggregator_index, target_epoch)) + if aggregate_data_root not in seen.aggregate_data_roots: + seen.aggregate_data_roots[aggregate_data_root] = set() + seen.aggregate_data_roots[aggregate_data_root].add(aggregate_bits) + + +- name: validate_beacon_aggregate_and_proof_gossip#electra + sources: [] + spec: | + + def validate_beacon_aggregate_and_proof_gossip( + seen: Seen, + store: Store, + state: BeaconState, + signed_aggregate_and_proof: SignedAggregateAndProof, + current_time_ms: uint64, + ) -> None: + """ + Validate a SignedAggregateAndProof for gossip propagation. + Raises GossipIgnore or GossipReject on validation failure. + """ + aggregate_and_proof = signed_aggregate_and_proof.message + aggregate = aggregate_and_proof.aggregate + aggregation_bits = aggregate.aggregation_bits + + # [New in Electra:EIP7549] + # [REJECT] The aggregate attestation's data index is zero + if aggregate.data.index != 0: + raise GossipReject("aggregate data index is non-zero") + + # [New in Electra:EIP7549] + # [REJECT] Exactly one committee is specified by the committee bits + committee_indices = get_committee_indices(aggregate.committee_bits) + if len(committee_indices) != 1: + raise GossipReject("aggregate committee bits must specify exactly one committee") + index = committee_indices[0] + + # [REJECT] The committee index is within the expected range + committee_count = get_committee_count_per_slot(state, aggregate.data.target.epoch) + if index >= committee_count: + raise GossipReject("committee index out of range") + + # [IGNORE] The aggregate attestation's slot is not from a future slot + # (MAY be queued for processing at the appropriate slot) + if not is_not_from_future_slot(state, aggregate.data.slot, current_time_ms): + raise GossipIgnore("aggregate slot is from a future slot") + + # [IGNORE] The aggregate attestation's epoch is either the current or previous epoch + attestation_epoch = compute_epoch_at_slot(aggregate.data.slot) + is_previous_epoch_attestation = is_within_slot_range( + state, + compute_start_slot_at_epoch(Epoch(attestation_epoch + 1)), + SLOTS_PER_EPOCH - 1, + current_time_ms, + ) + is_current_epoch_attestation = is_within_slot_range( + state, + compute_start_slot_at_epoch(attestation_epoch), + SLOTS_PER_EPOCH - 1, + current_time_ms, + ) + if not (is_previous_epoch_attestation or is_current_epoch_attestation): + raise GossipIgnore("aggregate epoch is not previous or current epoch") + + # [REJECT] The aggregate attestation's epoch matches its target + if aggregate.data.target.epoch != compute_epoch_at_slot(aggregate.data.slot): + raise GossipReject("attestation epoch does not match target epoch") + + # [REJECT] The number of aggregation bits matches the committee size + committee = get_beacon_committee(state, aggregate.data.slot, index) + if len(aggregation_bits) != len(committee): + raise GossipReject("aggregation bits length does not match committee size") + + # [REJECT] The aggregate attestation has participants + attesting_indices = get_attesting_indices(state, aggregate) + if len(attesting_indices) < 1: + raise GossipReject("aggregate has no participants") + + # [Modified in Electra:EIP7549] + # [IGNORE] A valid aggregate with a superset of aggregation bits has not already been seen + aggregate_data_root = hash_tree_root(aggregate.data) + aggregate_cache_key = (aggregate_data_root, index) + aggregate_bits = tuple(bool(bit) for bit in aggregation_bits) + seen_bits = seen.aggregate_data_roots.get(aggregate_cache_key, set()) + if is_non_strict_superset(seen_bits, aggregate_bits): + raise GossipIgnore("already seen aggregate for this data") + + # [IGNORE] This is the first valid aggregate for this aggregator in this epoch + aggregator_index = aggregate_and_proof.aggregator_index + target_epoch = aggregate.data.target.epoch + if (aggregator_index, target_epoch) in seen.aggregator_epochs: + raise GossipIgnore("already seen aggregate from this aggregator for this epoch") + + # [REJECT] The selection proof selects the validator as an aggregator + if not is_aggregator(state, aggregate.data.slot, index, aggregate_and_proof.selection_proof): + raise GossipReject("validator is not selected as aggregator") + + # [REJECT] The aggregator's validator index is within the committee + if aggregator_index not in committee: + raise GossipReject("aggregator index not in committee") + + # [REJECT] The selection proof signature is valid + aggregator = state.validators[aggregator_index] + domain = get_domain(state, DOMAIN_SELECTION_PROOF, target_epoch) + signing_root = compute_signing_root(aggregate.data.slot, domain) + if not bls.Verify(aggregator.pubkey, signing_root, aggregate_and_proof.selection_proof): + raise GossipReject("invalid selection proof signature") + + # [REJECT] The aggregator signature is valid + domain = get_domain(state, DOMAIN_AGGREGATE_AND_PROOF, target_epoch) + signing_root = compute_signing_root(aggregate_and_proof, domain) + if not bls.Verify(aggregator.pubkey, signing_root, signed_aggregate_and_proof.signature): + raise GossipReject("invalid aggregator signature") + + # [REJECT] The aggregate signature is valid + if not is_valid_indexed_attestation(state, get_indexed_attestation(state, aggregate)): + raise GossipReject("invalid aggregate signature") + + # [IGNORE] The block being voted for has been seen (via gossip or non-gossip sources) + # (MAY be queued until block is retrieved) + if aggregate.data.beacon_block_root not in store.blocks: + raise GossipIgnore("block being voted for has not been seen") + + # [REJECT] The block being voted for passes validation + if aggregate.data.beacon_block_root not in store.block_states: + raise GossipReject("block being voted for failed validation") + + # [REJECT] The target block is an ancestor of the LMD vote block + checkpoint_block = get_checkpoint_block( + store, aggregate.data.beacon_block_root, aggregate.data.target.epoch + ) + if checkpoint_block != aggregate.data.target.root: + raise GossipReject("target block is not an ancestor of LMD vote block") + + # [IGNORE] The finalized checkpoint is an ancestor of the block + finalized_checkpoint_block = get_checkpoint_block( + store, aggregate.data.beacon_block_root, store.finalized_checkpoint.epoch + ) + if finalized_checkpoint_block != store.finalized_checkpoint.root: + raise GossipIgnore("finalized checkpoint is not an ancestor of block") + + # Mark this aggregate as seen + seen.aggregator_epochs.add((aggregator_index, target_epoch)) + if aggregate_cache_key not in seen.aggregate_data_roots: + seen.aggregate_data_roots[aggregate_cache_key] = set() + seen.aggregate_data_roots[aggregate_cache_key].add(aggregate_bits) + + +- 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, + ) -> None: + """ + Validate an Attestation for gossip propagation on a subnet. + Raises GossipIgnore or GossipReject on validation failure. + """ + data = attestation.data + committee_index = data.index + target_epoch = data.target.epoch + aggregation_bits = attestation.aggregation_bits + + # [REJECT] The committee index is within the expected range + committees_per_slot = get_committee_count_per_slot(state, target_epoch) + if committee_index >= committees_per_slot: + raise GossipReject("committee index out of range") + + # [REJECT] The attestation is for the correct subnet + expected_subnet = compute_subnet_for_attestation( + committees_per_slot, data.slot, committee_index + ) + if expected_subnet != subnet_id: + raise GossipReject("attestation is for wrong subnet") + + # [Modified in Deneb:EIP7045] + # [IGNORE] The attestation's slot is not from a future slot + # (MAY be queued for processing at the appropriate slot) + if not is_not_from_future_slot(state, data.slot, current_time_ms): + raise GossipIgnore("attestation slot is from a future slot") + + # [Modified in Deneb:EIP7045] + # [IGNORE] The attestation's epoch is either the current or previous epoch + attestation_epoch = compute_epoch_at_slot(data.slot) + is_previous_epoch_attestation = is_within_slot_range( + state, + compute_start_slot_at_epoch(Epoch(attestation_epoch + 1)), + SLOTS_PER_EPOCH - 1, + current_time_ms, + ) + is_current_epoch_attestation = is_within_slot_range( + state, + compute_start_slot_at_epoch(attestation_epoch), + SLOTS_PER_EPOCH - 1, + current_time_ms, + ) + if not (is_previous_epoch_attestation or is_current_epoch_attestation): + raise GossipIgnore("attestation epoch is not previous or current epoch") + + # [REJECT] The attestation's epoch matches its target + if target_epoch != compute_epoch_at_slot(data.slot): + raise GossipReject("attestation epoch does not match target epoch") + + # [REJECT] The attestation is unaggregated (exactly one bit set) + num_bits_set = sum(1 for bit in aggregation_bits if bit) + if num_bits_set != 1: + raise GossipReject("attestation is not unaggregated") + + # [REJECT] The number of aggregation bits matches the committee size + committee = get_beacon_committee(state, data.slot, committee_index) + if len(aggregation_bits) != len(committee): + raise GossipReject("aggregation bits length does not match committee size") + + # [IGNORE] No other valid attestation seen for this validator and target epoch + participant_index = committee[aggregation_bits.index(True)] + if (participant_index, target_epoch) in seen.attestation_validator_epochs: + raise GossipIgnore("already seen attestation from this validator for this epoch") + + # [REJECT] The attestation signature is valid + indexed_attestation = get_indexed_attestation(state, attestation) + if not is_valid_indexed_attestation(state, indexed_attestation): + raise GossipReject("invalid attestation signature") + + # [IGNORE] The block being voted for has been seen (via gossip or non-gossip sources) + # (MAY be queued until block is retrieved) + beacon_block_root = data.beacon_block_root + if beacon_block_root not in store.blocks: + raise GossipIgnore("block being voted for has not been seen") + + # [REJECT] The block being voted for passes validation + if beacon_block_root not in store.block_states: + raise GossipReject("block being voted for failed validation") + + # [REJECT] The attestation's target block is an ancestor of the LMD vote block + target_checkpoint_block = get_checkpoint_block(store, beacon_block_root, target_epoch) + if target_checkpoint_block != data.target.root: + raise GossipReject("target block is not an ancestor of LMD vote block") + + # [IGNORE] The current finalized_checkpoint is an ancestor of the block + finalized_checkpoint_block = get_checkpoint_block( + store, beacon_block_root, store.finalized_checkpoint.epoch + ) + if finalized_checkpoint_block != store.finalized_checkpoint.root: + raise GossipIgnore("finalized checkpoint is not an ancestor of block") + + # Mark this attestation as seen + seen.attestation_validator_epochs.add((participant_index, target_epoch)) + + +- 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, + ) -> None: + """ + Validate a SingleAttestation for gossip propagation on a subnet. + Raises GossipIgnore or GossipReject on validation failure. + """ + data = attestation.data + # [Modified in Electra:EIP7549] + committee_index = attestation.committee_index + attester_index = attestation.attester_index + target_epoch = data.target.epoch + + # [New in Electra:EIP7549] + # [REJECT] The attestation's data index is zero + if data.index != 0: + raise GossipReject("attestation data index is non-zero") + + # [REJECT] The committee index is within the expected range + committees_per_slot = get_committee_count_per_slot(state, target_epoch) + if committee_index >= committees_per_slot: + raise GossipReject("committee index out of range") + + # [REJECT] The attestation is for the correct subnet + expected_subnet = compute_subnet_for_attestation( + committees_per_slot, data.slot, committee_index + ) + if expected_subnet != subnet_id: + raise GossipReject("attestation is for wrong subnet") + + # [IGNORE] The attestation's slot is not from a future slot + # (MAY be queued for processing at the appropriate slot) + if not is_not_from_future_slot(state, data.slot, current_time_ms): + raise GossipIgnore("attestation slot is from a future slot") + + # [IGNORE] The attestation's epoch is either the current or previous epoch + attestation_epoch = compute_epoch_at_slot(data.slot) + is_previous_epoch_attestation = is_within_slot_range( + state, + compute_start_slot_at_epoch(Epoch(attestation_epoch + 1)), + SLOTS_PER_EPOCH - 1, + current_time_ms, + ) + is_current_epoch_attestation = is_within_slot_range( + state, + compute_start_slot_at_epoch(attestation_epoch), + SLOTS_PER_EPOCH - 1, + current_time_ms, + ) + if not (is_previous_epoch_attestation or is_current_epoch_attestation): + raise GossipIgnore("attestation epoch is not previous or current epoch") + + # [REJECT] The attestation's epoch matches its target + if target_epoch != compute_epoch_at_slot(data.slot): + raise GossipReject("attestation epoch does not match target epoch") + + # [New in Electra:EIP7549] + # [REJECT] The attester is a member of the committee + committee = get_beacon_committee(state, data.slot, committee_index) + if attester_index not in committee: + raise GossipReject("attester is not a member of the committee") + + # [Modified in Electra:EIP7549] + # [IGNORE] No other valid attestation seen for this validator and target epoch + if (attester_index, target_epoch) in seen.attestation_validator_epochs: + raise GossipIgnore("already seen attestation from this validator for this epoch") + + # [Modified in Electra:EIP7549] + # [REJECT] The attestation signature is valid + attester = state.validators[attester_index] + domain = get_domain(state, DOMAIN_BEACON_ATTESTER, target_epoch) + signing_root = compute_signing_root(data, domain) + if not bls.Verify(attester.pubkey, signing_root, attestation.signature): + raise GossipReject("invalid attestation signature") + + # [IGNORE] The block being voted for has been seen (via gossip or non-gossip sources) + # (MAY be queued until block is retrieved) + beacon_block_root = data.beacon_block_root + if beacon_block_root not in store.blocks: + raise GossipIgnore("block being voted for has not been seen") + + # [REJECT] The block being voted for passes validation + if beacon_block_root not in store.block_states: + raise GossipReject("block being voted for failed validation") + + # [REJECT] The attestation's target block is an ancestor of the LMD vote block + target_checkpoint_block = get_checkpoint_block(store, beacon_block_root, target_epoch) + if target_checkpoint_block != data.target.root: + raise GossipReject("target block is not an ancestor of LMD vote block") + + # [IGNORE] The current finalized_checkpoint is an ancestor of the block + finalized_checkpoint_block = get_checkpoint_block( + store, beacon_block_root, store.finalized_checkpoint.epoch + ) + if finalized_checkpoint_block != store.finalized_checkpoint.root: + raise GossipIgnore("finalized checkpoint is not an ancestor of block") + + # Mark this attestation as seen + seen.attestation_validator_epochs.add((attester_index, target_epoch)) + + - name: validate_beacon_block_gossip#bellatrix sources: [] spec: | - + def validate_beacon_block_gossip( seen: Seen, store: Store, @@ -12973,7 +13526,7 @@ # [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). + # -- 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") @@ -13044,14 +13597,14 @@ if block.proposer_index != expected_proposer: raise GossipReject("block proposer_index does not match expected proposer") - # Mark this block as seen for this proposer/slot combination + # Mark this block as seen seen.proposer_slots.add((block.proposer_index, block.slot)) - name: validate_beacon_block_gossip#capella sources: [] spec: | - + def validate_beacon_block_gossip( seen: Seen, store: Store, @@ -13074,7 +13627,7 @@ # [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). + # -- 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") @@ -13138,26 +13691,415 @@ if block.proposer_index != expected_proposer: raise GossipReject("block proposer_index does not match expected proposer") - # Mark this block as seen for this proposer/slot combination + # Mark this block as seen seen.proposer_slots.add((block.proposer_index, block.slot)) -- name: validate_bls_to_execution_change_gossip#capella +- name: validate_beacon_block_gossip#deneb sources: [] spec: | - - def validate_bls_to_execution_change_gossip( + + def validate_beacon_block_gossip( seen: Seen, + store: Store, state: BeaconState, - signed_bls_to_execution_change: SignedBLSToExecutionChange, + signed_beacon_block: SignedBeaconBlock, current_time_ms: uint64, + block_payload_statuses: Dict[Root, PayloadValidationStatus] = {}, ) -> None: """ - Validate a SignedBLSToExecutionChange for gossip propagation. + Validate a SignedBeaconBlock for gossip propagation. Raises GossipIgnore or GossipReject on validation failure. """ - bls_to_execution_change = signed_bls_to_execution_change.message - validator_index = bls_to_execution_change.validator_index + 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") + + # [New in Deneb:EIP4844] + # [REJECT] The length of KZG commitments is less than or equal to the limit + if len(block.body.blob_kzg_commitments) > MAX_BLOBS_PER_BLOCK: + 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_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] = {}, + ) -> None: + """ + Validate a SignedBeaconBlock for gossip propagation. + Raises GossipIgnore or GossipReject on validation failure. + """ + 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 Electra:EIP7691] + # [REJECT] The length of KZG commitments is less than or equal to the limit + if len(block.body.blob_kzg_commitments) > MAX_BLOBS_PER_BLOCK_ELECTRA: + 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, + ) -> None: + """ + Validate a BlobSidecar for gossip propagation on a subnet. + Raises GossipIgnore or GossipReject on validation failure. + """ + block_header = blob_sidecar.signed_block_header.message + + # [REJECT] The sidecar's index is consistent with MAX_BLOBS_PER_BLOCK + if blob_sidecar.index >= MAX_BLOBS_PER_BLOCK: + raise GossipReject("blob index out of range") + + # [REJECT] The sidecar is for the correct subnet + if compute_subnet_for_blob_sidecar(blob_sidecar.index) != subnet_id: + raise GossipReject("blob 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("blob 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("blob 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 blob_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, blob_sidecar.signed_block_header.signature): + raise GossipReject("invalid proposer signature on blob 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("blob 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("blob 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("blob 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 blob sidecar's block") + + # [REJECT] The sidecar's inclusion proof is valid as verified by verify_blob_sidecar_inclusion_proof + if not verify_blob_sidecar_inclusion_proof(blob_sidecar): + raise GossipReject("invalid blob sidecar inclusion proof") + + # [REJECT] The sidecar's blob is valid as verified by verify_blob_kzg_proof + if not verify_blob_kzg_proof( + blob_sidecar.blob, blob_sidecar.kzg_commitment, blob_sidecar.kzg_proof + ): + raise GossipReject("invalid blob kzg proof") + + # [IGNORE] The sidecar is the first sidecar for the tuple + # (block_header.slot, block_header.proposer_index, blob_sidecar.index) + sidecar_tuple = (block_header.slot, block_header.proposer_index, blob_sidecar.index) + if sidecar_tuple in seen.blob_sidecar_tuples: + raise GossipIgnore("already seen blob sidecar from this proposer for this slot and index") + + # [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("blob sidecar proposer_index does not match expected proposer") + + # Mark this blob sidecar as seen + seen.blob_sidecar_tuples.add(sidecar_tuple) + + +- 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, + ) -> None: + """ + Validate a BlobSidecar for gossip propagation on a subnet. + Raises GossipIgnore or GossipReject on validation failure. + """ + block_header = blob_sidecar.signed_block_header.message + + # [Modified in Electra:EIP7691] + # [REJECT] The sidecar's index is consistent with MAX_BLOBS_PER_BLOCK_ELECTRA + if blob_sidecar.index >= MAX_BLOBS_PER_BLOCK_ELECTRA: + raise GossipReject("blob index out of range") + + # [REJECT] The sidecar is for the correct subnet + if compute_subnet_for_blob_sidecar(blob_sidecar.index) != subnet_id: + raise GossipReject("blob 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("blob 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("blob 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 blob_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, blob_sidecar.signed_block_header.signature): + raise GossipReject("invalid proposer signature on blob 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("blob 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("blob 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("blob 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 blob sidecar's block") + + # [REJECT] The sidecar's inclusion proof is valid as verified by verify_blob_sidecar_inclusion_proof + if not verify_blob_sidecar_inclusion_proof(blob_sidecar): + raise GossipReject("invalid blob sidecar inclusion proof") + + # [REJECT] The sidecar's blob is valid as verified by verify_blob_kzg_proof + if not verify_blob_kzg_proof( + blob_sidecar.blob, blob_sidecar.kzg_commitment, blob_sidecar.kzg_proof + ): + raise GossipReject("invalid blob kzg proof") + + # [IGNORE] The sidecar is the first sidecar for the tuple + # (block_header.slot, block_header.proposer_index, blob_sidecar.index) + sidecar_tuple = (block_header.slot, block_header.proposer_index, blob_sidecar.index) + if sidecar_tuple in seen.blob_sidecar_tuples: + raise GossipIgnore("already seen blob sidecar from this proposer for this slot and index") + + # [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("blob sidecar proposer_index does not match expected proposer") + + # Mark this blob sidecar as seen + seen.blob_sidecar_tuples.add(sidecar_tuple) + + +- name: validate_bls_to_execution_change_gossip#capella + sources: [] + spec: | + + def validate_bls_to_execution_change_gossip( + seen: Seen, + state: BeaconState, + signed_bls_to_execution_change: SignedBLSToExecutionChange, + current_time_ms: uint64, + ) -> None: + """ + Validate a SignedBLSToExecutionChange for gossip propagation. + Raises GossipIgnore or GossipReject on validation failure. + """ + bls_to_execution_change = signed_bls_to_execution_change.message + validator_index = bls_to_execution_change.validator_index # [IGNORE] The current epoch is at or after the Capella fork epoch # (where current_epoch is defined by the current wall-clock time) @@ -13430,7 +14372,7 @@ - name: validate_sync_committee_contribution_and_proof_gossip#altair sources: [] spec: | - + def validate_sync_committee_contribution_and_proof_gossip( seen: Seen, state: BeaconState, @@ -13445,7 +14387,6 @@ contribution = contribution_and_proof.contribution # [IGNORE] The contribution's slot is for the current slot - # (with a MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance) if not is_current_slot(state, contribution.slot, current_time_ms): raise GossipIgnore("contribution is not for the current slot") @@ -13539,7 +14480,7 @@ - name: validate_sync_committee_message_gossip#altair sources: [] spec: | - + def validate_sync_committee_message_gossip( seen: Seen, state: BeaconState, @@ -13552,7 +14493,6 @@ Raises GossipIgnore or GossipReject on validation failure. """ # [IGNORE] The message's slot is for the current slot - # (with a MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance) if not is_current_slot(state, sync_committee_message.slot, current_time_ms): raise GossipIgnore("message is not for the current slot") @@ -13606,6 +14546,62 @@ assert target.epoch in [current_epoch, previous_epoch] +- name: validate_voluntary_exit_gossip#deneb + sources: [] + spec: | + + def validate_voluntary_exit_gossip( + seen: Seen, + state: BeaconState, + signed_voluntary_exit: SignedVoluntaryExit, + ) -> None: + """ + Validate a SignedVoluntaryExit for gossip propagation. + Raises GossipIgnore or GossipReject on validation failure. + """ + voluntary_exit = signed_voluntary_exit.message + validator_index = voluntary_exit.validator_index + + # [IGNORE] The voluntary exit is the first valid voluntary exit received for the validator + if validator_index in seen.voluntary_exit_indices: + raise GossipIgnore("already seen voluntary exit for this validator") + + # [REJECT] The validator index is valid + if validator_index >= len(state.validators): + raise GossipReject("validator index out of range") + + validator = state.validators[validator_index] + current_epoch = get_current_epoch(state) + + # [REJECT] The validator is active + if not is_active_validator(validator, current_epoch): + raise GossipReject("validator is not active") + + # [REJECT] The validator has not already initiated exit + if validator.exit_epoch != FAR_FUTURE_EPOCH: + raise GossipReject("validator has already initiated exit") + + # [REJECT] The voluntary exit epoch is not in the future + if current_epoch < voluntary_exit.epoch: + raise GossipReject("voluntary exit epoch is in the future") + + # [REJECT] The validator has been active long enough + if current_epoch < validator.activation_epoch + SHARD_COMMITTEE_PERIOD: + raise GossipReject("validator has not been active long enough") + + # [Modified in Deneb:EIP7044] + # [REJECT] The signature is valid + domain = compute_domain( + DOMAIN_VOLUNTARY_EXIT, CAPELLA_FORK_VERSION, state.genesis_validators_root + ) + signing_root = compute_signing_root(voluntary_exit, domain) + if not bls.Verify(validator.pubkey, signing_root, signed_voluntary_exit.signature): + raise GossipReject("invalid voluntary exit signature") + + # Mark this voluntary exit as seen + seen.voluntary_exit_indices.add(validator_index) + + - name: verify_blob_sidecar_inclusion_proof#deneb sources: - file: packages/beacon-node/src/chain/validation/blobSidecar.ts