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 62ff2cd1935..5f877f5f213 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 @@ -41,6 +41,8 @@ import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.ExecutionPayloadBid; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.ProposerPreferences; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadBid; +import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadEnvelope; +import tech.pegasys.teku.spec.datastructures.execution.versions.gloas.ExecutionRequestsGloas; import tech.pegasys.teku.spec.datastructures.state.beaconstate.BeaconState; import tech.pegasys.teku.spec.datastructures.state.beaconstate.versions.gloas.BeaconStateGloas; import tech.pegasys.teku.spec.datastructures.state.versions.gloas.Builder; @@ -302,8 +304,8 @@ public SafeFuture validate( * [REJECT] the builder is a payload builder -- i.e. * state.builders[bid.builder_index].version == PAYLOAD_BUILDER_VERSION. */ - final int builderVersion = - builders.get(bid.getBuilderIndex().intValue()).getVersion(); + final Builder builder = builders.get(bid.getBuilderIndex().intValue()); + final int builderVersion = builder.getVersion(); if (builderVersion != PAYLOAD_BUILDER_VERSION) { return rejectBid( bid, @@ -322,6 +324,39 @@ public SafeFuture validate( return ignoreBid(bid, "value exceeds the builder's excess balance"); } + /* + * [IGNORE] The parent's payload does not try to exit the builder. + */ + if (bid.getParentBlockHash() + .equals( + BeaconStateGloas.required(state) + .getLatestExecutionPayloadBid() + .getBlockHash())) { + final Optional maybeParentPayload = + gossipValidationHelper.getRecentlyImportedExecutionPayload( + bid.getParentBlockRoot()); + if (maybeParentPayload.isEmpty()) { + return saveBidForFuture( + bid, "parent execution payload is unavailable. Saving for future processing"); + } + final boolean parentPayloadMayExitBuilder = + ExecutionRequestsGloas.required( + maybeParentPayload.get().getMessage().getExecutionRequests()) + .getBuilderExits() + .stream() + .anyMatch( + request -> + request.getPubkey().equals(builder.getPublicKey()) + && request + .getSourceAddress() + .getWrappedBytes() + .equals(builder.getExecutionAddress().getWrappedBytes())); + if (parentPayloadMayExitBuilder) { + return ignoreBid( + bid, "parent payload may exit builder %s", bid.getBuilderIndex()); + } + } + /* * [REJECT] signed_execution_payload_bid.signature is valid with respect to the bid.builder_index. */ 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 4fd35d990bc..09f1a1a925e 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 @@ -31,6 +31,7 @@ import com.google.errorprone.annotations.FormatMethod; import it.unimi.dsi.fastutil.ints.IntList; +import java.util.List; import java.util.Optional; import org.apache.logging.log4j.Level; import org.apache.tuweni.bytes.Bytes; @@ -47,12 +48,18 @@ 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.ExecutionPayloadEnvelope; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.ProposerPreferences; import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadBid; +import tech.pegasys.teku.spec.datastructures.epbs.versions.gloas.SignedExecutionPayloadEnvelope; +import tech.pegasys.teku.spec.datastructures.execution.versions.gloas.BuilderExitRequest; +import tech.pegasys.teku.spec.datastructures.execution.versions.gloas.ExecutionRequestsGloas; import tech.pegasys.teku.spec.datastructures.state.beaconstate.BeaconState; +import tech.pegasys.teku.spec.datastructures.state.beaconstate.versions.gloas.BeaconStateGloas; import tech.pegasys.teku.spec.datastructures.state.beaconstate.versions.gloas.MutableBeaconStateGloas; import tech.pegasys.teku.spec.datastructures.state.versions.gloas.Builder; import tech.pegasys.teku.spec.logic.common.helpers.MiscHelpers; +import tech.pegasys.teku.spec.schemas.SchemaDefinitionsGloas; import tech.pegasys.teku.spec.util.DataStructureUtil; import tech.pegasys.teku.statetransition.execution.ProposerPreferencesManager; @@ -74,10 +81,12 @@ public class ExecutionPayloadBidGossipValidatorTest { private Bytes32 parentBlockRoot; private Bytes32 parentBlockHash; private BeaconState postState; + private SchemaDefinitionsGloas schemaDefinitions; @BeforeEach void setup(final TestSpecInvocationContextProvider.SpecContext specContext) { this.dataStructureUtil = specContext.getDataStructureUtil(); + this.schemaDefinitions = SchemaDefinitionsGloas.required(specContext.getSchemaDefinitions()); this.bidValidator = new ExecutionPayloadBidGossipValidator( spec, gossipValidationHelper, proposerPreferencesManager, MIN_BID_INCREMENT_PERCENTAGE); @@ -486,6 +495,82 @@ void shouldIgnore_whenBuilderHasInsufficientBalance() { ignoreBid(insufficientBalanceBid, "value exceeds the builder's excess balance")); } + @TestTemplate + void shouldIgnore_whenParentPayloadMayExitBuilder() { + final SignedExecutionPayloadBid bidBuildingOnLatestPayload = bidBuildingOnLatestPayload(); + final Builder builder = getBuilder(builderIndex); + final BuilderExitRequest matchingExit = + schemaDefinitions + .getBuilderExitRequestSchema() + .create(builder.getExecutionAddress(), builder.getPublicKey()); + when(gossipValidationHelper.getRecentlyImportedExecutionPayload(parentBlockRoot)) + .thenReturn(Optional.of(parentEnvelopeWithExits(matchingExit))); + mockBidValidation(bidBuildingOnLatestPayload); + + assertThatSafeFuture(bidValidator.validate(bidBuildingOnLatestPayload)) + .isCompletedWithValue( + ignoreBid( + bidBuildingOnLatestPayload, "parent payload may exit builder %s", builderIndex)); + } + + @TestTemplate + void shouldAccept_whenParentPayloadExitDoesNotMatchBothBuilderFields() { + final SignedExecutionPayloadBid bidBuildingOnLatestPayload = bidBuildingOnLatestPayload(); + final Builder builder = getBuilder(builderIndex); + final Builder otherBuilder = getBuilder(builderIndex.increment()); + assertThat(otherBuilder.getPublicKey()).isNotEqualTo(builder.getPublicKey()); + assertThat(otherBuilder.getExecutionAddress()).isNotEqualTo(builder.getExecutionAddress()); + final BuilderExitRequest matchingAddressOnly = + schemaDefinitions + .getBuilderExitRequestSchema() + .create(builder.getExecutionAddress(), otherBuilder.getPublicKey()); + final BuilderExitRequest matchingPublicKeyOnly = + schemaDefinitions + .getBuilderExitRequestSchema() + .create(otherBuilder.getExecutionAddress(), builder.getPublicKey()); + when(gossipValidationHelper.getRecentlyImportedExecutionPayload(parentBlockRoot)) + .thenReturn( + Optional.of(parentEnvelopeWithExits(matchingAddressOnly, matchingPublicKeyOnly))); + mockBidValidation(bidBuildingOnLatestPayload); + + assertThatSafeFuture(bidValidator.validate(bidBuildingOnLatestPayload)) + .isCompletedWithValue(ACCEPT); + } + + @TestTemplate + void shouldAccept_whenBidDoesNotBuildOnLatestPayload() { + final Bytes32 latestPayloadBlockHash = getLatestPayloadBlockHash(); + final Bytes32 differentParentBlockHash = + latestPayloadBlockHash.equals(Bytes32.ZERO) ? Bytes32.fromHexString("0x01") : Bytes32.ZERO; + final SignedExecutionPayloadBid bidBuildingOnDifferentPayload = + signedBidForParent(differentParentBlockHash, parentBlockRoot, builderIndex, bid.getValue()); + final Builder builder = getBuilder(builderIndex); + final BuilderExitRequest matchingExit = + schemaDefinitions + .getBuilderExitRequestSchema() + .create(builder.getExecutionAddress(), builder.getPublicKey()); + when(gossipValidationHelper.getRecentlyImportedExecutionPayload(parentBlockRoot)) + .thenReturn(Optional.of(parentEnvelopeWithExits(matchingExit))); + mockBidValidation(bidBuildingOnDifferentPayload); + + assertThatSafeFuture(bidValidator.validate(bidBuildingOnDifferentPayload)) + .isCompletedWithValue(ACCEPT); + } + + @TestTemplate + void shouldSaveForFuture_whenRequiredParentPayloadIsUnavailable() { + final SignedExecutionPayloadBid bidBuildingOnLatestPayload = bidBuildingOnLatestPayload(); + when(gossipValidationHelper.getRecentlyImportedExecutionPayload(parentBlockRoot)) + .thenReturn(Optional.empty()); + mockBidValidation(bidBuildingOnLatestPayload); + + assertThatSafeFuture(bidValidator.validate(bidBuildingOnLatestPayload)) + .isCompletedWithValue( + saveBidForFuture( + bidBuildingOnLatestPayload, + "parent execution payload is unavailable. Saving for future processing")); + } + @TestTemplate void shouldReject_whenSignatureIsInvalid() { when(gossipValidationHelper.isSignatureValidWithRespectToBuilderIndex( @@ -831,4 +916,42 @@ private void mockProposerPreferences( when(proposerPreferencesManager.getProposerPreferences(signedBid.getMessage().getSlot())) .thenReturn(Optional.of(proposerPreferences)); } + + private SignedExecutionPayloadBid bidBuildingOnLatestPayload() { + return signedBidForParent( + getLatestPayloadBlockHash(), parentBlockRoot, builderIndex, bid.getValue()); + } + + private Bytes32 getLatestPayloadBlockHash() { + return BeaconStateGloas.required(postState).getLatestExecutionPayloadBid().getBlockHash(); + } + + private Builder getBuilder(final UInt64 index) { + return BeaconStateGloas.required(postState).getBuilders().get(index.intValue()); + } + + private SignedExecutionPayloadEnvelope parentEnvelopeWithExits( + final BuilderExitRequest... builderExits) { + final SignedExecutionPayloadEnvelope randomEnvelope = + dataStructureUtil.randomSignedExecutionPayloadEnvelope(slot.longValue()); + final ExecutionPayloadEnvelope message = randomEnvelope.getMessage(); + final ExecutionRequestsGloas executionRequests = + ExecutionRequestsGloas.required( + dataStructureUtil + .randomExecutionRequestsBuilder(slot) + .builderExits(() -> List.of(builderExits)) + .build()); + final ExecutionPayloadEnvelope parentEnvelope = + schemaDefinitions + .getExecutionPayloadEnvelopeSchema() + .create( + message.getPayload(), + executionRequests, + message.getBuilderIndex(), + parentBlockRoot, + message.getParentBeaconBlockRoot()); + return schemaDefinitions + .getSignedExecutionPayloadEnvelopeSchema() + .create(parentEnvelope, randomEnvelope.getSignature()); + } }