diff --git a/data/beaconrestapi/src/test/resources/tech/pegasys/teku/beaconrestapi/handlers/v1/config/mainnetConfig.json b/data/beaconrestapi/src/test/resources/tech/pegasys/teku/beaconrestapi/handlers/v1/config/mainnetConfig.json index 60d5d14a15a..d7b7424216a 100644 --- a/data/beaconrestapi/src/test/resources/tech/pegasys/teku/beaconrestapi/handlers/v1/config/mainnetConfig.json +++ b/data/beaconrestapi/src/test/resources/tech/pegasys/teku/beaconrestapi/handlers/v1/config/mainnetConfig.json @@ -128,7 +128,7 @@ "MAXIMUM_GOSSIP_CLOCK_DISPARITY" : "500", "TARGET_COMMITTEE_SIZE" : "128", "TERMINAL_BLOCK_HASH" : "0x0000000000000000000000000000000000000000000000000000000000000000", - "MIN_BUILDER_WITHDRAWABILITY_DELAY" : "64", + "MIN_BUILDER_WITHDRAWABILITY_DELAY" : "8192", "CONSOLIDATION_CHURN_LIMIT_QUOTIENT" : "65536", "DOMAIN_DEPOSIT" : "0x03000000", "DOMAIN_CONTRIBUTION_AND_PROOF" : "0x09000000", @@ -195,4 +195,4 @@ "PROPOSER_REORG_CUTOFF_BPS" : "1667", "BLS_WITHDRAWAL_PREFIX" : "0x00", "MIN_ACTIVATION_BALANCE" : "32000000000" -} \ No newline at end of file +} diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/state/beaconstate/common/BuilderIndexCache.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/state/beaconstate/common/BuilderIndexCache.java new file mode 100644 index 00000000000..c5d2d376eaf --- /dev/null +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/state/beaconstate/common/BuilderIndexCache.java @@ -0,0 +1,107 @@ +/* + * Copyright Consensys Software Inc., 2026 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package tech.pegasys.teku.spec.datastructures.state.beaconstate.common; + +import com.google.common.annotations.VisibleForTesting; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; +import tech.pegasys.teku.bls.BLSPublicKey; +import tech.pegasys.teku.infrastructure.collections.cache.Cache; +import tech.pegasys.teku.infrastructure.collections.cache.LRUCache; +import tech.pegasys.teku.infrastructure.collections.cache.NoOpCache; +import tech.pegasys.teku.infrastructure.ssz.SszList; +import tech.pegasys.teku.spec.datastructures.state.beaconstate.BeaconState; +import tech.pegasys.teku.spec.datastructures.state.beaconstate.versions.gloas.BeaconStateGloas; +import tech.pegasys.teku.spec.datastructures.state.versions.gloas.Builder; + +public class BuilderIndexCache { + private final Cache builderIndices; + private final AtomicInteger lastCachedIndex; + + private static final int INDEX_NONE = -1; + public static final BuilderIndexCache NO_OP_INSTANCE = + new BuilderIndexCache(NoOpCache.getNoOpCache(), INDEX_NONE); + + @VisibleForTesting + BuilderIndexCache(final Cache builderIndices, final int lastCachedIndex) { + this.builderIndices = builderIndices; + this.lastCachedIndex = new AtomicInteger(lastCachedIndex); + } + + public BuilderIndexCache() { + this.builderIndices = LRUCache.create(Integer.MAX_VALUE - 1); + this.lastCachedIndex = new AtomicInteger(INDEX_NONE); + } + + public Optional getBuilderIndex(final BeaconState state, final BLSPublicKey publicKey) { + final SszList builders = BeaconStateGloas.required(state).getBuilders(); + final Optional builderIndex = builderIndices.getCached(publicKey); + if (builderIndex.isPresent()) { + // The cache is shared across states, so a cached index may be stale for the state being + // queried + return builderIndex.filter(index -> index < builders.size()); + } + return findIndexFromState(builders, publicKey); + } + + public void invalidateWithNewValue(final BLSPublicKey pubKey, final int updatedIndex) { + builderIndices.invalidateWithNewValue(pubKey, updatedIndex); + } + + public void invalidate(final BLSPublicKey pubKey) { + builderIndices.invalidate(pubKey); + } + + public BuilderIndexCache copy() { + return new BuilderIndexCache(builderIndices.copy(), lastCachedIndex.get()); + } + + @VisibleForTesting + int getLastCachedIndex() { + return lastCachedIndex.get(); + } + + @VisibleForTesting + int getCacheSize() { + return builderIndices.size(); + } + + private void updateLastIndex(final int i) { + lastCachedIndex.updateAndGet(curr -> Math.max(curr, i)); + } + + @VisibleForTesting + Cache getBuilderIndices() { + return builderIndices; + } + + private Optional findIndexFromState( + final SszList builders, final BLSPublicKey publicKey) { + final int initialCacheSize = getCacheSize(); + for (int i = Math.max(lastCachedIndex.get() + 1, 0); i < builders.size(); i++) { + final BLSPublicKey pubKey = builders.get(i).getPublicKey(); + builderIndices.invalidateWithNewValue(pubKey, i); + if (pubKey.equals(publicKey)) { + if (initialCacheSize < getCacheSize()) { + updateLastIndex(i); + } + return Optional.of(i); + } + } + if (initialCacheSize < getCacheSize()) { + updateLastIndex(getCacheSize() - 1); + } + return Optional.empty(); + } +} diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/state/beaconstate/common/TransitionCaches.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/state/beaconstate/common/TransitionCaches.java index f755632230c..a2af2039e6f 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/state/beaconstate/common/TransitionCaches.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/state/beaconstate/common/TransitionCaches.java @@ -58,7 +58,7 @@ public class TransitionCaches { NoOpCache.getNoOpCache(), ProgressiveTotalBalancesUpdates.NOOP, NoOpCache.getNoOpCache(), - NoOpCache.getNoOpCache()) { + BuilderIndexCache.NO_OP_INSTANCE) { @Override public TransitionCaches copy() { @@ -88,7 +88,7 @@ public static TransitionCaches getNoOp() { private final Cache> effectiveBalances; private final Cache baseRewardPerIncrement; private final Cache buildersPubKeys; - private final Cache builderIndexCache; + private final BuilderIndexCache builderIndexCache; private final Cache> syncCommitteeCache; @@ -110,7 +110,7 @@ private TransitionCaches() { baseRewardPerIncrement = LRUCache.create(MAX_BASE_REWARD_PER_INCREMENT_CACHE); progressiveTotalBalances = ProgressiveTotalBalancesUpdates.NOOP; buildersPubKeys = LRUCache.create(Integer.MAX_VALUE - 1); - builderIndexCache = LRUCache.create(Integer.MAX_VALUE - 1); + builderIndexCache = new BuilderIndexCache(); } private TransitionCaches( @@ -128,7 +128,7 @@ private TransitionCaches( final Cache baseRewardPerIncrement, final ProgressiveTotalBalancesUpdates progressiveTotalBalances, final Cache buildersPubKeys, - final Cache builderIndexCache) { + final BuilderIndexCache builderIndexCache) { this.activeValidators = activeValidators; this.beaconProposerIndex = beaconProposerIndex; this.beaconCommittee = beaconCommittee; @@ -239,13 +239,8 @@ public Cache getBuildersPubKeys() { return buildersPubKeys; } - /** - * (builder pub key) -> (builder index) cache - * - *

More complicated cache such as the {@link ValidatorIndexCache} is not required since the - * builders in the state are expected to be a tiny number initially - */ - public Cache getBuilderIndexCache() { + /** (builder pub key) -> (builder index) cache */ + public BuilderIndexCache getBuilderIndexCache() { return builderIndexCache; } @@ -268,6 +263,8 @@ public TransitionCaches copy() { syncCommitteeCache.copy(), baseRewardPerIncrement.copy(), progressiveTotalBalances.copy(), + // Unlike validators, builder indices can be reassigned, so the builder caches must be + // copied to prevent invalidations in one branch from corrupting lookups in another. buildersPubKeys.copy(), builderIndexCache.copy()); } diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/state/beaconstate/common/ValidatorIndexCache.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/state/beaconstate/common/ValidatorIndexCache.java index 41505941871..5a063496618 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/state/beaconstate/common/ValidatorIndexCache.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/state/beaconstate/common/ValidatorIndexCache.java @@ -46,12 +46,14 @@ public ValidatorIndexCache() { public Optional getValidatorIndex( final BeaconState state, final BLSPublicKey publicKey) { + final SszList validators = state.getValidators(); final Optional validatorIndex = validatorIndices.getCached(publicKey); if (validatorIndex.isPresent()) { - return validatorIndex.filter(index -> index < state.getValidators().size()); + // The cache is shared across states, so a cached index may be stale for the state being + // queried + return validatorIndex.filter(index -> index < validators.size()); } - - return findIndexFromState(state.getValidators(), publicKey); + return findIndexFromState(validators, publicKey); } public void invalidateWithNewValue(final BLSPublicKey pubKey, final int updatedIndex) { diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/SpecLogicGloas.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/SpecLogicGloas.java index 65016dff999..46861e79a42 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/SpecLogicGloas.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/SpecLogicGloas.java @@ -231,7 +231,7 @@ public static SpecLogicGloas create( attestationUtil, miscHelpers, withdrawalsHelpers, - executionRequestsProcessor); + blockProcessor); final BlockProposalUtil blockProposalUtil = new BlockProposalUtilFulu(schemaDefinitions, blockProcessor, config.getFuluForkEpoch()); @@ -248,7 +248,8 @@ public static SpecLogicGloas create( beaconStateAccessors, predicates, beaconStateMutators, - miscHelpers); + miscHelpers, + validatorsUtil); // Data column sidecar util final DataColumnSidecarUtil dataColumnSidecarUtil = new DataColumnSidecarUtilGloas(miscHelpers); diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/block/BlockProcessorGloas.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/block/BlockProcessorGloas.java index 7f02e62d8b9..a940e6a32d0 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/block/BlockProcessorGloas.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/block/BlockProcessorGloas.java @@ -18,6 +18,8 @@ import java.util.Optional; import java.util.OptionalInt; import java.util.function.Supplier; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import tech.pegasys.teku.bls.BLSSignatureVerifier; import tech.pegasys.teku.infrastructure.ssz.SszList; import tech.pegasys.teku.infrastructure.unsigned.UInt64; @@ -63,6 +65,8 @@ public class BlockProcessorGloas extends BlockProcessorFulu { + private static final Logger LOG = LogManager.getLogger(); + private final PredicatesGloas predicatesGloas; private final SchemaDefinitionsGloas schemaDefinitionsGloas; private final MiscHelpersGloas miscHelpersGloas; @@ -153,9 +157,62 @@ public void processParentExecutionPayload( throw new BlockProcessingException( "The execution requests root in the latest committed bid does not match the parent execution requests in the block"); } + applyParentExecutionPayload(stateGloas, requests, validatorExitContextSupplier); + } + + // apply_parent_execution_payload + public void applyParentExecutionPayload( + final MutableBeaconStateGloas state, + final ExecutionRequests requests, + final Supplier validatorExitContextSupplier) { + final ExecutionPayloadBid parentBid = state.getLatestExecutionPayloadBid(); + final UInt64 parentSlot = parentBid.getSlot(); + final UInt64 parentEpoch = miscHelpers.computeEpochAtSlot(parentSlot); + + // Process execution requests from parent's payload. The execution requests are processed at + // state.slot (child's slot), not the parent's slot. + final long startTimeMillis = System.currentTimeMillis(); + LOG.debug("Starting processing {} deposit requests", requests.getDeposits().size()); + executionRequestsProcessorGloas.processDepositRequests(state, requests.getDeposits()); + LOG.debug( + "Finished processing {} deposit requests. Pending deposits: {}, builders: {}. Took {} ms.", + requests.getDeposits().size(), + state.getPendingDeposits().size(), + state.getBuilders().size(), + System.currentTimeMillis() - startTimeMillis); + executionRequestsProcessorGloas.processWithdrawalRequests( + state, requests.getWithdrawals(), validatorExitContextSupplier); + executionRequestsProcessorGloas.processConsolidationRequests( + state, requests.getConsolidations()); + + // Settle the builder payment + if (parentEpoch.equals(beaconStateAccessorsGloas.getCurrentEpoch(state))) { + final UInt64 paymentIndex = + parentSlot.mod(specConfig.getSlotsPerEpoch()).plus(specConfig.getSlotsPerEpoch()); + beaconStateMutatorsGloas.settleBuilderPayment(state, paymentIndex); + } else if (parentEpoch.equals(beaconStateAccessorsGloas.getPreviousEpoch(state))) { + final UInt64 paymentIndex = parentSlot.mod(specConfig.getSlotsPerEpoch()); + beaconStateMutatorsGloas.settleBuilderPayment(state, paymentIndex); + } else if (parentBid.getValue().isGreaterThan(UInt64.ZERO)) { + // Parent is older than the previous epoch, its payment entry has been + // evicted from builder_pending_payments. Append the withdrawal directly. + state + .getBuilderPendingWithdrawals() + .append( + schemaDefinitionsGloas + .getBuilderPendingWithdrawalSchema() + .create( + parentBid.getFeeRecipient(), + parentBid.getValue(), + parentBid.getBuilderIndex())); + } - executionRequestsProcessorGloas.applyParentExecutionPayload( - stateGloas, requests, validatorExitContextSupplier); + // Update parent payload availability and latest block hash + state.setExecutionPayloadAvailability( + state + .getExecutionPayloadAvailability() + .withBit(parentSlot.mod(specConfig.getSlotsPerHistoricalRoot()).intValue())); + state.setLatestBlockHash(parentBid.getBlockHash()); } // process_withdrawals with only state as a parameter diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/execution/ExecutionRequestsProcessorGloas.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/execution/ExecutionRequestsProcessorGloas.java index ba75a5d5887..6e02091fbfa 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/execution/ExecutionRequestsProcessorGloas.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/execution/ExecutionRequestsProcessorGloas.java @@ -17,9 +17,6 @@ import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.function.Supplier; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import org.apache.tuweni.bytes.Bytes; import tech.pegasys.teku.bls.BLSPublicKey; import tech.pegasys.teku.bls.BLSSignature; @@ -27,18 +24,13 @@ import tech.pegasys.teku.infrastructure.ssz.SszMutableList; import tech.pegasys.teku.infrastructure.ssz.primitive.SszBytes32; import tech.pegasys.teku.infrastructure.ssz.primitive.SszUInt64; -import tech.pegasys.teku.infrastructure.unsigned.UInt64; import tech.pegasys.teku.spec.config.SpecConfigGloas; -import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.ExecutionPayloadBid; import tech.pegasys.teku.spec.datastructures.execution.versions.electra.DepositRequest; -import tech.pegasys.teku.spec.datastructures.execution.versions.electra.ExecutionRequests; import tech.pegasys.teku.spec.datastructures.state.beaconstate.MutableBeaconState; import tech.pegasys.teku.spec.datastructures.state.beaconstate.versions.electra.MutableBeaconStateElectra; -import tech.pegasys.teku.spec.datastructures.state.beaconstate.versions.gloas.MutableBeaconStateGloas; import tech.pegasys.teku.spec.datastructures.state.versions.electra.PendingDeposit; import tech.pegasys.teku.spec.datastructures.type.SszPublicKey; import tech.pegasys.teku.spec.datastructures.type.SszSignature; -import tech.pegasys.teku.spec.logic.common.helpers.BeaconStateMutators.ValidatorExitContext; import tech.pegasys.teku.spec.logic.common.util.ValidatorsUtil; import tech.pegasys.teku.spec.logic.versions.electra.execution.ExecutionRequestsProcessorElectra; import tech.pegasys.teku.spec.logic.versions.gloas.helpers.BeaconStateAccessorsGloas; @@ -48,10 +40,7 @@ import tech.pegasys.teku.spec.schemas.SchemaDefinitionsGloas; public class ExecutionRequestsProcessorGloas extends ExecutionRequestsProcessorElectra { - private static final Logger LOG = LogManager.getLogger(); - private final SchemaDefinitionsGloas schemaDefinitionsGloas; - private final SpecConfigGloas specConfigGloas; private final MiscHelpersGloas miscHelpersGloas; private final PredicatesGloas predicatesGloas; private final BeaconStateMutatorsGloas beaconStateMutatorsGloas; @@ -73,73 +62,12 @@ public ExecutionRequestsProcessorGloas( validatorsUtil, beaconStateMutators, beaconStateAccessors); - this.schemaDefinitionsGloas = schemaDefinitions; - this.specConfigGloas = specConfig; this.miscHelpersGloas = miscHelpers; this.predicatesGloas = predicates; this.beaconStateMutatorsGloas = beaconStateMutators; this.beaconStateAccessorsGloas = beaconStateAccessors; } - // apply_parent_execution_payload - public void applyParentExecutionPayload( - final MutableBeaconStateGloas state, - final ExecutionRequests requests, - final Supplier validatorExitContextSupplier) { - final ExecutionPayloadBid parentBid = state.getLatestExecutionPayloadBid(); - final UInt64 parentSlot = parentBid.getSlot(); - final UInt64 parentEpoch = miscHelpers.computeEpochAtSlot(parentSlot); - - // Process execution requests from parent's payload. The execution requests are processed at - // state.slot (child's slot), not the parent's slot. - final long startTimeNanos = System.nanoTime(); - LOG.debug( - "Starting processing builder deposits from {} execution request deposits at timestampNanos={}", - requests.getDeposits().size(), - startTimeNanos); - processDepositRequests(state, requests.getDeposits()); - final long finishTimeNanos = System.nanoTime(); - LOG.debug( - "Finished processing builder deposits at timestampNanos={}. Pending deposits: {}, builders: {}, elapsedNanos={}", - finishTimeNanos, - state.getPendingDeposits().size(), - state.getBuilders().size(), - finishTimeNanos - startTimeNanos); - processWithdrawalRequests(state, requests.getWithdrawals(), validatorExitContextSupplier); - processConsolidationRequests(state, requests.getConsolidations()); - - // Settle the builder payment - if (parentEpoch.equals(beaconStateAccessorsGloas.getCurrentEpoch(state))) { - final UInt64 paymentIndex = - parentSlot - .mod(specConfigGloas.getSlotsPerEpoch()) - .plus(specConfigGloas.getSlotsPerEpoch()); - beaconStateMutatorsGloas.settleBuilderPayment(state, paymentIndex); - } else if (parentEpoch.equals(beaconStateAccessorsGloas.getPreviousEpoch(state))) { - final UInt64 paymentIndex = parentSlot.mod(specConfigGloas.getSlotsPerEpoch()); - beaconStateMutatorsGloas.settleBuilderPayment(state, paymentIndex); - } else if (parentBid.getValue().isGreaterThan(UInt64.ZERO)) { - // Parent is older than the previous epoch, its payment entry has been - // evicted from builder_pending_payments. Append the withdrawal directly. - state - .getBuilderPendingWithdrawals() - .append( - schemaDefinitionsGloas - .getBuilderPendingWithdrawalSchema() - .create( - parentBid.getFeeRecipient(), - parentBid.getValue(), - parentBid.getBuilderIndex())); - } - - // Update parent payload availability and latest block hash - state.setExecutionPayloadAvailability( - state - .getExecutionPayloadAvailability() - .withBit(parentSlot.mod(specConfigGloas.getSlotsPerHistoricalRoot()).intValue())); - state.setLatestBlockHash(parentBid.getBlockHash()); - } - @Override public void processDepositRequests( final MutableBeaconState state, final List depositRequests) { @@ -155,17 +83,17 @@ public void processDepositRequests( // this pubkey, apply the deposit to their balance final boolean isBuilder = beaconStateAccessorsGloas.getBuilderIndex(state, pubkey).isPresent(); - final boolean isValidator = validatorsUtil.getValidatorIndex(state, pubkey).isPresent(); - final boolean isPendingValidator = - verifiedPendingValidatorPubkeys.contains(pubkey) - || (miscHelpersGloas.isPendingValidator(stateElectra.getPendingDeposits(), pubkey) - && verifiedPendingValidatorPubkeys.add(pubkey)); final boolean isNewBuilderDeposit = !isBuilder && predicatesGloas.isBuilderWithdrawalCredential( depositRequest.getWithdrawalCredentials()) - && !isValidator - && !isPendingValidator; + // not is_validator + && validatorsUtil.getValidatorIndex(state, pubkey).isEmpty() + // not is_pending_validator + && !(verifiedPendingValidatorPubkeys.contains(pubkey) + || (miscHelpersGloas.isPendingValidator( + stateElectra.getPendingDeposits().asList(), pubkey) + && verifiedPendingValidatorPubkeys.add(pubkey))); if (isNewBuilderDeposit) { // new builder deposits will be processed at the end so we can batch the signature diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/forktransition/GloasStateUpgrade.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/forktransition/GloasStateUpgrade.java index 55d691be05e..11dae7453c9 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/forktransition/GloasStateUpgrade.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/forktransition/GloasStateUpgrade.java @@ -13,22 +13,21 @@ package tech.pegasys.teku.spec.logic.versions.gloas.forktransition; +import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.stream.Collectors; import java.util.stream.IntStream; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.tuweni.bytes.Bytes32; import tech.pegasys.teku.bls.BLSPublicKey; import tech.pegasys.teku.infrastructure.bytes.Bytes20; -import tech.pegasys.teku.infrastructure.ssz.SszList; import tech.pegasys.teku.infrastructure.ssz.collections.SszBitvector; import tech.pegasys.teku.infrastructure.unsigned.UInt64; import tech.pegasys.teku.spec.config.SpecConfigGloas; import tech.pegasys.teku.spec.datastructures.state.Fork; -import tech.pegasys.teku.spec.datastructures.state.Validator; import tech.pegasys.teku.spec.datastructures.state.beaconstate.BeaconState; import tech.pegasys.teku.spec.datastructures.state.beaconstate.common.BeaconStateFields; import tech.pegasys.teku.spec.datastructures.state.beaconstate.versions.fulu.BeaconStateFulu; @@ -36,9 +35,9 @@ import tech.pegasys.teku.spec.datastructures.state.beaconstate.versions.gloas.BeaconStateSchemaGloas; import tech.pegasys.teku.spec.datastructures.state.beaconstate.versions.gloas.MutableBeaconStateGloas; import tech.pegasys.teku.spec.datastructures.state.versions.electra.PendingDeposit; -import tech.pegasys.teku.spec.datastructures.state.versions.gloas.Builder; import tech.pegasys.teku.spec.datastructures.state.versions.gloas.BuilderPendingPayment; import tech.pegasys.teku.spec.logic.common.forktransition.StateUpgrade; +import tech.pegasys.teku.spec.logic.common.util.ValidatorsUtil; import tech.pegasys.teku.spec.logic.versions.gloas.helpers.BeaconStateAccessorsGloas; import tech.pegasys.teku.spec.logic.versions.gloas.helpers.BeaconStateMutatorsGloas; import tech.pegasys.teku.spec.logic.versions.gloas.helpers.MiscHelpersGloas; @@ -55,6 +54,7 @@ public class GloasStateUpgrade implements StateUpgrade { private final PredicatesGloas predicates; private final BeaconStateMutatorsGloas beaconStateMutators; private final MiscHelpersGloas miscHelpers; + private final ValidatorsUtil validatorsUtil; public GloasStateUpgrade( final SpecConfigGloas specConfig, @@ -62,13 +62,15 @@ public GloasStateUpgrade( final BeaconStateAccessorsGloas beaconStateAccessors, final PredicatesGloas predicates, final BeaconStateMutatorsGloas beaconStateMutators, - final MiscHelpersGloas miscHelpers) { + final MiscHelpersGloas miscHelpers, + final ValidatorsUtil validatorsUtil) { this.specConfig = specConfig; this.schemaDefinitions = schemaDefinitions; this.beaconStateAccessors = beaconStateAccessors; this.predicates = predicates; this.beaconStateMutators = beaconStateMutators; this.miscHelpers = miscHelpers; + this.validatorsUtil = validatorsUtil; } @Override @@ -168,55 +170,58 @@ public BeaconStateGloas upgrade(final BeaconState preState) { /** Applies any pending deposit for builders, effectively onboarding builders at the fork. */ private void onboardBuildersFromPendingDeposits(final MutableBeaconStateGloas state) { - final long startTimeNanos = System.nanoTime(); + final long startTimeMillis = System.currentTimeMillis(); LOG.debug( - "Starting processing builder deposits from {} pending deposits at timestampNanos={}", - state.getPendingDeposits().size(), - startTimeNanos); - final Set validatorPubkeys = - state.getValidators().stream().map(Validator::getPublicKey).collect(Collectors.toSet()); - final SszList pendingDeposits = - state.getPendingDeposits().stream() - .filter( - deposit -> { - if (validatorPubkeys.contains(deposit.getPublicKey())) { - return true; - } - final Set builderPubkeys = - state.getBuilders().stream() - .map(Builder::getPublicKey) - .collect(Collectors.toSet()); - if (builderPubkeys.contains(deposit.getPublicKey()) - || predicates.isBuilderWithdrawalCredential( - deposit.getWithdrawalCredentials())) { - beaconStateMutators.applyDepositForBuilder( - state, - deposit.getPublicKey(), - deposit.getWithdrawalCredentials(), - deposit.getAmount(), - deposit.getSignature(), - deposit.getSlot(), - false); - return false; - } - if (miscHelpers.isValidDepositSignature( - deposit.getPublicKey(), - deposit.getWithdrawalCredentials(), - deposit.getAmount(), - deposit.getSignature())) { - validatorPubkeys.add(deposit.getPublicKey()); - return true; - } - return false; - }) - .collect(schemaDefinitions.getPendingDepositsSchema().collector()); - state.setPendingDeposits(pendingDeposits); - final long finishTimeNanos = System.nanoTime(); + "Starting onboarding builders at fork from {} pending deposits", + state.getPendingDeposits().size()); + final List pendingDeposits = new ArrayList<>(); + // Avoids re-scanning pending deposits and re-verifying signatures for repeated pubkeys + final Set verifiedPendingValidatorPubkeys = new HashSet<>(); + + for (final PendingDeposit deposit : state.getPendingDeposits()) { + final BLSPublicKey pubkey = deposit.getPublicKey(); + // Deposits for existing validators stay in the pending queue + if (validatorsUtil.getValidatorIndex(state, pubkey).isPresent()) { + pendingDeposits.add(deposit); + continue; + } + // Deposits for non-builders stay in the pending queue. If there is a valid pending deposit + // for a new validator with this pubkey, keep this deposit in the pending queue to be + // applied to that validator later. + if (beaconStateAccessors.getBuilderIndex(state, pubkey).isEmpty()) { + // Deposits without builder credentials stay in the pending queue + if (!predicates.isBuilderWithdrawalCredential(deposit.getWithdrawalCredentials())) { + pendingDeposits.add(deposit); + continue; + } + boolean isPendingValidator; + if (verifiedPendingValidatorPubkeys.contains(pubkey)) { + isPendingValidator = true; + } else { + isPendingValidator = + miscHelpers.isPendingValidator(pendingDeposits, pubkey) + && verifiedPendingValidatorPubkeys.add(pubkey); + } + if (isPendingValidator) { + pendingDeposits.add(deposit); + continue; + } + } + beaconStateMutators.applyDepositForBuilder( + state, + deposit.getPublicKey(), + deposit.getWithdrawalCredentials(), + deposit.getAmount(), + deposit.getSignature(), + deposit.getSlot(), + false); + } + state.setPendingDeposits( + schemaDefinitions.getPendingDepositsSchema().createFromElements(pendingDeposits)); LOG.debug( - "Finished processing builder deposits at timestampNanos={}. Pending deposits remaining: {}, builders: {}, elapsedNanos={}", - finishTimeNanos, + "Finished onboarding builders at fork. Pending deposits remaining: {}, builders: {}. Took {} ms.", pendingDeposits.size(), state.getBuilders().size(), - finishTimeNanos - startTimeNanos); + System.currentTimeMillis() - startTimeMillis); } } diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/helpers/BeaconStateAccessorsGloas.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/helpers/BeaconStateAccessorsGloas.java index 2cceef69ed9..a9ce01ccbeb 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/helpers/BeaconStateAccessorsGloas.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/helpers/BeaconStateAccessorsGloas.java @@ -26,7 +26,6 @@ import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes32; import tech.pegasys.teku.bls.BLSPublicKey; -import tech.pegasys.teku.infrastructure.collections.cache.Cache; import tech.pegasys.teku.infrastructure.crypto.Hash; import tech.pegasys.teku.infrastructure.ssz.SszList; import tech.pegasys.teku.infrastructure.ssz.SszVector; @@ -384,21 +383,8 @@ public Optional getBuilderPubKey( @Override public Optional getBuilderIndex(final BeaconState state, final BLSPublicKey publicKey) { - final SszList builders = BeaconStateGloas.required(state).getBuilders(); - final Cache builderIndexCache = - BeaconStateCache.getTransitionCaches(state).getBuilderIndexCache(); - return builderIndexCache - .getCached(publicKey) - .or( - () -> { - for (int i = 0; i < builders.size(); i++) { - final BLSPublicKey builderPubKey = builders.get(i).getPublicKey(); - if (builderPubKey.equals(publicKey)) { - builderIndexCache.invalidateWithNewValue(builderPubKey, i); - return Optional.of(i); - } - } - return Optional.empty(); - }); + return BeaconStateCache.getTransitionCaches(state) + .getBuilderIndexCache() + .getBuilderIndex(state, publicKey); } } diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/helpers/MiscHelpersGloas.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/helpers/MiscHelpersGloas.java index f43b86ed702..f5a161757ce 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/helpers/MiscHelpersGloas.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/helpers/MiscHelpersGloas.java @@ -242,7 +242,7 @@ public boolean isActiveBuilder(final BeaconState state, final UInt64 builderInde // Check if a pending deposit with a valid signature is in the queue for the given pubkey. public boolean isPendingValidator( - final SszList pendingDeposits, final BLSPublicKey pubkey) { + final List pendingDeposits, final BLSPublicKey pubkey) { for (final PendingDeposit pendingDeposit : pendingDeposits) { if (!pendingDeposit.getPublicKey().equals(pubkey)) { continue; diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/util/ForkChoiceUtilGloas.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/util/ForkChoiceUtilGloas.java index 4f1eff82da2..73fe76f93e5 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/util/ForkChoiceUtilGloas.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/gloas/util/ForkChoiceUtilGloas.java @@ -49,7 +49,7 @@ import tech.pegasys.teku.spec.logic.common.statetransition.results.BlockImportResult; import tech.pegasys.teku.spec.logic.common.util.ForkChoiceUtil; import tech.pegasys.teku.spec.logic.versions.fulu.util.ForkChoiceUtilFulu; -import tech.pegasys.teku.spec.logic.versions.gloas.execution.ExecutionRequestsProcessorGloas; +import tech.pegasys.teku.spec.logic.versions.gloas.block.BlockProcessorGloas; import tech.pegasys.teku.spec.logic.versions.gloas.helpers.BeaconStateAccessorsGloas; import tech.pegasys.teku.spec.logic.versions.gloas.helpers.BeaconStateMutatorsGloas; import tech.pegasys.teku.spec.logic.versions.gloas.helpers.MiscHelpersGloas; @@ -62,7 +62,7 @@ public class ForkChoiceUtilGloas extends ForkChoiceUtilFulu { private final BeaconStateMutatorsGloas beaconStateMutatorsGloas; private final WithdrawalsHelpersGloas withdrawalsHelpers; - private final ExecutionRequestsProcessorGloas executionRequestsProcessor; + private final BlockProcessorGloas blockProcessor; public ForkChoiceUtilGloas( final SpecConfigGloas specConfig, @@ -72,11 +72,11 @@ public ForkChoiceUtilGloas( final AttestationUtilGloas attestationUtil, final MiscHelpersGloas miscHelpers, final WithdrawalsHelpersGloas withdrawalsHelpers, - final ExecutionRequestsProcessorGloas executionRequestsProcessor) { + final BlockProcessorGloas blockProcessor) { super(specConfig, beaconStateAccessors, epochProcessor, attestationUtil, miscHelpers); this.beaconStateMutatorsGloas = beaconStateMutators; this.withdrawalsHelpers = withdrawalsHelpers; - this.executionRequestsProcessor = executionRequestsProcessor; + this.blockProcessor = blockProcessor; } @Override @@ -101,14 +101,14 @@ public SszList getPayloadAttributeWithdrawals( final BeaconState state, final ExecutionRequests parentExecutionRequests) { final BeaconState effectiveState = state.updated( - stateMutable -> { - final MutableBeaconStateGloas stateGloas = - MutableBeaconStateGloas.required(stateMutable); - executionRequestsProcessor.applyParentExecutionPayload( - stateGloas, + mutableState -> { + final MutableBeaconStateGloas mutableStateGloas = + MutableBeaconStateGloas.required(mutableState); + blockProcessor.applyParentExecutionPayload( + mutableStateGloas, parentExecutionRequests, - beaconStateMutatorsGloas.createValidatorExitContextSupplier(stateGloas)); - withdrawalsHelpers.processWithdrawals(stateGloas); + beaconStateMutatorsGloas.createValidatorExitContextSupplier(mutableStateGloas)); + withdrawalsHelpers.processWithdrawals(mutableStateGloas); }); return BeaconStateGloas.required(effectiveState).getPayloadExpectedWithdrawals(); } diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/heze/SpecLogicHeze.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/heze/SpecLogicHeze.java index 791b3667ec5..e1ce2035e36 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/heze/SpecLogicHeze.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/heze/SpecLogicHeze.java @@ -229,7 +229,7 @@ public static SpecLogicHeze create( attestationUtil, miscHelpers, withdrawalsHelpers, - executionRequestsProcessor); + blockProcessor); final BlockProposalUtil blockProposalUtil = new BlockProposalUtilFulu(schemaDefinitions, blockProcessor, config.getFuluForkEpoch()); diff --git a/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/mainnet.yaml b/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/mainnet.yaml index edf4edc2010..f0bfecd42cb 100644 --- a/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/mainnet.yaml +++ b/ethereum/spec/src/main/resources/tech/pegasys/teku/spec/config/configs/mainnet.yaml @@ -88,8 +88,8 @@ SYNC_MESSAGE_DUE_BPS: 3333 CONTRIBUTION_DUE_BPS: 6667 # Gloas -# 2**6 (= 64) epochs -MIN_BUILDER_WITHDRAWABILITY_DELAY: 64 +# 2**13 (= 8,192) epochs +MIN_BUILDER_WITHDRAWABILITY_DELAY: 8192 # 2500 basis points, 25% of SLOT_DURATION_MS ATTESTATION_DUE_BPS_GLOAS: 2500 # 5000 basis points, 50% of SLOT_DURATION_MS @@ -224,4 +224,4 @@ BLOB_SCHEDULE: - EPOCH: 412672 # December 9, 2025, 02:21:11pm UTC MAX_BLOBS_PER_BLOCK: 15 - EPOCH: 419072 # January 7, 2026, 01:01:11am UTC - MAX_BLOBS_PER_BLOCK: 21 \ No newline at end of file + MAX_BLOBS_PER_BLOCK: 21 diff --git a/ethereum/spec/src/test/java/tech/pegasys/teku/spec/datastructures/state/beaconstate/common/BuilderIndexCacheTest.java b/ethereum/spec/src/test/java/tech/pegasys/teku/spec/datastructures/state/beaconstate/common/BuilderIndexCacheTest.java new file mode 100644 index 00000000000..cf3f801b84a --- /dev/null +++ b/ethereum/spec/src/test/java/tech/pegasys/teku/spec/datastructures/state/beaconstate/common/BuilderIndexCacheTest.java @@ -0,0 +1,113 @@ +/* + * Copyright Consensys Software Inc., 2026 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package tech.pegasys.teku.spec.datastructures.state.beaconstate.common; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Optional; +import org.junit.jupiter.api.Test; +import tech.pegasys.teku.bls.BLSPublicKey; +import tech.pegasys.teku.infrastructure.collections.cache.Cache; +import tech.pegasys.teku.spec.TestSpecFactory; +import tech.pegasys.teku.spec.datastructures.state.beaconstate.versions.gloas.BeaconStateGloas; +import tech.pegasys.teku.spec.util.DataStructureUtil; + +public class BuilderIndexCacheTest { + final DataStructureUtil dataStructureUtil = + new DataStructureUtil(TestSpecFactory.createMinimalGloas()); + final BeaconStateGloas state = BeaconStateGloas.required(dataStructureUtil.randomBeaconState()); + final BLSPublicKey missingPublicKey = dataStructureUtil.randomPublicKey(); + + @SuppressWarnings("unchecked") + final Cache cache = mock(Cache.class); + + @Test + public void shouldNotScanStateIfAlreadyHaveBuilders() { + final BuilderIndexCache builderIndexCache = + new BuilderIndexCache(cache, state.getBuilders().size()); + + when(cache.getCached(missingPublicKey)).thenReturn(Optional.empty()); + final Optional index = builderIndexCache.getBuilderIndex(state, missingPublicKey); + + verify(cache).getCached(missingPublicKey); + verify(cache, never()).invalidateWithNewValue(any(), any()); + assertThat(index).isEmpty(); + } + + @Test + public void shouldScanNewBuildersInSuppliedState() { + final int initialLastCachedIndex = state.getBuilders().size() - 3; + final BuilderIndexCache builderIndexCache = + new BuilderIndexCache(cache, initialLastCachedIndex); + + when(cache.getCached(missingPublicKey)).thenReturn(Optional.empty()); + final Optional index = builderIndexCache.getBuilderIndex(state, missingPublicKey); + verify(cache).getCached(missingPublicKey); + verify(cache, times(state.getBuilders().size() - initialLastCachedIndex - 1)) + .invalidateWithNewValue(any(), any()); + assertThat(index).isEmpty(); + } + + @Test + public void shouldGetAllBuilderKeysCachedIfMissingKeyPassed() { + final BuilderIndexCache builderIndexCache = new BuilderIndexCache(); + final Optional index = builderIndexCache.getBuilderIndex(state, missingPublicKey); + assertThat(index).isEmpty(); + assertThat(builderIndexCache.getBuilderIndices().size()).isEqualTo(state.getBuilders().size()); + } + + @Test + public void shouldPopulateCacheItemsFromState() { + final BuilderIndexCache builderIndexCache = new BuilderIndexCache(); + final int targetIndex = state.getBuilders().size() - 1; + final BLSPublicKey foundKey = state.getBuilders().get(targetIndex).getPublicKey(); + + final Optional index = builderIndexCache.getBuilderIndex(state, foundKey); + assertThat(index).contains(targetIndex); + assertThat(builderIndexCache.getLastCachedIndex()).isEqualTo(targetIndex); + assertThat(builderIndexCache.getBuilderIndices().size()).isEqualTo(targetIndex + 1); + } + + @Test + public void shouldFilterItemsBeyondStateIndex() { + final BuilderIndexCache builderIndexCache = new BuilderIndexCache(); + builderIndexCache.invalidateWithNewValue(missingPublicKey, 100); + final Optional index = builderIndexCache.getBuilderIndex(state, missingPublicKey); + + assertThat(index).isEmpty(); + // state didn't get scanned, because we had the index but it was out of bounds + assertThat(builderIndexCache.getLastCachedIndex()).isEqualTo(-1); + assertThat(builderIndexCache.getBuilderIndices().size()).isEqualTo(1); + } + + @Test + public void shouldInvalidateMapping() { + final BuilderIndexCache builderIndexCache = new BuilderIndexCache(); + final BLSPublicKey existingKey = state.getBuilders().get(0).getPublicKey(); + + // populate the cache + assertThat(builderIndexCache.getBuilderIndex(state, existingKey)).contains(0); + assertThat(builderIndexCache.getBuilderIndices().getCached(existingKey)).contains(0); + + // invalidate the mapping (simulates builder reassignment) + builderIndexCache.invalidate(existingKey); + assertThat(builderIndexCache.getBuilderIndices().getCached(existingKey)).isEmpty(); + } +}