diff --git a/packages/state-transition/src/block/processConsolidationRequest.ts b/packages/state-transition/src/block/processConsolidationRequest.ts index 691ecd5eca0..c14612579c5 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/cache/epochTransitionCache.ts b/packages/state-transition/src/cache/epochTransitionCache.ts index 27b781e8a6a..2a35317fbc9 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/epoch/processEffectiveBalanceUpdates.ts b/packages/state-transition/src/epoch/processEffectiveBalanceUpdates.ts index 26180d0d9f3..22a46c84f52 100644 --- a/packages/state-transition/src/epoch/processEffectiveBalanceUpdates.ts +++ b/packages/state-transition/src/epoch/processEffectiveBalanceUpdates.ts @@ -46,7 +46,6 @@ export function processEffectiveBalanceUpdates( // 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,9 +60,9 @@ export function processEffectiveBalanceUpdates( effectiveBalanceLimit = MAX_EFFECTIVE_BALANCE; } else { // from electra, effectiveBalanceLimit is per validator - const isCompoundingValidator = - hasCompoundingWithdrawalCredential(currentEpochValidators[i].withdrawalCredentials) || - newCompoundingValidators.has(i); + const isCompoundingValidator = hasCompoundingWithdrawalCredential( + currentEpochValidators[i].withdrawalCredentials + ); effectiveBalanceLimit = isCompoundingValidator ? MAX_EFFECTIVE_BALANCE_ELECTRA : MIN_ACTIVATION_BALANCE; } diff --git a/packages/state-transition/src/epoch/processPendingConsolidations.ts b/packages/state-transition/src/epoch/processPendingConsolidations.ts index 28178a509bb..0c0d61fd78a 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"; /** * 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,9 +33,6 @@ 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); @@ -51,6 +45,5 @@ export function processPendingConsolidations(state: CachedBeaconStateElectra, ca nextPendingConsolidation++; } - cache.newCompoundingValidators = newCompoundingValidators; state.pendingConsolidations = state.pendingConsolidations.sliceFrom(nextPendingConsolidation); } diff --git a/packages/state-transition/src/util/electra.ts b/packages/state-transition/src/util/electra.ts index 90dd9a3881d..c8522e17e29 100644 --- a/packages/state-transition/src/util/electra.ts +++ b/packages/state-transition/src/util/electra.ts @@ -17,15 +17,13 @@ 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 {