diff --git a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts index 37670e4b8f82..a353c4e54ff9 100644 --- a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts +++ b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts @@ -220,6 +220,14 @@ export async function produceBlockBody( } else { blobsResult = {type: BlobsResultType.preDeneb}; } + + if (ForkSeq[fork] >= ForkSeq.electra) { + const {executionRequests} = builderRes; + if (executionRequests === undefined) { + throw Error(`Invalid builder getHeader response for fork=${fork}, missing executionRequests`); + } + (blockBody as electra.BlindedBeaconBlockBody).executionRequests = executionRequests; + } } // blockType === BlockType.Full @@ -285,7 +293,6 @@ export async function produceBlockBody( throw Error(`Missing blobsBundle response from getPayload at fork=${fork}`); } - // validate blindedBlobsBundle if (this.opts.sanityCheckExecutionEngineBlobs) { validateBlobsAndKzgCommitments(executionPayload, blobsBundle); } @@ -455,6 +462,7 @@ async function prepareExecutionPayloadHeader( header: ExecutionPayloadHeader; executionPayloadValue: Wei; blobKzgCommitments?: deneb.BlobKzgCommitments; + executionRequests?: electra.ExecutionRequests; }> { if (!chain.executionBuilder) { throw Error("executionBuilder required"); diff --git a/packages/beacon-node/src/execution/builder/http.ts b/packages/beacon-node/src/execution/builder/http.ts index 7bc47e354c6c..87920e833f5d 100644 --- a/packages/beacon-node/src/execution/builder/http.ts +++ b/packages/beacon-node/src/execution/builder/http.ts @@ -8,6 +8,7 @@ import { SignedBeaconBlockOrContents, SignedBlindedBeaconBlock, ExecutionPayloadHeader, + electra, } from "@lodestar/types"; import {parseExecutionPayloadAndBlobsBundle, reconstructFullBlockOrContents} from "@lodestar/state-transition"; import {ChainForkConfig} from "@lodestar/config"; @@ -120,6 +121,7 @@ export class ExecutionBuilderHttp implements IExecutionBuilder { header: ExecutionPayloadHeader; executionPayloadValue: Wei; blobKzgCommitments?: deneb.BlobKzgCommitments; + executionRequests?: electra.ExecutionRequests; }> { const signedBuilderBid = ( await this.api.getHeader({slot, parentHash, proposerPubkey}, {timeoutMs: BUILDER_PROPOSAL_DELAY_TOLERANCE}) @@ -131,7 +133,8 @@ export class ExecutionBuilderHttp implements IExecutionBuilder { const {header, value: executionPayloadValue} = signedBuilderBid.message; const {blobKzgCommitments} = signedBuilderBid.message as deneb.BuilderBid; - return {header, executionPayloadValue, blobKzgCommitments}; + const {executionRequests} = signedBuilderBid.message as electra.BuilderBid; + return {header, executionPayloadValue, blobKzgCommitments, executionRequests}; } async submitBlindedBlock(signedBlindedBlock: SignedBlindedBeaconBlock): Promise { diff --git a/packages/beacon-node/src/execution/builder/interface.ts b/packages/beacon-node/src/execution/builder/interface.ts index 9a655a68de02..5a6a4eb82f63 100644 --- a/packages/beacon-node/src/execution/builder/interface.ts +++ b/packages/beacon-node/src/execution/builder/interface.ts @@ -8,6 +8,7 @@ import { SignedBeaconBlockOrContents, ExecutionPayloadHeader, SignedBlindedBeaconBlock, + electra, } from "@lodestar/types"; import {ForkExecution} from "@lodestar/params"; @@ -36,6 +37,7 @@ export interface IExecutionBuilder { header: ExecutionPayloadHeader; executionPayloadValue: Wei; blobKzgCommitments?: deneb.BlobKzgCommitments; + executionRequests?: electra.ExecutionRequests; }>; submitBlindedBlock(signedBlock: SignedBlindedBeaconBlock): Promise; } diff --git a/packages/beacon-node/src/execution/engine/types.ts b/packages/beacon-node/src/execution/engine/types.ts index 9b4f5e5c83e6..52ddb8548629 100644 --- a/packages/beacon-node/src/execution/engine/types.ts +++ b/packages/beacon-node/src/execution/engine/types.ts @@ -1,4 +1,4 @@ -import {capella, deneb, electra, Wei, bellatrix, Root, ExecutionPayload, ExecutionRequests} from "@lodestar/types"; +import {capella, deneb, electra, Wei, bellatrix, Root, ExecutionPayload, ExecutionRequests, ssz} from "@lodestar/types"; import { BYTES_PER_LOGS_BLOOM, FIELD_ELEMENTS_PER_BLOB, @@ -116,7 +116,7 @@ type ExecutionPayloadRpcWithValue = { // even though CL tracks this as executionPayloadValue, EL returns this as blockValue blockValue: QUANTITY; blobsBundle?: BlobsBundleRpc; - requests?: ExecutionRequestsRpc; + executionRequests?: ExecutionRequestsRpc; shouldOverrideBuilder?: boolean; }; type ExecutionPayloadResponse = ExecutionPayloadRpc | ExecutionPayloadRpcWithValue; @@ -159,29 +159,17 @@ export type WithdrawalRpc = { amount: QUANTITY; }; -export type ExecutionRequestsRpc = { - deposits: DepositRequestRpc[]; - withdrawals: WithdrawalRequestRpc[]; - consolidations: ConsolidationRequestRpc[]; -}; +/** + * ExecutionRequestsRpc only holds 3 elements in the following order: + * - ssz'ed DepositRequests + * - ssz'ed WithdrawalRequests + * - ssz'ed ConsolidationRequests + */ +export type ExecutionRequestsRpc = [DepositRequestsRpc, WithdrawalRequestsRpc, ConsolidationRequestsRpc]; -export type DepositRequestRpc = { - pubkey: DATA; - withdrawalCredentials: DATA; - amount: QUANTITY; - signature: DATA; - index: QUANTITY; -}; -export type WithdrawalRequestRpc = { - sourceAddress: DATA; - validatorPubkey: DATA; - amount: QUANTITY; -}; -export type ConsolidationRequestRpc = { - sourceAddress: DATA; - sourcePubkey: DATA; - targetPubkey: DATA; -}; +export type DepositRequestsRpc = DATA; +export type WithdrawalRequestsRpc = DATA; +export type ConsolidationRequestsRpc = DATA; export type VersionedHashesRpc = DATA[]; @@ -278,7 +266,9 @@ export function parseExecutionPayload( executionPayloadValue = quantityToBigint(response.blockValue); data = response.executionPayload; blobsBundle = response.blobsBundle ? parseBlobsBundle(response.blobsBundle) : undefined; - executionRequests = response.requests ? deserializeExecutionRequests(response.requests) : undefined; + executionRequests = response.executionRequests + ? deserializeExecutionRequests(response.executionRequests) + : undefined; shouldOverrideBuilder = response.shouldOverrideBuilder ?? false; } else { data = response; @@ -404,73 +394,53 @@ export function deserializeWithdrawal(serialized: WithdrawalRpc): capella.Withdr } as capella.Withdrawal; } -function serializeDepositRequest(depositRequest: electra.DepositRequest): DepositRequestRpc { - return { - pubkey: bytesToData(depositRequest.pubkey), - withdrawalCredentials: bytesToData(depositRequest.withdrawalCredentials), - amount: numToQuantity(depositRequest.amount), - signature: bytesToData(depositRequest.signature), - index: numToQuantity(depositRequest.index), - }; +function serializeDepositRequests(depositRequests: electra.DepositRequests): DepositRequestsRpc { + return bytesToData(ssz.electra.DepositRequests.serialize(depositRequests)); } -function deserializeDepositRequest(serialized: DepositRequestRpc): electra.DepositRequest { - return { - pubkey: dataToBytes(serialized.pubkey, 48), - withdrawalCredentials: dataToBytes(serialized.withdrawalCredentials, 32), - amount: quantityToNum(serialized.amount), - signature: dataToBytes(serialized.signature, 96), - index: quantityToNum(serialized.index), - } as electra.DepositRequest; +function deserializeDepositRequests(serialized: DepositRequestsRpc): electra.DepositRequests { + return ssz.electra.DepositRequests.deserialize(dataToBytes(serialized, null)); } -function serializeWithdrawalRequest(withdrawalRequest: electra.WithdrawalRequest): WithdrawalRequestRpc { - return { - sourceAddress: bytesToData(withdrawalRequest.sourceAddress), - validatorPubkey: bytesToData(withdrawalRequest.validatorPubkey), - amount: numToQuantity(withdrawalRequest.amount), - }; +function serializeWithdrawalRequests(withdrawalRequests: electra.WithdrawalRequests): WithdrawalRequestsRpc { + return bytesToData(ssz.electra.WithdrawalRequests.serialize(withdrawalRequests)); } -function deserializeWithdrawalRequest(withdrawalRequest: WithdrawalRequestRpc): electra.WithdrawalRequest { - return { - sourceAddress: dataToBytes(withdrawalRequest.sourceAddress, 20), - validatorPubkey: dataToBytes(withdrawalRequest.validatorPubkey, 48), - amount: quantityToBigint(withdrawalRequest.amount), - }; +function deserializeWithdrawalRequest(serialized: WithdrawalRequestsRpc): electra.WithdrawalRequests { + return ssz.electra.WithdrawalRequests.deserialize(dataToBytes(serialized, null)); } -function serializeConsolidationRequest(consolidationRequest: electra.ConsolidationRequest): ConsolidationRequestRpc { - return { - sourceAddress: bytesToData(consolidationRequest.sourceAddress), - sourcePubkey: bytesToData(consolidationRequest.sourcePubkey), - targetPubkey: bytesToData(consolidationRequest.targetPubkey), - }; +function serializeConsolidationRequests( + consolidationRequests: electra.ConsolidationRequests +): ConsolidationRequestsRpc { + return bytesToData(ssz.electra.ConsolidationRequests.serialize(consolidationRequests)); } -function deserializeConsolidationRequest(consolidationRequest: ConsolidationRequestRpc): electra.ConsolidationRequest { - return { - sourceAddress: dataToBytes(consolidationRequest.sourceAddress, 20), - sourcePubkey: dataToBytes(consolidationRequest.sourcePubkey, 48), - targetPubkey: dataToBytes(consolidationRequest.targetPubkey, 48), - }; +function deserializeConsolidationRequests(serialized: ConsolidationRequestsRpc): electra.ConsolidationRequests { + return ssz.electra.ConsolidationRequests.deserialize(dataToBytes(serialized, null)); } +/** + * This is identical to get_execution_requests_list in + * https://github.com/ethereum/consensus-specs/blob/v1.5.0-alpha.8/specs/electra/beacon-chain.md#new-get_execution_requests_list + */ export function serializeExecutionRequests(executionRequests: ExecutionRequests): ExecutionRequestsRpc { const {deposits, withdrawals, consolidations} = executionRequests; - return { - deposits: deposits.map(serializeDepositRequest), - withdrawals: withdrawals.map(serializeWithdrawalRequest), - consolidations: consolidations.map(serializeConsolidationRequest), - }; + + return [ + serializeDepositRequests(deposits), + serializeWithdrawalRequests(withdrawals), + serializeConsolidationRequests(consolidations), + ]; } -export function deserializeExecutionRequests(executionRequests: ExecutionRequestsRpc): ExecutionRequests { - const {deposits, withdrawals, consolidations} = executionRequests; +export function deserializeExecutionRequests(serialized: ExecutionRequestsRpc): ExecutionRequests { + const [deposits, withdrawals, consolidations] = serialized; + return { - deposits: deposits.map(deserializeDepositRequest), - withdrawals: withdrawals.map(deserializeWithdrawalRequest), - consolidations: consolidations.map(deserializeConsolidationRequest), + deposits: deserializeDepositRequests(deposits), + withdrawals: deserializeWithdrawalRequest(withdrawals), + consolidations: deserializeConsolidationRequests(consolidations), }; } 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 146131f897ca..c61aec40fee3 100644 --- a/packages/beacon-node/test/spec/presets/epoch_processing.test.ts +++ b/packages/beacon-node/test/spec/presets/epoch_processing.test.ts @@ -44,7 +44,7 @@ const epochTransitionFns: Record = { epochFns.processSyncCommitteeUpdates(fork, state as CachedBeaconStateAltair); }, historical_summaries_update: epochFns.processHistoricalSummariesUpdate as EpochTransitionFn, - pending_balance_deposits: epochFns.processPendingBalanceDeposits as EpochTransitionFn, + pending_deposits: epochFns.processPendingDeposits as EpochTransitionFn, pending_consolidations: epochFns.processPendingConsolidations 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 b6a479ee3794..4c90831ef155 100644 --- a/packages/beacon-node/test/spec/presets/operations.test.ts +++ b/packages/beacon-node/test/spec/presets/operations.test.ts @@ -92,8 +92,7 @@ const operationFns: Record> = }, deposit_request: (state, testCase: {deposit_request: electra.DepositRequest}) => { - const fork = state.config.getForkSeq(state.slot); - blockFns.processDepositRequest(fork, state as CachedBeaconStateElectra, testCase.deposit_request); + blockFns.processDepositRequest(state as CachedBeaconStateElectra, testCase.deposit_request); }, consolidation_request: (state, testCase: {consolidation_request: electra.ConsolidationRequest}) => { diff --git a/packages/beacon-node/test/spec/specTestVersioning.ts b/packages/beacon-node/test/spec/specTestVersioning.ts index 5e15b696e4a1..89c701a83514 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.5.0-alpha.6", + specVersion: "v1.5.0-alpha.8", // Target directory is the host package root: 'packages/*/spec-tests' outputDir: path.join(__dirname, "../../spec-tests"), specTestsRepoUrl: "https://github.com/ethereum/consensus-spec-tests", diff --git a/packages/params/src/index.ts b/packages/params/src/index.ts index aa6e97641526..544113e3f8e1 100644 --- a/packages/params/src/index.ts +++ b/packages/params/src/index.ts @@ -96,7 +96,7 @@ export const { MAX_EFFECTIVE_BALANCE_ELECTRA, MIN_ACTIVATION_BALANCE, - PENDING_BALANCE_DEPOSITS_LIMIT, + PENDING_DEPOSITS_LIMIT, PENDING_PARTIAL_WITHDRAWALS_LIMIT, PENDING_CONSOLIDATIONS_LIMIT, MIN_SLASHING_PENALTY_QUOTIENT_ELECTRA, @@ -107,6 +107,7 @@ export const { MAX_ATTESTER_SLASHINGS_ELECTRA, MAX_ATTESTATIONS_ELECTRA, MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP, + MAX_PENDING_DEPOSITS_PER_EPOCH, WHISTLEBLOWER_REWARD_QUOTIENT_ELECTRA, } = activePreset; diff --git a/packages/params/src/presets/mainnet.ts b/packages/params/src/presets/mainnet.ts index a7225739037f..9a03001375f2 100644 --- a/packages/params/src/presets/mainnet.ts +++ b/packages/params/src/presets/mainnet.ts @@ -124,11 +124,12 @@ export const mainnetPreset: BeaconPreset = { MAX_ATTESTER_SLASHINGS_ELECTRA: 1, MAX_ATTESTATIONS_ELECTRA: 8, MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP: 8, + MAX_PENDING_DEPOSITS_PER_EPOCH: 16, // 2**11 * 10**9 (= 2,048,000,000,000) Gwei MAX_EFFECTIVE_BALANCE_ELECTRA: 2048000000000, MIN_SLASHING_PENALTY_QUOTIENT_ELECTRA: 4096, MIN_ACTIVATION_BALANCE: 32000000000, - PENDING_BALANCE_DEPOSITS_LIMIT: 134217728, + PENDING_DEPOSITS_LIMIT: 134217728, PENDING_PARTIAL_WITHDRAWALS_LIMIT: 134217728, PENDING_CONSOLIDATIONS_LIMIT: 262144, MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD: 1, diff --git a/packages/params/src/presets/minimal.ts b/packages/params/src/presets/minimal.ts index d7ab2c27f342..6edd7e2858f1 100644 --- a/packages/params/src/presets/minimal.ts +++ b/packages/params/src/presets/minimal.ts @@ -124,12 +124,13 @@ export const minimalPreset: BeaconPreset = { MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD: 2, MAX_ATTESTER_SLASHINGS_ELECTRA: 1, MAX_ATTESTATIONS_ELECTRA: 8, - MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP: 1, + MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP: 2, + MAX_PENDING_DEPOSITS_PER_EPOCH: 16, // 2**11 * 10**9 (= 2,048,000,000,000) Gwei MAX_EFFECTIVE_BALANCE_ELECTRA: 2048000000000, MIN_SLASHING_PENALTY_QUOTIENT_ELECTRA: 4096, MIN_ACTIVATION_BALANCE: 32000000000, - PENDING_BALANCE_DEPOSITS_LIMIT: 134217728, + PENDING_DEPOSITS_LIMIT: 134217728, PENDING_PARTIAL_WITHDRAWALS_LIMIT: 64, PENDING_CONSOLIDATIONS_LIMIT: 64, MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD: 1, diff --git a/packages/params/src/types.ts b/packages/params/src/types.ts index 2fd88da9fde3..5e17adaace12 100644 --- a/packages/params/src/types.ts +++ b/packages/params/src/types.ts @@ -87,10 +87,11 @@ export type BeaconPreset = { MAX_ATTESTER_SLASHINGS_ELECTRA: number; MAX_ATTESTATIONS_ELECTRA: number; MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP: number; + MAX_PENDING_DEPOSITS_PER_EPOCH: number; MAX_EFFECTIVE_BALANCE_ELECTRA: number; MIN_SLASHING_PENALTY_QUOTIENT_ELECTRA: number; MIN_ACTIVATION_BALANCE: number; - PENDING_BALANCE_DEPOSITS_LIMIT: number; + PENDING_DEPOSITS_LIMIT: number; PENDING_PARTIAL_WITHDRAWALS_LIMIT: number; PENDING_CONSOLIDATIONS_LIMIT: number; MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD: number; @@ -187,10 +188,11 @@ export const beaconPresetTypes: BeaconPresetTypes = { MAX_ATTESTER_SLASHINGS_ELECTRA: "number", MAX_ATTESTATIONS_ELECTRA: "number", MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP: "number", + MAX_PENDING_DEPOSITS_PER_EPOCH: "number", MAX_EFFECTIVE_BALANCE_ELECTRA: "number", MIN_SLASHING_PENALTY_QUOTIENT_ELECTRA: "number", MIN_ACTIVATION_BALANCE: "number", - PENDING_BALANCE_DEPOSITS_LIMIT: "number", + PENDING_DEPOSITS_LIMIT: "number", PENDING_PARTIAL_WITHDRAWALS_LIMIT: "number", PENDING_CONSOLIDATIONS_LIMIT: "number", MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD: "number", 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 2322b2e1ff10..c54b0d4d44f8 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,13 @@ import {loadConfigYaml} from "../yaml.js"; // Not e2e, but slow. Run with e2e tests /** https://github.com/ethereum/consensus-specs/releases */ -const specConfigCommit = "v1.5.0-alpha.3"; +const specConfigCommit = "v1.5.0-alpha.8"; +/** + * Fields that we filter from local config when doing comparison. + * Ideally this should be empty as it is not spec compliant + * For `MAX_BLOBS_PER_BLOCK`, see https://github.com/ChainSafe/lodestar/issues/7172 + */ +const ignoredLocalPresetFields: (keyof BeaconPreset)[] = ["MAX_BLOBS_PER_BLOCK"]; describe("Ensure config is synced", () => { vi.setConfig({testTimeout: 60 * 1000}); @@ -25,12 +31,22 @@ describe("Ensure config is synced", () => { }); function assertCorrectPreset(localPreset: BeaconPreset, remotePreset: BeaconPreset): void { + const filteredLocalPreset: Partial = Object.keys(localPreset) + .filter((key) => !ignoredLocalPresetFields.includes(key as keyof BeaconPreset)) + .reduce( + (acc, key) => { + acc[key as keyof BeaconPreset] = localPreset[key as keyof BeaconPreset]; + return acc; + }, + {} as Partial + ); + // Check each key for better debuggability for (const key of Object.keys(remotePreset) as (keyof BeaconPreset)[]) { - expect(localPreset[key]).toBe(remotePreset[key]); + expect(filteredLocalPreset[key]).toBe(remotePreset[key]); } - expect(localPreset).toEqual(remotePreset); + expect(filteredLocalPreset).toEqual(remotePreset); } async function downloadRemoteConfig(preset: "mainnet" | "minimal", commit: string): Promise { diff --git a/packages/state-transition/src/block/processConsolidationRequest.ts b/packages/state-transition/src/block/processConsolidationRequest.ts index 691ecd5eca0b..c14612579c58 100644 --- a/packages/state-transition/src/block/processConsolidationRequest.ts +++ b/packages/state-transition/src/block/processConsolidationRequest.ts @@ -3,31 +3,38 @@ import {FAR_FUTURE_EPOCH, MIN_ACTIVATION_BALANCE, PENDING_CONSOLIDATIONS_LIMIT} import {CachedBeaconStateElectra} from "../types.js"; import {getConsolidationChurnLimit, isActiveValidator} from "../util/validator.js"; -import {hasExecutionWithdrawalCredential} from "../util/electra.js"; +import {hasExecutionWithdrawalCredential, switchToCompoundingValidator} from "../util/electra.js"; import {computeConsolidationEpochAndUpdateChurn} from "../util/epoch.js"; +import {hasEth1WithdrawalCredential} from "../util/capella.js"; +// TODO Electra: Clean up necessary as there is a lot of overlap with isValidSwitchToCompoundRequest export function processConsolidationRequest( state: CachedBeaconStateElectra, consolidationRequest: electra.ConsolidationRequest ): void { - // If the pending consolidations queue is full, consolidation requests are ignored - if (state.pendingConsolidations.length >= PENDING_CONSOLIDATIONS_LIMIT) { + const {sourcePubkey, targetPubkey, sourceAddress} = consolidationRequest; + const sourceIndex = state.epochCtx.getValidatorIndex(sourcePubkey); + const targetIndex = state.epochCtx.getValidatorIndex(targetPubkey); + + if (sourceIndex === null || targetIndex === null) { return; } - // If there is too little available consolidation churn limit, consolidation requests are ignored - if (getConsolidationChurnLimit(state.epochCtx) <= MIN_ACTIVATION_BALANCE) { + if (isValidSwitchToCompoundRequest(state, consolidationRequest)) { + switchToCompoundingValidator(state, sourceIndex); + // Early return since we have already switched validator to compounding return; } - const {sourcePubkey, targetPubkey} = consolidationRequest; - const sourceIndex = state.epochCtx.getValidatorIndex(sourcePubkey); - const targetIndex = state.epochCtx.getValidatorIndex(targetPubkey); - - if (sourceIndex === null || targetIndex === null) { + // If the pending consolidations queue is full, consolidation requests are ignored + if (state.pendingConsolidations.length >= PENDING_CONSOLIDATIONS_LIMIT) { return; } + // If there is too little available consolidation churn limit, consolidation requests are ignored + if (getConsolidationChurnLimit(state.epochCtx) <= MIN_ACTIVATION_BALANCE) { + return; + } // Verify that source != target, so a consolidation cannot be used as an exit. if (sourceIndex === targetIndex) { return; @@ -46,7 +53,7 @@ export function processConsolidationRequest( return; } - if (Buffer.compare(sourceWithdrawalAddress, consolidationRequest.sourceAddress) !== 0) { + if (Buffer.compare(sourceWithdrawalAddress, sourceAddress) !== 0) { return; } @@ -70,4 +77,55 @@ export function processConsolidationRequest( targetIndex, }); state.pendingConsolidations.push(pendingConsolidation); + + // Churn any target excess active balance of target and raise its max + if (hasEth1WithdrawalCredential(targetValidator.withdrawalCredentials)) { + switchToCompoundingValidator(state, targetIndex); + } +} + +/** + * Determine if we should set consolidation target validator to compounding credential + */ +function isValidSwitchToCompoundRequest( + state: CachedBeaconStateElectra, + consolidationRequest: electra.ConsolidationRequest +): boolean { + const {sourcePubkey, targetPubkey, sourceAddress} = consolidationRequest; + const sourceIndex = state.epochCtx.getValidatorIndex(sourcePubkey); + const targetIndex = state.epochCtx.getValidatorIndex(targetPubkey); + + // Verify pubkey exists + if (sourceIndex === null) { + return false; + } + + // Switch to compounding requires source and target be equal + if (sourceIndex !== targetIndex) { + return false; + } + + const sourceValidator = state.validators.getReadonly(sourceIndex); + const sourceWithdrawalAddress = sourceValidator.withdrawalCredentials.subarray(12); + // Verify request has been authorized + if (Buffer.compare(sourceWithdrawalAddress, sourceAddress) !== 0) { + return false; + } + + // Verify source withdrawal credentials + if (!hasEth1WithdrawalCredential(sourceValidator.withdrawalCredentials)) { + return false; + } + + // Verify the source is active + if (!isActiveValidator(sourceValidator, state.epochCtx.epoch)) { + return false; + } + + // Verify exit for source has not been initiated + if (sourceValidator.exitEpoch !== FAR_FUTURE_EPOCH) { + return false; + } + + return true; } diff --git a/packages/state-transition/src/block/processDeposit.ts b/packages/state-transition/src/block/processDeposit.ts index fd671dc5f6f6..b7e9827c4cd7 100644 --- a/packages/state-transition/src/block/processDeposit.ts +++ b/packages/state-transition/src/block/processDeposit.ts @@ -8,6 +8,7 @@ import { EFFECTIVE_BALANCE_INCREMENT, FAR_FUTURE_EPOCH, ForkSeq, + GENESIS_SLOT, MAX_EFFECTIVE_BALANCE, } from "@lodestar/params"; @@ -15,14 +16,7 @@ import {DepositData} from "@lodestar/types/lib/phase0/types.js"; import {DepositRequest} from "@lodestar/types/lib/electra/types.js"; import {BeaconConfig} from "@lodestar/config"; import {ZERO_HASH} from "../constants/index.js"; -import { - computeDomain, - computeSigningRoot, - hasCompoundingWithdrawalCredential, - hasEth1WithdrawalCredential, - increaseBalance, - switchToCompoundingValidator, -} from "../util/index.js"; +import {computeDomain, computeSigningRoot, getMaxEffectiveBalance, increaseBalance} from "../util/index.js"; import {CachedBeaconStateAllForks, CachedBeaconStateAltair, CachedBeaconStateElectra} from "../types.js"; /** @@ -61,38 +55,43 @@ export function applyDeposit( state: CachedBeaconStateAllForks, deposit: DepositData | DepositRequest ): void { - const {config, validators, epochCtx} = state; - const {pubkey, withdrawalCredentials, amount} = deposit; + const {config, epochCtx, validators} = state; + const {pubkey, withdrawalCredentials, amount, signature} = deposit; const cachedIndex = epochCtx.getValidatorIndex(pubkey); - if (cachedIndex === null || !Number.isSafeInteger(cachedIndex) || cachedIndex >= validators.length) { - if (isValidDepositSignature(config, pubkey, withdrawalCredentials, amount, deposit.signature)) { - addValidatorToRegistry(fork, state, pubkey, withdrawalCredentials, amount); - } - } else { - if (fork < ForkSeq.electra) { + const isNewValidator = cachedIndex === null || !Number.isSafeInteger(cachedIndex) || cachedIndex >= validators.length; + + if (fork < ForkSeq.electra) { + if (isNewValidator) { + if (isValidDepositSignature(config, pubkey, withdrawalCredentials, amount, signature)) { + addValidatorToRegistry(fork, state, pubkey, withdrawalCredentials, amount); + } + } else { // increase balance by deposit amount right away pre-electra increaseBalance(state, cachedIndex, amount); - } else if (fork >= ForkSeq.electra) { - const stateElectra = state as CachedBeaconStateElectra; - const pendingBalanceDeposit = ssz.electra.PendingBalanceDeposit.toViewDU({ - index: cachedIndex, - amount: BigInt(amount), - }); - stateElectra.pendingBalanceDeposits.push(pendingBalanceDeposit); - - if ( - hasCompoundingWithdrawalCredential(withdrawalCredentials) && - hasEth1WithdrawalCredential(validators.getReadonly(cachedIndex).withdrawalCredentials) && - isValidDepositSignature(config, pubkey, withdrawalCredentials, amount, deposit.signature) - ) { - switchToCompoundingValidator(stateElectra, cachedIndex); + } + } else { + const stateElectra = state as CachedBeaconStateElectra; + const pendingDeposit = ssz.electra.PendingDeposit.toViewDU({ + pubkey, + withdrawalCredentials, + amount, + signature, + slot: GENESIS_SLOT, // Use GENESIS_SLOT to distinguish from a pending deposit request + }); + + if (isNewValidator) { + if (isValidDepositSignature(config, pubkey, withdrawalCredentials, amount, deposit.signature)) { + addValidatorToRegistry(fork, state, pubkey, withdrawalCredentials, 0); + stateElectra.pendingDeposits.push(pendingDeposit); } + } else { + stateElectra.pendingDeposits.push(pendingDeposit); } } } -function addValidatorToRegistry( +export function addValidatorToRegistry( fork: ForkSeq, state: CachedBeaconStateAllForks, pubkey: BLSPubkey, @@ -101,8 +100,10 @@ function addValidatorToRegistry( ): void { const {validators, epochCtx} = state; // add validator and balance entries - const effectiveBalance = - fork < ForkSeq.electra ? Math.min(amount - (amount % EFFECTIVE_BALANCE_INCREMENT), MAX_EFFECTIVE_BALANCE) : 0; + const effectiveBalance = Math.min( + amount - (amount % EFFECTIVE_BALANCE_INCREMENT), + fork < ForkSeq.electra ? MAX_EFFECTIVE_BALANCE : getMaxEffectiveBalance(withdrawalCredentials) + ); validators.push( ssz.phase0.Validator.toViewDU({ pubkey, @@ -138,20 +139,10 @@ function addValidatorToRegistry( stateAltair.currentEpochParticipation.push(0); } - if (fork < ForkSeq.electra) { - state.balances.push(amount); - } else if (fork >= ForkSeq.electra) { - state.balances.push(0); - const stateElectra = state as CachedBeaconStateElectra; - const pendingBalanceDeposit = ssz.electra.PendingBalanceDeposit.toViewDU({ - index: validatorIndex, - amount: BigInt(amount), - }); - stateElectra.pendingBalanceDeposits.push(pendingBalanceDeposit); - } + state.balances.push(amount); } -function isValidDepositSignature( +export function isValidDepositSignature( config: BeaconConfig, pubkey: Uint8Array, withdrawalCredentials: Uint8Array, diff --git a/packages/state-transition/src/block/processDepositRequest.ts b/packages/state-transition/src/block/processDepositRequest.ts index e5dd99a40c4e..7c6ea4928f54 100644 --- a/packages/state-transition/src/block/processDepositRequest.ts +++ b/packages/state-transition/src/block/processDepositRequest.ts @@ -1,17 +1,20 @@ -import {electra} from "@lodestar/types"; +import {electra, ssz} from "@lodestar/types"; import {ForkSeq, UNSET_DEPOSIT_REQUESTS_START_INDEX} from "@lodestar/params"; import {CachedBeaconStateElectra} from "../types.js"; -import {applyDeposit} from "./processDeposit.js"; -export function processDepositRequest( - fork: ForkSeq, - state: CachedBeaconStateElectra, - depositRequest: electra.DepositRequest -): void { +export function processDepositRequest(state: CachedBeaconStateElectra, depositRequest: electra.DepositRequest): void { if (state.depositRequestsStartIndex === UNSET_DEPOSIT_REQUESTS_START_INDEX) { state.depositRequestsStartIndex = BigInt(depositRequest.index); } - applyDeposit(fork, state, depositRequest); + // Create pending deposit + const pendingDeposit = ssz.electra.PendingDeposit.toViewDU({ + pubkey: depositRequest.pubkey, + withdrawalCredentials: depositRequest.withdrawalCredentials, + amount: depositRequest.amount, + signature: depositRequest.signature, + slot: state.slot, + }); + state.pendingDeposits.push(pendingDeposit); } diff --git a/packages/state-transition/src/block/processOperations.ts b/packages/state-transition/src/block/processOperations.ts index bb52af14ba32..d611581584c1 100644 --- a/packages/state-transition/src/block/processOperations.ts +++ b/packages/state-transition/src/block/processOperations.ts @@ -68,7 +68,7 @@ export function processOperations( const bodyElectra = body as electra.BeaconBlockBody; for (const depositRequest of bodyElectra.executionRequests.deposits) { - processDepositRequest(fork, stateElectra, depositRequest); + processDepositRequest(stateElectra, depositRequest); } for (const elWithdrawalRequest of bodyElectra.executionRequests.withdrawals) { diff --git a/packages/state-transition/src/block/processWithdrawals.ts b/packages/state-transition/src/block/processWithdrawals.ts index d4dfd47b4d94..610a2ed62b41 100644 --- a/packages/state-transition/src/block/processWithdrawals.ts +++ b/packages/state-transition/src/block/processWithdrawals.ts @@ -99,6 +99,8 @@ export function getExpectedWithdrawals( const withdrawals: capella.Withdrawal[] = []; const isPostElectra = fork >= ForkSeq.electra; + // partialWithdrawalsCount is withdrawals coming from EL since electra (EIP-7002) + let partialWithdrawalsCount = 0; if (isPostElectra) { const stateElectra = state as CachedBeaconStateElectra; @@ -138,11 +140,10 @@ export function getExpectedWithdrawals( }); withdrawalIndex++; } + partialWithdrawalsCount++; } } - // partialWithdrawalsCount is withdrawals coming from EL since electra (EIP-7002) - const partialWithdrawalsCount = withdrawals.length; const bound = Math.min(validators.length, MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP); let n = 0; // Just run a bounded loop max iterating over all withdrawals diff --git a/packages/state-transition/src/cache/epochTransitionCache.ts b/packages/state-transition/src/cache/epochTransitionCache.ts index 27b781e8a6a1..2a35317fbc93 100644 --- a/packages/state-transition/src/cache/epochTransitionCache.ts +++ b/packages/state-transition/src/cache/epochTransitionCache.ts @@ -139,12 +139,6 @@ export interface EpochTransitionCache { */ validators: phase0.Validator[]; - /** - * This is for electra only - * Validators that're switched to compounding during processPendingConsolidations(), not available in beforeProcessEpoch() - */ - newCompoundingValidators?: Set; - /** * balances array will be populated by processRewardsAndPenalties() and consumed by processEffectiveBalanceUpdates(). * processRewardsAndPenalties() already has a regular Javascript array of balances. @@ -518,8 +512,6 @@ export function beforeProcessEpoch( inclusionDelays, flags, validators, - // will be assigned in processPendingConsolidations() - newCompoundingValidators: undefined, // Will be assigned in processRewardsAndPenalties() balances: undefined, }; diff --git a/packages/state-transition/src/constants/constants.ts b/packages/state-transition/src/constants/constants.ts index c3ff3f9ac79e..3afb4e687508 100644 --- a/packages/state-transition/src/constants/constants.ts +++ b/packages/state-transition/src/constants/constants.ts @@ -1,10 +1,12 @@ -export const ZERO_HASH = Buffer.alloc(32, 0); -export const EMPTY_SIGNATURE = Buffer.alloc(96, 0); +export const ZERO_HASH = new Uint8Array(32).fill(0); +export const EMPTY_SIGNATURE = new Uint8Array(96).fill(0); export const SECONDS_PER_DAY = 86400; export const BASE_REWARDS_PER_EPOCH = 4; -export const G2_POINT_AT_INFINITY = Buffer.from( - "c000000000000000000000000000000000000000000000000000000000000000" + - "0000000000000000000000000000000000000000000000000000000000000000" + - "0000000000000000000000000000000000000000000000000000000000000000", - "hex" +export const G2_POINT_AT_INFINITY = new Uint8Array( + Buffer.from( + "c000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000", + "hex" + ) ); diff --git a/packages/state-transition/src/epoch/index.ts b/packages/state-transition/src/epoch/index.ts index bfb415b9ed6a..b0b1651321d5 100644 --- a/packages/state-transition/src/epoch/index.ts +++ b/packages/state-transition/src/epoch/index.ts @@ -28,7 +28,7 @@ import {processRewardsAndPenalties} from "./processRewardsAndPenalties.js"; import {processSlashings} from "./processSlashings.js"; import {processSlashingsReset} from "./processSlashingsReset.js"; import {processSyncCommitteeUpdates} from "./processSyncCommitteeUpdates.js"; -import {processPendingBalanceDeposits} from "./processPendingBalanceDeposits.js"; +import {processPendingDeposits} from "./processPendingDeposits.js"; import {processPendingConsolidations} from "./processPendingConsolidations.js"; // For spec tests @@ -48,7 +48,7 @@ export { processParticipationFlagUpdates, processSyncCommitteeUpdates, processHistoricalSummariesUpdate, - processPendingBalanceDeposits, + processPendingDeposits, processPendingConsolidations, }; @@ -70,7 +70,7 @@ export enum EpochTransitionStep { processEffectiveBalanceUpdates = "processEffectiveBalanceUpdates", processParticipationFlagUpdates = "processParticipationFlagUpdates", processSyncCommitteeUpdates = "processSyncCommitteeUpdates", - processPendingBalanceDeposits = "processPendingBalanceDeposits", + processPendingDeposits = "processPendingDeposits", processPendingConsolidations = "processPendingConsolidations", } @@ -131,9 +131,9 @@ export function processEpoch( const stateElectra = state as CachedBeaconStateElectra; { const timer = metrics?.epochTransitionStepTime.startTimer({ - step: EpochTransitionStep.processPendingBalanceDeposits, + step: EpochTransitionStep.processPendingDeposits, }); - processPendingBalanceDeposits(stateElectra, cache); + processPendingDeposits(stateElectra, cache); timer?.(); } diff --git a/packages/state-transition/src/epoch/processEffectiveBalanceUpdates.ts b/packages/state-transition/src/epoch/processEffectiveBalanceUpdates.ts index 0ea4b49dddf4..1fe5c92eea1a 100644 --- a/packages/state-transition/src/epoch/processEffectiveBalanceUpdates.ts +++ b/packages/state-transition/src/epoch/processEffectiveBalanceUpdates.ts @@ -5,12 +5,10 @@ import { HYSTERESIS_QUOTIENT, HYSTERESIS_UPWARD_MULTIPLIER, MAX_EFFECTIVE_BALANCE, - MAX_EFFECTIVE_BALANCE_ELECTRA, - MIN_ACTIVATION_BALANCE, TIMELY_TARGET_FLAG_INDEX, } from "@lodestar/params"; import {EpochTransitionCache, CachedBeaconStateAllForks, BeaconStateAltair} from "../types.js"; -import {hasCompoundingWithdrawalCredential} from "../util/electra.js"; +import {getMaxEffectiveBalance} from "../util/validator.js"; /** Same to https://github.com/ethereum/eth2.0-specs/blob/v1.1.0-alpha.5/specs/altair/beacon-chain.md#has_flag */ const TIMELY_TARGET = 1 << TIMELY_TARGET_FLAG_INDEX; @@ -42,11 +40,10 @@ export function processEffectiveBalanceUpdates( // update effective balances with hysteresis // epochTransitionCache.balances is initialized in processRewardsAndPenalties() - // and updated in processPendingBalanceDeposits() and processPendingConsolidations() + // and updated in processPendingDeposits() and processPendingConsolidations() // so it's recycled here for performance. const balances = cache.balances ?? state.balances.getAll(); const currentEpochValidators = cache.validators; - const newCompoundingValidators = cache.newCompoundingValidators ?? new Set(); let numUpdate = 0; for (let i = 0, len = balances.length; i < len; i++) { @@ -61,10 +58,7 @@ export function processEffectiveBalanceUpdates( effectiveBalanceLimit = MAX_EFFECTIVE_BALANCE; } else { // from electra, effectiveBalanceLimit is per validator - const isCompoundingValidator = - hasCompoundingWithdrawalCredential(currentEpochValidators[i].withdrawalCredentials) || - newCompoundingValidators.has(i); - effectiveBalanceLimit = isCompoundingValidator ? MAX_EFFECTIVE_BALANCE_ELECTRA : MIN_ACTIVATION_BALANCE; + effectiveBalanceLimit = getMaxEffectiveBalance(currentEpochValidators[i].withdrawalCredentials); } if ( diff --git a/packages/state-transition/src/epoch/processPendingBalanceDeposits.ts b/packages/state-transition/src/epoch/processPendingBalanceDeposits.ts deleted file mode 100644 index 1cd9e17efbad..000000000000 --- a/packages/state-transition/src/epoch/processPendingBalanceDeposits.ts +++ /dev/null @@ -1,69 +0,0 @@ -import {FAR_FUTURE_EPOCH} from "@lodestar/params"; -import {CachedBeaconStateElectra, EpochTransitionCache} from "../types.js"; -import {increaseBalance} from "../util/balance.js"; -import {getActivationExitChurnLimit} from "../util/validator.js"; - -/** - * Starting from Electra: - * Process pending balance deposits from state subject to churn limit and depsoitBalanceToConsume. - * For each eligible `deposit`, call `increaseBalance()`. - * Remove the processed deposits from `state.pendingBalanceDeposits`. - * Update `state.depositBalanceToConsume` for the next epoch - * - * TODO Electra: Update ssz library to support batch push to `pendingBalanceDeposits` - */ -export function processPendingBalanceDeposits(state: CachedBeaconStateElectra, cache: EpochTransitionCache): void { - const nextEpoch = state.epochCtx.epoch + 1; - const availableForProcessing = state.depositBalanceToConsume + BigInt(getActivationExitChurnLimit(state.epochCtx)); - let processedAmount = 0n; - let nextDepositIndex = 0; - const depositsToPostpone = []; - const validators = state.validators; - const cachedBalances = cache.balances; - - for (const deposit of state.pendingBalanceDeposits.getAllReadonly()) { - const {amount, index: depositIndex} = deposit; - const validator = validators.getReadonly(depositIndex); - - // Validator is exiting, postpone the deposit until after withdrawable epoch - if (validator.exitEpoch < FAR_FUTURE_EPOCH) { - if (nextEpoch <= validator.withdrawableEpoch) { - depositsToPostpone.push(deposit); - } else { - // Deposited balance will never become active. Increase balance but do not consume churn - increaseBalance(state, depositIndex, Number(amount)); - if (cachedBalances) { - cachedBalances[depositIndex] += Number(amount); - } - } - } else { - // Validator is not exiting, attempt to process deposit - if (processedAmount + amount > availableForProcessing) { - // Deposit does not fit in the churn, no more deposit processing in this epoch. - break; - } - // Deposit fits in the churn, process it. Increase balance and consume churn. - increaseBalance(state, depositIndex, Number(amount)); - if (cachedBalances) { - cachedBalances[depositIndex] += Number(amount); - } - processedAmount = processedAmount + amount; - } - // Regardless of how the deposit was handled, we move on in the queue. - nextDepositIndex++; - } - - const remainingPendingBalanceDeposits = state.pendingBalanceDeposits.sliceFrom(nextDepositIndex); - state.pendingBalanceDeposits = remainingPendingBalanceDeposits; - - if (remainingPendingBalanceDeposits.length === 0) { - state.depositBalanceToConsume = 0n; - } else { - state.depositBalanceToConsume = availableForProcessing - processedAmount; - } - - // TODO Electra: add a function in ListCompositeTreeView to support batch push operation - for (const deposit of depositsToPostpone) { - state.pendingBalanceDeposits.push(deposit); - } -} diff --git a/packages/state-transition/src/epoch/processPendingConsolidations.ts b/packages/state-transition/src/epoch/processPendingConsolidations.ts index 28178a509bba..0ec39409f8a7 100644 --- a/packages/state-transition/src/epoch/processPendingConsolidations.ts +++ b/packages/state-transition/src/epoch/processPendingConsolidations.ts @@ -1,8 +1,6 @@ -import {ValidatorIndex} from "@lodestar/types"; import {CachedBeaconStateElectra, EpochTransitionCache} from "../types.js"; import {decreaseBalance, increaseBalance} from "../util/balance.js"; -import {getActiveBalance} from "../util/validator.js"; -import {switchToCompoundingValidator} from "../util/electra.js"; +import {getMaxEffectiveBalance} from "../util/validator.js"; /** * Starting from Electra: @@ -22,7 +20,6 @@ export function processPendingConsolidations(state: CachedBeaconStateElectra, ca let nextPendingConsolidation = 0; const validators = state.validators; const cachedBalances = cache.balances; - const newCompoundingValidators = new Set(); for (const pendingConsolidation of state.pendingConsolidations.getAllReadonly()) { const {sourceIndex, targetIndex} = pendingConsolidation; @@ -36,21 +33,18 @@ export function processPendingConsolidations(state: CachedBeaconStateElectra, ca if (sourceValidator.withdrawableEpoch > nextEpoch) { break; } - // Churn any target excess active balance of target and raise its max - switchToCompoundingValidator(state, targetIndex); - newCompoundingValidators.add(targetIndex); // Move active balance to target. Excess balance is withdrawable. - const activeBalance = getActiveBalance(state, sourceIndex); - decreaseBalance(state, sourceIndex, activeBalance); - increaseBalance(state, targetIndex, activeBalance); + const maxEffectiveBalance = getMaxEffectiveBalance(state.validators.getReadonly(sourceIndex).withdrawalCredentials); + const sourceEffectiveBalance = Math.min(state.balances.get(sourceIndex), maxEffectiveBalance); + decreaseBalance(state, sourceIndex, sourceEffectiveBalance); + increaseBalance(state, targetIndex, sourceEffectiveBalance); if (cachedBalances) { - cachedBalances[sourceIndex] -= activeBalance; - cachedBalances[targetIndex] += activeBalance; + cachedBalances[sourceIndex] -= sourceEffectiveBalance; + cachedBalances[targetIndex] += sourceEffectiveBalance; } nextPendingConsolidation++; } - cache.newCompoundingValidators = newCompoundingValidators; state.pendingConsolidations = state.pendingConsolidations.sliceFrom(nextPendingConsolidation); } diff --git a/packages/state-transition/src/epoch/processPendingDeposits.ts b/packages/state-transition/src/epoch/processPendingDeposits.ts new file mode 100644 index 000000000000..53af3ab38763 --- /dev/null +++ b/packages/state-transition/src/epoch/processPendingDeposits.ts @@ -0,0 +1,117 @@ +import {FAR_FUTURE_EPOCH, ForkSeq, GENESIS_SLOT, MAX_PENDING_DEPOSITS_PER_EPOCH} from "@lodestar/params"; +import {PendingDeposit} from "@lodestar/types/lib/electra/types.js"; +import {CachedBeaconStateElectra, EpochTransitionCache} from "../types.js"; +import {increaseBalance} from "../util/balance.js"; +import {getActivationExitChurnLimit} from "../util/validator.js"; +import {computeStartSlotAtEpoch} from "../util/epoch.js"; +import {addValidatorToRegistry, isValidDepositSignature} from "../block/processDeposit.js"; + +/** + * Starting from Electra: + * Process pending balance deposits from state subject to churn limit and depsoitBalanceToConsume. + * For each eligible `deposit`, call `increaseBalance()`. + * Remove the processed deposits from `state.pendingDeposits`. + * Update `state.depositBalanceToConsume` for the next epoch + * + * TODO Electra: Update ssz library to support batch push to `pendingDeposits` + */ +export function processPendingDeposits(state: CachedBeaconStateElectra, cache: EpochTransitionCache): void { + const nextEpoch = state.epochCtx.epoch + 1; + const availableForProcessing = state.depositBalanceToConsume + BigInt(getActivationExitChurnLimit(state.epochCtx)); + let processedAmount = 0; + let nextDepositIndex = 0; + const depositsToPostpone = []; + let isChurnLimitReached = false; + const finalizedSlot = computeStartSlotAtEpoch(state.finalizedCheckpoint.epoch); + + for (const deposit of state.pendingDeposits.getAllReadonly()) { + // Do not process deposit requests if Eth1 bridge deposits are not yet applied. + if ( + // Is deposit request + deposit.slot > GENESIS_SLOT && + // There are pending Eth1 bridge deposits + state.eth1DepositIndex < state.depositRequestsStartIndex + ) { + break; + } + + // Check if deposit has been finalized, otherwise, stop processing. + if (deposit.slot > finalizedSlot) { + break; + } + + // Check if number of processed deposits has not reached the limit, otherwise, stop processing. + if (nextDepositIndex >= MAX_PENDING_DEPOSITS_PER_EPOCH) { + break; + } + + // Read validator state + let isValidatorExited = false; + let isValidatorWithdrawn = false; + + const validatorIndex = state.epochCtx.getValidatorIndex(deposit.pubkey); + if (validatorIndex !== null) { + const validator = state.validators.getReadonly(validatorIndex); + isValidatorExited = validator.exitEpoch < FAR_FUTURE_EPOCH; + isValidatorWithdrawn = validator.withdrawableEpoch < nextEpoch; + } + + if (isValidatorWithdrawn) { + // Deposited balance will never become active. Increase balance but do not consume churn + applyPendingDeposit(state, deposit, cache); + } else if (isValidatorExited) { + // Validator is exiting, postpone the deposit until after withdrawable epoch + depositsToPostpone.push(deposit); + } else { + // Check if deposit fits in the churn, otherwise, do no more deposit processing in this epoch. + isChurnLimitReached = processedAmount + deposit.amount > availableForProcessing; + if (isChurnLimitReached) { + break; + } + // Consume churn and apply deposit. + processedAmount += deposit.amount; + applyPendingDeposit(state, deposit, cache); + } + + // Regardless of how the deposit was handled, we move on in the queue. + nextDepositIndex++; + } + + const remainingPendingDeposits = state.pendingDeposits.sliceFrom(nextDepositIndex); + state.pendingDeposits = remainingPendingDeposits; + + // TODO Electra: add a function in ListCompositeTreeView to support batch push operation + for (const deposit of depositsToPostpone) { + state.pendingDeposits.push(deposit); + } + + // Accumulate churn only if the churn limit has been hit. + if (isChurnLimitReached) { + state.depositBalanceToConsume = availableForProcessing - BigInt(processedAmount); + } else { + state.depositBalanceToConsume = 0n; + } +} + +function applyPendingDeposit( + state: CachedBeaconStateElectra, + deposit: PendingDeposit, + cache: EpochTransitionCache +): void { + const validatorIndex = state.epochCtx.getValidatorIndex(deposit.pubkey); + const {pubkey, withdrawalCredentials, amount, signature} = deposit; + const cachedBalances = cache.balances; + + if (validatorIndex === null) { + // Verify the deposit signature (proof of possession) which is not checked by the deposit contract + if (isValidDepositSignature(state.config, pubkey, withdrawalCredentials, amount, signature)) { + addValidatorToRegistry(ForkSeq.electra, state, pubkey, withdrawalCredentials, amount); + } + } else { + // Increase balance + increaseBalance(state, validatorIndex, amount); + if (cachedBalances) { + cachedBalances[validatorIndex] += amount; + } + } +} diff --git a/packages/state-transition/src/slot/upgradeStateToElectra.ts b/packages/state-transition/src/slot/upgradeStateToElectra.ts index b7cdde86a479..a3c8981ab13f 100644 --- a/packages/state-transition/src/slot/upgradeStateToElectra.ts +++ b/packages/state-transition/src/slot/upgradeStateToElectra.ts @@ -1,14 +1,11 @@ import {Epoch, ValidatorIndex, ssz} from "@lodestar/types"; -import {FAR_FUTURE_EPOCH, UNSET_DEPOSIT_REQUESTS_START_INDEX} from "@lodestar/params"; +import {FAR_FUTURE_EPOCH, GENESIS_SLOT, UNSET_DEPOSIT_REQUESTS_START_INDEX} from "@lodestar/params"; import {CachedBeaconStateDeneb} from "../types.js"; import {CachedBeaconStateElectra, getCachedBeaconState} from "../cache/stateCache.js"; -import { - hasCompoundingWithdrawalCredential, - queueEntireBalanceAndResetValidator, - queueExcessActiveBalance, -} from "../util/electra.js"; +import {hasCompoundingWithdrawalCredential, queueExcessActiveBalance} from "../util/electra.js"; import {computeActivationExitEpoch} from "../util/epoch.js"; import {getActivationExitChurnLimit, getConsolidationChurnLimit} from "../util/validator.js"; +import {G2_POINT_AT_INFINITY} from "../constants/constants.js"; /** * Upgrade a state from Deneb to Electra. @@ -81,8 +78,6 @@ export function upgradeStateToElectra(stateDeneb: CachedBeaconStateDeneb): Cache stateElectraView.earliestExitEpoch = Math.max(...exitEpochs) + 1; stateElectraView.consolidationBalanceToConsume = BigInt(0); stateElectraView.earliestConsolidationEpoch = computeActivationExitEpoch(currentEpochPre); - // stateElectraView.pendingBalanceDeposits = ssz.electra.PendingBalanceDeposits.defaultViewDU(); - // pendingBalanceDeposits, pendingPartialWithdrawals, pendingConsolidations are default values // TODO-electra: can we improve this? stateElectraView.commit(); const tmpElectraState = getCachedBeaconState(stateElectraView, stateDeneb); @@ -95,7 +90,23 @@ export function upgradeStateToElectra(stateDeneb: CachedBeaconStateDeneb): Cache }); for (const validatorIndex of preActivation) { - queueEntireBalanceAndResetValidator(stateElectraView as CachedBeaconStateElectra, validatorIndex); + const stateElectra = stateElectraView as CachedBeaconStateElectra; + const balance = stateElectra.balances.get(validatorIndex); + stateElectra.balances.set(validatorIndex, 0); + + const validator = stateElectra.validators.get(validatorIndex); + validator.effectiveBalance = 0; + stateElectra.epochCtx.effectiveBalanceIncrementsSet(validatorIndex, 0); + validator.activationEligibilityEpoch = FAR_FUTURE_EPOCH; + + const pendingDeposit = ssz.electra.PendingDeposit.toViewDU({ + pubkey: validator.pubkey, + withdrawalCredentials: validator.withdrawalCredentials, + amount: balance, + signature: G2_POINT_AT_INFINITY, + slot: GENESIS_SLOT, + }); + stateElectra.pendingDeposits.push(pendingDeposit); } for (let i = 0; i < validatorsArr.length; i++) { @@ -117,46 +128,3 @@ export function upgradeStateToElectra(stateDeneb: CachedBeaconStateDeneb): Cache return stateElectra; } - -export function upgradeStateToElectraOriginal(stateDeneb: CachedBeaconStateDeneb): CachedBeaconStateElectra { - const {config} = stateDeneb; - - const stateElectraNode = ssz.deneb.BeaconState.commitViewDU(stateDeneb); - const stateElectraView = ssz.electra.BeaconState.getViewDU(stateElectraNode); - - const stateElectra = getCachedBeaconState(stateElectraView, stateDeneb); - - stateElectra.fork = ssz.phase0.Fork.toViewDU({ - previousVersion: stateDeneb.fork.currentVersion, - currentVersion: config.ELECTRA_FORK_VERSION, - epoch: stateDeneb.epochCtx.epoch, - }); - - // default value of depositRequestsStartIndex is UNSET_DEPOSIT_REQUESTS_START_INDEX - stateElectra.depositRequestsStartIndex = UNSET_DEPOSIT_REQUESTS_START_INDEX; - - const validatorsArr = stateElectra.validators.getAllReadonly(); - - for (let i = 0; i < validatorsArr.length; i++) { - const validator = validatorsArr[i]; - - // [EIP-7251]: add validators that are not yet active to pending balance deposits - if (validator.activationEligibilityEpoch === FAR_FUTURE_EPOCH) { - queueEntireBalanceAndResetValidator(stateElectra, i); - } - - // [EIP-7251]: Ensure early adopters of compounding credentials go through the activation churn - const withdrawalCredential = validator.withdrawalCredentials; - if (hasCompoundingWithdrawalCredential(withdrawalCredential)) { - queueExcessActiveBalance(stateElectra, i); - } - } - - // Commit new added fields ViewDU to the root node - stateElectra.commit(); - // Clear cache to ensure the cache of deneb fields is not used by new ELECTRA fields - // biome-ignore lint/complexity/useLiteralKeys: It is a protected attribute - stateElectra["clearCache"](); - - return stateElectra; -} diff --git a/packages/state-transition/src/util/electra.ts b/packages/state-transition/src/util/electra.ts index ac34da6407de..f1082c6d4603 100644 --- a/packages/state-transition/src/util/electra.ts +++ b/packages/state-transition/src/util/electra.ts @@ -1,6 +1,7 @@ -import {COMPOUNDING_WITHDRAWAL_PREFIX, FAR_FUTURE_EPOCH, MIN_ACTIVATION_BALANCE} from "@lodestar/params"; +import {COMPOUNDING_WITHDRAWAL_PREFIX, GENESIS_SLOT, MIN_ACTIVATION_BALANCE} from "@lodestar/params"; import {ValidatorIndex, ssz} from "@lodestar/types"; import {CachedBeaconStateElectra} from "../types.js"; +import {G2_POINT_AT_INFINITY} from "../constants/constants.js"; import {hasEth1WithdrawalCredential} from "./capella.js"; export function hasCompoundingWithdrawalCredential(withdrawalCredentials: Uint8Array): boolean { @@ -16,43 +17,31 @@ export function hasExecutionWithdrawalCredential(withdrawalCredentials: Uint8Arr export function switchToCompoundingValidator(state: CachedBeaconStateElectra, index: ValidatorIndex): void { const validator = state.validators.get(index); - if (hasEth1WithdrawalCredential(validator.withdrawalCredentials)) { - // directly modifying the byte leads to ssz missing the modification resulting into - // wrong root compute, although slicing can be avoided but anyway this is not going - // to be a hot path so its better to clean slice and avoid side effects - const newWithdrawalCredentials = validator.withdrawalCredentials.slice(); - newWithdrawalCredentials[0] = COMPOUNDING_WITHDRAWAL_PREFIX; - validator.withdrawalCredentials = newWithdrawalCredentials; - queueExcessActiveBalance(state, index); - } + // directly modifying the byte leads to ssz missing the modification resulting into + // wrong root compute, although slicing can be avoided but anyway this is not going + // to be a hot path so its better to clean slice and avoid side effects + const newWithdrawalCredentials = validator.withdrawalCredentials.slice(); + newWithdrawalCredentials[0] = COMPOUNDING_WITHDRAWAL_PREFIX; + validator.withdrawalCredentials = newWithdrawalCredentials; + queueExcessActiveBalance(state, index); } export function queueExcessActiveBalance(state: CachedBeaconStateElectra, index: ValidatorIndex): void { const balance = state.balances.get(index); if (balance > MIN_ACTIVATION_BALANCE) { + const validator = state.validators.getReadonly(index); const excessBalance = balance - MIN_ACTIVATION_BALANCE; state.balances.set(index, MIN_ACTIVATION_BALANCE); - const pendingBalanceDeposit = ssz.electra.PendingBalanceDeposit.toViewDU({ - index, - amount: BigInt(excessBalance), + const pendingDeposit = ssz.electra.PendingDeposit.toViewDU({ + pubkey: validator.pubkey, + withdrawalCredentials: validator.withdrawalCredentials, + amount: excessBalance, + // Use bls.G2_POINT_AT_INFINITY as a signature field placeholder + signature: G2_POINT_AT_INFINITY, + // Use GENESIS_SLOT to distinguish from a pending deposit request + slot: GENESIS_SLOT, }); - state.pendingBalanceDeposits.push(pendingBalanceDeposit); + state.pendingDeposits.push(pendingDeposit); } } - -export function queueEntireBalanceAndResetValidator(state: CachedBeaconStateElectra, index: ValidatorIndex): void { - const balance = state.balances.get(index); - state.balances.set(index, 0); - - const validator = state.validators.get(index); - validator.effectiveBalance = 0; - state.epochCtx.effectiveBalanceIncrementsSet(index, 0); - validator.activationEligibilityEpoch = FAR_FUTURE_EPOCH; - - const pendingBalanceDeposit = ssz.electra.PendingBalanceDeposit.toViewDU({ - index, - amount: BigInt(balance), - }); - state.pendingBalanceDeposits.push(pendingBalanceDeposit); -} diff --git a/packages/state-transition/src/util/genesis.ts b/packages/state-transition/src/util/genesis.ts index 54507d0ef235..aca81258a47a 100644 --- a/packages/state-transition/src/util/genesis.ts +++ b/packages/state-transition/src/util/genesis.ts @@ -171,10 +171,15 @@ export function applyDeposits( if (fork >= ForkSeq.electra) { const stateElectra = state as CachedBeaconStateElectra; stateElectra.commit(); - for (const {index: validatorIndex, amount} of stateElectra.pendingBalanceDeposits.getAllReadonly()) { - increaseBalance(state, validatorIndex, Number(amount)); + for (const {pubkey, amount} of stateElectra.pendingDeposits.getAllReadonly()) { + const validatorIndex = state.epochCtx.getValidatorIndex(pubkey); + if (validatorIndex === null) { + // Should not happen if the gensis state is correct + continue; + } + increaseBalance(state, validatorIndex, amount); } - stateElectra.pendingBalanceDeposits = ssz.electra.PendingBalanceDeposits.defaultViewDU(); + stateElectra.pendingDeposits = ssz.electra.PendingDeposits.defaultViewDU(); } // Process activations diff --git a/packages/state-transition/src/util/validator.ts b/packages/state-transition/src/util/validator.ts index bf79f9264342..4906a9349402 100644 --- a/packages/state-transition/src/util/validator.ts +++ b/packages/state-transition/src/util/validator.ts @@ -82,14 +82,6 @@ export function getMaxEffectiveBalance(withdrawalCredentials: Uint8Array): numbe return MIN_ACTIVATION_BALANCE; } -export function getActiveBalance(state: CachedBeaconStateElectra, validatorIndex: ValidatorIndex): number { - const validatorMaxEffectiveBalance = getMaxEffectiveBalance( - state.validators.getReadonly(validatorIndex).withdrawalCredentials - ); - - return Math.min(state.balances.get(validatorIndex), validatorMaxEffectiveBalance); -} - export function getPendingBalanceToWithdraw(state: CachedBeaconStateElectra, validatorIndex: ValidatorIndex): number { return state.pendingPartialWithdrawals .getAllReadonly() diff --git a/packages/state-transition/test/perf/analyzeEpochs.ts b/packages/state-transition/test/perf/analyzeEpochs.ts index 9fe04bf7c522..ebad63ccf0ad 100644 --- a/packages/state-transition/test/perf/analyzeEpochs.ts +++ b/packages/state-transition/test/perf/analyzeEpochs.ts @@ -152,7 +152,7 @@ async function analyzeEpochs(network: NetworkName, fromEpoch?: number): Promise< // processSlashingsAllForks: function of process.indicesToSlash // processSlashingsReset: free // -- electra - // processPendingBalanceDeposits: - + // processPendingDeposits: - // processPendingConsolidations: - // -- altair // processInactivityUpdates: - diff --git a/packages/types/src/electra/sszTypes.ts b/packages/types/src/electra/sszTypes.ts index 9d995c38efd5..079af08352ec 100644 --- a/packages/types/src/electra/sszTypes.ts +++ b/packages/types/src/electra/sszTypes.ts @@ -17,7 +17,7 @@ import { MAX_ATTESTER_SLASHINGS_ELECTRA, MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD, MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD, - PENDING_BALANCE_DEPOSITS_LIMIT, + PENDING_DEPOSITS_LIMIT, PENDING_PARTIAL_WITHDRAWALS_LIMIT, PENDING_CONSOLIDATIONS_LIMIT, FINALIZED_ROOT_DEPTH_ELECTRA, @@ -211,6 +211,7 @@ export const BlindedBeaconBlockBody = new ContainerType( executionPayloadHeader: ExecutionPayloadHeader, blsToExecutionChanges: capellaSsz.BeaconBlockBody.fields.blsToExecutionChanges, blobKzgCommitments: denebSsz.BeaconBlockBody.fields.blobKzgCommitments, + executionRequests: ExecutionRequests, // New in ELECTRA }, {typeName: "BlindedBeaconBlockBody", jsonCase: "eth2", cachePermanentRootStruct: true} ); @@ -234,7 +235,8 @@ export const SignedBlindedBeaconBlock = new ContainerType( export const BuilderBid = new ContainerType( { header: ExecutionPayloadHeader, // Modified in ELECTRA - blindedBlobsBundle: denebSsz.BlobKzgCommitments, + blobKzgCommitments: denebSsz.BlobKzgCommitments, + executionRequests: ExecutionRequests, // New in ELECTRA value: UintBn256, pubkey: BLSPubkey, }, @@ -249,15 +251,20 @@ export const SignedBuilderBid = new ContainerType( {typeName: "SignedBuilderBid", jsonCase: "eth2"} ); -export const PendingBalanceDeposit = new ContainerType( +export const PendingDeposit = new ContainerType( { - index: ValidatorIndex, - amount: Gwei, + pubkey: BLSPubkey, + withdrawalCredentials: Bytes32, + // this is actually gwei uintbn64 type, but super unlikely to get a high amount here + // to warrant a bn type + amount: UintNum64, + signature: BLSSignature, + slot: Slot, }, - {typeName: "PendingBalanceDeposit", jsonCase: "eth2"} + {typeName: "PendingDeposit", jsonCase: "eth2"} ); -export const PendingBalanceDeposits = new ListCompositeType(PendingBalanceDeposit, PENDING_BALANCE_DEPOSITS_LIMIT); +export const PendingDeposits = new ListCompositeType(PendingDeposit, PENDING_DEPOSITS_LIMIT); export const PendingPartialWithdrawal = new ContainerType( { @@ -325,7 +332,7 @@ export const BeaconState = new ContainerType( earliestExitEpoch: Epoch, // New in ELECTRA:EIP7251 consolidationBalanceToConsume: Gwei, // New in ELECTRA:EIP7251 earliestConsolidationEpoch: Epoch, // New in ELECTRA:EIP7251 - pendingBalanceDeposits: PendingBalanceDeposits, // New in ELECTRA:EIP7251 + pendingDeposits: PendingDeposits, // New in ELECTRA:EIP7251 pendingPartialWithdrawals: new ListCompositeType(PendingPartialWithdrawal, PENDING_PARTIAL_WITHDRAWALS_LIMIT), // New in ELECTRA:EIP7251 pendingConsolidations: new ListCompositeType(PendingConsolidation, PENDING_CONSOLIDATIONS_LIMIT), // New in ELECTRA:EIP7251 }, diff --git a/packages/types/src/electra/types.ts b/packages/types/src/electra/types.ts index f7996cf336f9..691de409ed91 100644 --- a/packages/types/src/electra/types.ts +++ b/packages/types/src/electra/types.ts @@ -42,7 +42,7 @@ export type LightClientFinalityUpdate = ValueOf; export type LightClientStore = ValueOf; -export type PendingBalanceDeposit = ValueOf; +export type PendingDeposit = ValueOf; export type PendingPartialWithdrawal = ValueOf; export type PendingConsolidation = ValueOf; diff --git a/packages/validator/src/util/params.ts b/packages/validator/src/util/params.ts index f83aa9828633..16374af2b837 100644 --- a/packages/validator/src/util/params.ts +++ b/packages/validator/src/util/params.ts @@ -226,10 +226,11 @@ function getSpecCriticalParams(localConfig: ChainConfig): Record