import {FAR_FUTURE_EPOCH, ForkSeq, GENESIS_SLOT, MAX_PENDING_DEPOSITS_PER_EPOCH} from "@lodestar/params"; import {electra} from "@lodestar/types"; import {addValidatorToRegistry, isValidDepositSignature} from "../block/processDeposit.js"; import {CachedBeaconStateElectra, EpochTransitionCache} from "../types.js"; import {increaseBalance} from "../util/balance.js"; import {hasCompoundingWithdrawalCredential, isValidatorKnown} from "../util/electra.js"; import {computeStartSlotAtEpoch} from "../util/epoch.js"; import {getActivationExitChurnLimit} from "../util/validator.js"; /** * Starting from Electra: * Process pending balance deposits from state subject to churn limit and depositBalanceToConsume. * 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); let startIndex = 0; // TODO: is this a good number? const chunk = 100; const pendingDepositsLength = state.pendingDeposits.length; outer: while (startIndex < pendingDepositsLength) { const deposits = state.pendingDeposits.getReadonlyByRange(startIndex, chunk); for (const deposit of deposits) { // 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 outer; } // Check if deposit has been finalized, otherwise, stop processing. if (deposit.slot > finalizedSlot) { break outer; } // Check if number of processed deposits has not reached the limit, otherwise, stop processing. if (nextDepositIndex >= MAX_PENDING_DEPOSITS_PER_EPOCH) { break outer; } // Read validator state let isValidatorExited = false; let isValidatorWithdrawn = false; const validatorIndex = state.epochCtx.getValidatorIndex(deposit.pubkey); if (isValidatorKnown(state, validatorIndex)) { 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 outer; } // 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++; } startIndex += chunk; } 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: electra.PendingDeposit, cache: EpochTransitionCache ): void { const validatorIndex = state.epochCtx.getValidatorIndex(deposit.pubkey); const {pubkey, withdrawalCredentials, amount, signature} = deposit; const cachedBalances = cache.balances; if (!isValidatorKnown(state, validatorIndex)) { // 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); const newValidatorIndex = state.validators.length - 1; cache.isCompoundingValidatorArr[newValidatorIndex] = hasCompoundingWithdrawalCredential(withdrawalCredentials); // set balance, so that the next deposit of same pubkey will increase the balance correctly // this is to fix the double deposit issue found in mekong // see https://github.com/ChainSafe/lodestar/pull/7255 if (cachedBalances) { cachedBalances[newValidatorIndex] = amount; } } } else { // Increase balance increaseBalance(state, validatorIndex, amount); if (cachedBalances) { cachedBalances[validatorIndex] += amount; } } }