diff --git a/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/v3/newBlockGLOAS.json b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/v3/newBlockGLOAS.json index 2031384b1c6..c2beda32a10 100644 --- a/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/v3/newBlockGLOAS.json +++ b/data/beaconrestapi/src/integration-test/resources/tech/pegasys/teku/beaconrestapi/v3/newBlockGLOAS.json @@ -332,16 +332,16 @@ ], "signed_execution_payload_bid": { "message": { - "parent_block_hash": "0x0c15f142b0175c6e8491002de9596cba91433934b44834491c80e04920a257ea", - "parent_block_root": "0xd3b10243d034be9fa5c8caaa6ebf2e537e3a3c7e91eeb91a93fc15f1917f314a", - "block_hash": "0xe6d2fc4270809de49a0b32d641484320853d3b1047b7e2d4c07d59b96ce0e8d4", - "prev_randao": "0x4678df4290faf93c645a36af63f4a921a44c36ead7a2ae77a503aba2afc47d8a", - "fee_recipient": "0x5999d9423046d981599d9dda377dbeeeaa4f357c", - "gas_limit": "4822007335809147728", - "builder_index": "740284", - "slot": "1310451960", - "value": "4820354852592463600", - "execution_payment": "4759212943510379790", + "parent_block_hash": "0xf9f3f64210cc7c298f4e990115d157ed8b403aa2fe7f0b8feefe9c814641a05f", + "parent_block_root": "0x4678df4290faf93c645a36af63f4a921a44c36ead7a2ae77a503aba2afc47d8a", + "block_hash": "0x5999d9423046d981599d9dda377dbeeeaa4f357c8d6bd731d384ee6a8a253515", + "prev_randao": "0x2036eb4250633bb379d46758bce28087974638c66a115d034a012412fb020f75", + "fee_recipient": "0x3357e542f0ae1af86f17cf83906b95549e493758", + "gas_limit": "4759212943510379790", + "builder_index": "1125033", + "slot": "21512915928", + "value": "4826964785459200112", + "execution_payment": "4833574722620903920", "blob_kzg_commitments_root": "0x08400642aee83f31d60723f5fabaa1c58bbc110432a5935f43af6c943fc4fe96" }, "signature": "0xb58eaaba3ba51d7098d65fbec3829ace78576a2276fd9c97c293aabdb634a2c50f52611f48088da5d4a5b5fa2c5f4c0513d8dd91c8534b50a7b8ae0072583612610ada0c81a261641c66ac542428cedf20f1b954ad03505fc058b40ce0bf4182" diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/Constants.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/Constants.java index 751e2895c2b..a07dbf5dbf4 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/Constants.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/config/Constants.java @@ -27,6 +27,8 @@ public class Constants { // Target holding two slots worth of aggregators (16 aggregators, 64 committees and 2 slots) public static final int VALID_AGGREGATE_SET_SIZE = 16 * 64 * 2; // Target 2 different attestation data (aggregators normally agree) for two slots + public static final int SEEN_EXECUTION_PAYLOAD_BID_SET_SIZE = 10000; + public static final int HIGHEST_BID_SET_SIZE = 10; public static final int VALID_ATTESTATION_DATA_SET_SIZE = 2 * 64 * 2; public static final int VALID_VALIDATOR_SET_SIZE = 10000; // Only need to maintain a cache for the current slot, so just needs to be as large as the 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 1d361ee0af1..601ecddce8b 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 @@ -34,6 +34,7 @@ import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.ExecutionPayloadEnvelope; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadEnvelope; import tech.pegasys.teku.spec.datastructures.execution.BlobAndCellProofs; +import tech.pegasys.teku.spec.datastructures.state.Validator; import tech.pegasys.teku.spec.datastructures.state.beaconstate.BeaconState; import tech.pegasys.teku.spec.logic.common.helpers.MiscHelpers; import tech.pegasys.teku.spec.logic.versions.fulu.helpers.MiscHelpersFulu; @@ -51,11 +52,14 @@ public static MiscHelpersGloas required(final MiscHelpers miscHelpers) { + miscHelpers.getClass().getSimpleName())); } + private final PredicatesGloas predicates; + public MiscHelpersGloas( final SpecConfigGloas specConfig, final PredicatesGloas predicates, final SchemaDefinitionsGloas schemaDefinitions) { super(specConfig, predicates, schemaDefinitions); + this.predicates = predicates; } /** @@ -149,6 +153,10 @@ public List constructDataColumnSidecars( extendedMatrix); } + public boolean hasBuilderWithdrawalCredential(final Validator validator) { + return predicates.hasBuilderWithdrawalCredential(validator); + } + @Override public Optional toVersionGloas() { return Optional.of(this); diff --git a/ethereum/spec/src/test/java/tech/pegasys/teku/spec/signatures/LocalSignerTest.java b/ethereum/spec/src/test/java/tech/pegasys/teku/spec/signatures/LocalSignerTest.java index a54de3559ef..808316624bc 100644 --- a/ethereum/spec/src/test/java/tech/pegasys/teku/spec/signatures/LocalSignerTest.java +++ b/ethereum/spec/src/test/java/tech/pegasys/teku/spec/signatures/LocalSignerTest.java @@ -225,7 +225,7 @@ public void shouldSignExecutionPayloadBid() { final BLSSignature expectedSignature = BLSSignature.fromBytesCompressed( Bytes.fromBase64String( - "jKXnVjb5vBjxXDb3SDaoU0nXkfZEvbHjtUlR5X4YQxpjECxhGCbuWDU736kvV5C2BUD7aGjk6iHMPv3qyr/YNcbnoyZczx5c9hKy5nLZAxbkQLIhsVUElFDn0TfolUy2")); + "uf0Y3WpY34xCgIPgpuAxf7WlHAcusR6Es3QyFJSpZCX3euwieDweuMUs7nRpFgL8D809lF72Pyr4lYbS7wrGFYuqUcOmFQzscW55zqODVW/iEZo6krNQUnv4DLOT12WC")); final SafeFuture result = signer.signExecutionPayloadBid(bid, fork); asyncRunner.executeQueuedActions(); diff --git a/ethereum/spec/src/testFixtures/java/tech/pegasys/teku/spec/util/DataStructureUtil.java b/ethereum/spec/src/testFixtures/java/tech/pegasys/teku/spec/util/DataStructureUtil.java index 3fdef261599..17907d4d0e8 100644 --- a/ethereum/spec/src/testFixtures/java/tech/pegasys/teku/spec/util/DataStructureUtil.java +++ b/ethereum/spec/src/testFixtures/java/tech/pegasys/teku/spec/util/DataStructureUtil.java @@ -3133,15 +3133,36 @@ public IndexedPayloadAttestation randomIndexedPayloadAttestation() { } public ExecutionPayloadBid randomExecutionPayloadBid() { - return randomExecutionPayloadBid(randomSlot(), randomBuilderIndex()); + return randomExecutionPayloadBid(randomUInt64()); + } + + public ExecutionPayloadBid randomExecutionPayloadBid(final UInt64 executionPayment) { + return randomExecutionPayloadBid( + randomBytes32(), randomSlot(), randomBuilderIndex(), randomUInt64(), executionPayment); } public ExecutionPayloadBid randomExecutionPayloadBid( final UInt64 slot, final UInt64 builderIndex) { + return randomExecutionPayloadBid( + randomBytes32(), slot, builderIndex, randomUInt64(), randomUInt64()); + } + + private ExecutionPayloadBid randomExecutionPayloadBid( + final UInt64 value, final Bytes32 parentBlockHash) { + return randomExecutionPayloadBid( + parentBlockHash, randomSlot(), randomBuilderIndex(), value, randomUInt64()); + } + + public ExecutionPayloadBid randomExecutionPayloadBid( + final Bytes32 parentBlockHash, + final UInt64 slot, + final UInt64 builderIndex, + final UInt64 value, + final UInt64 executionPayment) { return getGloasSchemaDefinitions() .getExecutionPayloadBidSchema() .create( - randomBytes32(), + parentBlockHash, randomBytes32(), randomBytes32(), randomBytes32(), @@ -3149,15 +3170,33 @@ public ExecutionPayloadBid randomExecutionPayloadBid( randomUInt64(), builderIndex, slot, - randomUInt64(), - randomUInt64(), + value, + executionPayment, randomBytes32()); } public SignedExecutionPayloadBid randomSignedExecutionPayloadBid() { + return randomSignedExecutionPayloadBid(randomUInt64()); + } + + public SignedExecutionPayloadBid randomSignedExecutionPayloadBid(final UInt64 executionPayment) { + return getGloasSchemaDefinitions() + .getSignedExecutionPayloadBidSchema() + .create(randomExecutionPayloadBid(executionPayment), randomSignature()); + } + + public SignedExecutionPayloadBid randomSignedExecutionPayloadBid( + final UInt64 value, final Bytes32 parentBlockHash) { + return getGloasSchemaDefinitions() + .getSignedExecutionPayloadBidSchema() + .create(randomExecutionPayloadBid(value, parentBlockHash), randomSignature()); + } + + public SignedExecutionPayloadBid randomSignedExecutionPayloadBid( + final ExecutionPayloadBid executionPayloadBid) { return getGloasSchemaDefinitions() .getSignedExecutionPayloadBidSchema() - .create(randomExecutionPayloadBid(), randomSignature()); + .create(executionPayloadBid, randomSignature()); } public ExecutionPayloadEnvelope randomExecutionPayloadEnvelope() { diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/execution/DefaultExecutionPayloadBidManager.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/execution/DefaultExecutionPayloadBidManager.java index 3d4bf5ac328..b42e9fa2f59 100644 --- a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/execution/DefaultExecutionPayloadBidManager.java +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/execution/DefaultExecutionPayloadBidManager.java @@ -31,6 +31,7 @@ import tech.pegasys.teku.spec.datastructures.execution.GetPayloadResponse; import tech.pegasys.teku.spec.datastructures.state.beaconstate.BeaconState; import tech.pegasys.teku.spec.schemas.SchemaDefinitionsGloas; +import tech.pegasys.teku.statetransition.validation.ExecutionPayloadBidGossipValidator; import tech.pegasys.teku.statetransition.validation.InternalValidationResult; public class DefaultExecutionPayloadBidManager implements ExecutionPayloadBidManager { @@ -38,16 +39,29 @@ public class DefaultExecutionPayloadBidManager implements ExecutionPayloadBidMan private static final Logger LOG = LogManager.getLogger(); private final Spec spec; + private final ExecutionPayloadBidGossipValidator executionPayloadBidGossipValidator; - public DefaultExecutionPayloadBidManager(final Spec spec) { + public DefaultExecutionPayloadBidManager( + final Spec spec, + final ExecutionPayloadBidGossipValidator executionPayloadBidGossipValidator) { this.spec = spec; + this.executionPayloadBidGossipValidator = executionPayloadBidGossipValidator; } - // TODO-GLOAS: https://github.com/Consensys/teku/issues/9960 (not required for devnet-0) @Override + @SuppressWarnings("FutureReturnValueIgnored") public SafeFuture validateAndAddBid( final SignedExecutionPayloadBid signedBid, final RemoteBidOrigin remoteBidOrigin) { - return SafeFuture.failedFuture(new UnsupportedOperationException("Not yet implemented")); + final SafeFuture validationResult = + executionPayloadBidGossipValidator.validate(signedBid); + validationResult.thenAccept( + result -> { + switch (result.code()) { + // TODO-GLOAS handle bids + case ACCEPT, REJECT, SAVE_FOR_FUTURE, IGNORE -> {} + } + }); + return validationResult; } @Override 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 new file mode 100644 index 00000000000..db616630bde --- /dev/null +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/ExecutionPayloadBidGossipValidator.java @@ -0,0 +1,221 @@ +/* + * Copyright Consensys Software Inc., 2025 + * + * 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.validation; + +import static tech.pegasys.teku.infrastructure.async.SafeFuture.completedFuture; +import static tech.pegasys.teku.infrastructure.unsigned.UInt64.ZERO; +import static tech.pegasys.teku.spec.config.Constants.HIGHEST_BID_SET_SIZE; +import static tech.pegasys.teku.spec.config.Constants.SEEN_EXECUTION_PAYLOAD_BID_SET_SIZE; +import static tech.pegasys.teku.statetransition.validation.InternalValidationResult.ACCEPT; +import static tech.pegasys.teku.statetransition.validation.InternalValidationResult.SAVE_FOR_FUTURE; +import static tech.pegasys.teku.statetransition.validation.InternalValidationResult.ignore; +import static tech.pegasys.teku.statetransition.validation.InternalValidationResult.reject; + +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import tech.pegasys.teku.infrastructure.async.SafeFuture; +import tech.pegasys.teku.infrastructure.collections.LimitedMap; +import tech.pegasys.teku.infrastructure.collections.LimitedSet; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.Spec; +import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.ExecutionPayloadBid; +import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadBid; +import tech.pegasys.teku.spec.datastructures.state.beaconstate.BeaconState; +import tech.pegasys.teku.spec.signatures.SigningRootUtil; + +public class ExecutionPayloadBidGossipValidator { + + private static final Logger LOG = LogManager.getLogger(); + final GossipValidationHelper gossipValidationHelper; + final SigningRootUtil signingRootUtil; + + private final Set seenExecutionPayloadBids = + LimitedSet.createSynchronized(SEEN_EXECUTION_PAYLOAD_BID_SET_SIZE); + private final Map highestBids = + LimitedMap.createSynchronizedLRU(HIGHEST_BID_SET_SIZE); + + public ExecutionPayloadBidGossipValidator( + final Spec spec, final GossipValidationHelper gossipValidationHelper) { + this.gossipValidationHelper = gossipValidationHelper; + signingRootUtil = new SigningRootUtil(spec); + } + + public SafeFuture validate( + final SignedExecutionPayloadBid signedExecutionPayloadBid) { + final ExecutionPayloadBid bid = signedExecutionPayloadBid.getMessage(); + + /* + * [REJECT] bid.execution_payment is zero. + */ + final UInt64 executionPayment = bid.getExecutionPayment(); + if (!executionPayment.isZero()) { + LOG.trace("Bid's execution payment should be 0 but was {}", executionPayment); + return completedFuture( + reject("Bid's execution payment should be 0 but was %s", executionPayment)); + } + + /* + * [IGNORE] this is the first signed bid seen with a valid signature from the given builder for this slot. + */ + final BuilderIndexAndSlot key = new BuilderIndexAndSlot(bid.getBuilderIndex(), bid.getSlot()); + if (seenExecutionPayloadBids.contains(key)) { + return completedFuture( + ignore( + "Already received a bid from builder with index %s at slot %s", + bid.getBuilderIndex(), bid.getSlot())); + } + + /* + * [IGNORE] this bid is the highest value bid seen for the corresponding slot and the given parent block hash. + */ + final SlotAndBlockHash bidValueKey = + new SlotAndBlockHash(bid.getSlot(), bid.getParentBlockHash()); + if (highestBids.containsKey(bidValueKey)) { + final UInt64 existingBidValue = highestBids.getOrDefault(bidValueKey, ZERO); + if (bid.getValue().isLessThan(existingBidValue)) { + LOG.trace( + "Already received a bid with a higher value {} for block with parent hash {}. Current bid's value is {}", + existingBidValue, + bid.getParentBlockHash(), + bid.getValue()); + return completedFuture( + ignore( + "Already received a bid with a higher value %s for block with parent hash %s. Current bid's value is %s", + existingBidValue, bid.getParentBlockHash(), bid.getValue())); + } + } + + /* + * [IGNORE] bid.slot is the current slot or the next slot. + */ + + if (!gossipValidationHelper.isSlotCurrentOrNext(bid.getSlot())) { + LOG.trace("Bid must be for current or next slot but was for slot {}", bid.getSlot()); + return completedFuture( + ignore("Bid must be for current or next slot but was for slot %s", bid.getSlot())); + } + + /* + * [IGNORE] bid.parent_block_hash is the block hash of a known execution payload in fork choice. + */ + if (!gossipValidationHelper.isBlockHashKnown( + bid.getParentBlockHash(), bid.getParentBlockRoot())) { + LOG.trace( + "Bid's parent block hash {} is not the block hash of a known execution payload in fork choice. It will be saved for future processing", + bid.getParentBlockHash()); + return completedFuture(SAVE_FOR_FUTURE); + } + + /* + * [IGNORE] bid.parent_block_root is the hash tree root of a known beacon block in fork choice. + */ + final Optional maybeParentBlockSlot = + gossipValidationHelper.getSlotForBlockRoot(bid.getParentBlockRoot()); + if (maybeParentBlockSlot.isEmpty()) { + LOG.trace("Bid's parent block does not exist. It will be saved for future processing"); + return completedFuture(SAVE_FOR_FUTURE); + } + final UInt64 parentBlockSlot = maybeParentBlockSlot.get(); + + return gossipValidationHelper + .getParentStateInBlockEpoch(parentBlockSlot, bid.getParentBlockRoot(), bid.getSlot()) + .thenApply( + maybeState -> { + if (maybeState.isEmpty()) { + LOG.trace( + "State for block root {} and slot {} is unavailable.", + bid.getParentBlockRoot(), + bid.getSlot()); + return SAVE_FOR_FUTURE; + } + final BeaconState state = maybeState.get(); + + /* + * [REJECT] bid.builder_index is a valid, active, and non-slashed builder index. + */ + + final UInt64 buildrIndex = bid.getBuilderIndex(); + if (!gossipValidationHelper.isValidBuilderIndex(buildrIndex, state, bid.getSlot())) { + LOG.trace( + "Invalid builder index {}. Builder should be valid, active and non-slashed.", + buildrIndex); + return reject( + "Invalid builder index %s. Builder should be valid, active and non-slashed.", + buildrIndex); + } + + /* + * [REJECT] the builder's withdrawal credentials' prefix is BUILDER_WITHDRAWAL_PREFIX + * -- i.e. is_builder_withdrawal_credential(state.validators[bid.builder_index].withdrawal_credentials) + * returns True. + */ + if (!gossipValidationHelper.hasBuilderWithdrawalCredential( + bid.getBuilderIndex(), state, bid.getSlot())) { + LOG.trace( + "Builder with index {} must have builder withdrawal credential", + bid.getBuilderIndex()); + return reject( + "Builder with index %s must have builder withdrawal credential", + bid.getBuilderIndex()); + } + + /* + * [IGNORE] bid.value is less or equal than the builder's excess balance + * -- i.e. MIN_ACTIVATION_BALANCE + bid.value <= state.balances[bid.builder_index]. + */ + if (!gossipValidationHelper.builderHasEnoughBalanceForBid( + bid.getValue(), bid.getBuilderIndex(), state, bid.getSlot())) { + LOG.trace( + "Bid value {} exceeds builder with index {} excess balance", + bid.getValue(), + bid.getBuilderIndex()); + return ignore( + "Bid value %s exceeds builder with index %s excess balance", + bid.getValue(), bid.getBuilderIndex()); + } + + /* + * [REJECT] signed_execution_payload_bid.signature is valid with respect to the bid.builder_index. + */ + if (!isSignatureValid(signedExecutionPayloadBid, state)) { + LOG.trace("Invalid payload execution bid signature"); + return reject("Invalid payload execution bid signature"); + } + highestBids.merge(bidValueKey, bid.getValue(), UInt64::max); + seenExecutionPayloadBids.add(key); + return ACCEPT; + }); + } + + private boolean isSignatureValid( + final SignedExecutionPayloadBid signedExecutionPayloadBid, final BeaconState state) { + final Bytes signingRoot = + signingRootUtil.signingRootForSignExecutionPayloadBid( + signedExecutionPayloadBid.getMessage(), state.getForkInfo()); + return gossipValidationHelper.isSignatureValidWithRespectToBuilderIndex( + signingRoot, + signedExecutionPayloadBid.getMessage().getBuilderIndex(), + signedExecutionPayloadBid.getSignature(), + state); + } + + record BuilderIndexAndSlot(UInt64 validatorIndex, UInt64 slot) {} + + record SlotAndBlockHash(UInt64 slot, Bytes32 blockHash) {} +} 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 71d57492860..e3d5732e707 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 @@ -13,6 +13,8 @@ package tech.pegasys.teku.statetransition.validation; +import static tech.pegasys.teku.infrastructure.unsigned.UInt64.ONE; + import java.util.Optional; import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes32; @@ -22,11 +24,14 @@ import tech.pegasys.teku.infrastructure.async.SafeFuture; import tech.pegasys.teku.infrastructure.unsigned.UInt64; import tech.pegasys.teku.spec.Spec; +import tech.pegasys.teku.spec.config.SpecConfigGloas; import tech.pegasys.teku.spec.datastructures.blocks.SlotAndBlockRoot; import tech.pegasys.teku.spec.datastructures.forkchoice.ReadOnlyForkChoiceStrategy; import tech.pegasys.teku.spec.datastructures.forkchoice.ReadOnlyStore; import tech.pegasys.teku.spec.datastructures.operations.AttestationData; +import tech.pegasys.teku.spec.datastructures.state.Validator; import tech.pegasys.teku.spec.datastructures.state.beaconstate.BeaconState; +import tech.pegasys.teku.spec.logic.versions.gloas.helpers.MiscHelpersGloas; import tech.pegasys.teku.storage.client.RecentChainData; public class GossipValidationHelper { @@ -59,13 +64,28 @@ public boolean isSlotFromFuture(final UInt64 slot) { return slot.isGreaterThan(maxCurrSlot); } + public boolean isSignatureValidWithRespectToBuilderIndex( + final Bytes signingRoot, + final UInt64 builderIndex, + final BLSSignature signature, + final BeaconState state) { + return isSignatureValidForIndex(signingRoot, builderIndex, signature, state); + } + public boolean isSignatureValidWithRespectToProposerIndex( final Bytes signingRoot, final UInt64 proposerIndex, final BLSSignature signature, - final BeaconState postState) { + final BeaconState state) { + return isSignatureValidForIndex(signingRoot, proposerIndex, signature, state); + } - return spec.getValidatorPubKey(postState, proposerIndex) + private boolean isSignatureValidForIndex( + final Bytes signingRoot, + final UInt64 validatorIndex, + final BLSSignature signature, + final BeaconState state) { + return spec.getValidatorPubKey(state, validatorIndex) .map(publicKey -> BLS.verify(publicKey, signingRoot, signature)) .orElse(false); } @@ -128,15 +148,32 @@ public ReadOnlyForkChoiceStrategy getForkChoiceStrategy() { return recentChainData.getForkChoiceStrategy().orElseThrow(); } + public boolean isValidBuilderIndex( + final UInt64 builderIndex, final BeaconState state, final UInt64 slot) { + final int index = builderIndex.intValue(); + if (index >= state.getValidators().size()) { + return false; + } + final Validator builder = state.getValidators().get(index); + final boolean isActiveBuilder = + spec.getActiveValidatorIndices(state, spec.computeEpochAtSlot(slot)) + .contains(builderIndex.intValue()); + return !builder.isSlashed() && isActiveBuilder; + } + public SafeFuture> getStateAtSlotAndBlockRoot( final SlotAndBlockRoot slotAndBlockRoot) { return recentChainData.retrieveStateAtSlot(slotAndBlockRoot); } public boolean isCurrentSlotWithGossipDisparityAllowance(final UInt64 slot) { - final int maximumGossipClockDisparityMillis = - spec.getNetworkingConfig().getMaximumGossipClockDisparity(); - return isTimeWithinSlotWindow(slot, maximumGossipClockDisparityMillis); + return isTimeWithinSlotWindow(slot, maxOffsetTimeInMillis); + } + + public boolean isSlotCurrentOrNext(final UInt64 slot) { + return getCurrentSlot() + .map(currentSlot -> slot.equals(currentSlot) || slot.equals(currentSlot.plus(ONE))) + .orElse(false); } public boolean isValidatorInPayloadTimelinessCommittee( @@ -144,8 +181,33 @@ public boolean isValidatorInPayloadTimelinessCommittee( return spec.getPtc(state, slot).contains(validatorIndex.intValue()); } + public boolean hasBuilderWithdrawalCredential( + final UInt64 builderIndex, final BeaconState state, final UInt64 slot) { + return MiscHelpersGloas.required(spec.atSlot(slot).miscHelpers()) + .hasBuilderWithdrawalCredential(state.getValidators().get(builderIndex.intValue())); + } + + public boolean builderHasEnoughBalanceForBid( + final UInt64 value, final UInt64 builderIndex, final BeaconState state, final UInt64 slot) { + final UInt64 builderBalance = state.getBalances().get(builderIndex.intValue()).get(); + final UInt64 minActivationBalance = + SpecConfigGloas.required(spec.atSlot(slot).getConfig()).getMinActivationBalance(); + return minActivationBalance.plus(value).isLessThanOrEqualTo(builderBalance); + } + + public boolean isBlockHashKnown(final Bytes32 blockHash, final Bytes32 blockRoot) { + // TODO-GLOAS check this logic. We might need to check the block hash existence in the store + final Optional maybeBlockHash = + recentChainData.getExecutionBlockHashForBlockRoot(blockRoot); + return maybeBlockHash.isPresent() && blockHash.equals(maybeBlockHash.get()); + } + + private Optional getCurrentSlot() { + return recentChainData.getCurrentSlot(); + } + private boolean isTimeWithinSlotWindow(final UInt64 slot, final int disparity) { - if (recentChainData.getCurrentSlot().isEmpty()) { + if (getCurrentSlot().isEmpty()) { return false; } final UInt64 slotStartTimeMillis = diff --git a/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/execution/DefaultExecutionPayloadBidManagerTest.java b/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/execution/DefaultExecutionPayloadBidManagerTest.java index 5931d0ab53d..55de6ca1a1c 100644 --- a/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/execution/DefaultExecutionPayloadBidManagerTest.java +++ b/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/execution/DefaultExecutionPayloadBidManagerTest.java @@ -35,6 +35,7 @@ import tech.pegasys.teku.spec.datastructures.state.beaconstate.BeaconState; import tech.pegasys.teku.spec.schemas.SchemaDefinitionsGloas; import tech.pegasys.teku.spec.util.DataStructureUtil; +import tech.pegasys.teku.statetransition.validation.ExecutionPayloadBidGossipValidator; public class DefaultExecutionPayloadBidManagerTest { @@ -44,8 +45,11 @@ public class DefaultExecutionPayloadBidManagerTest { private final BlockProductionPerformance blockProductionPerformance = mock(BlockProductionPerformance.class); + private final ExecutionPayloadBidGossipValidator executionPayloadBidGossipValidator = + mock(ExecutionPayloadBidGossipValidator.class); + private final DefaultExecutionPayloadBidManager executionPayloadBidManager = - new DefaultExecutionPayloadBidManager(spec); + new DefaultExecutionPayloadBidManager(spec, executionPayloadBidGossipValidator); @Test public void createsLocalBidForBlock() { 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 new file mode 100644 index 00000000000..b066d9bfb1f --- /dev/null +++ b/ethereum/statetransition/src/test/java/tech/pegasys/teku/statetransition/validation/ExecutionPayloadBidGossipValidatorTest.java @@ -0,0 +1,333 @@ +/* + * Copyright Consensys Software Inc., 2025 + * + * 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.validation; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static tech.pegasys.teku.infrastructure.async.SafeFutureAssert.assertThatSafeFuture; +import static tech.pegasys.teku.statetransition.validation.InternalValidationResult.ACCEPT; +import static tech.pegasys.teku.statetransition.validation.InternalValidationResult.SAVE_FOR_FUTURE; +import static tech.pegasys.teku.statetransition.validation.InternalValidationResult.ignore; +import static tech.pegasys.teku.statetransition.validation.InternalValidationResult.reject; + +import it.unimi.dsi.fastutil.ints.IntList; +import java.util.Optional; +import org.apache.tuweni.bytes.Bytes32; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestTemplate; +import tech.pegasys.teku.infrastructure.async.SafeFuture; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.Spec; +import tech.pegasys.teku.spec.SpecMilestone; +import tech.pegasys.teku.spec.SpecVersion; +import tech.pegasys.teku.spec.TestSpecContext; +import tech.pegasys.teku.spec.TestSpecInvocationContextProvider; +import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.ExecutionPayloadBid; +import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadBid; +import tech.pegasys.teku.spec.datastructures.state.beaconstate.BeaconState; +import tech.pegasys.teku.spec.logic.common.helpers.MiscHelpers; +import tech.pegasys.teku.spec.util.DataStructureUtil; + +@TestSpecContext(milestone = {SpecMilestone.GLOAS}) +public class ExecutionPayloadBidGossipValidatorTest { + private final Spec spec = mock(Spec.class); + private final GossipValidationHelper gossipValidationHelper = mock(GossipValidationHelper.class); + private ExecutionPayloadBidGossipValidator bidValidator; + private DataStructureUtil dataStructureUtil; + private SignedExecutionPayloadBid signedBid; + private ExecutionPayloadBid bid; + private UInt64 slot; + private UInt64 builderIndex; + private Bytes32 parentBlockRoot; + private Bytes32 parentBlockHash; + private BeaconState postState; + + @BeforeEach + void setup(final TestSpecInvocationContextProvider.SpecContext specContext) { + this.dataStructureUtil = specContext.getDataStructureUtil(); + this.bidValidator = new ExecutionPayloadBidGossipValidator(spec, gossipValidationHelper); + + signedBid = dataStructureUtil.randomSignedExecutionPayloadBid(UInt64.ZERO); + bid = signedBid.getMessage(); + slot = bid.getSlot(); + builderIndex = bid.getBuilderIndex(); + parentBlockRoot = bid.getParentBlockRoot(); + parentBlockHash = bid.getParentBlockHash(); + postState = dataStructureUtil.randomBeaconState(); + + when(gossipValidationHelper.isSlotCurrentOrNext(slot)).thenReturn(true); + when(gossipValidationHelper.isBlockHashKnown(parentBlockHash, parentBlockRoot)) + .thenReturn(true); + when(gossipValidationHelper.getSlotForBlockRoot(parentBlockRoot)) + .thenReturn(Optional.of(slot.decrement())); + when(gossipValidationHelper.getParentStateInBlockEpoch(slot.decrement(), parentBlockRoot, slot)) + .thenReturn(SafeFuture.completedFuture(Optional.of(postState))); + when(gossipValidationHelper.isValidBuilderIndex(builderIndex, postState, slot)) + .thenReturn(true); + when(gossipValidationHelper.hasBuilderWithdrawalCredential(builderIndex, postState, slot)) + .thenReturn(true); + when(gossipValidationHelper.builderHasEnoughBalanceForBid( + bid.getValue(), builderIndex, postState, slot)) + .thenReturn(true); + when(gossipValidationHelper.isSignatureValidWithRespectToBuilderIndex( + any(), any(), any(), any())) + .thenReturn(true); + final SpecVersion specVersion = mock(SpecVersion.class); + final MiscHelpers miscHelpers = mock(MiscHelpers.class); + final Bytes32 signingRoot = Bytes32.random(); + when(miscHelpers.computeSigningRoot(eq(signedBid.getMessage()), any())).thenReturn(signingRoot); + when(specVersion.miscHelpers()).thenReturn(miscHelpers); + when(spec.atSlot(slot)).thenReturn(specVersion); + when(spec.getActiveValidatorIndices(postState, slot)) + .thenReturn(IntList.of(builderIndex.intValue())); + } + + @TestTemplate + void shouldAccept() { + assertThatSafeFuture(bidValidator.validate(signedBid)).isCompletedWithValue(ACCEPT); + } + + @TestTemplate + void shouldReject_whenExecutionPaymentIsNonZero() { + final SignedExecutionPayloadBid bidWithPayment = + dataStructureUtil.randomSignedExecutionPayloadBid(UInt64.ONE); + assertThatSafeFuture(bidValidator.validate(bidWithPayment)) + .isCompletedWithValue( + reject( + "Bid's execution payment should be 0 but was %s", + bidWithPayment.getMessage().getExecutionPayment())); + } + + @TestTemplate + void shouldIgnore_whenSlotIsNotCurrentOrNext() { + when(gossipValidationHelper.isSlotCurrentOrNext(slot)).thenReturn(false); + assertThatSafeFuture(bidValidator.validate(signedBid)) + .isCompletedWithValue( + ignore("Bid must be for current or next slot but was for slot %s", slot)); + } + + @TestTemplate + void shouldIgnore_whenAlreadySeen() { + assertThatSafeFuture(bidValidator.validate(signedBid)).isCompletedWithValue(ACCEPT); + assertThatSafeFuture(bidValidator.validate(signedBid)) + .isCompletedWithValue( + ignore( + "Already received a bid from builder with index %s at slot %s", + builderIndex, slot)); + } + + @TestTemplate + void shouldIgnore_whenBidValueIsLowerThanSeen() { + // a higher value bid is accepted + assertThatSafeFuture(bidValidator.validate(signedBid)).isCompletedWithValue(ACCEPT); + + // a lower value bid from a different builder with the same parent block hash and slot + final UInt64 lowerBidValue = bid.getValue().minus(1); + final SignedExecutionPayloadBid lowerValueBid = + dataStructureUtil.randomSignedExecutionPayloadBid( + dataStructureUtil.randomExecutionPayloadBid( + parentBlockHash, + slot, + builderIndex.plus(1), + lowerBidValue, + bid.getExecutionPayment())); + + assertThatSafeFuture(bidValidator.validate(lowerValueBid)) + .isCompletedWithValue( + ignore( + "Already received a bid with a higher value %s for block with parent hash %s. Current bid's value is %s", + bid.getValue(), parentBlockHash, lowerBidValue)); + } + + @TestTemplate + void shouldNotCacheHigherBidIfInvalid() { + // valid, lower value bid is accepted and cached + final UInt64 lowerValue = signedBid.getMessage().getValue(); + assertThatSafeFuture(bidValidator.validate(signedBid)).isCompletedWithValue(ACCEPT); + + // modified copy of the original bid with a higher value and different builder index + final UInt64 higherValue = lowerValue.plus(10); + final UInt64 differentBuilderIndex = builderIndex.plus(1); + final ExecutionPayloadBid originalBidMessage = signedBid.getMessage(); + final ExecutionPayloadBid higherValueBidMessage = + originalBidMessage + .getSchema() + .create( + originalBidMessage.getParentBlockHash(), + originalBidMessage.getParentBlockRoot(), + originalBidMessage.getBlockHash(), + originalBidMessage.getPrevRandao(), + originalBidMessage.getFeeRecipient(), + originalBidMessage.getGasLimit(), + differentBuilderIndex, + originalBidMessage.getSlot(), + higherValue, + originalBidMessage.getExecutionPayment(), + originalBidMessage.getBlobKzgCommitmentsRoot()); + + final SignedExecutionPayloadBid higherValueInvalidBid = + dataStructureUtil.randomSignedExecutionPayloadBid(higherValueBidMessage); + + when(gossipValidationHelper.isValidBuilderIndex(differentBuilderIndex, postState, slot)) + .thenReturn(true); + when(gossipValidationHelper.builderHasEnoughBalanceForBid( + higherValue, differentBuilderIndex, postState, slot)) + .thenReturn(true); + when(gossipValidationHelper.hasBuilderWithdrawalCredential( + differentBuilderIndex, postState, slot)) + .thenReturn(true); + // bad signature to make it fail validation + when(gossipValidationHelper.isSignatureValidWithRespectToBuilderIndex( + any(), eq(higherValueInvalidBid.getMessage().getBuilderIndex()), any(), any())) + .thenReturn(false); + // invalid, higher value bid is rejected + assertThatSafeFuture(bidValidator.validate(higherValueInvalidBid)) + .isCompletedWithValue(reject("Invalid payload execution bid signature")); + + // valid, intermediate value bid with a higher value that the initially cached one + final UInt64 intermediateValue = lowerValue.plus(5); + final UInt64 intermediateBidBuilderIndex = builderIndex.plus(2); + final SignedExecutionPayloadBid intermediateValueValidBid = + dataStructureUtil.randomSignedExecutionPayloadBid( + dataStructureUtil.randomExecutionPayloadBid( + parentBlockHash, + slot, + intermediateBidBuilderIndex, + intermediateValue, + UInt64.ZERO)); + + when(gossipValidationHelper.isBlockHashKnown( + parentBlockHash, intermediateValueValidBid.getMessage().getParentBlockRoot())) + .thenReturn(true); + when(gossipValidationHelper.isValidBuilderIndex(intermediateBidBuilderIndex, postState, slot)) + .thenReturn(true); + when(gossipValidationHelper.getSlotForBlockRoot( + intermediateValueValidBid.getMessage().getParentBlockRoot())) + .thenReturn(Optional.of(slot)); + when(gossipValidationHelper.builderHasEnoughBalanceForBid( + intermediateValue, intermediateBidBuilderIndex, postState, slot)) + .thenReturn(true); + when(gossipValidationHelper.getParentStateInBlockEpoch( + slot, intermediateValueValidBid.getMessage().getParentBlockRoot(), slot)) + .thenReturn(SafeFuture.completedFuture(Optional.of(postState))); + when(gossipValidationHelper.hasBuilderWithdrawalCredential( + intermediateBidBuilderIndex, postState, slot)) + .thenReturn(true); + when(gossipValidationHelper.isSignatureValidWithRespectToBuilderIndex( + any(), eq(intermediateValueValidBid.getMessage().getBuilderIndex()), any(), any())) + .thenReturn(true); + + // the intermediate value bid is accepted + assertThatSafeFuture(bidValidator.validate(intermediateValueValidBid)) + .isCompletedWithValue(ACCEPT); + } + + @TestTemplate + void shouldSaveForFuture_whenParentBlockHashIsUnknown() { + when(gossipValidationHelper.isBlockHashKnown(parentBlockHash, parentBlockRoot)) + .thenReturn(false); + assertThatSafeFuture(bidValidator.validate(signedBid)).isCompletedWithValue(SAVE_FOR_FUTURE); + } + + @TestTemplate + void shouldSaveForFuture_whenParentBlockIsNotAvailable() { + when(gossipValidationHelper.getSlotForBlockRoot(parentBlockRoot)).thenReturn(Optional.empty()); + assertThatSafeFuture(bidValidator.validate(signedBid)).isCompletedWithValue(SAVE_FOR_FUTURE); + } + + @TestTemplate + void shouldSaveForFuture_whenStateIsUnavailable() { + when(gossipValidationHelper.getParentStateInBlockEpoch(slot.decrement(), parentBlockRoot, slot)) + .thenReturn(SafeFuture.completedFuture(Optional.empty())); + assertThatSafeFuture(bidValidator.validate(signedBid)).isCompletedWithValue(SAVE_FOR_FUTURE); + } + + @TestTemplate + void shouldReject_whenBuilderIndexIsInvalid() { + when(gossipValidationHelper.isValidBuilderIndex(builderIndex, postState, slot)) + .thenReturn(false); + assertThatSafeFuture(bidValidator.validate(signedBid)) + .isCompletedWithValue( + reject( + "Invalid builder index %s. Builder should be valid, active and non-slashed.", + builderIndex)); + } + + @TestTemplate + void shouldReject_whenBuilderLacksBuilderWithdrawalCredential() { + when(gossipValidationHelper.hasBuilderWithdrawalCredential(builderIndex, postState, slot)) + .thenReturn(false); + assertThatSafeFuture(bidValidator.validate(signedBid)) + .isCompletedWithValue( + reject("Builder with index %s must have builder withdrawal credential", builderIndex)); + } + + @TestTemplate + void shouldIgnore_whenBuilderHasInsufficientBalance() { + when(gossipValidationHelper.builderHasEnoughBalanceForBid( + bid.getValue(), builderIndex, postState, slot)) + .thenReturn(false); + assertThatSafeFuture(bidValidator.validate(signedBid)) + .isCompletedWithValue( + ignore( + "Bid value %s exceeds builder with index %s excess balance", + bid.getValue(), builderIndex)); + } + + @TestTemplate + void shouldReject_whenSignatureIsInvalid() { + when(gossipValidationHelper.isSignatureValidWithRespectToBuilderIndex( + any(), any(), any(), any())) + .thenReturn(false); + assertThatSafeFuture(bidValidator.validate(signedBid)) + .isCompletedWithValue(reject("Invalid payload execution bid signature")); + } + + @TestTemplate + void shouldIgnoreSeenBid() { + assertThatSafeFuture(bidValidator.validate(signedBid)).isCompletedWithValue(ACCEPT); + verify(gossipValidationHelper).isBlockHashKnown(parentBlockHash, parentBlockRoot); + verify(gossipValidationHelper).getParentStateInBlockEpoch(any(), any(), any()); + verify(gossipValidationHelper) + .isSignatureValidWithRespectToBuilderIndex(any(), any(), any(), any()); + clearInvocations(gossipValidationHelper); + + assertThatSafeFuture(bidValidator.validate(signedBid)) + .isCompletedWithValue( + ignore( + "Already received a bid from builder with index %s at slot %s", + builderIndex, slot)); + } + + @TestTemplate + void shouldNotMarkAsSeenIfValidationFails() { + when(gossipValidationHelper.isSignatureValidWithRespectToBuilderIndex( + any(), any(), any(), any())) + .thenReturn(false); + assertThatSafeFuture(bidValidator.validate(signedBid)) + .isCompletedWithValue(reject("Invalid payload execution bid signature")); + + // Fix the signature mock and try again + when(gossipValidationHelper.isSignatureValidWithRespectToBuilderIndex( + any(), any(), any(), any())) + .thenReturn(true); + + // It should be accepted now, not ignored + assertThatSafeFuture(bidValidator.validate(signedBid)).isCompletedWithValue(ACCEPT); + } +} 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 3e98b2eea75..4c72af5b48c 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 @@ -312,36 +312,26 @@ void currentFinalizedCheckpointIsAncestorOfBlock_shouldReturnValid() { } @TestTemplate - void isForCurrentSlot_shouldRejectOutsideLowerBound() { + void isForCurrentSlotWithGossipDisparityAllowance() { final UInt64 slot = UInt64.valueOf(1000); final UInt64 slotStartTimeMillis = getSlotStartTimeMillis(slot); - final UInt64 currentTime = slotStartTimeMillis.minus(maximumGossipClockDisparity).decrement(); - assertIsCurrentSlot(slot, currentTime, false); - } - - @TestTemplate - void isForCurrentSlot_shouldAcceptLowerBound() { - final UInt64 slot = UInt64.valueOf(1000); - final UInt64 slotStartTimeMillis = getSlotStartTimeMillis(slot); - final UInt64 currentTime = slotStartTimeMillis.minus(maximumGossipClockDisparity); - assertIsCurrentSlot(slot, currentTime, true); - } - - @TestTemplate - void isForCurrentSlot_shouldAcceptUpperBound() { - final UInt64 slot = UInt64.valueOf(1000); + assertIsCurrentSlot( + slot, slotStartTimeMillis.minus(maximumGossipClockDisparity).decrement(), false); + assertIsCurrentSlot(slot, slotStartTimeMillis.minus(maximumGossipClockDisparity), true); final UInt64 nextSlotStartTimeMillis = getSlotStartTimeMillis(slot.increment()); - final UInt64 currentTime = nextSlotStartTimeMillis.plus(maximumGossipClockDisparity); - assertIsCurrentSlot(slot, currentTime, true); + assertIsCurrentSlot(slot, nextSlotStartTimeMillis.plus(maximumGossipClockDisparity), true); + assertIsCurrentSlot( + slot, nextSlotStartTimeMillis.plus(maximumGossipClockDisparity).increment(), false); } @TestTemplate - void isForCurrentSlot_shouldRejectOutsideUpperBound() { - final UInt64 slot = UInt64.valueOf(1000); - final UInt64 nextSlotStartTimeMillis = getSlotStartTimeMillis(slot.increment()); - final UInt64 currentTime = - nextSlotStartTimeMillis.plus(maximumGossipClockDisparity).increment(); - assertIsCurrentSlot(slot, currentTime, false); + void isCurrentOrNextSlot() { + final UInt64 currentSlot = UInt64.valueOf(10); + storageSystem.chainUpdater().setCurrentSlot(currentSlot); + assertThat(gossipValidationHelper.isSlotCurrentOrNext(currentSlot)).isTrue(); + assertThat(gossipValidationHelper.isSlotCurrentOrNext(currentSlot.plus(ONE))).isTrue(); + assertThat(gossipValidationHelper.isSlotCurrentOrNext(currentSlot.minus(ONE))).isFalse(); + assertThat(gossipValidationHelper.isSlotCurrentOrNext(currentSlot.plus(2))).isFalse(); } private UInt64 getSlotStartTimeMillis(final UInt64 slot) { diff --git a/services/beaconchain/src/main/java/tech/pegasys/teku/services/beaconchain/BeaconChainController.java b/services/beaconchain/src/main/java/tech/pegasys/teku/services/beaconchain/BeaconChainController.java index 190403ca71a..17f456aec2a 100644 --- a/services/beaconchain/src/main/java/tech/pegasys/teku/services/beaconchain/BeaconChainController.java +++ b/services/beaconchain/src/main/java/tech/pegasys/teku/services/beaconchain/BeaconChainController.java @@ -233,6 +233,7 @@ import tech.pegasys.teku.statetransition.validation.BlockGossipValidator; import tech.pegasys.teku.statetransition.validation.BlockValidator; import tech.pegasys.teku.statetransition.validation.DataColumnSidecarGossipValidator; +import tech.pegasys.teku.statetransition.validation.ExecutionPayloadBidGossipValidator; import tech.pegasys.teku.statetransition.validation.ExecutionPayloadGossipValidator; import tech.pegasys.teku.statetransition.validation.ExecutionProofGossipValidator; import tech.pegasys.teku.statetransition.validation.GossipValidationHelper; @@ -867,7 +868,10 @@ protected void initDataColumnSidecarManager() { protected void initExecutionPayloadBidManager() { if (spec.isMilestoneSupported(SpecMilestone.GLOAS)) { - executionPayloadBidManager = new DefaultExecutionPayloadBidManager(spec); + final ExecutionPayloadBidGossipValidator executionPayloadBidGossipValidator = + new ExecutionPayloadBidGossipValidator(spec, gossipValidationHelper); + executionPayloadBidManager = + new DefaultExecutionPayloadBidManager(spec, executionPayloadBidGossipValidator); } else { executionPayloadBidManager = ExecutionPayloadBidManager.NOOP; } diff --git a/validator/remote/src/integration-test/resources/responses/produce_block_responses/newBlockGLOAS.json b/validator/remote/src/integration-test/resources/responses/produce_block_responses/newBlockGLOAS.json index e69a298b891..9d471caa7ec 100644 --- a/validator/remote/src/integration-test/resources/responses/produce_block_responses/newBlockGLOAS.json +++ b/validator/remote/src/integration-test/resources/responses/produce_block_responses/newBlockGLOAS.json @@ -332,16 +332,16 @@ ], "signed_execution_payload_bid": { "message": { - "parent_block_hash": "0x0c15f142b0175c6e8491002de9596cba91433934b44834491c80e04920a257ea", - "parent_block_root": "0xd3b10243d034be9fa5c8caaa6ebf2e537e3a3c7e91eeb91a93fc15f1917f314a", - "block_hash": "0xe6d2fc4270809de49a0b32d641484320853d3b1047b7e2d4c07d59b96ce0e8d4", - "prev_randao": "0x4678df4290faf93c645a36af63f4a921a44c36ead7a2ae77a503aba2afc47d8a", - "fee_recipient": "0x5999d9423046d981599d9dda377dbeeeaa4f357c", - "gas_limit": "4822007335809147728", - "builder_index": "740284", - "slot": "1310451960", - "value": "4820354852592463600", - "execution_payment": "4759212943510379790", + "parent_block_hash": "0xf9f3f64210cc7c298f4e990115d157ed8b403aa2fe7f0b8feefe9c814641a05f", + "parent_block_root": "0x4678df4290faf93c645a36af63f4a921a44c36ead7a2ae77a503aba2afc47d8a", + "block_hash": "0x5999d9423046d981599d9dda377dbeeeaa4f357c8d6bd731d384ee6a8a253515", + "prev_randao": "0x2036eb4250633bb379d46758bce28087974638c66a115d034a012412fb020f75", + "fee_recipient": "0x3357e542f0ae1af86f17cf83906b95549e493758", + "gas_limit": "4759212943510379790", + "builder_index": "1125033", + "slot": "21512915928", + "value": "4826964785459200112", + "execution_payment": "4833574722620903920", "blob_kzg_commitments_root": "0x08400642aee83f31d60723f5fabaa1c58bbc110432a5935f43af6c943fc4fe96" }, "signature": "0xb58eaaba3ba51d7098d65fbec3829ace78576a2276fd9c97c293aabdb634a2c50f52611f48088da5d4a5b5fa2c5f4c0513d8dd91c8534b50a7b8ae0072583612610ada0c81a261641c66ac542428cedf20f1b954ad03505fc058b40ce0bf4182"