From 1f392438e0d76ed1a9d62e9b6ecfc88019d5cf0d Mon Sep 17 00:00:00 2001
From: Tuyen Nguyen <vutuyen2636@gmail.com>
Date: Wed, 27 Nov 2024 09:18:22 +0700
Subject: [PATCH] fix: sync cached isCompoundingValidatorArr at epoch
 transition

---
 .../src/cache/epochTransitionCache.ts         | 25 +++++++++++++------
 .../epoch/processEffectiveBalanceUpdates.ts   |  7 +++---
 .../src/epoch/processPendingDeposits.ts       |  3 +++
 3 files changed, 25 insertions(+), 10 deletions(-)

diff --git a/packages/state-transition/src/cache/epochTransitionCache.ts b/packages/state-transition/src/cache/epochTransitionCache.ts
index d86431c72eee..16279e851188 100644
--- a/packages/state-transition/src/cache/epochTransitionCache.ts
+++ b/packages/state-transition/src/cache/epochTransitionCache.ts
@@ -9,7 +9,12 @@ import {Epoch, RootHex, ValidatorIndex, phase0} from "@lodestar/types";
 import {intDiv, toRootHex} from "@lodestar/utils";
 
 import {processPendingAttestations} from "../epoch/processPendingAttestations.js";
-import {CachedBeaconStateAllForks, CachedBeaconStateAltair, CachedBeaconStatePhase0} from "../index.js";
+import {
+  CachedBeaconStateAllForks,
+  CachedBeaconStateAltair,
+  CachedBeaconStatePhase0,
+  hasCompoundingWithdrawalCredential,
+} from "../index.js";
 import {computeBaseRewardPerIncrement} from "../util/altair.js";
 import {
   FLAG_CURR_HEAD_ATTESTER,
@@ -133,11 +138,7 @@ export interface EpochTransitionCache {
 
   flags: number[];
 
-  /**
-   * Validators in the current epoch, should use it for read-only value instead of accessing state.validators directly.
-   * Note that during epoch processing, validators could be updated so need to use it with care.
-   */
-  validators: phase0.Validator[];
+  isCompoundingValidatorArr: boolean[];
 
   /**
    * balances array will be populated by processRewardsAndPenalties() and consumed by processEffectiveBalanceUpdates().
@@ -209,6 +210,8 @@ const inclusionDelays = new Array<number>();
 /** WARNING: reused, never gc'd */
 const flags = new Array<number>();
 /** WARNING: reused, never gc'd */
+const isCompoundingValidatorArr = new Array<boolean>();
+/** WARNING: reused, never gc'd */
 const nextEpochShufflingActiveValidatorIndices = new Array<number>();
 
 export function beforeProcessEpoch(
@@ -262,6 +265,10 @@ export function beforeProcessEpoch(
   // TODO: optimize by combining the two loops
   // likely will require splitting into phase0 and post-phase0 versions
 
+  if (forkSeq >= ForkSeq.electra) {
+    isCompoundingValidatorArr.length = validatorCount;
+  }
+
   // Clone before being mutated in processEffectiveBalanceUpdates
   epochCtx.beforeEpochTransition();
 
@@ -298,6 +305,10 @@ export function beforeProcessEpoch(
 
     flags[i] = flag;
 
+    if (forkSeq >= ForkSeq.electra) {
+      isCompoundingValidatorArr[i] = hasCompoundingWithdrawalCredential(validator.withdrawalCredentials);
+    }
+
     if (isActiveCurr) {
       totalActiveStakeByIncrement += effectiveBalancesByIncrements[i];
     } else {
@@ -511,7 +522,7 @@ export function beforeProcessEpoch(
     proposerIndices,
     inclusionDelays,
     flags,
-    validators,
+    isCompoundingValidatorArr,
     // 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 d55f37aba178..b98e5cce9f35 100644
--- a/packages/state-transition/src/epoch/processEffectiveBalanceUpdates.ts
+++ b/packages/state-transition/src/epoch/processEffectiveBalanceUpdates.ts
@@ -5,10 +5,11 @@ import {
   HYSTERESIS_QUOTIENT,
   HYSTERESIS_UPWARD_MULTIPLIER,
   MAX_EFFECTIVE_BALANCE,
+  MAX_EFFECTIVE_BALANCE_ELECTRA,
+  MIN_ACTIVATION_BALANCE,
   TIMELY_TARGET_FLAG_INDEX,
 } from "@lodestar/params";
 import {BeaconStateAltair, CachedBeaconStateAllForks, EpochTransitionCache} from "../types.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;
@@ -43,7 +44,7 @@ export function processEffectiveBalanceUpdates(
   // 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 isCompoundingValidatorArr = cache.isCompoundingValidatorArr;
 
   let numUpdate = 0;
   for (let i = 0, len = balances.length; i < len; i++) {
@@ -58,7 +59,7 @@ export function processEffectiveBalanceUpdates(
       effectiveBalanceLimit = MAX_EFFECTIVE_BALANCE;
     } else {
       // from electra, effectiveBalanceLimit is per validator
-      effectiveBalanceLimit = getMaxEffectiveBalance(currentEpochValidators[i].withdrawalCredentials);
+      effectiveBalanceLimit = isCompoundingValidatorArr[i] ? MAX_EFFECTIVE_BALANCE_ELECTRA : MIN_ACTIVATION_BALANCE;
     }
 
     if (
diff --git a/packages/state-transition/src/epoch/processPendingDeposits.ts b/packages/state-transition/src/epoch/processPendingDeposits.ts
index 866dcc2510ad..e00e1ef93c8c 100644
--- a/packages/state-transition/src/epoch/processPendingDeposits.ts
+++ b/packages/state-transition/src/epoch/processPendingDeposits.ts
@@ -3,6 +3,7 @@ import {PendingDeposit} from "@lodestar/types/lib/electra/types.js";
 import {addValidatorToRegistry, isValidDepositSignature} from "../block/processDeposit.js";
 import {CachedBeaconStateElectra, EpochTransitionCache} from "../types.js";
 import {increaseBalance} from "../util/balance.js";
+import {hasCompoundingWithdrawalCredential} from "../util/electra.js";
 import {computeStartSlotAtEpoch} from "../util/epoch.js";
 import {getActivationExitChurnLimit} from "../util/validator.js";
 
@@ -106,6 +107,8 @@ function applyPendingDeposit(
     // 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);
     }
   } else {
     // Increase balance