diff --git a/packages/beacon-node/src/execution/engine/types.ts b/packages/beacon-node/src/execution/engine/types.ts index 32fe4cb79d3d..9545ded1d92e 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, @@ -161,29 +161,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[]; @@ -406,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 604243400aa0..932ad25b481e 100644 --- a/packages/beacon-node/test/spec/presets/epoch_processing.test.ts +++ b/packages/beacon-node/test/spec/presets/epoch_processing.test.ts @@ -46,7 +46,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/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 ca599e990df4..b488927e77b3 100644 --- a/packages/params/src/presets/mainnet.ts +++ b/packages/params/src/presets/mainnet.ts @@ -125,11 +125,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 5dc8fc10d803..fa061697d2b3 100644 --- a/packages/params/src/presets/minimal.ts +++ b/packages/params/src/presets/minimal.ts @@ -125,12 +125,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 e867b4a3cf71..bb32f3690e49 100644 --- a/packages/params/src/types.ts +++ b/packages/params/src/types.ts @@ -89,10 +89,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; @@ -189,10 +190,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/state-transition/src/block/processDeposit.ts b/packages/state-transition/src/block/processDeposit.ts index 44a31ea4b88c..00fe33cbd328 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,7 +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, increaseBalance} from "../util/index.js"; +import {computeDomain, computeSigningRoot, getMaxEffectiveBalance, increaseBalance} from "../util/index.js"; import {CachedBeaconStateAllForks, CachedBeaconStateAltair, CachedBeaconStateElectra} from "../types.js"; /** @@ -54,30 +55,42 @@ export function applyDeposit( state: CachedBeaconStateAllForks, deposit: DepositData | DepositRequest ): void { - const {config, validators, epochCtx} = state; - const {pubkey, withdrawalCredentials, amount} = deposit; + const {config, epochCtx} = 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); + + 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); + increaseBalance(state, cachedIndex, amount); } + } 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, @@ -86,8 +99,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, @@ -123,20 +138,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..a349c372d51b 100644 --- a/packages/state-transition/src/block/processDepositRequest.ts +++ b/packages/state-transition/src/block/processDepositRequest.ts @@ -1,8 +1,7 @@ -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, @@ -13,5 +12,13 @@ export function processDepositRequest( 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/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/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 fdaea2f83163..22a46c84f52b 100644 --- a/packages/state-transition/src/epoch/processEffectiveBalanceUpdates.ts +++ b/packages/state-transition/src/epoch/processEffectiveBalanceUpdates.ts @@ -42,7 +42,7 @@ 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; diff --git a/packages/state-transition/src/epoch/processPendingBalanceDeposits.ts b/packages/state-transition/src/epoch/processPendingBalanceDeposits.ts deleted file mode 100644 index bef3ec0b2724..000000000000 --- a/packages/state-transition/src/epoch/processPendingBalanceDeposits.ts +++ /dev/null @@ -1,70 +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; - } else { - // 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/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 0bd36a909b46..b64ac242f83c 100644 --- a/packages/state-transition/src/slot/upgradeStateToElectra.ts +++ b/packages/state-transition/src/slot/upgradeStateToElectra.ts @@ -81,8 +81,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); diff --git a/packages/state-transition/src/util/electra.ts b/packages/state-transition/src/util/electra.ts index ffa09a96bd3e..c8522e17e29b 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, FAR_FUTURE_EPOCH, 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 { @@ -28,14 +29,20 @@ export function switchToCompoundingValidator(state: CachedBeaconStateElectra, in 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); } } @@ -48,9 +55,12 @@ export function queueEntireBalanceAndResetValidator(state: CachedBeaconStateElec state.epochCtx.effectiveBalanceIncrementsSet(index, 0); validator.activationEligibilityEpoch = FAR_FUTURE_EPOCH; - const pendingBalanceDeposit = ssz.electra.PendingBalanceDeposit.toViewDU({ - index, - amount: BigInt(balance), + const pendingDeposit = ssz.electra.PendingDeposit.toViewDU({ + pubkey: validator.pubkey, + withdrawalCredentials: validator.withdrawalCredentials, + amount: balance, + signature: G2_POINT_AT_INFINITY, + slot: GENESIS_SLOT, }); - state.pendingBalanceDeposits.push(pendingBalanceDeposit); + state.pendingDeposits.push(pendingDeposit); } 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/test/perf/analyzeEpochs.ts b/packages/state-transition/test/perf/analyzeEpochs.ts index deb0861427bf..2f1485a809b1 100644 --- a/packages/state-transition/test/perf/analyzeEpochs.ts +++ b/packages/state-transition/test/perf/analyzeEpochs.ts @@ -153,7 +153,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..7200af5f4ead 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, @@ -249,15 +249,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 +330,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 6d6705f512df..51df32ab4848 100644 --- a/packages/validator/src/util/params.ts +++ b/packages/validator/src/util/params.ts @@ -228,10 +228,11 @@ function getSpecCriticalParams(localConfig: ChainConfig): Record