Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MEV Boost\Builder] Validate SignedBuilderBid #5612

Merged
merged 6 commits into from
May 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ public Consumer<BeaconBlockBodyBuilder> createSelector(
"Merge transition not finalized: forcing block production using local execution engine");
}
return executionLayerChannel.builderGetHeader(
executionPayloadContext, blockSlotState.getSlot(), forceLocalFallback);
executionPayloadContext, blockSlotState, forceLocalFallback);
}));
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ class BlockFactoryTest {
final Eth1DataCache eth1DataCache = mock(Eth1DataCache.class);
ExecutionPayload executionPayload = null;
ExecutionPayloadHeader executionPayloadHeader = null;
;

@BeforeAll
public static void initSession() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ void shouldIncludeExecutionPayloadHeaderIfBlindedBlockRequested() {

when(forkChoiceNotifier.getPayloadId(any(), any()))
.thenReturn(SafeFuture.completedFuture(Optional.of(executionPayloadContext)));
when(executionLayer.builderGetHeader(executionPayloadContext, slot, false))
when(executionLayer.builderGetHeader(executionPayloadContext, blockSlotState, false))
.thenReturn(SafeFuture.completedFuture(randomExecutionPayloadHeader));

factory
Expand Down Expand Up @@ -379,7 +379,7 @@ void shouldIncludeExecutionPayloadIfBlindedBlockRequestedButPreMerge() {

when(forkChoiceNotifier.getPayloadId(any(), any()))
.thenReturn(SafeFuture.completedFuture(Optional.of(executionPayloadContext)));
when(executionLayer.builderGetHeader(executionPayloadContext, slot, true))
when(executionLayer.builderGetHeader(executionPayloadContext, blockSlotState, true))
.thenReturn(SafeFuture.completedFuture(randomExecutionPayloadHeader));

factory
Expand Down
2 changes: 2 additions & 0 deletions ethereum/executionlayer/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ dependencies {
implementation project(':ethereum:spec')
implementation project(':ethereum:executionclient')
implementation project(':infrastructure:async')
implementation project(':infrastructure:bls')
implementation project(':infrastructure:events')
implementation project(':infrastructure:exceptions')
implementation project(':infrastructure:logging')
implementation project(':infrastructure:metrics')
implementation project(':infrastructure:time')

testImplementation testFixtures(project(':infrastructure:async'))
testImplementation testFixtures(project(':infrastructure:bls'))
testImplementation testFixtures(project(':ethereum:spec'))
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright 2022 ConsenSys AG.
*
* 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.ethereum.executionlayer;

public class BuilderBidValidationException extends Exception {
public BuilderBidValidationException(String message) {
super(message);
}

public BuilderBidValidationException(String message, Throwable cause) {
super(message, cause);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* Copyright 2022 ConsenSys AG.
*
* 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.ethereum.executionlayer;

import java.util.Optional;
import org.apache.tuweni.bytes.Bytes;
import tech.pegasys.teku.bls.BLS;
import tech.pegasys.teku.infrastructure.unsigned.UInt64;
import tech.pegasys.teku.spec.Spec;
import tech.pegasys.teku.spec.datastructures.execution.ExecutionPayloadHeader;
import tech.pegasys.teku.spec.datastructures.execution.SignedBuilderBid;
import tech.pegasys.teku.spec.datastructures.execution.SignedValidatorRegistration;
import tech.pegasys.teku.spec.datastructures.state.beaconstate.BeaconState;
import tech.pegasys.teku.spec.logic.common.statetransition.exceptions.BlockProcessingException;

@FunctionalInterface
public interface BuilderBidValidator {
BuilderBidValidator NOOP =
(spec, signedBuilderBid, signedValidatorRegistration, state) ->
signedBuilderBid.getMessage().getExecutionPayloadHeader();

BuilderBidValidator VALIDATOR =
(spec, signedBuilderBid, signedValidatorRegistration, state) -> {
// validating Bid Signature
final Bytes signingRoot =
spec.computeBuilderApplicationSigningRoot(
state.getSlot(), signedBuilderBid.getMessage());

if (!BLS.verify(
signedBuilderBid.getMessage().getPublicKey(),
signingRoot,
signedBuilderBid.getSignature())) {
throw new BuilderBidValidationException("Invalid Bid Signature");
}

final ExecutionPayloadHeader executionPayloadHeader =
signedBuilderBid.getMessage().getExecutionPayloadHeader();

// validating payload wrt consensus
try {
spec.atSlot(state.getSlot())
.getBlockProcessor()
.validateExecutionPayload(
state, executionPayloadHeader, Optional.empty(), Optional.empty());
} catch (BlockProcessingException e) {
throw new BuilderBidValidationException(
"Invalid proposed payload with respect to consensus.", e);
}

// validating payload gas limit
final UInt64 parentGasLimit =
state
.toVersionBellatrix()
.orElseThrow()
.getLatestExecutionPayloadHeader()
.getGasLimit();
final UInt64 preferredGasLimit = signedValidatorRegistration.getMessage().getGasLimit();

final UInt64 maxGasLimitDelta = UInt64.fromLongBits(parentGasLimit.longValue() >>> 10);

final UInt64 expectedGasLimit;
final int diff = parentGasLimit.compareTo(preferredGasLimit);
if (diff < 0) {
// parentGasLimit < preferredGasLimit
expectedGasLimit = parentGasLimit.plus(maxGasLimitDelta).min(preferredGasLimit);
} else if (diff == 0) {
// parentGasLimit == preferredGasLimit
expectedGasLimit = parentGasLimit;
} else {
// parentGasLimit > preferredGasLimit
expectedGasLimit = parentGasLimit.minus(maxGasLimitDelta).max(preferredGasLimit);
}

final UInt64 proposedGasLimit = executionPayloadHeader.getGasLimit();

if (!proposedGasLimit.equals(expectedGasLimit)) {
throw new BuilderBidValidationException(
"Proposed gasLimit not honouring validator preference. Parent gasLimit: "
+ parentGasLimit
+ " Proposed gasLimit: "
+ proposedGasLimit
+ " Preferred gasLimit: "
+ preferredGasLimit
+ " Expected proposed gasLimit: "
+ expectedGasLimit);
}

return executionPayloadHeader;
};

ExecutionPayloadHeader validateAndGetPayloadHeader(
final Spec spec,
final SignedBuilderBid signedBuilderBid,
final SignedValidatorRegistration signedValidatorRegistration,
final BeaconState state)
throws BuilderBidValidationException;
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.tuweni.bytes.Bytes32;
import org.apache.tuweni.bytes.Bytes48;
import org.hyperledger.besu.plugin.services.MetricsSystem;
import tech.pegasys.teku.bls.BLSPublicKey;
import tech.pegasys.teku.ethereum.executionclient.ExecutionBuilderClient;
Expand All @@ -52,13 +51,12 @@
import tech.pegasys.teku.spec.Spec;
import tech.pegasys.teku.spec.SpecMilestone;
import tech.pegasys.teku.spec.datastructures.blocks.SignedBeaconBlock;
import tech.pegasys.teku.spec.datastructures.execution.BuilderBid;
import tech.pegasys.teku.spec.datastructures.execution.ExecutionPayload;
import tech.pegasys.teku.spec.datastructures.execution.ExecutionPayloadContext;
import tech.pegasys.teku.spec.datastructures.execution.ExecutionPayloadHeader;
import tech.pegasys.teku.spec.datastructures.execution.PowBlock;
import tech.pegasys.teku.spec.datastructures.execution.SignedBuilderBid;
import tech.pegasys.teku.spec.datastructures.execution.SignedValidatorRegistration;
import tech.pegasys.teku.spec.datastructures.state.beaconstate.BeaconState;
import tech.pegasys.teku.spec.executionlayer.ForkChoiceState;
import tech.pegasys.teku.spec.executionlayer.PayloadBuildingAttributes;
import tech.pegasys.teku.spec.executionlayer.PayloadStatus;
Expand Down Expand Up @@ -89,19 +87,23 @@ public class ExecutionLayerManagerImpl implements ExecutionLayerManager {
private final Spec spec;

private final EventLogger eventLogger;
private final BuilderBidValidator builderBidValidator;

public static ExecutionLayerManagerImpl create(
final Web3JClient engineWeb3JClient,
final Optional<RestClient> builderRestClient,
final Version version,
final Spec spec,
final MetricsSystem metricsSystem) {
final MetricsSystem metricsSystem,
final BuilderBidValidator builderBidValidator) {
checkNotNull(version);

return new ExecutionLayerManagerImpl(
createEngineClient(version, engineWeb3JClient, metricsSystem),
createBuilderClient(builderRestClient, spec, metricsSystem),
spec,
EVENT_LOG);
EVENT_LOG,
builderBidValidator);
}

private static ExecutionEngineClient createEngineClient(
Expand Down Expand Up @@ -130,12 +132,14 @@ private static Optional<ExecutionBuilderClient> createBuilderClient(
final ExecutionEngineClient executionEngineClient,
final Optional<ExecutionBuilderClient> executionBuilderClient,
final Spec spec,
final EventLogger eventLogger) {
final EventLogger eventLogger,
final BuilderBidValidator builderBidValidator) {
this.executionEngineClient = executionEngineClient;
this.executionBuilderClient = executionBuilderClient;
this.latestBuilderAvailability = new AtomicBoolean(executionBuilderClient.isPresent());
this.spec = spec;
this.eventLogger = eventLogger;
this.builderBidValidator = builderBidValidator;
}

@Override
Expand Down Expand Up @@ -287,48 +291,49 @@ public SafeFuture<Void> builderRegisterValidators(
@Override
public SafeFuture<ExecutionPayloadHeader> builderGetHeader(
final ExecutionPayloadContext executionPayloadContext,
final UInt64 slot,
final BeaconState state,
final boolean forceLocalFallback) {
final UInt64 slot = state.getSlot();

final SafeFuture<ExecutionPayload> localExecutionPayload =
engineGetPayload(executionPayloadContext, slot, true);

final Optional<BLSPublicKey> registeredValidatorPublicKey =
executionPayloadContext.getPayloadBuildingAttributes().getValidatorRegistrationPublicKey();
final Optional<SignedValidatorRegistration> validatorRegistration =
executionPayloadContext.getPayloadBuildingAttributes().getValidatorRegistration();

if (forceLocalFallback || !isBuilderAvailable() || registeredValidatorPublicKey.isEmpty()) {
if (forceLocalFallback || !isBuilderAvailable() || validatorRegistration.isEmpty()) {
// fallback to local execution engine
return doFallbackToLocal(localExecutionPayload, slot);
}

final BLSPublicKey validatorPublicKey = validatorRegistration.get().getMessage().getPublicKey();

LOG.trace(
"calling builderGetHeader(slot={}, pubKey={}, parentHash={})",
slot,
registeredValidatorPublicKey.get(),
validatorPublicKey,
executionPayloadContext.getParentHash());

return executionBuilderClient
.orElseThrow()
.getHeader(
slot, registeredValidatorPublicKey.get(), executionPayloadContext.getParentHash())
.getHeader(slot, validatorPublicKey, executionPayloadContext.getParentHash())
.thenApply(ExecutionLayerManagerImpl::unwrapResponseOrThrow)
.thenApply(SignedBuilderBid::getMessage)
.thenApply(BuilderBid::getExecutionPayloadHeader)
.thenPeek(
executionPayloadHeader -> {
// store that we haven't fallen back for this slot
slotToLocalElFallbackPayload.put(slot, Optional.empty());
LOG.trace(
"builderGetHeader(slot={}, pubKey={}, parentHash={}) -> {}",
slot,
Bytes48.ZERO,
executionPayloadContext.getParentHash(),
executionPayloadHeader);
})
signedBuilderBid ->
LOG.trace(
"builderGetHeader(slot={}, pubKey={}, parentHash={}) -> {}",
slot,
validatorPublicKey,
executionPayloadContext.getParentHash(),
signedBuilderBid))
.thenApplyChecked(
signedBuilderBid ->
builderBidValidator.validateAndGetPayloadHeader(
spec, signedBuilderBid, validatorRegistration.get(), state))
.exceptionallyCompose(
error -> {
LOG.error(
"builderGetHeader returned an error. Falling back to local execution engine",
"Unable to obtain a valid payload from builder. Falling back to local execution engine.",
error);
return doFallbackToLocal(localExecutionPayload, slot);
});
Expand Down
Loading