diff --git a/eth-reference-tests/src/referenceTest/java/tech/pegasys/teku/reference/phase0/gossip/GossipExecutionPayloadBidTestExecutor.java b/eth-reference-tests/src/referenceTest/java/tech/pegasys/teku/reference/phase0/gossip/GossipExecutionPayloadBidTestExecutor.java index 6e1baace90d..540eea915fe 100644 --- a/eth-reference-tests/src/referenceTest/java/tech/pegasys/teku/reference/phase0/gossip/GossipExecutionPayloadBidTestExecutor.java +++ b/eth-reference-tests/src/referenceTest/java/tech/pegasys/teku/reference/phase0/gossip/GossipExecutionPayloadBidTestExecutor.java @@ -23,6 +23,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -247,7 +248,8 @@ public void onBlockImported( // acceptedPreferences tracks proposer preferences that have been accepted by the validator, // so the bid validator can look them up to check bid compatibility. - final Map acceptedPreferences = new ConcurrentHashMap<>(); + final Map> acceptedPreferences = + new ConcurrentHashMap<>(); final ProposerPreferencesManager proposerPreferencesManager = new ProposerPreferencesManager() { @Override @@ -263,8 +265,17 @@ public SafeFuture addRemote( } @Override - public Optional getProposerPreferences(final UInt64 slot) { - return Optional.ofNullable(acceptedPreferences.get(slot)); + public Optional getProposerPreferences( + final UInt64 slot, final Bytes32 dependentRoot) { + return Optional.ofNullable(acceptedPreferences.get(slot)) + .map(preferencesByDependentRoot -> preferencesByDependentRoot.get(dependentRoot)); + } + + @Override + public Collection getProposerPreferencesForSlot(final UInt64 slot) { + return Optional.ofNullable(acceptedPreferences.get(slot)) + .map(preferencesByDependentRoot -> List.copyOf(preferencesByDependentRoot.values())) + .orElse(List.of()); } @Override @@ -295,8 +306,10 @@ public void subscribeOperationAdded( proposerPreferencesSchema::sszDeserialize); result = safeJoin(preferencesValidator.validate(signedPreferences)); if (result.isAccept()) { - acceptedPreferences.put( - signedPreferences.getMessage().getProposalSlot(), signedPreferences.getMessage()); + final ProposerPreferences preferences = signedPreferences.getMessage(); + acceptedPreferences + .computeIfAbsent(preferences.getProposalSlot(), __ -> new ConcurrentHashMap<>()) + .put(preferences.getDependentRoot(), preferences); } } else if (messageName.startsWith("execution_payload_envelope_")) { final SignedExecutionPayloadEnvelope signedEnvelope = diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/execution/DefaultProposerPreferencesManager.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/execution/DefaultProposerPreferencesManager.java index 6504818482c..fbcdb8614cc 100644 --- a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/execution/DefaultProposerPreferencesManager.java +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/execution/DefaultProposerPreferencesManager.java @@ -15,11 +15,14 @@ import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentNavigableMap; import java.util.concurrent.ConcurrentSkipListMap; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.apache.tuweni.bytes.Bytes32; import tech.pegasys.teku.ethereum.events.SlotEventsChannel; import tech.pegasys.teku.infrastructure.async.SafeFuture; import tech.pegasys.teku.infrastructure.subscribers.Subscribers; @@ -40,8 +43,8 @@ public class DefaultProposerPreferencesManager private final ProposerPreferencesGossipValidator proposerPreferencesGossipValidator; private final PendingPool pendingProposerPreferences; - private final ConcurrentNavigableMap acceptedProposerPreferences = - new ConcurrentSkipListMap<>(); + private final ConcurrentNavigableMap> + acceptedProposerPreferences = new ConcurrentSkipListMap<>(); private final Subscribers> subscribers = Subscribers.create(true); @@ -65,8 +68,17 @@ public SafeFuture addRemote( } @Override - public Optional getProposerPreferences(final UInt64 slot) { - return Optional.ofNullable(acceptedProposerPreferences.get(slot)); + public Optional getProposerPreferences( + final UInt64 slot, final Bytes32 dependentRoot) { + return Optional.ofNullable(acceptedProposerPreferences.get(slot)) + .map(preferencesByDependentRoot -> preferencesByDependentRoot.get(dependentRoot)); + } + + @Override + public Collection getProposerPreferencesForSlot(final UInt64 slot) { + return Optional.ofNullable(acceptedProposerPreferences.get(slot)) + .map(preferencesByDependentRoot -> List.copyOf(preferencesByDependentRoot.values())) + .orElse(List.of()); } @Override @@ -117,9 +129,10 @@ private void processValidationResult( switch (result.code()) { case ACCEPT -> { removePendingPreferences(signedProposerPreferences); - acceptedProposerPreferences.put( - signedProposerPreferences.getMessage().getProposalSlot(), - signedProposerPreferences.getMessage()); + final ProposerPreferences proposerPreferences = signedProposerPreferences.getMessage(); + acceptedProposerPreferences + .computeIfAbsent(proposerPreferences.getProposalSlot(), __ -> new ConcurrentHashMap<>()) + .put(proposerPreferences.getDependentRoot(), proposerPreferences); subscribers.forEach( subscriber -> subscriber.onOperationAdded(signedProposerPreferences, result, fromNetwork)); diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/execution/ProposerPreferencesManager.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/execution/ProposerPreferencesManager.java index c500df89fd9..19c22c28694 100644 --- a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/execution/ProposerPreferencesManager.java +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/execution/ProposerPreferencesManager.java @@ -13,7 +13,10 @@ package tech.pegasys.teku.statetransition.execution; +import java.util.Collection; +import java.util.List; import java.util.Optional; +import org.apache.tuweni.bytes.Bytes32; import tech.pegasys.teku.infrastructure.async.SafeFuture; import tech.pegasys.teku.infrastructure.unsigned.UInt64; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.ProposerPreferences; @@ -38,10 +41,16 @@ public SafeFuture addRemote( } @Override - public Optional getProposerPreferences(final UInt64 slot) { + public Optional getProposerPreferences( + final UInt64 slot, final Bytes32 dependentRoot) { return Optional.empty(); } + @Override + public Collection getProposerPreferencesForSlot(final UInt64 slot) { + return List.of(); + } + @Override public void subscribeOperationAdded( final OperationAddedSubscriber subscriber) {} @@ -53,7 +62,9 @@ SafeFuture addLocal( SafeFuture addRemote( SignedProposerPreferences signedProposerPreferences); - Optional getProposerPreferences(UInt64 slot); + Optional getProposerPreferences(UInt64 slot, Bytes32 dependentRoot); + + Collection getProposerPreferencesForSlot(UInt64 slot); void subscribeOperationAdded(OperationAddedSubscriber subscriber); } diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/forkchoice/ForkChoice.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/forkchoice/ForkChoice.java index 1a38aa5b956..d983faa0440 100644 --- a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/forkchoice/ForkChoice.java +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/forkchoice/ForkChoice.java @@ -100,6 +100,7 @@ import tech.pegasys.teku.statetransition.forkchoice.fastconfirmation.ForkChoiceFastConfirmation; import tech.pegasys.teku.statetransition.payloadattestation.ValidatablePayloadAttestationMessage; import tech.pegasys.teku.statetransition.util.DebugDataDumper; +import tech.pegasys.teku.statetransition.util.ShufflingDependentRootUtil; import tech.pegasys.teku.statetransition.validation.AttestationStateSelector; import tech.pegasys.teku.statetransition.validation.BlockBroadcastValidator; import tech.pegasys.teku.statetransition.validation.InternalValidationResult; @@ -1014,11 +1015,7 @@ private Optional getShufflingDependentRoot( } private Optional getShufflingDependentSlot(final UInt64 epoch) { - final int minSeedLookahead = spec.getSpecConfig(epoch).getMinSeedLookahead(); - if (epoch.isLessThanOrEqualTo(UInt64.valueOf(minSeedLookahead))) { - return Optional.empty(); - } - return Optional.of(spec.computeStartSlotAtEpoch(epoch.minus(minSeedLookahead)).minus(1)); + return ShufflingDependentRootUtil.getShufflingDependentSlotForEpoch(spec, epoch); } private Optional> extractBlobSidecarsFromValidationResults( diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/forkchoice/ProposersDataManager.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/forkchoice/ProposersDataManager.java index 80e9fc27a23..59ebe158f36 100644 --- a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/forkchoice/ProposersDataManager.java +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/forkchoice/ProposersDataManager.java @@ -44,12 +44,14 @@ import tech.pegasys.teku.spec.datastructures.execution.versions.capella.Withdrawal; import tech.pegasys.teku.spec.datastructures.forkchoice.ForkChoiceNode; import tech.pegasys.teku.spec.datastructures.forkchoice.ForkChoicePayloadStatus; +import tech.pegasys.teku.spec.datastructures.forkchoice.ReadOnlyForkChoiceStrategy; import tech.pegasys.teku.spec.datastructures.state.beaconstate.BeaconState; import tech.pegasys.teku.spec.datastructures.validator.BeaconPreparableProposer; import tech.pegasys.teku.spec.executionlayer.ExecutionLayerChannel; import tech.pegasys.teku.spec.executionlayer.ForkChoiceState; import tech.pegasys.teku.spec.executionlayer.PayloadBuildingAttributes; import tech.pegasys.teku.statetransition.execution.ProposerPreferencesManager; +import tech.pegasys.teku.statetransition.util.ShufflingDependentRootUtil; import tech.pegasys.teku.storage.client.ChainHead; import tech.pegasys.teku.storage.client.RecentChainData; import tech.pegasys.teku.storage.client.ValidatorIsConnectedProvider; @@ -307,10 +309,12 @@ private SafeFuture> calculatePayloadBuilding final Optional validatorRegistration = Optional.ofNullable(validatorRegistrationInfoByValidatorIndex.get(proposerIndex)) .map(RegisteredValidatorInfo::getSignedValidatorRegistration); + final Optional dependentRoot = + getShufflingDependentRoot(currentHeadBlock.blockRoot(), blockSlot); final Eth1Address feeRecipient = getFeeRecipient(proposerInfo, blockSlot); final UInt64 targetGasLimit = - getTargetGasLimit(blockSlot, proposerIndex, validatorRegistration); + getTargetGasLimit(blockSlot, proposerIndex, dependentRoot, validatorRegistration); return getPayloadAttributeWithdrawals(currentHeadBlock, state) .thenApplyAsync( @@ -374,10 +378,11 @@ private SafeFuture> getStateForPayloadBuildingAttributes( UInt64 getTargetGasLimit( final UInt64 blockSlot, final UInt64 proposerIndex, + final Optional dependentRoot, final Optional validatorRegistration) { // post-Gloas, we use signed proposer preferences - return proposerPreferencesManager - .getProposerPreferences(blockSlot) + return dependentRoot + .flatMap(root -> proposerPreferencesManager.getProposerPreferences(blockSlot, root)) .filter( proposerPreferences -> proposerPreferences.getValidatorIndex().equals(proposerIndex)) .map(ProposerPreferences::getTargetGasLimit) @@ -388,6 +393,17 @@ UInt64 getTargetGasLimit( .orElse(UInt64.ZERO); } + private Optional getShufflingDependentRoot( + final Bytes32 blockRoot, final UInt64 proposalSlot) { + final Optional maybeForkChoiceStrategy = + recentChainData.getForkChoiceStrategy(); + if (maybeForkChoiceStrategy == null || maybeForkChoiceStrategy.isEmpty()) { + return Optional.empty(); + } + return ShufflingDependentRootUtil.getShufflingDependentRoot( + spec, maybeForkChoiceStrategy.get(), blockRoot, proposalSlot); + } + // this function MUST return a fee recipient. private Eth1Address getFeeRecipient( final PreparedProposerInfo preparedProposerInfo, final UInt64 blockSlot) { diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/util/ShufflingDependentRootUtil.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/util/ShufflingDependentRootUtil.java new file mode 100644 index 00000000000..627df9a5fac --- /dev/null +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/util/ShufflingDependentRootUtil.java @@ -0,0 +1,53 @@ +/* + * 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.statetransition.util; + +import java.util.Optional; +import org.apache.tuweni.bytes.Bytes32; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.Spec; +import tech.pegasys.teku.spec.datastructures.forkchoice.ReadOnlyForkChoiceStrategy; + +public final class ShufflingDependentRootUtil { + + private static final UInt64 ONE = UInt64.ONE; + + private ShufflingDependentRootUtil() {} + + public static Optional getShufflingDependentRoot( + final Spec spec, + final ReadOnlyForkChoiceStrategy forkChoiceStrategy, + final Bytes32 blockRoot, + final UInt64 proposalSlot) { + final UInt64 proposalEpoch = spec.computeEpochAtSlot(proposalSlot); + final UInt64 minSeedLookahead = + UInt64.valueOf(spec.getSpecConfig(proposalEpoch).getMinSeedLookahead()); + final UInt64 dependentSlot = + proposalEpoch.isLessThanOrEqualTo(minSeedLookahead) + ? UInt64.ZERO + : spec.computeStartSlotAtEpoch(proposalEpoch.minus(minSeedLookahead)).minus(ONE); + return forkChoiceStrategy.getAncestor(blockRoot, dependentSlot); + } + + public static Optional getShufflingDependentSlotForEpoch( + final Spec spec, final UInt64 proposalEpoch) { + final UInt64 minSeedLookahead = + UInt64.valueOf(spec.getSpecConfig(proposalEpoch).getMinSeedLookahead()); + if (proposalEpoch.isLessThanOrEqualTo(minSeedLookahead)) { + return Optional.empty(); + } + return Optional.of( + spec.computeStartSlotAtEpoch(proposalEpoch.minus(minSeedLookahead)).minus(ONE)); + } +} diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/ExecutionPayloadBidGossipValidator.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/ExecutionPayloadBidGossipValidator.java index 3f32b8a73bc..805afac54c1 100644 --- a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/ExecutionPayloadBidGossipValidator.java +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/ExecutionPayloadBidGossipValidator.java @@ -123,113 +123,6 @@ public SafeFuture validate( } } - /* - * [IGNORE] the SignedProposerPreferences where preferences.proposal_slot is equal to - * bid.slot has been seen - */ - final Optional proposerPreferences = - proposerPreferencesManager.getProposerPreferences(bid.getSlot()); - if (proposerPreferences.isEmpty()) { - return completedFuture( - saveBidForFuture(bid, "no proposer preferences available; saving for future processing")); - } - - /* - * [REJECT] bid.fee_recipient matches the fee_recipient from the proposer's - * SignedProposerPreferences associated with bid.slot - */ - if (!bid.getFeeRecipient().equals(proposerPreferences.get().getFeeRecipient())) { - return completedFuture( - ignoreBid( - bid, - "fee recipient %s does not match proposer preferences fee recipient %s", - bid.getFeeRecipient(), - proposerPreferences.get().getFeeRecipient())); - } - - /* - * [IGNORE] this is the first signed bid seen with a valid signature from the given builder for the tuple - * (bid.slot, bid.parent_block_hash, bid.parent_block_root). - */ - final BuilderAndParent builderAndParent = - new BuilderAndParent( - bid.getBuilderIndex(), bid.getParentBlockHash(), bid.getParentBlockRoot()); - if (seenExecutionPayloadBids.getOrDefault(bid.getSlot(), Set.of()).contains(builderAndParent)) { - return completedFuture( - ignoreBid( - bid, - "already received for parent block hash %s and parent block root %s", - bid.getParentBlockHash(), - bid.getParentBlockRoot())); - } - - /* - * [IGNORE] this bid is the highest value bid seen for the tuple - * (bid.slot, bid.parent_block_hash, bid.parent_block_root). - * - * Note: Implementations SHOULD include DoS prevention measures to - * mitigate spam from malicious builders submitting numerous bids with minimal value increments. - * Possible strategies include: (1) only forwarding bids that exceed the current highest bid by a - * minimum threshold, or (2) forwarding only the highest observed bid at regular time intervals. - * - */ - final BidParent bidValueKey = - new BidParent(bid.getSlot(), bid.getParentBlockHash(), bid.getParentBlockRoot()); - final UInt64 existingBidValue = highestBids.getOrDefault(bidValueKey, UInt64.ZERO); - if (!existingBidValue.isZero()) { - final UInt64 minRequiredBid = calculateMinimumRequiredBid(existingBidValue); - - if (bid.getValue().isLessThan(minRequiredBid)) { - return completedFuture( - ignoreBid( - bid, - "does not meet minimum increment threshold (%s%%); current highest is %s ETH and minimum required is %s ETH", - minBidIncrementPercentage, - gweiToEth(existingBidValue), - gweiToEth(minRequiredBid))); - } - } - - /* - * [IGNORE] bid.parent_block_hash is the block hash of a known execution payload in fork choice - * and is_gas_limit_target_compatible(parent_gas_limit, bid.gas_limit, proposer_preferences.target_gas_limit) - * is True where parent_gas_limit is the gas_limit of that execution payload. - */ - final Optional maybeParentGasLimit = - gossipValidationHelper.getGasLimitForExecutionPayload( - bid.getParentBlockRoot(), bid.getParentBlockHash()); - if (maybeParentGasLimit.isEmpty()) { - return completedFuture( - saveBidForFuture( - bid, - "parent execution payload gas limit is unavailable for parent block hash %s; saving for future processing", - bid.getParentBlockHash())); - } - final UInt64 parentGasLimit = maybeParentGasLimit.get(); - final UInt64 targetGasLimit = proposerPreferences.get().getTargetGasLimit(); - if (!isGasLimitTargetCompatible(parentGasLimit, bid.getGasLimit(), targetGasLimit)) { - return completedFuture( - ignoreBid( - bid, - "gas limit %s is not compatible with parent gas limit %s and proposer preferences target gas limit %s", - bid.getGasLimit(), - parentGasLimit, - targetGasLimit)); - } - - /* - * [IGNORE] The bid is compatible with the current head branch, i.e. - * is_bid_compatible_with_head(store, bid) returns True. - */ - if (!gossipValidationHelper.isBidCompatibleWithHead(bid)) { - return completedFuture( - ignoreBid( - bid, - "is not compatible with the current head branch (parent block hash %s, parent block root %s)", - bid.getParentBlockHash(), - bid.getParentBlockRoot())); - } - /* * Retrieve the bid's parent block slot for the remaining validation rules. */ @@ -269,6 +162,116 @@ public SafeFuture validate( } final BeaconState state = maybeState.get(); + /* + * [IGNORE] The matching proposer preferences have been seen + */ + final Optional maybeDependentRoot = + gossipValidationHelper.getShufflingDependentRoot( + bid.getParentBlockRoot(), bid.getSlot()); + if (maybeDependentRoot.isEmpty()) { + return saveBidForFuture( + bid, "shuffling dependent root is unavailable; saving for future processing"); + } + final Optional proposerPreferences = + proposerPreferencesManager.getProposerPreferences( + bid.getSlot(), maybeDependentRoot.get()); + if (proposerPreferences.isEmpty()) { + return saveBidForFuture( + bid, "no proposer preferences available; saving for future processing"); + } + + /* + * [REJECT] bid.fee_recipient matches the fee_recipient from the proposer's + * SignedProposerPreferences associated with bid.slot + */ + if (!bid.getFeeRecipient().equals(proposerPreferences.get().getFeeRecipient())) { + return ignoreBid( + bid, + "fee recipient %s does not match proposer preferences fee recipient %s", + bid.getFeeRecipient(), + proposerPreferences.get().getFeeRecipient()); + } + + /* + * [IGNORE] this is the first signed bid seen with a valid signature from the given builder for the tuple + * (bid.slot, bid.parent_block_hash, bid.parent_block_root). + */ + final BuilderAndParent builderAndParent = + new BuilderAndParent( + bid.getBuilderIndex(), bid.getParentBlockHash(), bid.getParentBlockRoot()); + if (seenExecutionPayloadBids + .getOrDefault(bid.getSlot(), Set.of()) + .contains(builderAndParent)) { + return ignoreBid( + bid, + "already received for parent block hash %s and parent block root %s", + bid.getParentBlockHash(), + bid.getParentBlockRoot()); + } + + /* + * [IGNORE] this bid is the highest value bid seen for the tuple + * (bid.slot, bid.parent_block_hash, bid.parent_block_root). + * + * Note: Implementations SHOULD include DoS prevention measures to + * mitigate spam from malicious builders submitting numerous bids with minimal value increments. + * Possible strategies include: (1) only forwarding bids that exceed the current highest bid by a + * minimum threshold, or (2) forwarding only the highest observed bid at regular time intervals. + * + */ + final BidParent bidValueKey = + new BidParent(bid.getSlot(), bid.getParentBlockHash(), bid.getParentBlockRoot()); + final UInt64 existingBidValue = highestBids.getOrDefault(bidValueKey, UInt64.ZERO); + if (!existingBidValue.isZero()) { + final UInt64 minRequiredBid = calculateMinimumRequiredBid(existingBidValue); + + if (bid.getValue().isLessThan(minRequiredBid)) { + return ignoreBid( + bid, + "does not meet minimum increment threshold (%s%%); current highest is %s ETH and minimum required is %s ETH", + minBidIncrementPercentage, + gweiToEth(existingBidValue), + gweiToEth(minRequiredBid)); + } + } + + /* + * [IGNORE] bid.parent_block_hash is the block hash of a known execution payload in fork choice + * and is_gas_limit_target_compatible(parent_gas_limit, bid.gas_limit, proposer_preferences.target_gas_limit) + * is True where parent_gas_limit is the gas_limit of that execution payload. + */ + final Optional maybeParentGasLimit = + gossipValidationHelper.getGasLimitForExecutionPayload( + bid.getParentBlockRoot(), bid.getParentBlockHash()); + if (maybeParentGasLimit.isEmpty()) { + return saveBidForFuture( + bid, + "parent execution payload gas limit is unavailable for parent block hash %s; saving for future processing", + bid.getParentBlockHash()); + } + final UInt64 parentGasLimit = maybeParentGasLimit.get(); + final UInt64 targetGasLimit = proposerPreferences.get().getTargetGasLimit(); + if (!isGasLimitTargetCompatible(parentGasLimit, bid.getGasLimit(), targetGasLimit)) { + return ignoreBid( + bid, + "gas limit %s is not compatible with parent gas limit %s and proposer preferences target gas limit %s", + bid.getGasLimit(), + parentGasLimit, + targetGasLimit); + } + + /* + * [IGNORE] The bid is compatible with the current head branch, i.e. + * is_bid_compatible_with_head(store, bid) returns True. + */ + if (!gossipValidationHelper.isBidCompatibleWithHead(bid)) { + return ignoreBid( + bid, + "is not compatible with the current head branch (parent block hash %s, parent block root %s)", + bid.getParentBlockHash(), + bid.getParentBlockRoot()); + } + /* * [REJECT] bid.prev_randao is the correct RANDAO mix -- i.e. validate that * bid.prev_randao == get_randao_mix(parent_state, get_current_epoch(parent_state)). diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/GossipValidationHelper.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/GossipValidationHelper.java index c90f043d36d..2352de56590 100644 --- a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/GossipValidationHelper.java +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/GossipValidationHelper.java @@ -44,6 +44,7 @@ import tech.pegasys.teku.spec.logic.common.util.DataColumnSidecarUtil; import tech.pegasys.teku.spec.logic.versions.gloas.helpers.BeaconStateAccessorsGloas; import tech.pegasys.teku.spec.logic.versions.gloas.helpers.PredicatesGloas; +import tech.pegasys.teku.statetransition.util.ShufflingDependentRootUtil; import tech.pegasys.teku.storage.client.ChainHead; import tech.pegasys.teku.storage.client.RecentChainData; @@ -351,6 +352,17 @@ public boolean isPossibleDependentRoot(final Bytes32 root, final UInt64 epochSta return recentChainData.getBestBlockRoot().filter(root::equals).isPresent(); } + public Optional getShufflingDependentRoot( + final Bytes32 blockRoot, final UInt64 proposalSlot) { + final Optional maybeForkChoiceStrategy = + recentChainData.getForkChoiceStrategy(); + if (maybeForkChoiceStrategy == null || maybeForkChoiceStrategy.isEmpty()) { + return Optional.empty(); + } + return ShufflingDependentRootUtil.getShufflingDependentRoot( + spec, maybeForkChoiceStrategy.get(), blockRoot, proposalSlot); + } + public boolean builderHasEnoughBalanceForBid( final UInt64 bidValue, final UInt64 builderIndex, diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/ProposerPreferencesGossipValidator.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/ProposerPreferencesGossipValidator.java index c785d728525..2eacb27b6ad 100644 --- a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/ProposerPreferencesGossipValidator.java +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/ProposerPreferencesGossipValidator.java @@ -110,12 +110,12 @@ public SafeFuture validate( /* * [IGNORE] The signed_proposer_preferences is the first valid message for the tuple - * (preferences.dependent_root, preferences.proposal_slot). The validator index is deliberately + * (preferences.proposal_slot, preferences.dependent_root). The validator index is deliberately * excluded: only one validator can be the proposer for that pair, so keying on it as well would * let a peer force full validation of arbitrarily many messages for the same slot. */ final ProposerPreferencesDedupKey dedupKey = - new ProposerPreferencesDedupKey(dependentRoot, proposalSlot); + new ProposerPreferencesDedupKey(proposalSlot, dependentRoot); if (seenProposerPreferences.contains(dedupKey)) { return completedFuture(ignoreAlreadySeen(proposerPreferences)); } @@ -289,5 +289,5 @@ private InternalValidationResult ignoreAlreadySeen( return ignorePreferences(proposerPreferences, "already received"); } - private record ProposerPreferencesDedupKey(Bytes32 dependentRoot, UInt64 proposalSlot) {} + private record ProposerPreferencesDedupKey(UInt64 proposalSlot, Bytes32 dependentRoot) {} } diff --git a/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/execution/DefaultProposerPreferencesManagerTest.java b/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/execution/DefaultProposerPreferencesManagerTest.java index 4c78a5c91fe..7a43e21f74b 100644 --- a/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/execution/DefaultProposerPreferencesManagerTest.java +++ b/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/execution/DefaultProposerPreferencesManagerTest.java @@ -14,6 +14,7 @@ package tech.pegasys.teku.statetransition.execution; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; @@ -76,6 +77,7 @@ void shouldStoreAndReturnAcceptedPreferences() { final SignedProposerPreferences signedProposerPreferences = dataStructureUtil.randomSignedProposerPreferences(); final UInt64 slot = signedProposerPreferences.getMessage().getProposalSlot(); + final Bytes32 dependentRoot = signedProposerPreferences.getMessage().getDependentRoot(); when(gossipValidator.validate(signedProposerPreferences)) .thenReturn(SafeFuture.completedFuture(ACCEPT)); @@ -83,10 +85,46 @@ void shouldStoreAndReturnAcceptedPreferences() { final InternalValidationResult result = safeJoin(manager.addLocal(signedProposerPreferences)); assertThat(result).isEqualTo(ACCEPT); - assertThat(manager.getProposerPreferences(slot)) + assertThat(manager.getProposerPreferences(slot, dependentRoot)) .hasValue(signedProposerPreferences.getMessage()); } + @TestTemplate + void shouldStorePreferencesForDistinctDependentRootsAtTheSameSlot() { + final UInt64 proposalSlot = UInt64.valueOf(10); + final Bytes32 firstDependentRoot = dataStructureUtil.randomBytes32(); + final Bytes32 secondDependentRoot = dataStructureUtil.randomBytes32(); + final SignedProposerPreferences firstPreferences = + createSignedProposerPreferences(proposalSlot, firstDependentRoot); + final SignedProposerPreferences secondPreferences = + createSignedProposerPreferences(proposalSlot, secondDependentRoot); + when(gossipValidator.validate(any())).thenReturn(SafeFuture.completedFuture(ACCEPT)); + + safeJoin(manager.addRemote(firstPreferences)); + safeJoin(manager.addRemote(secondPreferences)); + + assertThat(manager.getProposerPreferences(proposalSlot, firstDependentRoot)) + .contains(firstPreferences.getMessage()); + assertThat(manager.getProposerPreferences(proposalSlot, secondDependentRoot)) + .contains(secondPreferences.getMessage()); + assertThat(manager.getProposerPreferencesForSlot(proposalSlot)) + .containsExactlyInAnyOrder(firstPreferences.getMessage(), secondPreferences.getMessage()); + } + + @TestTemplate + void shouldReturnImmutableSnapshotOfPreferencesForSlot() { + final SignedProposerPreferences preferences = + dataStructureUtil.randomSignedProposerPreferences(); + final UInt64 proposalSlot = preferences.getMessage().getProposalSlot(); + when(gossipValidator.validate(preferences)).thenReturn(SafeFuture.completedFuture(ACCEPT)); + safeJoin(manager.addRemote(preferences)); + + assertThatThrownBy(() -> manager.getProposerPreferencesForSlot(proposalSlot).clear()) + .isInstanceOf(UnsupportedOperationException.class); + assertThat(manager.getProposerPreferencesForSlot(proposalSlot)) + .containsExactly(preferences.getMessage()); + } + @TestTemplate void shouldNotStoreRejectedPreferences() { final SignedProposerPreferences signedProposerPreferences = @@ -100,7 +138,10 @@ void shouldNotStoreRejectedPreferences() { assertThat(result.isReject()).isTrue(); - assertThat(manager.getProposerPreferences(slot)).isEmpty(); + assertThat( + manager.getProposerPreferences( + slot, signedProposerPreferences.getMessage().getDependentRoot())) + .isEmpty(); } @TestTemplate @@ -147,7 +188,7 @@ void shouldNotNotifySubscriberOnReject() { @TestTemplate void shouldReturnEmptyForUnknownSlot() { - assertThat(manager.getProposerPreferences(UInt64.valueOf(999))).isEmpty(); + assertThat(manager.getProposerPreferencesForSlot(UInt64.valueOf(999))).isEmpty(); } @TestTemplate @@ -162,9 +203,9 @@ void shouldPruneAcceptedPreferencesBeforeCurrentSlot() { manager.onSlot(UInt64.valueOf(10)); - assertThat(manager.getProposerPreferences(UInt64.valueOf(9))).isEmpty(); - assertThat(manager.getProposerPreferences(UInt64.valueOf(10))).isPresent(); - assertThat(manager.getProposerPreferences(UInt64.valueOf(11))).isPresent(); + assertThat(manager.getProposerPreferencesForSlot(UInt64.valueOf(9))).isEmpty(); + assertThat(manager.getProposerPreferencesForSlot(UInt64.valueOf(10))).isNotEmpty(); + assertThat(manager.getProposerPreferencesForSlot(UInt64.valueOf(11))).isNotEmpty(); } @TestTemplate @@ -178,7 +219,7 @@ void shouldRetainCurrentPreferencesWhenAddingNextEpochPreferences() { UInt64.valueOf(slot), dataStructureUtil.randomBytes32()))); } for (int slot = 0; slot < 32; slot++) { - assertThat(manager.getProposerPreferences(UInt64.valueOf(slot))).isPresent(); + assertThat(manager.getProposerPreferencesForSlot(UInt64.valueOf(slot))).isNotEmpty(); } manager.onSlot(UInt64.valueOf(32)); @@ -190,7 +231,7 @@ void shouldRetainCurrentPreferencesWhenAddingNextEpochPreferences() { } for (int slot = 32; slot < 64; slot++) { - assertThat(manager.getProposerPreferences(UInt64.valueOf(slot))).isPresent(); + assertThat(manager.getProposerPreferencesForSlot(UInt64.valueOf(slot))).isNotEmpty(); } } @@ -207,7 +248,7 @@ void shouldQueuePreferencesSavedForFuture() { assertThat(pendingProposerPreferences.get(preferences.hashTreeRoot())) .contains(new PendingProposerPreferences(preferences, true)); - assertThat(manager.getProposerPreferences(slot)).isEmpty(); + assertThat(manager.getProposerPreferencesForSlot(slot)).isEmpty(); } @TestTemplate @@ -225,7 +266,10 @@ void shouldRetryPreferencesWhenDependentBlockIsImported() { manager.onBlockImported(dependentBlock, false); verify(gossipValidator, times(2)).validate(preferences); - assertThat(manager.getProposerPreferences(proposalSlot)).contains(preferences.getMessage()); + assertThat( + manager.getProposerPreferences( + proposalSlot, preferences.getMessage().getDependentRoot())) + .contains(preferences.getMessage()); assertThat(pendingProposerPreferences.get(preferences.hashTreeRoot())).isEmpty(); } @@ -243,7 +287,10 @@ void shouldRetryPreferencesOnSlot() { manager.onSlot(proposalSlot); verify(gossipValidator, times(2)).validate(preferences); - assertThat(manager.getProposerPreferences(proposalSlot)).contains(preferences.getMessage()); + assertThat( + manager.getProposerPreferences( + proposalSlot, preferences.getMessage().getDependentRoot())) + .contains(preferences.getMessage()); } @TestTemplate diff --git a/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/forkchoice/ProposerDataManagerTest.java b/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/forkchoice/ProposerDataManagerTest.java index 5bb9b99d735..d6e72a8f672 100644 --- a/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/forkchoice/ProposerDataManagerTest.java +++ b/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/forkchoice/ProposerDataManagerTest.java @@ -25,6 +25,7 @@ import java.util.List; import java.util.Optional; import java.util.OptionalDouble; +import org.apache.tuweni.bytes.Bytes32; import org.junit.jupiter.api.Test; import tech.pegasys.teku.ethereum.execution.types.Eth1Address; import tech.pegasys.teku.infrastructure.async.SafeFuture; @@ -150,16 +151,21 @@ void shouldUseProposerPreferencesGasLimitForPayloadAttributes() { final UInt64 blockSlot = UInt64.valueOf(12); final UInt64 proposerIndex = UInt64.ONE; final UInt64 targetGasLimit = UInt64.valueOf(45_000_000); + final Bytes32 dependentRoot = dataStructureUtil.randomBytes32(); final SignedValidatorRegistration validatorRegistration = validatorRegistrationWithGasLimit(UInt64.valueOf(30_000_000)); final ProposerPreferencesManager proposerPreferencesManager = mock(ProposerPreferencesManager.class); final ProposersDataManager manager = createProposersDataManager(proposerPreferencesManager); - when(proposerPreferencesManager.getProposerPreferences(blockSlot)) - .thenReturn(Optional.of(proposerPreferences(proposerIndex, targetGasLimit))); + when(proposerPreferencesManager.getProposerPreferences(blockSlot, dependentRoot)) + .thenReturn(Optional.of(proposerPreferences(dependentRoot, proposerIndex, targetGasLimit))); assertThat( - manager.getTargetGasLimit(blockSlot, proposerIndex, Optional.of(validatorRegistration))) + manager.getTargetGasLimit( + blockSlot, + proposerIndex, + Optional.of(dependentRoot), + Optional.of(validatorRegistration))) .isEqualTo(targetGasLimit); } @@ -173,7 +179,7 @@ void shouldUseValidatorRegistrationGasLimitWithoutProposerPreferences() { assertThat( proposersDataManager.getTargetGasLimit( - blockSlot, proposerIndex, Optional.of(validatorRegistration))) + blockSlot, proposerIndex, Optional.empty(), Optional.of(validatorRegistration))) .isEqualTo(registrationGasLimit); } @@ -182,18 +188,24 @@ void shouldIgnoreProposerPreferencesGasLimitForDifferentValidator() { final UInt64 blockSlot = UInt64.valueOf(12); final UInt64 proposerIndex = UInt64.ONE; final UInt64 registrationGasLimit = UInt64.valueOf(30_000_000); + final Bytes32 dependentRoot = dataStructureUtil.randomBytes32(); final ProposerPreferencesManager proposerPreferencesManager = mock(ProposerPreferencesManager.class); final ProposersDataManager manager = createProposersDataManager(proposerPreferencesManager); final SignedValidatorRegistration validatorRegistration = validatorRegistrationWithGasLimit(registrationGasLimit); - when(proposerPreferencesManager.getProposerPreferences(blockSlot)) + when(proposerPreferencesManager.getProposerPreferences(blockSlot, dependentRoot)) .thenReturn( - Optional.of(proposerPreferences(UInt64.valueOf(2), UInt64.valueOf(45_000_000)))); + Optional.of( + proposerPreferences(dependentRoot, UInt64.valueOf(2), UInt64.valueOf(45_000_000)))); assertThat( - manager.getTargetGasLimit(blockSlot, proposerIndex, Optional.of(validatorRegistration))) + manager.getTargetGasLimit( + blockSlot, + proposerIndex, + Optional.of(dependentRoot), + Optional.of(validatorRegistration))) .isEqualTo(registrationGasLimit); } @@ -201,7 +213,7 @@ void shouldIgnoreProposerPreferencesGasLimitForDifferentValidator() { void shouldUseZeroTargetGasLimitWithoutProposerPreferencesOrValidatorRegistration() { assertThat( proposersDataManager.getTargetGasLimit( - UInt64.valueOf(12), UInt64.ONE, Optional.empty())) + UInt64.valueOf(12), UInt64.ONE, Optional.empty(), Optional.empty())) .isEqualTo(UInt64.ZERO); } @@ -231,10 +243,10 @@ private ProposersDataManager createProposersDataManager( } private ProposerPreferences proposerPreferences( - final UInt64 validatorIndex, final UInt64 gasLimit) { + final Bytes32 dependentRoot, final UInt64 validatorIndex, final UInt64 gasLimit) { return new ProposerPreferencesSchema() .create( - dataStructureUtil.randomBytes32(), + dependentRoot, dataStructureUtil.randomSlot(), validatorIndex, dataStructureUtil.randomEth1Address(), diff --git a/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/validation/ExecutionPayloadBidGossipValidatorTest.java b/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/validation/ExecutionPayloadBidGossipValidatorTest.java index 355c5cdacb4..dc155c6ae6c 100644 --- a/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/validation/ExecutionPayloadBidGossipValidatorTest.java +++ b/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/validation/ExecutionPayloadBidGossipValidatorTest.java @@ -18,6 +18,7 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static tech.pegasys.teku.infrastructure.async.SafeFutureAssert.assertThatSafeFuture; @@ -80,6 +81,7 @@ public class ExecutionPayloadBidGossipValidatorTest { private UInt64 builderIndex; private Bytes32 parentBlockRoot; private Bytes32 parentBlockHash; + private Bytes32 dependentRoot; private BeaconState postState; private SchemaDefinitionsGloas schemaDefinitions; @@ -114,6 +116,7 @@ void setup(final TestSpecInvocationContextProvider.SpecContext specContext) { builderIndex = bid.getBuilderIndex(); parentBlockRoot = bid.getParentBlockRoot(); parentBlockHash = bid.getParentBlockHash(); + dependentRoot = dataStructureUtil.randomBytes32(); // Replace the random builders so that the bid's builder index is in range and every builder // carries PAYLOAD_BUILDER_VERSION; randomBuilder() assigns a random version, which the // payload-builder-version rule would reject. @@ -137,10 +140,12 @@ void setup(final TestSpecInvocationContextProvider.SpecContext specContext) { final ProposerPreferences proposerPreferences = mock(ProposerPreferences.class); when(proposerPreferences.getFeeRecipient()).thenReturn(bid.getFeeRecipient()); when(proposerPreferences.getTargetGasLimit()).thenReturn(bid.getGasLimit()); - when(proposerPreferencesManager.getProposerPreferences(any())) + when(proposerPreferencesManager.getProposerPreferences(bid.getSlot(), dependentRoot)) .thenReturn(Optional.of(proposerPreferences)); when(gossipValidationHelper.isSlotCurrentOrNext(slot)).thenReturn(true); + when(gossipValidationHelper.getShufflingDependentRoot(parentBlockRoot, slot)) + .thenReturn(Optional.of(dependentRoot)); when(gossipValidationHelper.getGasLimitForExecutionPayload(parentBlockRoot, parentBlockHash)) .thenReturn(Optional.of(bid.getGasLimit())); when(gossipValidationHelper.isBidCompatibleWithHead(any())).thenReturn(true); @@ -202,7 +207,31 @@ void shouldIgnore_whenSlotIsNotCurrentOrNext() { @TestTemplate void shouldSaveForFuture_whenProposerPreferencesNotSeen() { - when(proposerPreferencesManager.getProposerPreferences(slot)).thenReturn(Optional.empty()); + when(proposerPreferencesManager.getProposerPreferences(slot, dependentRoot)) + .thenReturn(Optional.empty()); + assertThatSafeFuture(bidValidator.validate(signedBid)) + .isCompletedWithValue( + saveBidForFuture( + signedBid, "no proposer preferences available; saving for future processing")); + } + + @TestTemplate + void shouldSaveForFuture_whenShufflingDependentRootIsUnavailable() { + when(gossipValidationHelper.getShufflingDependentRoot(parentBlockRoot, slot)) + .thenReturn(Optional.empty()); + + assertThatSafeFuture(bidValidator.validate(signedBid)) + .isCompletedWithValue( + saveBidForFuture( + signedBid, + "shuffling dependent root is unavailable; saving for future processing")); + } + + @TestTemplate + void shouldSaveForFuture_whenPreferencesDoNotMatchShufflingDependentRoot() { + when(proposerPreferencesManager.getProposerPreferences(slot, dependentRoot)) + .thenReturn(Optional.empty()); + assertThatSafeFuture(bidValidator.validate(signedBid)) .isCompletedWithValue( saveBidForFuture( @@ -214,7 +243,7 @@ void shouldIgnore_whenFeeRecipientDoesNotMatchProposerPreferences() { final ProposerPreferences mismatchedPreferences = mock(ProposerPreferences.class); when(mismatchedPreferences.getFeeRecipient()).thenReturn(dataStructureUtil.randomEth1Address()); when(mismatchedPreferences.getTargetGasLimit()).thenReturn(bid.getGasLimit()); - when(proposerPreferencesManager.getProposerPreferences(slot)) + when(proposerPreferencesManager.getProposerPreferences(slot, dependentRoot)) .thenReturn(Optional.of(mismatchedPreferences)); assertThatSafeFuture(bidValidator.validate(signedBid)) @@ -436,6 +465,7 @@ void shouldSaveForFuture_whenParentBlockIsNotAvailable() { signedBid, "parent block with root %s is unknown; saving for future processing", parentBlockRoot)); + verify(gossipValidationHelper, never()).getShufflingDependentRoot(any(), any()); } @TestTemplate @@ -677,7 +707,7 @@ void shouldIgnoreBidWhenBuilderAddedDuringValidation() { .isCompletedWithValue( ignoreBid( signedBid, - "another bid for parent block hash %s and parent block root %s was processed concurrently", + "already received for parent block hash %s and parent block root %s", parentBlockHash, parentBlockRoot)); } @@ -803,7 +833,9 @@ private void mockBidValidation( final ProposerPreferences matchingPreferences = mock(ProposerPreferences.class); when(matchingPreferences.getFeeRecipient()).thenReturn(message.getFeeRecipient()); when(matchingPreferences.getTargetGasLimit()).thenReturn(message.getGasLimit()); - when(proposerPreferencesManager.getProposerPreferences(slot)) + when(gossipValidationHelper.getShufflingDependentRoot(message.getParentBlockRoot(), slot)) + .thenReturn(Optional.of(dependentRoot)); + when(proposerPreferencesManager.getProposerPreferences(slot, dependentRoot)) .thenReturn(Optional.of(matchingPreferences)); when(gossipValidationHelper.isSlotCurrentOrNext(slot)).thenReturn(true); when(gossipValidationHelper.getGasLimitForExecutionPayload( @@ -955,7 +987,8 @@ private void mockProposerPreferences( when(proposerPreferences.getFeeRecipient()) .thenReturn(signedBid.getMessage().getFeeRecipient()); when(proposerPreferences.getTargetGasLimit()).thenReturn(targetGasLimit); - when(proposerPreferencesManager.getProposerPreferences(signedBid.getMessage().getSlot())) + when(proposerPreferencesManager.getProposerPreferences( + signedBid.getMessage().getSlot(), dependentRoot)) .thenReturn(Optional.of(proposerPreferences)); } diff --git a/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/validation/GossipValidationHelperTest.java b/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/validation/GossipValidationHelperTest.java index 85af0e186fb..0fce3667fa9 100644 --- a/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/validation/GossipValidationHelperTest.java +++ b/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/validation/GossipValidationHelperTest.java @@ -710,6 +710,47 @@ void isPossibleDependentRoot_shouldAcceptHeadWithNoChildYet() { .isTrue(); } + @TestTemplate + void getShufflingDependentRoot_shouldUseAncestorAtDependentSlot() { + final UInt64 minSeedLookahead = + UInt64.valueOf(spec.getGenesisSpecConfig().getMinSeedLookahead()); + final UInt64 proposalEpoch = minSeedLookahead.plus(2); + final UInt64 proposalSlot = spec.computeStartSlotAtEpoch(proposalEpoch); + final UInt64 dependentSlot = + spec.computeStartSlotAtEpoch(proposalEpoch.minus(minSeedLookahead)).minus(ONE); + final Bytes32 parentBlockRoot = dataStructureUtil.randomBytes32(); + final Bytes32 dependentRoot = dataStructureUtil.randomBytes32(); + final ReadOnlyForkChoiceStrategy forkChoiceStrategy = mock(ReadOnlyForkChoiceStrategy.class); + final RecentChainData recentChainData = mock(RecentChainData.class); + when(recentChainData.getForkChoiceStrategy()).thenReturn(Optional.of(forkChoiceStrategy)); + when(forkChoiceStrategy.getAncestor(parentBlockRoot, dependentSlot)) + .thenReturn(Optional.of(dependentRoot)); + final GossipValidationHelper helper = + new GossipValidationHelper(spec, recentChainData, storageSystem.getMetricsSystem()); + + assertThat(helper.getShufflingDependentRoot(parentBlockRoot, proposalSlot)) + .contains(dependentRoot); + } + + @TestTemplate + void getShufflingDependentRoot_shouldUseGenesisSlotDuringSeedLookahead() { + final UInt64 minSeedLookahead = + UInt64.valueOf(spec.getGenesisSpecConfig().getMinSeedLookahead()); + final UInt64 proposalSlot = spec.computeStartSlotAtEpoch(minSeedLookahead); + final Bytes32 parentBlockRoot = dataStructureUtil.randomBytes32(); + final Bytes32 genesisRoot = dataStructureUtil.randomBytes32(); + final ReadOnlyForkChoiceStrategy forkChoiceStrategy = mock(ReadOnlyForkChoiceStrategy.class); + final RecentChainData recentChainData = mock(RecentChainData.class); + when(recentChainData.getForkChoiceStrategy()).thenReturn(Optional.of(forkChoiceStrategy)); + when(forkChoiceStrategy.getAncestor(parentBlockRoot, ZERO)) + .thenReturn(Optional.of(genesisRoot)); + final GossipValidationHelper helper = + new GossipValidationHelper(spec, recentChainData, storageSystem.getMetricsSystem()); + + assertThat(helper.getShufflingDependentRoot(parentBlockRoot, proposalSlot)) + .contains(genesisRoot); + } + @TestTemplate void isValidBuilder_shouldReturnTrueForActiveBuilder(final SpecContext specContext) { specContext.assumeGloasActive();