Skip to content

Commit

Permalink
Negotiating Engine API capabilities with EL (Consensys#6944)
Browse files Browse the repository at this point in the history
  • Loading branch information
lucassaldanha authored Mar 15, 2023
1 parent af9f216 commit 067d750
Show file tree
Hide file tree
Showing 26 changed files with 1,137 additions and 124 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public enum EngineApiMethods {
ETH_GET_BLOCK_BY_NUMBER("eth_getBlockByNumber"),
ENGINE_NEW_PAYLOAD("engine_newPayload"),
ENGINE_GET_PAYLOAD("engine_getPayload"),
ENGINE_FORK_CHOICE_UPDATED("engine_forkChoiceUpdated"),
ENGINE_FORK_CHOICE_UPDATED("engine_forkchoiceUpdated"),
ENGINE_EXCHANGE_TRANSITION_CONFIGURATION("engine_exchangeTransitionConfiguration"),
ENGINE_GET_BLOBS_BUNDLE("engine_getBlobsBundle");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,9 @@ public void setUp() {

@Test
public void shouldReturnExpectedNameAndVersion() {
assertThat(jsonRpcMethod.getName()).isEqualTo("engine_forkChoiceUpdated");
assertThat(jsonRpcMethod.getName()).isEqualTo("engine_forkchoiceUpdated");
assertThat(jsonRpcMethod.getVersion()).isEqualTo(1);
assertThat(jsonRpcMethod.getVersionedName()).isEqualTo("engine_forkChoiceUpdatedV1");
assertThat(jsonRpcMethod.getVersionedName()).isEqualTo("engine_forkchoiceUpdatedV1");
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@ public void setUp() {

@Test
public void shouldReturnExpectedNameAndVersion() {
assertThat(jsonRpcMethod.getName()).isEqualTo("engine_forkChoiceUpdated");
assertThat(jsonRpcMethod.getName()).isEqualTo("engine_forkchoiceUpdated");
assertThat(jsonRpcMethod.getVersion()).isEqualTo(2);
assertThat(jsonRpcMethod.getVersionedName()).isEqualTo("engine_forkChoiceUpdatedV2");
assertThat(jsonRpcMethod.getVersionedName()).isEqualTo("engine_forkchoiceUpdatedV2");
}

@Test
Expand Down
2 changes: 2 additions & 0 deletions ethereum/executionlayer/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ dependencies {
testImplementation testFixtures(project(':infrastructure:async'))
testImplementation testFixtures(project(':infrastructure:bls'))
testImplementation testFixtures(project(':infrastructure:metrics'))
testImplementation testFixtures(project(':infrastructure:time'))
testImplementation testFixtures(project(':ethereum:spec'))
testImplementation 'io.github.hakky54:logcaptor'
}

publishing {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,18 @@
package tech.pegasys.teku.ethereum.executionlayer;

import java.util.Collection;
import java.util.stream.Collectors;
import tech.pegasys.teku.ethereum.executionclient.methods.EngineJsonRpcMethod;

public interface EngineApiCapabilitiesProvider {

Collection<EngineJsonRpcMethod<?>> supportedMethods();

default Collection<String> supportedMethodsVersionedNames() {
return supportedMethods().stream()
.map(EngineJsonRpcMethod::getVersionedName)
.collect(Collectors.toUnmodifiableList());
}

boolean isAvailable();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright ConsenSys Software Inc., 2022
*
* 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 ExchangeCapabilitiesException extends RuntimeException {
public ExchangeCapabilitiesException(final String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/*
* Copyright ConsenSys Software Inc., 2023
*
* 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 com.google.common.annotations.VisibleForTesting;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import tech.pegasys.teku.ethereum.executionclient.ExecutionEngineClient;
import tech.pegasys.teku.ethereum.executionclient.events.ExecutionClientEventsChannel;
import tech.pegasys.teku.ethereum.executionclient.methods.EngineJsonRpcMethod;
import tech.pegasys.teku.ethereum.executionclient.schema.Response;
import tech.pegasys.teku.infrastructure.async.AsyncRunner;
import tech.pegasys.teku.infrastructure.async.Cancellable;
import tech.pegasys.teku.infrastructure.async.SafeFuture;
import tech.pegasys.teku.infrastructure.events.EventChannels;

public class ExecutionClientEngineApiCapabilitiesProvider
implements EngineApiCapabilitiesProvider, ExecutionClientEventsChannel {

private static final Logger LOG = LogManager.getLogger();
public static final long EXCHANGE_CAPABILITIES_FETCH_INTERVAL_IN_SECONDS = 60 * 15;
public static final int EXCHANGE_CAPABILITIES_ATTEMPTS_BEFORE_LOG_WARN = 3;

private final AsyncRunner asyncRunner;
private final ExecutionEngineClient executionEngineClient;
private final EngineApiCapabilitiesProvider localEngineApiCapabilitiesProvider;
private final Set<String> remoteSupportedMethodNames =
Collections.synchronizedSet(new HashSet<>());
private final AtomicBoolean isExecutionClientAvailable = new AtomicBoolean(false);
private final AtomicInteger failedFetchCounter = new AtomicInteger(0);
private Cancellable fetchTask;

public ExecutionClientEngineApiCapabilitiesProvider(
final AsyncRunner asyncRunner,
final ExecutionEngineClient executionEngineClient,
final EngineApiCapabilitiesProvider localEngineApiCapabilitiesProvider,
final EventChannels eventChannels) {
this.asyncRunner = asyncRunner;
this.executionEngineClient = executionEngineClient;
this.localEngineApiCapabilitiesProvider = localEngineApiCapabilitiesProvider;

eventChannels.subscribe(ExecutionClientEventsChannel.class, this);
}

private Cancellable runFetchTask() {
LOG.trace("Exchange Capabilities Task - Starting...");

return asyncRunner.runWithFixedDelay(
this::fetchRemoteCapabilities,
Duration.ZERO,
Duration.ofSeconds(EXCHANGE_CAPABILITIES_FETCH_INTERVAL_IN_SECONDS),
this::handleFetchCapabilitiesUnexpectedError);
}

private void fetchRemoteCapabilities() {
if (!isExecutionClientAvailable.get()) {
return;
}

final SafeFuture<Response<List<String>>> exchangeCapabilitiesCall =
executionEngineClient.exchangeCapabilities(
new ArrayList<>(localEngineApiCapabilitiesProvider.supportedMethodsVersionedNames()));

exchangeCapabilitiesCall
.thenAccept(
response -> {
if (response.isSuccess()) {
handleSuccessfulResponse(response);
} else {
handleFailedResponse(response.getErrorMessage());
}
})
.ifExceptionGetsHereRaiseABug();
}

private void handleSuccessfulResponse(final Response<List<String>> response) {
LOG.trace("Handling successful response (response = {})", response.getPayload());

final List<String> remoteCapabilities =
response.getPayload() != null ? response.getPayload() : List.of();
remoteSupportedMethodNames.addAll(remoteCapabilities);
remoteSupportedMethodNames.removeIf(m -> !remoteCapabilities.contains(m));

failedFetchCounter.set(0);
}

private void handleFailedResponse(final String errorResponse) {
final int failedAttempts = failedFetchCounter.incrementAndGet();

final StringBuilder sb = new StringBuilder();
sb.append(
String.format(
"Error fetching remote capabilities from Execution Client (failed attempts = %d)",
failedAttempts));
if (StringUtils.isNotBlank(errorResponse)) {
sb.append(". ").append(errorResponse);
}

if (!remoteSupportedMethodNames.isEmpty()) {
if (failedAttempts >= EXCHANGE_CAPABILITIES_ATTEMPTS_BEFORE_LOG_WARN) {
LOG.warn(sb.toString());
} else {
LOG.trace(sb.toString());
}
} else {
LOG.error(
"Unable to fetch remote capabilities from Execution Client. Check if your Execution Client "
+ "supports engine_exchangeCapabilities method.");
}
}

private void handleFetchCapabilitiesUnexpectedError(final Throwable th) {
LOG.error("Unexpected failure fetching remote capabilities from Execution Client", th);
}

@Override
public boolean isAvailable() {
return !remoteSupportedMethodNames.isEmpty();
}

@Override
public Collection<EngineJsonRpcMethod<?>> supportedMethods() {
return localEngineApiCapabilitiesProvider.supportedMethods().stream()
.filter(m -> remoteSupportedMethodNames.contains(m.getVersionedName()))
.collect(Collectors.toList());
}

@Override
public void onAvailabilityUpdated(final boolean isAvailable) {
if (isExecutionClientAvailable.compareAndSet(!isAvailable, isAvailable)) {
if (fetchTask != null) {
fetchTask.cancel();
}

if (isAvailable) {
remoteSupportedMethodNames.clear();
fetchTask = runFetchTask();
}
}
}

@VisibleForTesting
int getFailedFetchCount() {
return failedFetchCounter.get();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
import tech.pegasys.teku.spec.executionlayer.PayloadStatus;
import tech.pegasys.teku.spec.executionlayer.TransitionConfiguration;

class ExecutionClientHandlerImpl implements ExecutionClientHandler {
public class ExecutionClientHandlerImpl implements ExecutionClientHandler {

private final ExecutionJsonRpcMethodsResolver methodsResolver;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,35 +115,6 @@ public static ExecutionLayerManagerImpl create(
builderBidChallengePercentage);
}

public static ExecutionLayerManagerImpl create(
final EventLogger eventLogger,
final ExecutionEngineClient executionEngineClient,
final Optional<BuilderClient> builderClient,
final Spec spec,
final MetricsSystem metricsSystem,
final BuilderBidValidator builderBidValidator,
final BuilderCircuitBreaker builderCircuitBreaker,
final BlobsBundleValidator blobsBundleValidator,
final Optional<Integer> builderBidChallengePercentage) {
final EngineApiCapabilitiesProvider localEngineApiCapabilitiesProvider =
new LocallySupportedEngineApiCapabilitiesProvider(spec, executionEngineClient);
final MilestoneBasedExecutionJsonRpcMethodsResolver milestoneBasedMethodResolver =
new MilestoneBasedExecutionJsonRpcMethodsResolver(spec, localEngineApiCapabilitiesProvider);
final ExecutionClientHandler executionClientHandler =
new ExecutionClientHandlerImpl(milestoneBasedMethodResolver);

return create(
eventLogger,
executionClientHandler,
builderClient,
spec,
metricsSystem,
builderBidValidator,
builderCircuitBreaker,
blobsBundleValidator,
builderBidChallengePercentage);
}

public static ExecutionEngineClient createEngineClient(
final Version version,
final Web3JClient web3JClient,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,11 @@
import tech.pegasys.teku.ethereum.executionclient.methods.EthGetBlockByNumber;
import tech.pegasys.teku.spec.Spec;

public class LocallySupportedEngineApiCapabilitiesProvider
implements EngineApiCapabilitiesProvider {
public class LocalEngineApiCapabilitiesProvider implements EngineApiCapabilitiesProvider {

private final Collection<EngineJsonRpcMethod<?>> supportedMethods = new HashSet<>();

public LocallySupportedEngineApiCapabilitiesProvider(
public LocalEngineApiCapabilitiesProvider(
final Spec spec, final ExecutionEngineClient executionEngineClient) {
// Eth1 methods
supportedMethods.add(new EthGetBlockByHash(executionEngineClient));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import tech.pegasys.teku.ethereum.executionclient.ExecutionEngineClient;
import tech.pegasys.teku.ethereum.executionclient.methods.EngineApiMethods;
import tech.pegasys.teku.ethereum.executionclient.methods.EngineJsonRpcMethod;
import tech.pegasys.teku.spec.Spec;
Expand All @@ -30,11 +29,6 @@ public class MilestoneBasedExecutionJsonRpcMethodsResolver
@SuppressWarnings("rawtypes")
private final List<EngineJsonRpcMethod> methods = new ArrayList<>();

public MilestoneBasedExecutionJsonRpcMethodsResolver(
final Spec spec, final ExecutionEngineClient executionEngineClient) {
this(spec, new LocallySupportedEngineApiCapabilitiesProvider(spec, executionEngineClient));
}

public MilestoneBasedExecutionJsonRpcMethodsResolver(
final Spec spec, final EngineApiCapabilitiesProvider capabilitiesProvider) {
final Collection<String> supportedMethods = new HashSet<>();
Expand Down
Loading

0 comments on commit 067d750

Please sign in to comment.