Skip to content

Commit

Permalink
feat: switch to compounding from consolidation requests (#7122)
Browse files Browse the repository at this point in the history
* New compound switching logic

* clean up

* fix: remove newCompoundingValidators in EpochTransitionCache

* Bump spec version

* Address comment

* address comment

* Lint

---------

Co-authored-by: Tuyen Nguyen <[email protected]>
  • Loading branch information
ensi321 and twoeths authored Oct 11, 2024
1 parent fb40cf0 commit f9d9ae5
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 39 deletions.
80 changes: 69 additions & 11 deletions packages/state-transition/src/block/processConsolidationRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -46,7 +53,7 @@ export function processConsolidationRequest(
return;
}

if (Buffer.compare(sourceWithdrawalAddress, consolidationRequest.sourceAddress) !== 0) {
if (Buffer.compare(sourceWithdrawalAddress, sourceAddress) !== 0) {
return;
}

Expand All @@ -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;
}
8 changes: 0 additions & 8 deletions packages/state-transition/src/cache/epochTransitionCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ValidatorIndex>;

/**
* balances array will be populated by processRewardsAndPenalties() and consumed by processEffectiveBalanceUpdates().
* processRewardsAndPenalties() already has a regular Javascript array of balances.
Expand Down Expand Up @@ -518,8 +512,6 @@ export function beforeProcessEpoch(
inclusionDelays,
flags,
validators,
// will be assigned in processPendingConsolidations()
newCompoundingValidators: undefined,
// Will be assigned in processRewardsAndPenalties()
balances: undefined,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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++) {
Expand All @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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<ValidatorIndex>();

for (const pendingConsolidation of state.pendingConsolidations.getAllReadonly()) {
const {sourceIndex, targetIndex} = pendingConsolidation;
Expand All @@ -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);
Expand All @@ -51,6 +45,5 @@ export function processPendingConsolidations(state: CachedBeaconStateElectra, ca
nextPendingConsolidation++;
}

cache.newCompoundingValidators = newCompoundingValidators;
state.pendingConsolidations = state.pendingConsolidations.sliceFrom(nextPendingConsolidation);
}
16 changes: 7 additions & 9 deletions packages/state-transition/src/util/electra.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit f9d9ae5

Please sign in to comment.