From 1f2a3a45240c78649c23001d57c1bdeb28dc6f60 Mon Sep 17 00:00:00 2001 From: NC <17676176+ensi321@users.noreply.github.com> Date: Thu, 4 Dec 2025 08:35:47 -0800 Subject: [PATCH] feat: implement epbs state transition (#8507) Implement epbs state transition function. Passes all operations, epoch_transition and rewards spec tests on v1.6.1 Part of #8439 --------- Co-authored-by: Nico Flaig --- .../opPools/aggregatedAttestationPool.ts | 6 +- .../spec/presets/epoch_processing.test.ts | 1 + .../test/spec/presets/operations.test.ts | 41 +++- .../test/spec/specTestVersioning.ts | 2 +- .../test/spec/utils/specTestIterator.ts | 2 +- .../test/e2e/ensure-config-is-synced.test.ts | 2 +- packages/state-transition/src/block/index.ts | 49 +++-- .../block/isValidIndexedPayloadAttestation.ts | 23 +++ .../src/block/processAttestationPhase0.ts | 6 +- .../src/block/processAttestationsAltair.ts | 67 ++++++- .../src/block/processConsolidationRequest.ts | 11 +- .../src/block/processDepositRequest.ts | 7 +- .../src/block/processExecutionPayloadBid.ts | 120 ++++++++++++ .../block/processExecutionPayloadEnvelope.ts | 181 ++++++++++++++++++ .../src/block/processOperations.ts | 20 +- .../src/block/processPayloadAttestation.ts | 25 +++ .../src/block/processProposerSlashing.ts | 27 ++- .../src/block/processVoluntaryExit.ts | 2 +- .../src/block/processWithdrawalRequest.ts | 8 +- .../src/block/processWithdrawals.ts | 145 +++++++++++--- .../state-transition/src/cache/epochCache.ts | 59 +++++- packages/state-transition/src/epoch/index.ts | 12 ++ .../epoch/processBuilderPendingPayments.ts | 31 +++ packages/state-transition/src/index.ts | 2 + .../src/signatureSets/index.ts | 1 + .../indexedPayloadAttestation.ts | 24 +++ packages/state-transition/src/slot/index.ts | 14 +- .../src/slot/upgradeStateToAltair.ts | 3 +- .../src/slot/upgradeStateToGloas.ts | 54 +++++- .../state-transition/src/stateTransition.ts | 8 +- packages/state-transition/src/util/electra.ts | 21 +- packages/state-transition/src/util/epoch.ts | 9 +- packages/state-transition/src/util/gloas.ts | 58 ++++++ packages/state-transition/src/util/seed.ts | 58 +++++- .../state-transition/src/util/validator.ts | 23 ++- .../test/perf/slot/slots.test.ts | 3 +- .../block/processConsolidationRequest.test.ts | 3 +- packages/types/src/gloas/sszTypes.ts | 8 +- 38 files changed, 1026 insertions(+), 110 deletions(-) create mode 100644 packages/state-transition/src/block/isValidIndexedPayloadAttestation.ts create mode 100644 packages/state-transition/src/block/processExecutionPayloadBid.ts create mode 100644 packages/state-transition/src/block/processExecutionPayloadEnvelope.ts create mode 100644 packages/state-transition/src/block/processPayloadAttestation.ts create mode 100644 packages/state-transition/src/epoch/processBuilderPendingPayments.ts create mode 100644 packages/state-transition/src/signatureSets/indexedPayloadAttestation.ts create mode 100644 packages/state-transition/src/util/gloas.ts diff --git a/packages/beacon-node/src/chain/opPools/aggregatedAttestationPool.ts b/packages/beacon-node/src/chain/opPools/aggregatedAttestationPool.ts index 01bc915e70..3e65aba3eb 100644 --- a/packages/beacon-node/src/chain/opPools/aggregatedAttestationPool.ts +++ b/packages/beacon-node/src/chain/opPools/aggregatedAttestationPool.ts @@ -22,6 +22,7 @@ import { import { CachedBeaconStateAllForks, CachedBeaconStateAltair, + CachedBeaconStateGloas, CachedBeaconStatePhase0, EffectiveBalanceIncrements, RootCache, @@ -486,7 +487,10 @@ export class AggregatedAttestationPool { consolidation.attData, inclusionDistance, stateEpoch, - rootCache + rootCache, + ForkSeq[fork] >= ForkSeq.gloas + ? (state as CachedBeaconStateGloas).executionPayloadAvailability.toBoolArray() + : null ); const weight = diff --git a/packages/beacon-node/test/spec/presets/epoch_processing.test.ts b/packages/beacon-node/test/spec/presets/epoch_processing.test.ts index 5962a62e29..37c0e42168 100644 --- a/packages/beacon-node/test/spec/presets/epoch_processing.test.ts +++ b/packages/beacon-node/test/spec/presets/epoch_processing.test.ts @@ -51,6 +51,7 @@ const epochTransitionFns: Record = { const fork = state.config.getForkSeq(state.slot); epochFns.processProposerLookahead(fork, state as CachedBeaconStateFulu, epochTransitionCache); }, + builder_pending_payments: epochFns.processBuilderPendingPayments as EpochTransitionFn, }; /** diff --git a/packages/beacon-node/test/spec/presets/operations.test.ts b/packages/beacon-node/test/spec/presets/operations.test.ts index 3508d8392f..4e7b178279 100644 --- a/packages/beacon-node/test/spec/presets/operations.test.ts +++ b/packages/beacon-node/test/spec/presets/operations.test.ts @@ -1,5 +1,5 @@ import path from "node:path"; -import {ACTIVE_PRESET, ForkName} from "@lodestar/params"; +import {ACTIVE_PRESET, ForkName, ForkSeq} from "@lodestar/params"; import {InputType} from "@lodestar/spec-test-util"; import { BeaconStateAllForks, @@ -7,11 +7,12 @@ import { CachedBeaconStateBellatrix, CachedBeaconStateCapella, CachedBeaconStateElectra, + CachedBeaconStateGloas, ExecutionPayloadStatus, getBlockRootAtSlot, } from "@lodestar/state-transition"; import * as blockFns from "@lodestar/state-transition/block"; -import {AttesterSlashing, altair, bellatrix, capella, electra, phase0, ssz, sszTypesFor} from "@lodestar/types"; +import {AttesterSlashing, altair, bellatrix, capella, electra, gloas, phase0, ssz, sszTypesFor} from "@lodestar/types"; import {createCachedBeaconStateTest} from "../../utils/cachedBeaconState.js"; import {getConfig} from "../../utils/config.js"; import {ethereumConsensusSpecsTests} from "../specTestVersioning.js"; @@ -67,13 +68,24 @@ const operationFns: Record> = blockFns.processVoluntaryExit(fork, state, testCase.voluntary_exit); }, - execution_payload: (state, testCase: {body: bellatrix.BeaconBlockBody; execution: {execution_valid: boolean}}) => { + execution_payload: ( + state, + testCase: { + body: bellatrix.BeaconBlockBody | gloas.BeaconBlockBody; + signed_envelope: gloas.SignedExecutionPayloadEnvelope; + execution: {execution_valid: boolean}; + } + ) => { const fork = state.config.getForkSeq(state.slot); - blockFns.processExecutionPayload(fork, state as CachedBeaconStateBellatrix, testCase.body, { - executionPayloadStatus: testCase.execution.execution_valid - ? ExecutionPayloadStatus.valid - : ExecutionPayloadStatus.invalid, - }); + if (fork >= ForkSeq.gloas) { + blockFns.processExecutionPayloadEnvelope(state as CachedBeaconStateGloas, testCase.signed_envelope, true); + } else { + blockFns.processExecutionPayload(fork, state as CachedBeaconStateBellatrix, testCase.body, { + executionPayloadStatus: testCase.execution.execution_valid + ? ExecutionPayloadStatus.valid + : ExecutionPayloadStatus.invalid, + }); + } }, bls_to_execution_change: (state, testCase: {address_change: capella.SignedBLSToExecutionChange}) => { @@ -95,7 +107,16 @@ const operationFns: Record> = }, consolidation_request: (state, testCase: {consolidation_request: electra.ConsolidationRequest}) => { - blockFns.processConsolidationRequest(state as CachedBeaconStateElectra, testCase.consolidation_request); + const fork = state.config.getForkSeq(state.slot); + blockFns.processConsolidationRequest(fork, state as CachedBeaconStateElectra, testCase.consolidation_request); + }, + + execution_payload_bid: (state, testCase: {block: gloas.BeaconBlock}) => { + blockFns.processExecutionPayloadBid(state as CachedBeaconStateGloas, testCase.block); + }, + + payload_attestation: (state, testCase: {payload_attestation: gloas.PayloadAttestation}) => { + blockFns.processPayloadAttestation(state as CachedBeaconStateGloas, testCase.payload_attestation); }, }; @@ -149,6 +170,8 @@ const operations: TestRunnerFn = (fork, withdrawal_request: ssz.electra.WithdrawalRequest, deposit_request: ssz.electra.DepositRequest, consolidation_request: ssz.electra.ConsolidationRequest, + payload_attestation: ssz.gloas.PayloadAttestation, + signed_envelope: ssz.gloas.SignedExecutionPayloadEnvelope, }, shouldError: (testCase) => testCase.post === undefined, getExpected: (testCase) => testCase.post, diff --git a/packages/beacon-node/test/spec/specTestVersioning.ts b/packages/beacon-node/test/spec/specTestVersioning.ts index bb2d5d79ae..c4826c0a8e 100644 --- a/packages/beacon-node/test/spec/specTestVersioning.ts +++ b/packages/beacon-node/test/spec/specTestVersioning.ts @@ -14,7 +14,7 @@ import {DownloadTestsOptions} from "@lodestar/spec-test-util/downloadTests"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); export const ethereumConsensusSpecsTests: DownloadTestsOptions = { - specVersion: "v1.6.0-beta.2", + specVersion: "v1.6.1", // Target directory is the host package root: 'packages/*/spec-tests' outputDir: path.join(__dirname, "../../spec-tests"), specTestsRepoUrl: "https://github.com/ethereum/consensus-specs", diff --git a/packages/beacon-node/test/spec/utils/specTestIterator.ts b/packages/beacon-node/test/spec/utils/specTestIterator.ts index 582c519286..b83d3726e1 100644 --- a/packages/beacon-node/test/spec/utils/specTestIterator.ts +++ b/packages/beacon-node/test/spec/utils/specTestIterator.ts @@ -69,7 +69,7 @@ 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).*$/, + /^gloas\/(finality|fork_choice|networking|sanity|transition)\/.*$/, /^gloas\/ssz_static\/ForkChoiceNode.*$/, ], skippedTests: [], diff --git a/packages/params/test/e2e/ensure-config-is-synced.test.ts b/packages/params/test/e2e/ensure-config-is-synced.test.ts index 09a19cb824..21dabba8ec 100644 --- a/packages/params/test/e2e/ensure-config-is-synced.test.ts +++ b/packages/params/test/e2e/ensure-config-is-synced.test.ts @@ -8,7 +8,7 @@ import {loadConfigYaml} from "../yaml.js"; // Not e2e, but slow. Run with e2e tests /** https://github.com/ethereum/consensus-specs/releases */ -const specConfigCommit = "v1.6.0-beta.2"; +const specConfigCommit = "v1.6.1"; /** * Fields that we filter from local config when doing comparison. * Ideally this should be empty as it is not spec compliant diff --git a/packages/state-transition/src/block/index.ts b/packages/state-transition/src/block/index.ts index 5f20ab50b6..b2a2ad06f6 100644 --- a/packages/state-transition/src/block/index.ts +++ b/packages/state-transition/src/block/index.ts @@ -1,14 +1,22 @@ -import {ForkSeq} from "@lodestar/params"; +import {ForkPostGloas, ForkSeq} from "@lodestar/params"; import {BeaconBlock, BlindedBeaconBlock, altair, capella} from "@lodestar/types"; import {BeaconStateTransitionMetrics} from "../metrics.js"; -import {CachedBeaconStateAllForks, CachedBeaconStateBellatrix, CachedBeaconStateCapella} from "../types.js"; +import { + CachedBeaconStateAllForks, + CachedBeaconStateBellatrix, + CachedBeaconStateCapella, + CachedBeaconStateGloas, +} from "../types.js"; import {getFullOrBlindedPayload, isExecutionEnabled} from "../util/execution.js"; import {BlockExternalData, DataAvailabilityStatus} from "./externalData.js"; import {processBlobKzgCommitments} from "./processBlobKzgCommitments.js"; import {processBlockHeader} from "./processBlockHeader.js"; import {processEth1Data} from "./processEth1Data.js"; import {processExecutionPayload} from "./processExecutionPayload.js"; +import {processExecutionPayloadBid} from "./processExecutionPayloadBid.ts"; +import {processExecutionPayloadEnvelope} from "./processExecutionPayloadEnvelope.ts"; import {processOperations} from "./processOperations.js"; +import {processPayloadAttestation} from "./processPayloadAttestation.ts"; import {processRandao} from "./processRandao.js"; import {processSyncAggregate} from "./processSyncCommittee.js"; import {processWithdrawals} from "./processWithdrawals.js"; @@ -22,6 +30,9 @@ export { processEth1Data, processSyncAggregate, processWithdrawals, + processExecutionPayloadBid, + processPayloadAttestation, + processExecutionPayloadEnvelope, }; export * from "./externalData.js"; @@ -41,23 +52,33 @@ export function processBlock( processBlockHeader(state, block); + if (fork >= ForkSeq.gloas) { + // After gloas, processWithdrawals does not take a payload parameter + processWithdrawals(fork, state as CachedBeaconStateGloas); + } else if (fork >= ForkSeq.capella) { + const fullOrBlindedPayload = getFullOrBlindedPayload(block); + processWithdrawals( + fork, + state as CachedBeaconStateCapella, + fullOrBlindedPayload as capella.FullOrBlindedExecutionPayload + ); + } + // The call to the process_execution_payload must happen before the call to the process_randao as the former depends // on the randao_mix computed with the reveal of the previous block. - if (fork >= ForkSeq.bellatrix && isExecutionEnabled(state as CachedBeaconStateBellatrix, block)) { - const fullOrBlindedPayload = getFullOrBlindedPayload(block); - // TODO Deneb: Allow to disable withdrawals for interop testing - // https://github.com/ethereum/consensus-specs/blob/b62c9e877990242d63aa17a2a59a49bc649a2f2e/specs/eip4844/beacon-chain.md#disabling-withdrawals - if (fork >= ForkSeq.capella) { - processWithdrawals( - fork, - state as CachedBeaconStateCapella, - fullOrBlindedPayload as capella.FullOrBlindedExecutionPayload - ); - } - + // TODO GLOAS: We call processExecutionPayload somewhere else post-gloas + if ( + fork >= ForkSeq.bellatrix && + fork < ForkSeq.gloas && + isExecutionEnabled(state as CachedBeaconStateBellatrix, block) + ) { processExecutionPayload(fork, state as CachedBeaconStateBellatrix, block.body, externalData); } + if (fork >= ForkSeq.gloas) { + processExecutionPayloadBid(state as CachedBeaconStateGloas, block as BeaconBlock); + } + processRandao(state, block, verifySignatures); processEth1Data(state, block.body.eth1Data); processOperations(fork, state, block.body, opts, metrics); diff --git a/packages/state-transition/src/block/isValidIndexedPayloadAttestation.ts b/packages/state-transition/src/block/isValidIndexedPayloadAttestation.ts new file mode 100644 index 0000000000..3ab208c6c8 --- /dev/null +++ b/packages/state-transition/src/block/isValidIndexedPayloadAttestation.ts @@ -0,0 +1,23 @@ +import {gloas} from "@lodestar/types"; +import {getIndexedPayloadAttestationSignatureSet} from "../signatureSets/index.ts"; +import {CachedBeaconStateGloas} from "../types.js"; +import {verifySignatureSet} from "../util/index.ts"; + +export function isValidIndexedPayloadAttestation( + state: CachedBeaconStateGloas, + indexedPayloadAttestation: gloas.IndexedPayloadAttestation, + verifySignature: boolean +): boolean { + const indices = indexedPayloadAttestation.attestingIndices; + const isSorted = indices.every((val, i, arr) => i === 0 || arr[i - 1] <= val); + + if (indices.length === 0 || !isSorted) { + return false; + } + + if (verifySignature) { + return verifySignatureSet(getIndexedPayloadAttestationSignatureSet(state, indexedPayloadAttestation)); + } + + return true; +} diff --git a/packages/state-transition/src/block/processAttestationPhase0.ts b/packages/state-transition/src/block/processAttestationPhase0.ts index 3a8ed7f8f4..2c3732ecc3 100644 --- a/packages/state-transition/src/block/processAttestationPhase0.ts +++ b/packages/state-transition/src/block/processAttestationPhase0.ts @@ -86,7 +86,11 @@ export function validateAttestation(fork: ForkSeq, state: CachedBeaconStateAllFo } if (fork >= ForkSeq.electra) { - assert.equal(data.index, 0, `AttestationData.index must be zero: index=${data.index}`); + if (fork >= ForkSeq.gloas) { + assert.lt(data.index, 2, `AttestationData.index must be 0 or 1: index=${data.index}`); + } else { + assert.equal(data.index, 0, `AttestationData.index must be 0: index=${data.index}`); + } const attestationElectra = attestation as electra.Attestation; const committeeIndices = attestationElectra.committeeBits.getTrueBitIndexes(); diff --git a/packages/state-transition/src/block/processAttestationsAltair.ts b/packages/state-transition/src/block/processAttestationsAltair.ts index 7b6654fba6..1018f2d774 100644 --- a/packages/state-transition/src/block/processAttestationsAltair.ts +++ b/packages/state-transition/src/block/processAttestationsAltair.ts @@ -1,9 +1,11 @@ import {byteArrayEquals} from "@chainsafe/ssz"; import { + EFFECTIVE_BALANCE_INCREMENT, ForkSeq, MIN_ATTESTATION_INCLUSION_DELAY, PROPOSER_WEIGHT, SLOTS_PER_EPOCH, + SLOTS_PER_HISTORICAL_ROOT, TIMELY_HEAD_FLAG_INDEX, TIMELY_HEAD_WEIGHT, TIMELY_SOURCE_FLAG_INDEX, @@ -16,7 +18,8 @@ import {Attestation, Epoch, phase0} from "@lodestar/types"; import {intSqrt} from "@lodestar/utils"; import {BeaconStateTransitionMetrics} from "../metrics.js"; import {getAttestationWithIndicesSignatureSet} from "../signatureSets/indexedAttestation.js"; -import {CachedBeaconStateAltair} from "../types.js"; +import {CachedBeaconStateAltair, CachedBeaconStateGloas} from "../types.js"; +import {isAttestationSameSlot, isAttestationSameSlotRootCache} from "../util/gloas.ts"; import {increaseBalance, verifySignatureSet} from "../util/index.js"; import {RootCache} from "../util/rootCache.js"; import {checkpointToStr, isTimelyTarget, validateAttestation} from "./processAttestationPhase0.js"; @@ -31,7 +34,7 @@ const SLOTS_PER_EPOCH_SQRT = intSqrt(SLOTS_PER_EPOCH); export function processAttestationsAltair( fork: ForkSeq, - state: CachedBeaconStateAltair, + state: CachedBeaconStateAltair | CachedBeaconStateGloas, attestations: Attestation[], verifySignature = true, metrics?: BeaconStateTransitionMetrics | null @@ -46,6 +49,9 @@ export function processAttestationsAltair( let proposerReward = 0; let newSeenAttesters = 0; let newSeenAttestersEffectiveBalance = 0; + + const builderWeightMap: Map = new Map(); + for (const attestation of attestations) { const data = attestation.data; @@ -66,13 +72,16 @@ export function processAttestationsAltair( const inCurrentEpoch = data.target.epoch === currentEpoch; const epochParticipation = inCurrentEpoch ? state.currentEpochParticipation : state.previousEpochParticipation; + // Count how much additional weight added to current or previous epoch's builder pending payment (in ETH increment) + let paymentWeightToAdd = 0; const flagsAttestation = getAttestationParticipationStatus( fork, data, stateSlot - data.slot, epochCtx.epoch, - rootCache + rootCache, + fork >= ForkSeq.gloas ? (state as CachedBeaconStateGloas).executionPayloadAvailability.toBoolArray() : null ); // For each participant, update their participation @@ -121,12 +130,35 @@ export function processAttestationsAltair( } } } + + if (fork >= ForkSeq.gloas && flagsNewSet !== 0 && isAttestationSameSlot(state as CachedBeaconStateGloas, data)) { + paymentWeightToAdd += effectiveBalanceIncrements[validatorIndex]; + } } // Do the discrete math inside the loop to ensure a deterministic result const totalIncrements = totalBalanceIncrementsWithWeight; const proposerRewardNumerator = totalIncrements * state.epochCtx.baseRewardPerIncrement; proposerReward += Math.floor(proposerRewardNumerator / PROPOSER_REWARD_DOMINATOR); + + if (fork >= ForkSeq.gloas) { + const builderPendingPaymentIndex = inCurrentEpoch + ? SLOTS_PER_EPOCH + (data.slot % SLOTS_PER_EPOCH) + : data.slot % SLOTS_PER_EPOCH; + + const existingWeight = + builderWeightMap.get(builderPendingPaymentIndex) ?? + (state as CachedBeaconStateGloas).builderPendingPayments.get(builderPendingPaymentIndex).weight; + const updatedWeight = existingWeight + paymentWeightToAdd * EFFECTIVE_BALANCE_INCREMENT; + builderWeightMap.set(builderPendingPaymentIndex, updatedWeight); + } + } + + for (const [index, weight] of builderWeightMap) { + const payment = (state as CachedBeaconStateGloas).builderPendingPayments.get(index); + if (payment.withdrawal.amount > 0) { + payment.weight = weight; + } } metrics?.newSeenAttestersPerBlock.set(newSeenAttesters); @@ -145,7 +177,8 @@ export function getAttestationParticipationStatus( data: phase0.AttestationData, inclusionDelay: number, currentEpoch: Epoch, - rootCache: RootCache + rootCache: RootCache, + executionPayloadAvailability: boolean[] | null ): number { const justifiedCheckpoint = data.target.epoch === currentEpoch ? rootCache.currentJustifiedCheckpoint : rootCache.previousJustifiedCheckpoint; @@ -168,9 +201,33 @@ export function getAttestationParticipationStatus( const isMatchingTarget = byteArrayEquals(data.target.root, rootCache.getBlockRoot(data.target.epoch)); // a timely head is only be set if the target is _also_ matching - const isMatchingHead = + // In gloas, this is called `head_root_matches` + let isMatchingHead = isMatchingTarget && byteArrayEquals(data.beaconBlockRoot, rootCache.getBlockRootAtSlot(data.slot)); + if (fork >= ForkSeq.gloas) { + let isMatchingPayload = false; + + if (isAttestationSameSlotRootCache(rootCache, data)) { + if (data.index !== 0) { + throw new Error("Attesting same slot must indicate empty payload"); + } + isMatchingPayload = true; + } else { + if (executionPayloadAvailability === null) { + throw new Error("Must supply executionPayloadAvailability post-gloas"); + } + + if (data.index !== 0 && data.index !== 1) { + throw new Error(`data index must be 0 or 1 index=${data.index}`); + } + + isMatchingPayload = Boolean(data.index) === executionPayloadAvailability[data.slot % SLOTS_PER_HISTORICAL_ROOT]; + } + + isMatchingHead = isMatchingHead && isMatchingPayload; + } + let flags = 0; if (isMatchingSource && inclusionDelay <= SLOTS_PER_EPOCH_SQRT) flags |= TIMELY_SOURCE; if (isMatchingTarget && isTimelyTarget(fork, inclusionDelay)) flags |= TIMELY_TARGET; diff --git a/packages/state-transition/src/block/processConsolidationRequest.ts b/packages/state-transition/src/block/processConsolidationRequest.ts index 02a40dbac6..8860b41c5b 100644 --- a/packages/state-transition/src/block/processConsolidationRequest.ts +++ b/packages/state-transition/src/block/processConsolidationRequest.ts @@ -1,6 +1,6 @@ -import {FAR_FUTURE_EPOCH, MIN_ACTIVATION_BALANCE, PENDING_CONSOLIDATIONS_LIMIT} from "@lodestar/params"; +import {FAR_FUTURE_EPOCH, ForkSeq, MIN_ACTIVATION_BALANCE, PENDING_CONSOLIDATIONS_LIMIT} from "@lodestar/params"; import {electra, ssz} from "@lodestar/types"; -import {CachedBeaconStateElectra} from "../types.js"; +import {CachedBeaconStateElectra, CachedBeaconStateGloas} from "../types.js"; import {hasEth1WithdrawalCredential} from "../util/capella.js"; import { hasCompoundingWithdrawalCredential, @@ -13,7 +13,8 @@ import {getConsolidationChurnLimit, getPendingBalanceToWithdraw, isActiveValidat // TODO Electra: Clean up necessary as there is a lot of overlap with isValidSwitchToCompoundRequest export function processConsolidationRequest( - state: CachedBeaconStateElectra, + fork: ForkSeq, + state: CachedBeaconStateElectra | CachedBeaconStateGloas, consolidationRequest: electra.ConsolidationRequest ): void { const {sourcePubkey, targetPubkey, sourceAddress} = consolidationRequest; @@ -82,7 +83,7 @@ export function processConsolidationRequest( } // Verify the source has no pending withdrawals in the queue - if (getPendingBalanceToWithdraw(state, sourceIndex) > 0) { + if (getPendingBalanceToWithdraw(fork, state, sourceIndex) > 0) { return; } @@ -103,7 +104,7 @@ export function processConsolidationRequest( * Determine if we should set consolidation target validator to compounding credential */ function isValidSwitchToCompoundRequest( - state: CachedBeaconStateElectra, + state: CachedBeaconStateElectra | CachedBeaconStateGloas, consolidationRequest: electra.ConsolidationRequest ): boolean { const {sourcePubkey, targetPubkey, sourceAddress} = consolidationRequest; diff --git a/packages/state-transition/src/block/processDepositRequest.ts b/packages/state-transition/src/block/processDepositRequest.ts index b4054640ab..96172eec86 100644 --- a/packages/state-transition/src/block/processDepositRequest.ts +++ b/packages/state-transition/src/block/processDepositRequest.ts @@ -1,8 +1,11 @@ import {UNSET_DEPOSIT_REQUESTS_START_INDEX} from "@lodestar/params"; import {electra, ssz} from "@lodestar/types"; -import {CachedBeaconStateElectra} from "../types.js"; +import {CachedBeaconStateElectra, CachedBeaconStateGloas} from "../types.js"; -export function processDepositRequest(state: CachedBeaconStateElectra, depositRequest: electra.DepositRequest): void { +export function processDepositRequest( + state: CachedBeaconStateElectra | CachedBeaconStateGloas, + depositRequest: electra.DepositRequest +): void { if (state.depositRequestsStartIndex === UNSET_DEPOSIT_REQUESTS_START_INDEX) { state.depositRequestsStartIndex = depositRequest.index; } diff --git a/packages/state-transition/src/block/processExecutionPayloadBid.ts b/packages/state-transition/src/block/processExecutionPayloadBid.ts new file mode 100644 index 0000000000..a4bcbf23af --- /dev/null +++ b/packages/state-transition/src/block/processExecutionPayloadBid.ts @@ -0,0 +1,120 @@ +import {PublicKey, Signature, verify} from "@chainsafe/blst"; +import {byteArrayEquals} from "@chainsafe/ssz"; +import { + DOMAIN_BEACON_BUILDER, + FAR_FUTURE_EPOCH, + ForkPostGloas, + MIN_ACTIVATION_BALANCE, + SLOTS_PER_EPOCH, +} from "@lodestar/params"; +import {BeaconBlock, gloas, ssz} from "@lodestar/types"; +import {toHex, toRootHex} from "@lodestar/utils"; +import {G2_POINT_AT_INFINITY} from "../constants/constants.ts"; +import {CachedBeaconStateGloas} from "../types.ts"; +import {hasBuilderWithdrawalCredential} from "../util/gloas.ts"; +import {computeSigningRoot, getCurrentEpoch, getRandaoMix, isActiveValidator} from "../util/index.ts"; + +export function processExecutionPayloadBid(state: CachedBeaconStateGloas, block: BeaconBlock): void { + const signedBid = block.body.signedExecutionPayloadBid; + const bid = signedBid.message; + const {builderIndex, value: amount} = bid; + const builder = state.validators.getReadonly(builderIndex); + + // For self-builds, amount must be zero regardless of withdrawal credential prefix + if (builderIndex === block.proposerIndex) { + if (amount !== 0) { + throw Error(`Invalid execution payload bid: self-build with non-zero amount ${amount}`); + } + if (!byteArrayEquals(signedBid.signature, G2_POINT_AT_INFINITY)) { + throw Error("Invalid execution payload bid: self-build with non-zero signature"); + } + // Non-self builds require builder withdrawal credential + } else { + if (!hasBuilderWithdrawalCredential(builder.withdrawalCredentials)) { + throw Error(`Invalid execution payload bid: builder ${builderIndex} does not have builder withdrawal credential`); + } + + if (!verifyExecutionPayloadBidSignature(state, builder.pubkey, signedBid)) { + throw Error(`Invalid execution payload bid: invalid signature for builder ${builderIndex}`); + } + } + + if (!isActiveValidator(builder, getCurrentEpoch(state))) { + throw Error(`Invalid execution payload bid: builder ${builderIndex} is not active`); + } + + if (builder.slashed) { + throw Error(`Invalid execution payload bid: builder ${builderIndex} is slashed`); + } + + const pendingPayments = state.builderPendingPayments + .getAllReadonly() + .filter((payment) => payment.withdrawal.builderIndex === builderIndex) + .reduce((acc, payment) => acc + payment.withdrawal.amount, 0); + const pendingWithdrawals = state.builderPendingWithdrawals + .getAllReadonly() + .filter((withdrawal) => withdrawal.builderIndex === builderIndex) + .reduce((acc, withdrawal) => acc + withdrawal.amount, 0); + + if ( + amount !== 0 && + state.balances.get(builderIndex) < amount + pendingPayments + pendingWithdrawals + MIN_ACTIVATION_BALANCE + ) { + throw Error("Insufficient builder balance"); + } + + if (bid.slot !== block.slot) { + throw Error(`Bid slot ${bid.slot} does not match block slot ${block.slot}`); + } + + if (!byteArrayEquals(bid.parentBlockHash, state.latestBlockHash)) { + throw Error( + `Parent block hash ${toRootHex(bid.parentBlockHash)} of bid does not match state's latest block hash ${toRootHex(state.latestBlockHash)}` + ); + } + + if (!byteArrayEquals(bid.parentBlockRoot, block.parentRoot)) { + throw Error( + `Parent block root ${toRootHex(bid.parentBlockRoot)} of bid does not match block's parent root ${toRootHex(block.parentRoot)}` + ); + } + + const stateRandao = getRandaoMix(state, getCurrentEpoch(state)); + if (!byteArrayEquals(bid.prevRandao, stateRandao)) { + throw Error(`Prev randao ${toHex(bid.prevRandao)} of bid does not match state's randao mix ${toHex(stateRandao)}`); + } + + if (amount > 0) { + const pendingPaymentView = ssz.gloas.BuilderPendingPayment.toViewDU({ + weight: 0, + withdrawal: ssz.gloas.BuilderPendingWithdrawal.toViewDU({ + feeRecipient: bid.feeRecipient, + amount, + builderIndex, + withdrawableEpoch: FAR_FUTURE_EPOCH, + }), + }); + + state.builderPendingPayments.set(SLOTS_PER_EPOCH + (bid.slot % SLOTS_PER_EPOCH), pendingPaymentView); + } + + state.latestExecutionPayloadBid = ssz.gloas.ExecutionPayloadBid.toViewDU(bid); +} + +function verifyExecutionPayloadBidSignature( + state: CachedBeaconStateGloas, + pubkey: Uint8Array, + signedBid: gloas.SignedExecutionPayloadBid +): boolean { + const domain = state.config.getDomain(state.slot, DOMAIN_BEACON_BUILDER); + const signingRoot = computeSigningRoot(ssz.gloas.ExecutionPayloadBid, signedBid.message, domain); + + try { + const publicKey = PublicKey.fromBytes(pubkey); + const signature = Signature.fromBytes(signedBid.signature, true); + + return verify(signingRoot, publicKey, signature); + } catch (_e) { + return false; // Catch all BLS errors: failed key validation, failed signature validation, invalid signature + } +} diff --git a/packages/state-transition/src/block/processExecutionPayloadEnvelope.ts b/packages/state-transition/src/block/processExecutionPayloadEnvelope.ts new file mode 100644 index 0000000000..c9ef937af3 --- /dev/null +++ b/packages/state-transition/src/block/processExecutionPayloadEnvelope.ts @@ -0,0 +1,181 @@ +import {PublicKey, Signature, verify} from "@chainsafe/blst"; +import {byteArrayEquals} from "@chainsafe/ssz"; +import {DOMAIN_BEACON_BUILDER, SLOTS_PER_EPOCH, SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params"; +import {gloas, ssz} from "@lodestar/types"; +import {toHex, toRootHex} from "@lodestar/utils"; +import {CachedBeaconStateGloas} from "../types.ts"; +import {computeExitEpochAndUpdateChurn, computeSigningRoot, computeTimeAtSlot} from "../util/index.ts"; +import {processConsolidationRequest} from "./processConsolidationRequest.ts"; +import {processDepositRequest} from "./processDepositRequest.ts"; +import {processWithdrawalRequest} from "./processWithdrawalRequest.ts"; + +// This function does not call execution engine to verify payload. Need to call it from other place +export function processExecutionPayloadEnvelope( + state: CachedBeaconStateGloas, + signedEnvelope: gloas.SignedExecutionPayloadEnvelope, + verify: boolean +): void { + const envelope = signedEnvelope.message; + const payload = envelope.payload; + const fork = state.config.getForkSeq(envelope.slot); + + if (verify) { + const builderIndex = envelope.builderIndex; + const pubkey = state.validators.getReadonly(builderIndex).pubkey; + + if (!verifyExecutionPayloadEnvelopeSignature(state, pubkey, signedEnvelope)) { + throw new Error("Payload Envelope has invalid signature"); + } + } + + validateExecutionPayloadEnvelope(state, envelope); + + const requests = envelope.executionRequests; + + for (const deposit of requests.deposits) { + processDepositRequest(state, deposit); + } + + for (const withdrawal of requests.withdrawals) { + processWithdrawalRequest(fork, state, withdrawal); + } + + for (const consolidation of requests.consolidations) { + processConsolidationRequest(fork, state, consolidation); + } + + // Queue the builder payment + const paymentIndex = SLOTS_PER_EPOCH + (state.slot % SLOTS_PER_EPOCH); + const payment = state.builderPendingPayments.get(paymentIndex).clone(); + const amount = payment.withdrawal.amount; + + if (amount > 0) { + const exitQueueEpoch = computeExitEpochAndUpdateChurn(state, BigInt(amount)); + + payment.withdrawal.withdrawableEpoch = exitQueueEpoch + state.config.MIN_VALIDATOR_WITHDRAWABILITY_DELAY; + state.builderPendingWithdrawals.push(payment.withdrawal); + } + + state.builderPendingPayments.set(paymentIndex, ssz.gloas.BuilderPendingPayment.defaultViewDU()); + + // Cache the execution payload hash + state.executionPayloadAvailability.set(state.slot % SLOTS_PER_HISTORICAL_ROOT, true); + state.latestBlockHash = payload.blockHash; + + if (verify && !byteArrayEquals(envelope.stateRoot, state.hashTreeRoot())) { + throw new Error( + `Envelope's state root does not match state envelope=${toRootHex(envelope.stateRoot)} state=${toRootHex(state.hashTreeRoot())}` + ); + } +} + +function validateExecutionPayloadEnvelope( + state: CachedBeaconStateGloas, + envelope: gloas.ExecutionPayloadEnvelope +): void { + const payload = envelope.payload; + + if (byteArrayEquals(state.latestBlockHeader.stateRoot, ssz.Root.defaultValue())) { + const previousStateRoot = state.hashTreeRoot(); + state.latestBlockHeader.stateRoot = previousStateRoot; + } + + // Verify consistency with the beacon block + if (!byteArrayEquals(envelope.beaconBlockRoot, state.latestBlockHeader.hashTreeRoot())) { + throw new Error( + `Envelope's block is not the latest block header envelope=${toRootHex(envelope.beaconBlockRoot)} latestBlockHeader=${toRootHex(state.latestBlockHeader.hashTreeRoot())}` + ); + } + + // Verify consistency with the beacon block + if (envelope.slot !== state.slot) { + throw new Error(`Slot mismatch between envelope and state envelope=${envelope.slot} state=${state.slot}`); + } + + const committedBid = state.latestExecutionPayloadBid; + // Verify consistency with the committed bid + if (envelope.builderIndex !== committedBid.builderIndex) { + throw new Error( + `Builder index mismatch between envelope and committed bid envelope=${envelope.builderIndex} committedBid=${committedBid.builderIndex}` + ); + } + + // Verify consistency with the committed bid + const envelopeKzgRoot = ssz.deneb.BlobKzgCommitments.hashTreeRoot(envelope.blobKzgCommitments); + if (!byteArrayEquals(committedBid.blobKzgCommitmentsRoot, envelopeKzgRoot)) { + throw new Error( + `Kzg commitment root mismatch between envelope and committed bid envelope=${toRootHex(envelopeKzgRoot)} committedBid=${toRootHex(committedBid.blobKzgCommitmentsRoot)}` + ); + } + + // Verify the withdrawals root + const envelopeWithdrawalsRoot = ssz.capella.Withdrawals.hashTreeRoot(envelope.payload.withdrawals); + if (!byteArrayEquals(state.latestWithdrawalsRoot, envelopeWithdrawalsRoot)) { + throw new Error( + `Withdrawals root mismatch between envelope and latest withdrawals root envelope=${toRootHex(envelopeWithdrawalsRoot)} latestWithdrawalRoot=${toRootHex(state.latestWithdrawalsRoot)}` + ); + } + + // Verify the gas_limit + if (Number(committedBid.gasLimit) !== payload.gasLimit) { + throw new Error( + `Gas limit mismatch between envelope's payload and committed bid envelope=${payload.gasLimit} committedBid=${Number(committedBid.gasLimit)}` + ); + } + + // Verify the block hash + if (!byteArrayEquals(committedBid.blockHash, payload.blockHash)) { + throw new Error( + `Block hash mismatch between envelope's payload and committed bid envelope=${toRootHex(payload.blockHash)} committedBid=${toRootHex(committedBid.blockHash)}` + ); + } + + // Verify consistency of the parent hash with respect to the previous execution payload + if (!byteArrayEquals(payload.parentHash, state.latestBlockHash)) { + throw new Error( + `Parent hash mismatch between envelope's payload and state envelope=${toRootHex(payload.parentHash)} state=${toRootHex(state.latestBlockHash)}` + ); + } + + // Verify prev_randao matches committed bid + if (!byteArrayEquals(committedBid.prevRandao, payload.prevRandao)) { + throw new Error( + `Prev randao mismatch between committed bid and payload committedBid=${toHex(committedBid.prevRandao)} payload=${toHex(payload.prevRandao)}` + ); + } + + // Verify timestamp + if (payload.timestamp !== computeTimeAtSlot(state.config, state.slot, state.genesisTime)) { + throw new Error( + `Timestamp mismatch between envelope's payload and state envelope=${payload.timestamp} state=${computeTimeAtSlot(state.config, state.slot, state.genesisTime)}` + ); + } + + // Verify commitments are under limit + const maxBlobsPerBlock = state.config.getMaxBlobsPerBlock(state.epochCtx.epoch); + if (envelope.blobKzgCommitments.length > maxBlobsPerBlock) { + throw new Error( + `Kzg commitments exceed limit commitment.length=${envelope.blobKzgCommitments.length} limit=${maxBlobsPerBlock}` + ); + } + + // Skipped: Verify the execution payload is valid +} + +function verifyExecutionPayloadEnvelopeSignature( + state: CachedBeaconStateGloas, + pubkey: Uint8Array, + signedEnvelope: gloas.SignedExecutionPayloadEnvelope +): boolean { + const domain = state.config.getDomain(state.slot, DOMAIN_BEACON_BUILDER); + const signingRoot = computeSigningRoot(ssz.gloas.ExecutionPayloadEnvelope, signedEnvelope.message, domain); + + try { + const publicKey = PublicKey.fromBytes(pubkey); + const signature = Signature.fromBytes(signedEnvelope.signature, true); + + return verify(signingRoot, publicKey, signature); + } catch (_e) { + return false; // Catch all BLS errors: failed key validation, failed signature validation, invalid signature + } +} diff --git a/packages/state-transition/src/block/processOperations.ts b/packages/state-transition/src/block/processOperations.ts index 76a9494438..3309111044 100644 --- a/packages/state-transition/src/block/processOperations.ts +++ b/packages/state-transition/src/block/processOperations.ts @@ -1,7 +1,12 @@ import {ForkSeq} from "@lodestar/params"; -import {BeaconBlockBody, capella, electra} from "@lodestar/types"; +import {BeaconBlockBody, capella, electra, gloas} from "@lodestar/types"; import {BeaconStateTransitionMetrics} from "../metrics.js"; -import {CachedBeaconStateAllForks, CachedBeaconStateCapella, CachedBeaconStateElectra} from "../types.js"; +import { + CachedBeaconStateAllForks, + CachedBeaconStateCapella, + CachedBeaconStateElectra, + CachedBeaconStateGloas, +} from "../types.js"; import {getEth1DepositCount} from "../util/deposit.js"; import {processAttestations} from "./processAttestations.js"; import {processAttesterSlashing} from "./processAttesterSlashing.js"; @@ -9,6 +14,7 @@ import {processBlsToExecutionChange} from "./processBlsToExecutionChange.js"; import {processConsolidationRequest} from "./processConsolidationRequest.js"; import {processDeposit} from "./processDeposit.js"; import {processDepositRequest} from "./processDepositRequest.js"; +import {processPayloadAttestation} from "./processPayloadAttestation.ts"; import {processProposerSlashing} from "./processProposerSlashing.js"; import {processVoluntaryExit} from "./processVoluntaryExit.js"; import {processWithdrawalRequest} from "./processWithdrawalRequest.js"; @@ -64,7 +70,7 @@ export function processOperations( } } - if (fork >= ForkSeq.electra) { + if (fork >= ForkSeq.electra && fork < ForkSeq.gloas) { const stateElectra = state as CachedBeaconStateElectra; const bodyElectra = body as electra.BeaconBlockBody; @@ -77,7 +83,13 @@ export function processOperations( } for (const elConsolidationRequest of bodyElectra.executionRequests.consolidations) { - processConsolidationRequest(stateElectra, elConsolidationRequest); + processConsolidationRequest(fork, stateElectra, elConsolidationRequest); + } + } + + if (fork >= ForkSeq.gloas) { + for (const payloadAttestation of (body as gloas.BeaconBlockBody).payloadAttestations) { + processPayloadAttestation(state as CachedBeaconStateGloas, payloadAttestation); } } } diff --git a/packages/state-transition/src/block/processPayloadAttestation.ts b/packages/state-transition/src/block/processPayloadAttestation.ts new file mode 100644 index 0000000000..de6c5980bb --- /dev/null +++ b/packages/state-transition/src/block/processPayloadAttestation.ts @@ -0,0 +1,25 @@ +import {byteArrayEquals} from "@chainsafe/ssz"; +import {gloas} from "@lodestar/types"; +import {CachedBeaconStateGloas} from "../types.ts"; +import {isValidIndexedPayloadAttestation} from "./isValidIndexedPayloadAttestation.ts"; + +export function processPayloadAttestation( + state: CachedBeaconStateGloas, + payloadAttestation: gloas.PayloadAttestation +): void { + const data = payloadAttestation.data; + + if (!byteArrayEquals(data.beaconBlockRoot, state.latestBlockHeader.parentRoot)) { + throw Error("Payload attestation is referring to the wrong block"); + } + + if (data.slot + 1 !== state.slot) { + throw Error("Payload attestation is not from previous slot"); + } + + const indexedPayloadAttestation = state.epochCtx.getIndexedPayloadAttestation(data.slot, payloadAttestation); + + if (!isValidIndexedPayloadAttestation(state, indexedPayloadAttestation, true)) { + throw Error("Invalid payload attestation"); + } +} diff --git a/packages/state-transition/src/block/processProposerSlashing.ts b/packages/state-transition/src/block/processProposerSlashing.ts index c65fd567aa..b06ac0a780 100644 --- a/packages/state-transition/src/block/processProposerSlashing.ts +++ b/packages/state-transition/src/block/processProposerSlashing.ts @@ -1,8 +1,8 @@ -import {ForkSeq} from "@lodestar/params"; +import {ForkSeq, SLOTS_PER_EPOCH} from "@lodestar/params"; import {phase0, ssz} from "@lodestar/types"; import {getProposerSlashingSignatureSets} from "../signatureSets/index.js"; -import {CachedBeaconStateAllForks} from "../types.js"; -import {isSlashableValidator} from "../util/index.js"; +import {CachedBeaconStateAllForks, CachedBeaconStateGloas} from "../types.js"; +import {computeEpochAtSlot, isSlashableValidator} from "../util/index.js"; import {verifySignatureSet} from "../util/signatureSets.js"; import {slashValidator} from "./slashValidator.js"; @@ -20,6 +20,27 @@ export function processProposerSlashing( ): void { assertValidProposerSlashing(state, proposerSlashing, verifySignatures); + if (fork >= ForkSeq.gloas) { + const slot = Number(proposerSlashing.signedHeader1.message.slot); + const proposalEpoch = computeEpochAtSlot(slot); + const currentEpoch = state.epochCtx.epoch; + const previousEpoch = currentEpoch - 1; + + const paymentIndex = + proposalEpoch === currentEpoch + ? SLOTS_PER_EPOCH + (slot % SLOTS_PER_EPOCH) + : proposalEpoch === previousEpoch + ? slot % SLOTS_PER_EPOCH + : undefined; + + if (paymentIndex !== undefined) { + (state as CachedBeaconStateGloas).builderPendingPayments.set( + paymentIndex, + ssz.gloas.BuilderPendingPayment.defaultViewDU() + ); + } + } + slashValidator(fork, state, proposerSlashing.signedHeader1.message.proposerIndex); } diff --git a/packages/state-transition/src/block/processVoluntaryExit.ts b/packages/state-transition/src/block/processVoluntaryExit.ts index 12c2c5be68..ca90fec477 100644 --- a/packages/state-transition/src/block/processVoluntaryExit.ts +++ b/packages/state-transition/src/block/processVoluntaryExit.ts @@ -69,7 +69,7 @@ export function getVoluntaryExitValidity( // only exit validator if it has no pending withdrawals in the queue if ( fork >= ForkSeq.electra && - getPendingBalanceToWithdraw(state as CachedBeaconStateElectra, voluntaryExit.validatorIndex) !== 0 + getPendingBalanceToWithdraw(fork, state as CachedBeaconStateElectra, voluntaryExit.validatorIndex) !== 0 ) { return VoluntaryExitValidity.pendingWithdrawals; } diff --git a/packages/state-transition/src/block/processWithdrawalRequest.ts b/packages/state-transition/src/block/processWithdrawalRequest.ts index e5bad1b6cb..435f99d054 100644 --- a/packages/state-transition/src/block/processWithdrawalRequest.ts +++ b/packages/state-transition/src/block/processWithdrawalRequest.ts @@ -7,7 +7,7 @@ import { } from "@lodestar/params"; import {electra, phase0, ssz} from "@lodestar/types"; import {toHex} from "@lodestar/utils"; -import {CachedBeaconStateElectra} from "../types.js"; +import {CachedBeaconStateElectra, CachedBeaconStateGloas} from "../types.js"; import {hasCompoundingWithdrawalCredential, hasExecutionWithdrawalCredential} from "../util/electra.js"; import {computeExitEpochAndUpdateChurn} from "../util/epoch.js"; import {getPendingBalanceToWithdraw, isActiveValidator} from "../util/validator.js"; @@ -15,7 +15,7 @@ import {initiateValidatorExit} from "./initiateValidatorExit.js"; export function processWithdrawalRequest( fork: ForkSeq, - state: CachedBeaconStateElectra, + state: CachedBeaconStateElectra | CachedBeaconStateGloas, withdrawalRequest: electra.WithdrawalRequest ): void { const amount = Number(withdrawalRequest.amount); @@ -42,7 +42,7 @@ export function processWithdrawalRequest( } // TODO Electra: Consider caching pendingPartialWithdrawals - const pendingBalanceToWithdraw = getPendingBalanceToWithdraw(state, validatorIndex); + const pendingBalanceToWithdraw = getPendingBalanceToWithdraw(fork, state, validatorIndex); const validatorBalance = state.balances.get(validatorIndex); if (isFullExitRequest) { @@ -81,7 +81,7 @@ export function processWithdrawalRequest( function isValidatorEligibleForWithdrawOrExit( validator: phase0.Validator, sourceAddress: Uint8Array, - state: CachedBeaconStateElectra + state: CachedBeaconStateElectra | CachedBeaconStateGloas ): boolean { const {withdrawalCredentials} = validator; const addressStr = toHex(withdrawalCredentials.subarray(12)); diff --git a/packages/state-transition/src/block/processWithdrawals.ts b/packages/state-transition/src/block/processWithdrawals.ts index cb242522e3..1a8fb40d65 100644 --- a/packages/state-transition/src/block/processWithdrawals.ts +++ b/packages/state-transition/src/block/processWithdrawals.ts @@ -10,7 +10,8 @@ import { } from "@lodestar/params"; import {ValidatorIndex, capella, ssz} from "@lodestar/types"; import {MapDef, toRootHex} from "@lodestar/utils"; -import {CachedBeaconStateCapella, CachedBeaconStateElectra} from "../types.js"; +import {CachedBeaconStateCapella, CachedBeaconStateElectra, CachedBeaconStateGloas} from "../types.js"; +import {isBuilderPaymentWithdrawable, isParentBlockFull} from "../util/gloas.ts"; import { decreaseBalance, getMaxEffectiveBalance, @@ -21,31 +22,48 @@ import { export function processWithdrawals( fork: ForkSeq, - state: CachedBeaconStateCapella | CachedBeaconStateElectra, - payload: capella.FullOrBlindedExecutionPayload + state: CachedBeaconStateCapella | CachedBeaconStateElectra | CachedBeaconStateGloas, + payload?: capella.FullOrBlindedExecutionPayload ): void { + // Return early if the parent block is empty + if (fork >= ForkSeq.gloas && !isParentBlockFull(state as CachedBeaconStateGloas)) { + return; + } + // processedPartialWithdrawalsCount is withdrawals coming from EL since electra (EIP-7002) - const {withdrawals: expectedWithdrawals, processedPartialWithdrawalsCount} = getExpectedWithdrawals(fork, state); + // processedBuilderWithdrawalsCount is withdrawals coming from builder payment since gloas (EIP-7732) + const { + withdrawals: expectedWithdrawals, + processedPartialWithdrawalsCount, + processedBuilderWithdrawalsCount, + } = getExpectedWithdrawals(fork, state); const numWithdrawals = expectedWithdrawals.length; - if (isCapellaPayloadHeader(payload)) { - const expectedWithdrawalsRoot = ssz.capella.Withdrawals.hashTreeRoot(expectedWithdrawals); - const actualWithdrawalsRoot = payload.withdrawalsRoot; - if (!byteArrayEquals(expectedWithdrawalsRoot, actualWithdrawalsRoot)) { - throw Error( - `Invalid withdrawalsRoot of executionPayloadHeader, expected=${toRootHex( - expectedWithdrawalsRoot - )}, actual=${toRootHex(actualWithdrawalsRoot)}` - ); + // After gloas, withdrawals are verified later in processExecutionPayloadEnvelope + if (fork < ForkSeq.gloas) { + if (payload === undefined) { + throw Error("payload is required for pre-gloas processWithdrawals"); } - } else { - if (expectedWithdrawals.length !== payload.withdrawals.length) { - throw Error(`Invalid withdrawals length expected=${numWithdrawals} actual=${payload.withdrawals.length}`); - } - for (let i = 0; i < numWithdrawals; i++) { - const withdrawal = expectedWithdrawals[i]; - if (!ssz.capella.Withdrawal.equals(withdrawal, payload.withdrawals[i])) { - throw Error(`Withdrawal mismatch at index=${i}`); + + if (isCapellaPayloadHeader(payload)) { + const expectedWithdrawalsRoot = ssz.capella.Withdrawals.hashTreeRoot(expectedWithdrawals); + const actualWithdrawalsRoot = payload.withdrawalsRoot; + if (!byteArrayEquals(expectedWithdrawalsRoot, actualWithdrawalsRoot)) { + throw Error( + `Invalid withdrawalsRoot of executionPayloadHeader, expected=${toRootHex( + expectedWithdrawalsRoot + )}, actual=${toRootHex(actualWithdrawalsRoot)}` + ); + } + } else { + if (expectedWithdrawals.length !== payload.withdrawals.length) { + throw Error(`Invalid withdrawals length expected=${numWithdrawals} actual=${payload.withdrawals.length}`); + } + for (let i = 0; i < numWithdrawals; i++) { + const withdrawal = expectedWithdrawals[i]; + if (!ssz.capella.Withdrawal.equals(withdrawal, payload.withdrawals[i])) { + throw Error(`Withdrawal mismatch at index=${i}`); + } } } } @@ -62,6 +80,24 @@ export function processWithdrawals( ); } + if (fork >= ForkSeq.gloas) { + const stateGloas = state as CachedBeaconStateGloas; + stateGloas.latestWithdrawalsRoot = ssz.capella.Withdrawals.hashTreeRoot(expectedWithdrawals); + + const unprocessedWithdrawals = stateGloas.builderPendingWithdrawals + .getAllReadonly() + .slice(0, processedBuilderWithdrawalsCount) + .filter((w) => !isBuilderPaymentWithdrawable(stateGloas, w)); + const remainingWithdrawals = stateGloas.builderPendingWithdrawals + .sliceFrom(processedBuilderWithdrawalsCount) + .getAllReadonly(); + + stateGloas.builderPendingWithdrawals = ssz.gloas.BeaconState.fields.builderPendingWithdrawals.toViewDU([ + ...unprocessedWithdrawals, + ...remainingWithdrawals, + ]); + } + // Update the nextWithdrawalIndex const latestWithdrawal = expectedWithdrawals.at(-1); if (latestWithdrawal) { @@ -82,11 +118,12 @@ export function processWithdrawals( export function getExpectedWithdrawals( fork: ForkSeq, - state: CachedBeaconStateCapella | CachedBeaconStateElectra + state: CachedBeaconStateCapella | CachedBeaconStateElectra | CachedBeaconStateGloas ): { withdrawals: capella.Withdrawal[]; sampledValidators: number; processedPartialWithdrawalsCount: number; + processedBuilderWithdrawalsCount: number; } { if (fork < ForkSeq.capella) { throw new Error(`getExpectedWithdrawals not supported at forkSeq=${fork} < ForkSeq.capella`); @@ -99,17 +136,71 @@ export function getExpectedWithdrawals( const withdrawals: capella.Withdrawal[] = []; const withdrawnBalances = new MapDef(() => 0); const isPostElectra = fork >= ForkSeq.electra; + const isPostGloas = fork >= ForkSeq.gloas; // partialWithdrawalsCount is withdrawals coming from EL since electra (EIP-7002) let processedPartialWithdrawalsCount = 0; + // builderWithdrawalsCount is withdrawals coming from builder payments since Gloas (EIP-7732) + let processedBuilderWithdrawalsCount = 0; + + if (isPostGloas) { + const stateGloas = state as CachedBeaconStateGloas; + + const allBuilderPendingWithdrawals = + stateGloas.builderPendingWithdrawals.length <= MAX_WITHDRAWALS_PER_PAYLOAD + ? stateGloas.builderPendingWithdrawals.getAllReadonly() + : null; + + for (let i = 0; i < stateGloas.builderPendingWithdrawals.length; i++) { + const withdrawal = allBuilderPendingWithdrawals + ? allBuilderPendingWithdrawals[i] + : stateGloas.builderPendingWithdrawals.getReadonly(i); + + if (withdrawal.withdrawableEpoch > epoch || withdrawals.length + 1 === MAX_WITHDRAWALS_PER_PAYLOAD) { + break; + } + + if (isBuilderPaymentWithdrawable(stateGloas, withdrawal)) { + const totalWithdrawn = withdrawnBalances.getOrDefault(withdrawal.builderIndex); + const balance = state.balances.get(withdrawal.builderIndex) - totalWithdrawn; + const builder = state.validators.get(withdrawal.builderIndex); + + let withdrawableBalance = 0; + + if (builder.slashed) { + withdrawableBalance = balance < withdrawal.amount ? balance : withdrawal.amount; + } else if (balance > MIN_ACTIVATION_BALANCE) { + withdrawableBalance = + balance - MIN_ACTIVATION_BALANCE < withdrawal.amount ? balance - MIN_ACTIVATION_BALANCE : withdrawal.amount; + } + + if (withdrawableBalance > 0) { + withdrawals.push({ + index: withdrawalIndex, + validatorIndex: withdrawal.builderIndex, + address: withdrawal.feeRecipient, + amount: BigInt(withdrawableBalance), + }); + withdrawalIndex++; + withdrawnBalances.set(withdrawal.builderIndex, totalWithdrawn + withdrawableBalance); + } + } + processedBuilderWithdrawalsCount++; + } + } if (isPostElectra) { + // In pre-gloas, partialWithdrawalBound == MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP + const partialWithdrawalBound = Math.min( + withdrawals.length + MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP, + MAX_WITHDRAWALS_PER_PAYLOAD - 1 + ); const stateElectra = state as CachedBeaconStateElectra; // MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP = 8, PENDING_PARTIAL_WITHDRAWALS_LIMIT: 134217728 so we should only call getAllReadonly() if it makes sense // pendingPartialWithdrawals comes from EIP-7002 smart contract where it takes fee so it's more likely than not validator is in correct condition to withdraw // also we may break early if withdrawableEpoch > epoch const allPendingPartialWithdrawals = - stateElectra.pendingPartialWithdrawals.length <= MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP + stateElectra.pendingPartialWithdrawals.length <= partialWithdrawalBound ? stateElectra.pendingPartialWithdrawals.getAllReadonly() : null; @@ -118,7 +209,7 @@ export function getExpectedWithdrawals( const withdrawal = allPendingPartialWithdrawals ? allPendingPartialWithdrawals[i] : stateElectra.pendingPartialWithdrawals.getReadonly(i); - if (withdrawal.withdrawableEpoch > epoch || withdrawals.length === MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP) { + if (withdrawal.withdrawableEpoch > epoch || withdrawals.length === partialWithdrawalBound) { break; } @@ -147,11 +238,11 @@ export function getExpectedWithdrawals( } } - const bound = Math.min(validators.length, MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP); + const withdrawalBound = Math.min(validators.length, MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP); let n = 0; // Just run a bounded loop max iterating over all withdrawals // however breaks out once we have MAX_WITHDRAWALS_PER_PAYLOAD - for (n = 0; n < bound; n++) { + for (n = 0; n < withdrawalBound; n++) { // Get next validator in turn const validatorIndex = (nextWithdrawalValidatorIndex + n) % validators.length; @@ -203,5 +294,5 @@ export function getExpectedWithdrawals( } } - return {withdrawals, sampledValidators: n, processedPartialWithdrawalsCount}; + return {withdrawals, sampledValidators: n, processedPartialWithdrawalsCount, processedBuilderWithdrawalsCount}; } diff --git a/packages/state-transition/src/cache/epochCache.ts b/packages/state-transition/src/cache/epochCache.ts index d4c3f4588c..63b23e0520 100644 --- a/packages/state-transition/src/cache/epochCache.ts +++ b/packages/state-transition/src/cache/epochCache.ts @@ -24,6 +24,7 @@ import { SyncPeriod, ValidatorIndex, electra, + gloas, phase0, } from "@lodestar/types"; import {LodestarError} from "@lodestar/utils"; @@ -46,6 +47,7 @@ import { getSeed, isActiveValidator, isAggregatorFromCommitteeLength, + naiveGetPayloadTimlinessCommitteeIndices, } from "../util/index.js"; import {computeBaseRewardPerIncrement, computeSyncParticipantReward} from "../util/syncCommittee.js"; import {sumTargetUnslashedBalanceIncrements} from "../util/targetUnslashedBalance.js"; @@ -59,7 +61,7 @@ import { computeSyncCommitteeCache, getSyncCommitteeCache, } from "./syncCommitteeCache.js"; -import {BeaconStateAllForks, BeaconStateAltair} from "./types.js"; +import {BeaconStateAllForks, BeaconStateAltair, BeaconStateGloas} from "./types.js"; /** `= PROPOSER_WEIGHT / (WEIGHT_DENOMINATOR - PROPOSER_WEIGHT)` */ export const PROPOSER_WEIGHT_FACTOR = PROPOSER_WEIGHT / (WEIGHT_DENOMINATOR - PROPOSER_WEIGHT); @@ -238,6 +240,10 @@ export class EpochCache { /** TODO: Indexed SyncCommitteeCache */ nextSyncCommitteeIndexed: SyncCommitteeCache; + // TODO GLOAS: See if we need to cached PTC for prev/next epoch + // PTC for current epoch + payloadTimelinessCommittee: ValidatorIndex[][]; + // TODO: Helper stats syncPeriod: SyncPeriod; @@ -276,6 +282,7 @@ export class EpochCache { previousTargetUnslashedBalanceIncrements: number; currentSyncCommitteeIndexed: SyncCommitteeCache; nextSyncCommitteeIndexed: SyncCommitteeCache; + payloadTimelinessCommittee: ValidatorIndex[][]; epoch: Epoch; syncPeriod: SyncPeriod; }) { @@ -307,6 +314,7 @@ export class EpochCache { this.previousTargetUnslashedBalanceIncrements = data.previousTargetUnslashedBalanceIncrements; this.currentSyncCommitteeIndexed = data.currentSyncCommitteeIndexed; this.nextSyncCommitteeIndexed = data.nextSyncCommitteeIndexed; + this.payloadTimelinessCommittee = data.payloadTimelinessCommittee; this.epoch = data.epoch; this.syncPeriod = data.syncPeriod; } @@ -485,6 +493,17 @@ export class EpochCache { nextSyncCommitteeIndexed = new SyncCommitteeCacheEmpty(); } + // Compute PTC for this epoch + let payloadTimelinessCommittee: ValidatorIndex[][] = []; + if (currentEpoch >= config.GLOAS_FORK_EPOCH) { + payloadTimelinessCommittee = naiveGetPayloadTimlinessCommitteeIndices( + state as BeaconStateGloas, + currentShuffling, + effectiveBalanceIncrements, + currentEpoch + ); + } + // Precompute churnLimit for efficient initiateValidatorExit() during block proposing MUST be recompute everytime the // active validator indices set changes in size. Validators change active status only when: // - validator.activation_epoch is set. Only changes in process_registry_updates() if validator can be activated. If @@ -559,6 +578,7 @@ export class EpochCache { currentTargetUnslashedBalanceIncrements, currentSyncCommitteeIndexed, nextSyncCommitteeIndexed, + payloadTimelinessCommittee: payloadTimelinessCommittee, epoch: currentEpoch, syncPeriod: computeSyncPeriodAtEpoch(currentEpoch), }); @@ -605,6 +625,7 @@ export class EpochCache { currentTargetUnslashedBalanceIncrements: this.currentTargetUnslashedBalanceIncrements, currentSyncCommitteeIndexed: this.currentSyncCommitteeIndexed, nextSyncCommitteeIndexed: this.nextSyncCommitteeIndexed, + payloadTimelinessCommittee: this.payloadTimelinessCommittee, epoch: this.epoch, syncPeriod: this.syncPeriod, }); @@ -750,6 +771,14 @@ export class EpochCache { const epochAfterUpcoming = upcomingEpoch + 1; this.proposersPrevEpoch = this.proposers; + if (upcomingEpoch >= this.config.GLOAS_FORK_EPOCH) { + this.payloadTimelinessCommittee = naiveGetPayloadTimlinessCommitteeIndices( + state as BeaconStateGloas, + this.currentShuffling, + this.effectiveBalanceIncrements, + upcomingEpoch + ); + } if (upcomingEpoch >= this.config.FULU_FORK_EPOCH) { // Populate proposer cache with lookahead from state const proposerLookahead = (state as CachedBeaconStateFulu).proposerLookahead.getAll(); @@ -1151,6 +1180,34 @@ export class EpochCache { isPostElectra(): boolean { return this.epoch >= this.config.ELECTRA_FORK_EPOCH; } + + getPayloadTimelinessCommittee(slot: Slot): ValidatorIndex[] { + const epoch = computeEpochAtSlot(slot); + + if (epoch < this.config.GLOAS_FORK_EPOCH) { + throw new Error("Payload Timeliness Committee is not available before gloas fork"); + } + + if (epoch === this.epoch) { + return this.payloadTimelinessCommittee[slot % SLOTS_PER_EPOCH]; + } + + throw new Error(`Payload Timeliness Committee is not available for slot=${slot}`); + } + + getIndexedPayloadAttestation( + slot: Slot, + payloadAttestation: gloas.PayloadAttestation + ): gloas.IndexedPayloadAttestation { + const payloadTimelinessCommittee = this.getPayloadTimelinessCommittee(slot); + const attestingIndices = payloadAttestation.aggregationBits.intersectValues(payloadTimelinessCommittee); + + return { + attestingIndices: attestingIndices.sort((a, b) => a - b), + data: payloadAttestation.data, + signature: payloadAttestation.signature, + }; + } } function getEffectiveBalanceIncrementsByteLen(validatorCount: number): number { diff --git a/packages/state-transition/src/epoch/index.ts b/packages/state-transition/src/epoch/index.ts index b9e77d5a43..562613b44b 100644 --- a/packages/state-transition/src/epoch/index.ts +++ b/packages/state-transition/src/epoch/index.ts @@ -12,9 +12,11 @@ import { CachedBeaconStateCapella, CachedBeaconStateElectra, CachedBeaconStateFulu, + CachedBeaconStateGloas, CachedBeaconStatePhase0, EpochTransitionCache, } from "../types.js"; +import {processBuilderPendingPayments} from "./processBuilderPendingPayments.ts"; import {processEffectiveBalanceUpdates} from "./processEffectiveBalanceUpdates.js"; import {processEth1DataReset} from "./processEth1DataReset.js"; import {processHistoricalRootsUpdate} from "./processHistoricalRootsUpdate.js"; @@ -53,6 +55,7 @@ export { processPendingDeposits, processPendingConsolidations, processProposerLookahead, + processBuilderPendingPayments, }; export {computeUnrealizedCheckpoints} from "./computeUnrealizedCheckpoints.js"; @@ -78,6 +81,7 @@ export enum EpochTransitionStep { processPendingDeposits = "processPendingDeposits", processPendingConsolidations = "processPendingConsolidations", processProposerLookahead = "processProposerLookahead", + processBuilderPendingPayments = "processBuilderPendingPayments", } export function processEpoch( @@ -154,6 +158,14 @@ export function processEpoch( } } + if (fork >= ForkSeq.gloas) { + const timer = metrics?.epochTransitionStepTime.startTimer({ + step: EpochTransitionStep.processBuilderPendingPayments, + }); + processBuilderPendingPayments(state as CachedBeaconStateGloas); + timer?.(); + } + { const timer = metrics?.epochTransitionStepTime.startTimer({ step: EpochTransitionStep.processEffectiveBalanceUpdates, diff --git a/packages/state-transition/src/epoch/processBuilderPendingPayments.ts b/packages/state-transition/src/epoch/processBuilderPendingPayments.ts new file mode 100644 index 0000000000..c6f76bc67d --- /dev/null +++ b/packages/state-transition/src/epoch/processBuilderPendingPayments.ts @@ -0,0 +1,31 @@ +import {SLOTS_PER_EPOCH} from "@lodestar/params"; +import {ssz} from "@lodestar/types"; +import {CachedBeaconStateGloas} from "../types.ts"; +import {computeExitEpochAndUpdateChurn} from "../util/epoch.ts"; +import {getBuilderPaymentQuorumThreshold} from "../util/gloas.ts"; + +/** + * Processes the builder pending payments from the previous epoch. + */ +export function processBuilderPendingPayments(state: CachedBeaconStateGloas): void { + const quorum = getBuilderPaymentQuorumThreshold(state); + + for (let i = 0; i < SLOTS_PER_EPOCH; i++) { + const payment = state.builderPendingPayments.get(i); + if (payment.weight > quorum) { + const exitQueueEpoch = computeExitEpochAndUpdateChurn(state, BigInt(payment.withdrawal.amount)); + payment.withdrawal.withdrawableEpoch = exitQueueEpoch + state.config.MIN_VALIDATOR_WITHDRAWABILITY_DELAY; + + state.builderPendingWithdrawals.push(payment.withdrawal); + } + } + + // TODO GLOAS: Optimize this + for (let i = 0; i < state.builderPendingPayments.length; i++) { + if (i < SLOTS_PER_EPOCH) { + state.builderPendingPayments.set(i, state.builderPendingPayments.get(i + SLOTS_PER_EPOCH).clone()); + } else { + state.builderPendingPayments.set(i, ssz.gloas.BuilderPendingPayment.defaultViewDU()); + } + } +} diff --git a/packages/state-transition/src/index.ts b/packages/state-transition/src/index.ts index 9053bf29b6..cdbfc8df54 100644 --- a/packages/state-transition/src/index.ts +++ b/packages/state-transition/src/index.ts @@ -52,6 +52,7 @@ export type { BeaconStateElectra, BeaconStateExecutions, BeaconStateFulu, + BeaconStateGloas, // Non-cached states BeaconStatePhase0, CachedBeaconStateAllForks, @@ -62,6 +63,7 @@ export type { CachedBeaconStateElectra, CachedBeaconStateExecutions, CachedBeaconStateFulu, + CachedBeaconStateGloas, CachedBeaconStatePhase0, } from "./types.js"; export * from "./util/index.js"; diff --git a/packages/state-transition/src/signatureSets/index.ts b/packages/state-transition/src/signatureSets/index.ts index 436cd35a24..9a12d7174a 100644 --- a/packages/state-transition/src/signatureSets/index.ts +++ b/packages/state-transition/src/signatureSets/index.ts @@ -14,6 +14,7 @@ import {getVoluntaryExitsSignatureSets} from "./voluntaryExits.js"; export * from "./attesterSlashings.js"; export * from "./blsToExecutionChange.js"; export * from "./indexedAttestation.js"; +export * from "./indexedPayloadAttestation.ts"; export * from "./proposer.js"; export * from "./proposerSlashings.js"; export * from "./randao.js"; diff --git a/packages/state-transition/src/signatureSets/indexedPayloadAttestation.ts b/packages/state-transition/src/signatureSets/indexedPayloadAttestation.ts new file mode 100644 index 0000000000..13b5d8842d --- /dev/null +++ b/packages/state-transition/src/signatureSets/indexedPayloadAttestation.ts @@ -0,0 +1,24 @@ +import {DOMAIN_PTC_ATTESTER} from "@lodestar/params"; +import {gloas, ssz} from "@lodestar/types"; +import {CachedBeaconStateGloas} from "../types.ts"; +import {ISignatureSet, computeSigningRoot, createAggregateSignatureSetFromComponents} from "../util/index.ts"; + +export function getIndexedPayloadAttestationSignatureSet( + state: CachedBeaconStateGloas, + indexedPayloadAttestation: gloas.IndexedPayloadAttestation +): ISignatureSet { + return createAggregateSignatureSetFromComponents( + indexedPayloadAttestation.attestingIndices.map((i) => state.epochCtx.index2pubkey[i]), + getPayloadAttestationDataSigningRoot(state, indexedPayloadAttestation.data), + indexedPayloadAttestation.signature + ); +} + +export function getPayloadAttestationDataSigningRoot( + state: CachedBeaconStateGloas, + data: gloas.PayloadAttestationData +): Uint8Array { + const domain = state.config.getDomain(state.slot, DOMAIN_PTC_ATTESTER); + + return computeSigningRoot(ssz.gloas.PayloadAttestationData, data, domain); +} diff --git a/packages/state-transition/src/slot/index.ts b/packages/state-transition/src/slot/index.ts index 3ce249cb00..e3afefccd1 100644 --- a/packages/state-transition/src/slot/index.ts +++ b/packages/state-transition/src/slot/index.ts @@ -1,7 +1,7 @@ import {byteArrayEquals} from "@chainsafe/ssz"; -import {SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params"; +import {ForkSeq, SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params"; import {ZERO_HASH} from "../constants/index.js"; -import {CachedBeaconStateAllForks} from "../types.js"; +import {CachedBeaconStateAllForks, CachedBeaconStateGloas} from "../types.js"; export {upgradeStateToAltair} from "./upgradeStateToAltair.js"; export {upgradeStateToBellatrix} from "./upgradeStateToBellatrix.js"; @@ -14,7 +14,7 @@ export {upgradeStateToGloas} from "./upgradeStateToGloas.js"; /** * Dial state to next slot. Common for all forks */ -export function processSlot(state: CachedBeaconStateAllForks): void { +export function processSlot(fork: ForkSeq, state: CachedBeaconStateAllForks): void { // Cache state root // Note: .hashTreeRoot() automatically commits() pending changes const previousStateRoot = state.hashTreeRoot(); @@ -29,4 +29,12 @@ export function processSlot(state: CachedBeaconStateAllForks): void { // Note: .hashTreeRoot() automatically commits() pending changes const previousBlockRoot = state.latestBlockHeader.hashTreeRoot(); state.blockRoots.set(state.slot % SLOTS_PER_HISTORICAL_ROOT, previousBlockRoot); + + if (fork >= ForkSeq.gloas) { + // Unset the next payload availability + (state as CachedBeaconStateGloas).executionPayloadAvailability.set( + (state.slot + 1) % SLOTS_PER_HISTORICAL_ROOT, + false + ); + } } diff --git a/packages/state-transition/src/slot/upgradeStateToAltair.ts b/packages/state-transition/src/slot/upgradeStateToAltair.ts index ec6661d0d7..392f69cf4a 100644 --- a/packages/state-transition/src/slot/upgradeStateToAltair.ts +++ b/packages/state-transition/src/slot/upgradeStateToAltair.ts @@ -135,7 +135,8 @@ function translateParticipation( data, attestation.inclusionDelay, epochCtx.epoch, - rootCache + rootCache, + null ); const committeeIndices = epochCtx.getBeaconCommittee(data.slot, data.index); diff --git a/packages/state-transition/src/slot/upgradeStateToGloas.ts b/packages/state-transition/src/slot/upgradeStateToGloas.ts index 3b364f764f..48c932e2ac 100644 --- a/packages/state-transition/src/slot/upgradeStateToGloas.ts +++ b/packages/state-transition/src/slot/upgradeStateToGloas.ts @@ -1,24 +1,68 @@ +import {SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params"; import {ssz} from "@lodestar/types"; import {getCachedBeaconState} from "../cache/stateCache.js"; import {CachedBeaconStateFulu, CachedBeaconStateGloas} from "../types.js"; /** * Upgrade a state from Fulu to Gloas. - * TODO GLOAS: Implement this */ export function upgradeStateToGloas(stateFulu: CachedBeaconStateFulu): CachedBeaconStateGloas { const {config} = stateFulu; - const stateFuluNode = ssz.fulu.BeaconState.commitViewDU(stateFulu); - const stateGloasView = ssz.gloas.BeaconState.getViewDU(stateFuluNode); + ssz.fulu.BeaconState.commitViewDU(stateFulu); + const stateGloasCloned = stateFulu; - const stateGloas = getCachedBeaconState(stateGloasView, stateFulu); + const stateGloasView = ssz.gloas.BeaconState.defaultViewDU(); - stateGloas.fork = ssz.phase0.Fork.toViewDU({ + stateGloasView.genesisTime = stateGloasCloned.genesisTime; + stateGloasView.genesisValidatorsRoot = stateGloasCloned.genesisValidatorsRoot; + stateGloasView.slot = stateGloasCloned.slot; + stateGloasView.fork = ssz.phase0.Fork.toViewDU({ previousVersion: stateFulu.fork.currentVersion, currentVersion: config.GLOAS_FORK_VERSION, epoch: stateFulu.epochCtx.epoch, }); + stateGloasView.latestBlockHeader = stateGloasCloned.latestBlockHeader; + stateGloasView.blockRoots = stateGloasCloned.blockRoots; + stateGloasView.stateRoots = stateGloasCloned.stateRoots; + stateGloasView.historicalRoots = stateGloasCloned.historicalRoots; + stateGloasView.eth1Data = stateGloasCloned.eth1Data; + stateGloasView.eth1DataVotes = stateGloasCloned.eth1DataVotes; + stateGloasView.eth1DepositIndex = stateGloasCloned.eth1DepositIndex; + stateGloasView.validators = stateGloasCloned.validators; + stateGloasView.balances = stateGloasCloned.balances; + stateGloasView.randaoMixes = stateGloasCloned.randaoMixes; + stateGloasView.slashings = stateGloasCloned.slashings; + stateGloasView.previousEpochParticipation = stateGloasCloned.previousEpochParticipation; + stateGloasView.currentEpochParticipation = stateGloasCloned.currentEpochParticipation; + stateGloasView.justificationBits = stateGloasCloned.justificationBits; + stateGloasView.previousJustifiedCheckpoint = stateGloasCloned.previousJustifiedCheckpoint; + stateGloasView.currentJustifiedCheckpoint = stateGloasCloned.currentJustifiedCheckpoint; + stateGloasView.finalizedCheckpoint = stateGloasCloned.finalizedCheckpoint; + stateGloasView.inactivityScores = stateGloasCloned.inactivityScores; + stateGloasView.currentSyncCommittee = stateGloasCloned.currentSyncCommittee; + stateGloasView.nextSyncCommittee = stateGloasCloned.nextSyncCommittee; + stateGloasView.latestExecutionPayloadBid.blockHash = stateFulu.latestExecutionPayloadHeader.blockHash; + stateGloasView.nextWithdrawalIndex = stateGloasCloned.nextWithdrawalIndex; + stateGloasView.nextWithdrawalValidatorIndex = stateGloasCloned.nextWithdrawalValidatorIndex; + stateGloasView.historicalSummaries = stateGloasCloned.historicalSummaries; + stateGloasView.depositRequestsStartIndex = stateGloasCloned.depositRequestsStartIndex; + stateGloasView.depositBalanceToConsume = stateGloasCloned.depositBalanceToConsume; + stateGloasView.exitBalanceToConsume = stateGloasCloned.exitBalanceToConsume; + stateGloasView.earliestExitEpoch = stateGloasCloned.earliestExitEpoch; + stateGloasView.consolidationBalanceToConsume = stateGloasCloned.consolidationBalanceToConsume; + stateGloasView.earliestConsolidationEpoch = stateGloasCloned.earliestConsolidationEpoch; + stateGloasView.pendingDeposits = stateGloasCloned.pendingDeposits; + stateGloasView.pendingPartialWithdrawals = stateGloasCloned.pendingPartialWithdrawals; + stateGloasView.pendingConsolidations = stateGloasCloned.pendingConsolidations; + stateGloasView.proposerLookahead = stateGloasCloned.proposerLookahead; + + for (let i = 0; i < SLOTS_PER_HISTORICAL_ROOT; i++) { + stateGloasView.executionPayloadAvailability.set(i, true); + } + stateGloasView.latestBlockHash = stateFulu.latestExecutionPayloadHeader.blockHash; + + const stateGloas = getCachedBeaconState(stateGloasView, stateFulu); stateGloas.commit(); // Clear cache to ensure the cache of fulu fields is not used by new gloas fields diff --git a/packages/state-transition/src/stateTransition.ts b/packages/state-transition/src/stateTransition.ts index 8e3996b2c2..a7d49c308a 100644 --- a/packages/state-transition/src/stateTransition.ts +++ b/packages/state-transition/src/stateTransition.ts @@ -217,13 +217,13 @@ function processSlotsWithTransientCache( } while (postState.slot < slot) { - processSlot(postState); + const fork = postState.config.getForkSeq(postState.slot); + processSlot(fork, postState); // Process epoch on the first slot of the next epoch + // We use `fork` because at fork boundary we don't want to process + // "next fork" epoch before upgrading state if ((postState.slot + 1) % SLOTS_PER_EPOCH === 0) { - // At fork boundary we don't want to process "next fork" epoch before upgrading state - const fork = postState.config.getForkSeq(postState.slot); - const epochTransitionTimer = metrics?.epochTransitionTime.startTimer(); let epochTransitionCache: EpochTransitionCache; diff --git a/packages/state-transition/src/util/electra.ts b/packages/state-transition/src/util/electra.ts index c5439d9a72..0932c985ae 100644 --- a/packages/state-transition/src/util/electra.ts +++ b/packages/state-transition/src/util/electra.ts @@ -1,11 +1,14 @@ import {COMPOUNDING_WITHDRAWAL_PREFIX, GENESIS_SLOT, MIN_ACTIVATION_BALANCE} from "@lodestar/params"; import {ValidatorIndex, ssz} from "@lodestar/types"; import {G2_POINT_AT_INFINITY} from "../constants/constants.js"; -import {CachedBeaconStateElectra} from "../types.js"; +import {CachedBeaconStateElectra, CachedBeaconStateGloas} from "../types.js"; import {hasEth1WithdrawalCredential} from "./capella.js"; +import {hasBuilderWithdrawalCredential} from "./gloas.ts"; export function hasCompoundingWithdrawalCredential(withdrawalCredentials: Uint8Array): boolean { - return withdrawalCredentials[0] === COMPOUNDING_WITHDRAWAL_PREFIX; + return ( + withdrawalCredentials[0] === COMPOUNDING_WITHDRAWAL_PREFIX || hasBuilderWithdrawalCredential(withdrawalCredentials) + ); } export function hasExecutionWithdrawalCredential(withdrawalCredentials: Uint8Array): boolean { @@ -14,7 +17,10 @@ export function hasExecutionWithdrawalCredential(withdrawalCredentials: Uint8Arr ); } -export function switchToCompoundingValidator(state: CachedBeaconStateElectra, index: ValidatorIndex): void { +export function switchToCompoundingValidator( + state: CachedBeaconStateElectra | CachedBeaconStateGloas, + index: ValidatorIndex +): void { const validator = state.validators.get(index); // directly modifying the byte leads to ssz missing the modification resulting into @@ -30,7 +36,10 @@ export function switchToCompoundingValidator(state: CachedBeaconStateElectra, in queueExcessActiveBalance(state, index); } -export function queueExcessActiveBalance(state: CachedBeaconStateElectra, index: ValidatorIndex): void { +export function queueExcessActiveBalance( + state: CachedBeaconStateElectra | CachedBeaconStateGloas, + index: ValidatorIndex +): void { const balance = state.balances.get(index); if (balance > MIN_ACTIVATION_BALANCE) { const validator = state.validators.getReadonly(index); @@ -53,7 +62,7 @@ export function queueExcessActiveBalance(state: CachedBeaconStateElectra, index: /** * Since we share pubkey2index, pubkey maybe added by other epoch transition but we don't have that validator in this state */ -export function isPubkeyKnown(state: CachedBeaconStateElectra, pubkey: Uint8Array): boolean { +export function isPubkeyKnown(state: CachedBeaconStateElectra | CachedBeaconStateGloas, pubkey: Uint8Array): boolean { return isValidatorKnown(state, state.epochCtx.getValidatorIndex(pubkey)); } @@ -61,7 +70,7 @@ export function isPubkeyKnown(state: CachedBeaconStateElectra, pubkey: Uint8Arra * Since we share pubkey2index, validatorIndex maybe not null but we don't have that validator in this state */ export function isValidatorKnown( - state: CachedBeaconStateElectra, + state: CachedBeaconStateElectra | CachedBeaconStateGloas, index: ValidatorIndex | null ): index is ValidatorIndex { return index !== null && index < state.validators.length; diff --git a/packages/state-transition/src/util/epoch.ts b/packages/state-transition/src/util/epoch.ts index cc95d0d192..a6bb6cef57 100644 --- a/packages/state-transition/src/util/epoch.ts +++ b/packages/state-transition/src/util/epoch.ts @@ -1,6 +1,6 @@ import {EPOCHS_PER_SYNC_COMMITTEE_PERIOD, GENESIS_EPOCH, MAX_SEED_LOOKAHEAD, SLOTS_PER_EPOCH} from "@lodestar/params"; import {BeaconState, Epoch, Gwei, Slot, SyncPeriod} from "@lodestar/types"; -import {CachedBeaconStateElectra} from "../types.js"; +import {CachedBeaconStateElectra, CachedBeaconStateGloas} from "../types.js"; import {getActivationExitChurnLimit, getConsolidationChurnLimit} from "./validator.js"; /** @@ -41,7 +41,10 @@ export function computeActivationExitEpoch(epoch: Epoch): Epoch { return epoch + 1 + MAX_SEED_LOOKAHEAD; } -export function computeExitEpochAndUpdateChurn(state: CachedBeaconStateElectra, exitBalance: Gwei): number { +export function computeExitEpochAndUpdateChurn( + state: CachedBeaconStateElectra | CachedBeaconStateGloas, + exitBalance: Gwei +): number { let earliestExitEpoch = Math.max(state.earliestExitEpoch, computeActivationExitEpoch(state.epochCtx.epoch)); const perEpochChurn = getActivationExitChurnLimit(state.epochCtx); @@ -65,7 +68,7 @@ export function computeExitEpochAndUpdateChurn(state: CachedBeaconStateElectra, } export function computeConsolidationEpochAndUpdateChurn( - state: CachedBeaconStateElectra, + state: CachedBeaconStateElectra | CachedBeaconStateGloas, consolidationBalance: Gwei ): number { let earliestConsolidationEpoch = Math.max( diff --git a/packages/state-transition/src/util/gloas.ts b/packages/state-transition/src/util/gloas.ts new file mode 100644 index 0000000000..ab82bda18a --- /dev/null +++ b/packages/state-transition/src/util/gloas.ts @@ -0,0 +1,58 @@ +import {byteArrayEquals} from "@chainsafe/ssz"; +import { + BUILDER_PAYMENT_THRESHOLD_DENOMINATOR, + BUILDER_PAYMENT_THRESHOLD_NUMERATOR, + BUILDER_WITHDRAWAL_PREFIX, + EFFECTIVE_BALANCE_INCREMENT, + SLOTS_PER_EPOCH, +} from "@lodestar/params"; +import {gloas} from "@lodestar/types"; +import {AttestationData} from "@lodestar/types/phase0"; +import {CachedBeaconStateGloas} from "../types.ts"; +import {getBlockRootAtSlot} from "./blockRoot.ts"; +import {computeEpochAtSlot} from "./epoch.ts"; +import {RootCache} from "./rootCache.ts"; + +export function hasBuilderWithdrawalCredential(withdrawalCredentials: Uint8Array): boolean { + return withdrawalCredentials[0] === BUILDER_WITHDRAWAL_PREFIX; +} + +export function getBuilderPaymentQuorumThreshold(state: CachedBeaconStateGloas): number { + const quorum = + Math.floor((state.epochCtx.totalActiveBalanceIncrements * EFFECTIVE_BALANCE_INCREMENT) / SLOTS_PER_EPOCH) * + BUILDER_PAYMENT_THRESHOLD_NUMERATOR; + + return Math.floor(quorum / BUILDER_PAYMENT_THRESHOLD_DENOMINATOR); +} + +export function isBuilderPaymentWithdrawable( + state: CachedBeaconStateGloas, + withdrawal: gloas.BuilderPendingWithdrawal +): boolean { + const builder = state.validators.getReadonly(withdrawal.builderIndex); + const currentEpoch = computeEpochAtSlot(state.slot); + + return builder.withdrawableEpoch >= currentEpoch || !builder.slashed; +} + +export function isAttestationSameSlot(state: CachedBeaconStateGloas, data: AttestationData): boolean { + if (data.slot === 0) return true; + + const isMatchingBlockRoot = byteArrayEquals(data.beaconBlockRoot, getBlockRootAtSlot(state, data.slot)); + const isCurrentBlockRoot = !byteArrayEquals(data.beaconBlockRoot, getBlockRootAtSlot(state, data.slot - 1)); + + return isMatchingBlockRoot && isCurrentBlockRoot; +} + +export function isAttestationSameSlotRootCache(rootCache: RootCache, data: AttestationData): boolean { + if (data.slot === 0) return true; + + const isMatchingBlockRoot = byteArrayEquals(data.beaconBlockRoot, rootCache.getBlockRootAtSlot(data.slot)); + const isCurrentBlockRoot = !byteArrayEquals(data.beaconBlockRoot, rootCache.getBlockRootAtSlot(data.slot - 1)); + + return isMatchingBlockRoot && isCurrentBlockRoot; +} + +export function isParentBlockFull(state: CachedBeaconStateGloas): boolean { + return byteArrayEquals(state.latestExecutionPayloadBid.blockHash, state.latestBlockHash); +} diff --git a/packages/state-transition/src/util/seed.ts b/packages/state-transition/src/util/seed.ts index 3953df0b9b..79d79eef17 100644 --- a/packages/state-transition/src/util/seed.ts +++ b/packages/state-transition/src/util/seed.ts @@ -5,6 +5,7 @@ import { } from "@chainsafe/swap-or-not-shuffle"; import { DOMAIN_BEACON_PROPOSER, + DOMAIN_PTC_ATTESTER, DOMAIN_SYNC_COMMITTEE, EFFECTIVE_BALANCE_INCREMENT, EPOCHS_PER_HISTORICAL_VECTOR, @@ -12,6 +13,7 @@ import { MAX_EFFECTIVE_BALANCE, MAX_EFFECTIVE_BALANCE_ELECTRA, MIN_SEED_LOOKAHEAD, + PTC_SIZE, SHUFFLE_ROUND_COUNT, SLOTS_PER_EPOCH, SYNC_COMMITTEE_SIZE, @@ -19,7 +21,7 @@ import { import {Bytes32, DomainType, Epoch, ValidatorIndex} from "@lodestar/types"; import {assert, bytesToBigInt, bytesToInt, intToBytes} from "@lodestar/utils"; import {EffectiveBalanceIncrements} from "../cache/effectiveBalanceIncrements.js"; -import {BeaconStateAllForks, CachedBeaconStateAllForks} from "../types.js"; +import {BeaconStateAllForks, BeaconStateGloas, CachedBeaconStateAllForks} from "../types.js"; import {computeEpochAtSlot, computeStartSlotAtEpoch} from "./epoch.js"; /** @@ -266,6 +268,60 @@ export function getNextSyncCommitteeIndices( ); } +export function naiveGetPayloadTimlinessCommitteeIndices( + state: BeaconStateGloas, + shuffling: {committees: Uint32Array[][]}, + effectiveBalanceIncrements: EffectiveBalanceIncrements, + epoch: Epoch +): ValidatorIndex[][] { + const epochSeed = getSeed(state, epoch, DOMAIN_PTC_ATTESTER); + const startSlot = computeStartSlotAtEpoch(epoch); + const committeeIndices = []; + + for (let slot = startSlot; slot < startSlot + SLOTS_PER_EPOCH; slot++) { + const slotCommittees = shuffling.committees[slot % SLOTS_PER_EPOCH]; + const indices = naiveComputePayloadTimelinessCommitteeIndices( + effectiveBalanceIncrements, + slotCommittees.flatMap((c) => Array.from(c)), + digest(Buffer.concat([epochSeed, intToBytes(slot, 8)])) + ); + committeeIndices.push(indices); + } + + return committeeIndices; +} + +export function naiveComputePayloadTimelinessCommitteeIndices( + effectiveBalanceIncrements: EffectiveBalanceIncrements, + indices: ArrayLike, + seed: Uint8Array +): ValidatorIndex[] { + if (indices.length === 0) { + throw Error("Validator indices must not be empty"); + } + + const result = []; + + const MAX_RANDOM_VALUE = 2 ** 16 - 1; + const MAX_EFFECTIVE_BALANCE_INCREMENT = MAX_EFFECTIVE_BALANCE_ELECTRA / EFFECTIVE_BALANCE_INCREMENT; + + let i = 0; + while (result.length < PTC_SIZE) { + const candidateIndex = indices[i % indices.length]; + const randomBytes = digest(Buffer.concat([seed, intToBytes(Math.floor(i / 16), 8, "le")])); + const offset = (i % 16) * 2; + const randomValue = bytesToInt(randomBytes.subarray(offset, offset + 2)); + + const effectiveBalanceIncrement = effectiveBalanceIncrements[candidateIndex]; + if (effectiveBalanceIncrement * MAX_RANDOM_VALUE >= MAX_EFFECTIVE_BALANCE_INCREMENT * randomValue) { + result.push(candidateIndex); + } + i += 1; + } + + return result; +} + /** * Return the shuffled validator index corresponding to ``seed`` (and ``index_count``). * diff --git a/packages/state-transition/src/util/validator.ts b/packages/state-transition/src/util/validator.ts index 0d7c011b99..59824def88 100644 --- a/packages/state-transition/src/util/validator.ts +++ b/packages/state-transition/src/util/validator.ts @@ -7,7 +7,7 @@ import { } from "@lodestar/params"; import {Epoch, ValidatorIndex, phase0} from "@lodestar/types"; import {intDiv} from "@lodestar/utils"; -import {BeaconStateAllForks, CachedBeaconStateElectra, EpochCache} from "../types.js"; +import {BeaconStateAllForks, CachedBeaconStateElectra, CachedBeaconStateGloas, EpochCache} from "../types.js"; import {hasCompoundingWithdrawalCredential} from "./electra.js"; /** @@ -94,12 +94,31 @@ export function getMaxEffectiveBalance(withdrawalCredentials: Uint8Array): numbe return MIN_ACTIVATION_BALANCE; } -export function getPendingBalanceToWithdraw(state: CachedBeaconStateElectra, validatorIndex: ValidatorIndex): number { +export function getPendingBalanceToWithdraw( + fork: ForkSeq, + state: CachedBeaconStateElectra | CachedBeaconStateGloas, + validatorIndex: ValidatorIndex +): number { let total = 0; for (const item of state.pendingPartialWithdrawals.getAllReadonly()) { if (item.validatorIndex === validatorIndex) { total += Number(item.amount); } } + + if (fork >= ForkSeq.gloas) { + const stateGloas = state as CachedBeaconStateGloas; + for (const item of stateGloas.builderPendingWithdrawals.getAllReadonly()) { + if (item.builderIndex === validatorIndex) { + total += item.amount; + } + } + for (const item of stateGloas.builderPendingPayments.getAllReadonly()) { + if (item.withdrawal.builderIndex === validatorIndex) { + total += item.withdrawal.amount; + } + } + } + return total; } diff --git a/packages/state-transition/test/perf/slot/slots.test.ts b/packages/state-transition/test/perf/slot/slots.test.ts index 29e6eb5759..ee8288e200 100644 --- a/packages/state-transition/test/perf/slot/slots.test.ts +++ b/packages/state-transition/test/perf/slot/slots.test.ts @@ -1,4 +1,5 @@ import {bench, describe} from "@chainsafe/benchmark"; +import {ForkSeq} from "@lodestar/params"; import {processSlot} from "../../../src/slot/index.js"; import {State} from "../types.js"; import {generatePerfTestCachedStatePhase0} from "../util.js"; @@ -14,7 +15,7 @@ describe("processSlot", () => { fn: (state) => { for (let i = 0; i < slotCount; i++) { state.slot++; - processSlot(state); + processSlot(ForkSeq.phase0, state); } }, }); diff --git a/packages/state-transition/test/unit/block/processConsolidationRequest.test.ts b/packages/state-transition/test/unit/block/processConsolidationRequest.test.ts index 86b9c646b3..921068c169 100644 --- a/packages/state-transition/test/unit/block/processConsolidationRequest.test.ts +++ b/packages/state-transition/test/unit/block/processConsolidationRequest.test.ts @@ -5,6 +5,7 @@ import { BLS_WITHDRAWAL_PREFIX, COMPOUNDING_WITHDRAWAL_PREFIX, FAR_FUTURE_EPOCH, + ForkSeq, SLOTS_PER_EPOCH, } from "@lodestar/params"; import {ssz} from "@lodestar/types"; @@ -51,7 +52,7 @@ describe("processConsolidationRequest", () => { expect(state.pendingConsolidations.length).eq(0); - processConsolidationRequest(state, request); + processConsolidationRequest(ForkSeq.electra, state, request); expect(state.pendingConsolidations.length).eq(0); }); diff --git a/packages/types/src/gloas/sszTypes.ts b/packages/types/src/gloas/sszTypes.ts index 1beedb02b6..8198b8a486 100644 --- a/packages/types/src/gloas/sszTypes.ts +++ b/packages/types/src/gloas/sszTypes.ts @@ -23,7 +23,7 @@ const {Gwei, ExecutionAddress, ValidatorIndex, Epoch, BLSSignature, Bytes32, Roo export const BuilderPendingWithdrawal = new ContainerType( { feeRecipient: ExecutionAddress, - amount: Gwei, + amount: UintNum64, builderIndex: ValidatorIndex, withdrawableEpoch: Epoch, }, @@ -32,7 +32,7 @@ export const BuilderPendingWithdrawal = new ContainerType( export const BuilderPendingPayment = new ContainerType( { - weight: Gwei, + weight: UintNum64, withdrawal: BuilderPendingWithdrawal, }, {typeName: "BuilderPendingPayment", jsonCase: "eth2"} @@ -80,11 +80,13 @@ export const ExecutionPayloadBid = new ContainerType( parentBlockHash: Bytes32, parentBlockRoot: Root, blockHash: Bytes32, + prevRandao: Bytes32, feeRecipient: ExecutionAddress, gasLimit: UintBn64, builderIndex: ValidatorIndex, slot: Slot, - value: Gwei, + value: UintNum64, + executionPayment: UintNum64, blobKzgCommitmentsRoot: Root, }, {typeName: "ExecutionPayloadBid", jsonCase: "eth2"}