From d90efb66c3af566a6d416b71fd310e86f2afbc95 Mon Sep 17 00:00:00 2001 From: markolazic01 Date: Thu, 9 Jul 2026 10:39:10 +0200 Subject: [PATCH] feat: implement compute_weak_subjectivity_period for Gloas (EIP-8061) --- .../state-transition/src/util/validator.ts | 17 ++++ .../src/util/weakSubjectivity.ts | 83 ++++++++++++++----- .../test/unit/util/weakSubjectivity.test.ts | 27 +++++- specrefs/.ethspecify.yml | 1 - specrefs/functions.yml | 4 +- 5 files changed, 107 insertions(+), 25 deletions(-) diff --git a/packages/state-transition/src/util/validator.ts b/packages/state-transition/src/util/validator.ts index 01d15a892425..1579102f89ea 100644 --- a/packages/state-transition/src/util/validator.ts +++ b/packages/state-transition/src/util/validator.ts @@ -128,6 +128,23 @@ export function getConsolidationChurnLimit(fork: ForkSeq, epochCtx: EpochCache): return getBalanceChurnLimitFromCache(epochCtx) - getActivationExitChurnLimit(epochCtx); } +// NOTE: duplicates the quotient/min choices in get{Exit,Activation,Consolidation}ChurnLimit. +export function getGloasChurnLimits( + config: ChainForkConfig, + totalActiveBalanceIncrements: number +): {exit: number; activation: number; consolidation: number} { + const exit = getBalanceChurnLimit( + totalActiveBalanceIncrements, + config.CHURN_LIMIT_QUOTIENT_GLOAS, + config.MIN_PER_EPOCH_CHURN_LIMIT_ELECTRA + ); + return { + exit, + activation: Math.min(config.MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT_GLOAS, exit), + consolidation: getBalanceChurnLimit(totalActiveBalanceIncrements, config.CONSOLIDATION_CHURN_LIMIT_QUOTIENT, 0), + }; +} + export function getMaxEffectiveBalance(withdrawalCredentials: Uint8Array): number { // Compounding withdrawal credential only available since Electra if (hasCompoundingWithdrawalCredential(withdrawalCredentials)) { diff --git a/packages/state-transition/src/util/weakSubjectivity.ts b/packages/state-transition/src/util/weakSubjectivity.ts index c1dd6b39966b..a2db215a178d 100644 --- a/packages/state-transition/src/util/weakSubjectivity.ts +++ b/packages/state-transition/src/util/weakSubjectivity.ts @@ -1,10 +1,12 @@ import {BeaconConfig, ChainForkConfig} from "@lodestar/config"; import { EFFECTIVE_BALANCE_INCREMENT, + ForkSeq, MAX_DEPOSITS, MAX_EFFECTIVE_BALANCE, SLOTS_PER_EPOCH, isForkPostElectra, + isForkPostGloas, } from "@lodestar/params"; import {Epoch, Root, ssz} from "@lodestar/types"; import {Checkpoint} from "@lodestar/types/phase0"; @@ -14,10 +16,14 @@ import {BeaconStateAllForks, CachedBeaconStateAllForks} from "../types.js"; import {computeCheckpointEpochAtStateSlot, computeEpochAtSlot, getCurrentEpoch} from "./epoch.js"; import {getCurrentSlot} from "./slot.js"; import { + getActivationChurnLimit, getActiveValidatorIndices, getBalanceChurnLimit, getBalanceChurnLimitFromCache, getChurnLimit, + getConsolidationChurnLimit, + getExitChurnLimit, + getGloasChurnLimits, } from "./validator.js"; export const ETH_TO_GWEI = 10 ** 9; @@ -49,18 +55,26 @@ export function computeWeakSubjectivityPeriodCachedState( const activeValidatorCount = state.epochCtx.currentShuffling.activeIndices.length; const fork = config.getForkName(state.slot); - return isForkPostElectra(fork) - ? computeWeakSubjectivityPeriodFromConstituentsElectra( + return isForkPostGloas(fork) + ? computeWeakSubjectivityPeriodFromConstituentsGloas( state.epochCtx.totalActiveBalanceIncrements, - getBalanceChurnLimitFromCache(state.epochCtx), + getExitChurnLimit(state.epochCtx), + getActivationChurnLimit(state.epochCtx), + getConsolidationChurnLimit(ForkSeq.gloas, state.epochCtx), config.MIN_VALIDATOR_WITHDRAWABILITY_DELAY ) - : computeWeakSubjectivityPeriodFromConstituentsPhase0( - activeValidatorCount, - state.epochCtx.totalActiveBalanceIncrements, - getChurnLimit(config, activeValidatorCount), - config.MIN_VALIDATOR_WITHDRAWABILITY_DELAY - ); + : isForkPostElectra(fork) + ? computeWeakSubjectivityPeriodFromConstituentsElectra( + state.epochCtx.totalActiveBalanceIncrements, + getBalanceChurnLimitFromCache(state.epochCtx), + config.MIN_VALIDATOR_WITHDRAWABILITY_DELAY + ) + : computeWeakSubjectivityPeriodFromConstituentsPhase0( + activeValidatorCount, + state.epochCtx.totalActiveBalanceIncrements, + getChurnLimit(config, activeValidatorCount), + config.MIN_VALIDATOR_WITHDRAWABILITY_DELAY + ); } /** @@ -80,22 +94,32 @@ export function computeWeakSubjectivityPeriod(config: ChainForkConfig, state: Be totalActiveBalanceIncrements = 1; } - return isForkPostElectra(fork) - ? computeWeakSubjectivityPeriodFromConstituentsElectra( + const churnLimitsGloas = getGloasChurnLimits(config, totalActiveBalanceIncrements); + + return isForkPostGloas(fork) + ? computeWeakSubjectivityPeriodFromConstituentsGloas( totalActiveBalanceIncrements, - getBalanceChurnLimit( - totalActiveBalanceIncrements, - config.CHURN_LIMIT_QUOTIENT, - config.MIN_PER_EPOCH_CHURN_LIMIT_ELECTRA - ), + churnLimitsGloas.exit, + churnLimitsGloas.activation, + churnLimitsGloas.consolidation, config.MIN_VALIDATOR_WITHDRAWABILITY_DELAY ) - : computeWeakSubjectivityPeriodFromConstituentsPhase0( - activeIndices.length, - totalActiveBalanceIncrements, - getChurnLimit(config, activeIndices.length), - config.MIN_VALIDATOR_WITHDRAWABILITY_DELAY - ); + : isForkPostElectra(fork) + ? computeWeakSubjectivityPeriodFromConstituentsElectra( + totalActiveBalanceIncrements, + getBalanceChurnLimit( + totalActiveBalanceIncrements, + config.CHURN_LIMIT_QUOTIENT, + config.MIN_PER_EPOCH_CHURN_LIMIT_ELECTRA + ), + config.MIN_VALIDATOR_WITHDRAWABILITY_DELAY + ) + : computeWeakSubjectivityPeriodFromConstituentsPhase0( + activeIndices.length, + totalActiveBalanceIncrements, + getChurnLimit(config, activeIndices.length), + config.MIN_VALIDATOR_WITHDRAWABILITY_DELAY + ); } export function computeWeakSubjectivityPeriodFromConstituentsPhase0( @@ -142,6 +166,21 @@ export function computeWeakSubjectivityPeriodFromConstituentsElectra( return minWithdrawabilityDelay + epochsForValidatorSetChurn; } +export function computeWeakSubjectivityPeriodFromConstituentsGloas( + totalBalanceByIncrement: number, + exitChurnLimit: number, + activationChurnLimit: number, + consolidationChurnLimit: number, + minWithdrawabilityDelay: number +): number { + // Keep t as increment for now. Multiply final result by EFFECTIVE_BALANCE_INCREMENT + const t = totalBalanceByIncrement; + const delta = Math.floor((2 * exitChurnLimit) / 3) + Math.floor(activationChurnLimit / 3) + consolidationChurnLimit; + const epochsForValidatorSetChurn = Math.floor(((SAFETY_DECAY * t) / (2 * delta * 100)) * EFFECTIVE_BALANCE_INCREMENT); + + return minWithdrawabilityDelay + epochsForValidatorSetChurn; +} + export function getLatestBlockRoot(state: BeaconStateAllForks): Root { const header = ssz.phase0.BeaconBlockHeader.clone(state.latestBlockHeader); if (ssz.Root.equals(header.stateRoot, ZERO_HASH)) { diff --git a/packages/state-transition/test/unit/util/weakSubjectivity.test.ts b/packages/state-transition/test/unit/util/weakSubjectivity.test.ts index 09c5ad691fa1..1285438094aa 100644 --- a/packages/state-transition/test/unit/util/weakSubjectivity.test.ts +++ b/packages/state-transition/test/unit/util/weakSubjectivity.test.ts @@ -1,8 +1,9 @@ import {describe, expect, it} from "vitest"; import {config} from "@lodestar/config/default"; -import {getBalanceChurnLimit, getChurnLimit} from "../../../src/util/validator.js"; +import {getBalanceChurnLimit, getChurnLimit, getGloasChurnLimits} from "../../../src/util/validator.js"; import { computeWeakSubjectivityPeriodFromConstituentsElectra, + computeWeakSubjectivityPeriodFromConstituentsGloas, computeWeakSubjectivityPeriodFromConstituentsPhase0, } from "../../../src/util/weakSubjectivity.js"; @@ -66,4 +67,28 @@ describe("weak subjectivity tests", () => { } ); }); + describe("computeWeakSubjectivityPeriodFromConstituentsGloas", () => { + const testValues = [ + {totalBalanceIncrement: 1_048_576, wsPeriod: 620}, + {totalBalanceIncrement: 2_097_152, wsPeriod: 911}, + {totalBalanceIncrement: 4_194_304, wsPeriod: 1348}, + {totalBalanceIncrement: 8_388_608, wsPeriod: 1348}, + {totalBalanceIncrement: 16_777_216, wsPeriod: 1484}, + {totalBalanceIncrement: 33_554_432, wsPeriod: 1566}, + ]; + it.each(testValues)( + "should have wsPeriod: $wsPeriod with totalActiveBalance: $totalBalanceIncrement", + ({totalBalanceIncrement, wsPeriod: expectedWsPeriod}) => { + const churn = getGloasChurnLimits(config, totalBalanceIncrement); + const wsPeriod = computeWeakSubjectivityPeriodFromConstituentsGloas( + totalBalanceIncrement, + churn.exit, + churn.activation, + churn.consolidation, + config.MIN_VALIDATOR_WITHDRAWABILITY_DELAY + ); + expect(wsPeriod).toBe(expectedWsPeriod); + } + ); + }); }); diff --git a/specrefs/.ethspecify.yml b/specrefs/.ethspecify.yml index d401b729c095..51d5a71ed15d 100644 --- a/specrefs/.ethspecify.yml +++ b/specrefs/.ethspecify.yml @@ -401,7 +401,6 @@ exceptions: - get_proposer_dependent_root#gloas # gloas (new in alpha.3, not yet implemented) - - compute_weak_subjectivity_period#gloas - block_to_light_client_header#gloas - get_lc_execution_root#gloas - is_valid_light_client_header#gloas diff --git a/specrefs/functions.yml b/specrefs/functions.yml index 54bfc184ec35..3e6a557f6e4a 100644 --- a/specrefs/functions.yml +++ b/specrefs/functions.yml @@ -1707,7 +1707,9 @@ - name: compute_weak_subjectivity_period#gloas - sources: [] + sources: + - file: packages/state-transition/src/util/weakSubjectivity.ts + search: export function computeWeakSubjectivityPeriod( spec: | def compute_weak_subjectivity_period(state: BeaconState) -> uint64: