diff --git a/build.gradle b/build.gradle index 3dece0a10f3..efb42ac78cd 100644 --- a/build.gradle +++ b/build.gradle @@ -82,7 +82,7 @@ dependencyRules { allowed = [":infrastructure:unsigned"] } - ['core', 'dataproviders', 'events', 'executionengine', 'executionlayer', 'networks', 'pow', 'signingrecord', 'spec'].forEach( { + ['core', 'dataproviders', 'events', 'executionclient', 'executionlayer', 'networks', 'pow', 'signingrecord', 'spec'].forEach( { register(":ethereum:${it}") { allowed = [ ":infrastructure:", diff --git a/ethereum/executionlayer/build.gradle b/ethereum/executionlayer/build.gradle index c0711ad894d..0f3cec36456 100644 --- a/ethereum/executionlayer/build.gradle +++ b/ethereum/executionlayer/build.gradle @@ -1,10 +1,16 @@ dependencies { + implementation project(':ethereum:events') implementation project(':ethereum:spec') implementation project(':ethereum:executionclient') implementation project(':infrastructure:async') 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(':ethereum:spec')) } publishing { diff --git a/ethereum/executionlayer/src/main/java/tech/pegasys/teku/ethereum/executionlayer/ExecutionLayerManager.java b/ethereum/executionlayer/src/main/java/tech/pegasys/teku/ethereum/executionlayer/ExecutionLayerManager.java new file mode 100644 index 00000000000..aab42e9691c --- /dev/null +++ b/ethereum/executionlayer/src/main/java/tech/pegasys/teku/ethereum/executionlayer/ExecutionLayerManager.java @@ -0,0 +1,19 @@ +/* + * 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 tech.pegasys.teku.ethereum.events.SlotEventsChannel; +import tech.pegasys.teku.spec.executionlayer.ExecutionLayerChannel; + +public interface ExecutionLayerManager extends SlotEventsChannel, ExecutionLayerChannel {} diff --git a/ethereum/executionlayer/src/main/java/tech/pegasys/teku/ethereum/executionlayer/ExecutionLayerChannelImpl.java b/ethereum/executionlayer/src/main/java/tech/pegasys/teku/ethereum/executionlayer/ExecutionLayerManagerImpl.java similarity index 85% rename from ethereum/executionlayer/src/main/java/tech/pegasys/teku/ethereum/executionlayer/ExecutionLayerChannelImpl.java rename to ethereum/executionlayer/src/main/java/tech/pegasys/teku/ethereum/executionlayer/ExecutionLayerManagerImpl.java index f3f55a3519f..df21e6a9d1f 100644 --- a/ethereum/executionlayer/src/main/java/tech/pegasys/teku/ethereum/executionlayer/ExecutionLayerChannelImpl.java +++ b/ethereum/executionlayer/src/main/java/tech/pegasys/teku/ethereum/executionlayer/ExecutionLayerManagerImpl.java @@ -16,10 +16,12 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; +import static tech.pegasys.teku.infrastructure.logging.EventLogger.EVENT_LOG; import static tech.pegasys.teku.spec.config.Constants.MAXIMUM_CONCURRENT_EB_REQUESTS; import static tech.pegasys.teku.spec.config.Constants.MAXIMUM_CONCURRENT_EE_REQUESTS; import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.tuweni.bytes.Bytes32; @@ -44,6 +46,7 @@ import tech.pegasys.teku.ethereum.executionclient.schema.TransitionConfigurationV1; import tech.pegasys.teku.infrastructure.async.SafeFuture; import tech.pegasys.teku.infrastructure.exceptions.InvalidConfigurationException; +import tech.pegasys.teku.infrastructure.logging.EventLogger; import tech.pegasys.teku.infrastructure.unsigned.UInt64; import tech.pegasys.teku.spec.Spec; import tech.pegasys.teku.spec.datastructures.blocks.SignedBeaconBlock; @@ -52,32 +55,36 @@ import tech.pegasys.teku.spec.datastructures.execution.ExecutionPayloadHeader; import tech.pegasys.teku.spec.datastructures.execution.ExecutionPayloadHeaderSchema; import tech.pegasys.teku.spec.datastructures.execution.PowBlock; -import tech.pegasys.teku.spec.executionlayer.ExecutionLayerChannel; import tech.pegasys.teku.spec.executionlayer.ForkChoiceState; import tech.pegasys.teku.spec.executionlayer.PayloadAttributes; import tech.pegasys.teku.spec.executionlayer.PayloadStatus; import tech.pegasys.teku.spec.executionlayer.TransitionConfiguration; import tech.pegasys.teku.spec.schemas.SchemaDefinitionsBellatrix; -public class ExecutionLayerChannelImpl implements ExecutionLayerChannel { +public class ExecutionLayerManagerImpl implements ExecutionLayerManager { private static final Logger LOG = LogManager.getLogger(); private final ExecutionEngineClient executionEngineClient; private final Optional executionBuilderClient; + private final AtomicBoolean latestBuilderAvailability; + private final Spec spec; - public static ExecutionLayerChannelImpl create( + private final EventLogger eventLogger; + + public static ExecutionLayerManagerImpl create( final Web3JClient engineWeb3JClient, final Optional builderWeb3JClient, final Version version, final Spec spec, final MetricsSystem metricsSystem) { checkNotNull(version); - return new ExecutionLayerChannelImpl( + return new ExecutionLayerManagerImpl( createEngineClient(version, engineWeb3JClient, metricsSystem), createBuilderClient(builderWeb3JClient, metricsSystem), - spec); + spec, + EVENT_LOG); } private static ExecutionEngineClient createEngineClient( @@ -101,21 +108,21 @@ private static Optional createBuilderClient( metricsSystem))); } - private ExecutionLayerChannelImpl( + ExecutionLayerManagerImpl( final ExecutionEngineClient executionEngineClient, final Optional executionBuilderClient, - final Spec spec) { - this.spec = spec; + final Spec spec, + final EventLogger eventLogger) { this.executionEngineClient = executionEngineClient; this.executionBuilderClient = executionBuilderClient; + this.latestBuilderAvailability = new AtomicBoolean(executionBuilderClient.isPresent()); + this.spec = spec; + this.eventLogger = eventLogger; } - private static K unwrapResponseOrThrow(Response response) { - checkArgument( - response.getErrorMessage() == null, - "Invalid remote response: %s", - response.getErrorMessage()); - return checkNotNull(response.getPayload(), "No payload content found"); + @Override + public void onSlot(UInt64 slot) { + updateBuilderAvailability(); } @Override @@ -152,7 +159,7 @@ public SafeFuture eth1GetPowChainHead() { .forkChoiceUpdated( ForkChoiceStateV1.fromInternalForkChoiceState(forkChoiceState), PayloadAttributesV1.fromInternalForkChoiceState(payloadAttributes)) - .thenApply(ExecutionLayerChannelImpl::unwrapResponseOrThrow) + .thenApply(ExecutionLayerManagerImpl::unwrapResponseOrThrow) .thenApply(ForkChoiceUpdatedResult::asInternalExecutionPayload) .thenPeek( forkChoiceUpdatedResult -> @@ -173,7 +180,7 @@ public SafeFuture engineGetPayload( return executionEngineClient .getPayload(executionPayloadContext.getPayloadId()) - .thenApply(ExecutionLayerChannelImpl::unwrapResponseOrThrow) + .thenApply(ExecutionLayerManagerImpl::unwrapResponseOrThrow) .thenCombine( SafeFuture.of( () -> @@ -195,7 +202,7 @@ public SafeFuture engineNewPayload(final ExecutionPayload executi return executionEngineClient .newPayload(ExecutionPayloadV1.fromInternalExecutionPayload(executionPayload)) - .thenApply(ExecutionLayerChannelImpl::unwrapResponseOrThrow) + .thenApply(ExecutionLayerManagerImpl::unwrapResponseOrThrow) .thenApply(PayloadStatusV1::asInternalExecutionPayload) .thenPeek( payloadStatus -> @@ -214,7 +221,7 @@ public SafeFuture engineExchangeTransitionConfiguration return executionEngineClient .exchangeTransitionConfiguration( TransitionConfigurationV1.fromInternalTransitionConfiguration(transitionConfiguration)) - .thenApply(ExecutionLayerChannelImpl::unwrapResponseOrThrow) + .thenApply(ExecutionLayerManagerImpl::unwrapResponseOrThrow) .thenApply(TransitionConfigurationV1::asInternalTransitionConfiguration) .thenPeek( remoteTransitionConfiguration -> @@ -224,6 +231,10 @@ public SafeFuture engineExchangeTransitionConfiguration remoteTransitionConfiguration)); } + boolean isBuilderAvailable() { + return latestBuilderAvailability.get(); + } + @Override public SafeFuture builderGetHeader( final ExecutionPayloadContext executionPayloadContext, final UInt64 slot) { @@ -238,7 +249,7 @@ public SafeFuture builderGetHeader( return executionBuilderClient .get() .getHeader(slot, Bytes48.ZERO, executionPayloadContext.getParentHash()) - .thenApply(ExecutionLayerChannelImpl::unwrapResponseOrThrow) + .thenApply(ExecutionLayerManagerImpl::unwrapResponseOrThrow) .thenApply( builderBidV1SignedMessage -> getExecutionHeaderFromBuilderBid(builderBidV1SignedMessage, slot)) @@ -282,7 +293,7 @@ public SafeFuture builderGetPayload( new SignedMessage<>( new BlindedBeaconBlockV1(signedBlindedBeaconBlock.getMessage()), signedBlindedBeaconBlock.getSignature())) - .thenApply(ExecutionLayerChannelImpl::unwrapResponseOrThrow) + .thenApply(ExecutionLayerManagerImpl::unwrapResponseOrThrow) .thenCombine( SafeFuture.of( () -> @@ -297,4 +308,37 @@ public SafeFuture builderGetPayload( signedBlindedBeaconBlock, executionPayload)); } + + private static K unwrapResponseOrThrow(Response response) { + checkArgument( + response.getErrorMessage() == null, + "Invalid remote response: %s", + response.getErrorMessage()); + return checkNotNull(response.getPayload(), "No payload content found"); + } + + private void updateBuilderAvailability() { + if (executionBuilderClient.isEmpty()) { + return; + } + executionBuilderClient + .get() + .status() + .finish( + statusResponse -> { + if (statusResponse.getErrorMessage() != null) { + markBuilderAsNotAvailable(statusResponse.getErrorMessage()); + } else { + if (latestBuilderAvailability.compareAndSet(false, true)) { + eventLogger.executionBuilderIsBackOnline(); + } + } + }, + throwable -> markBuilderAsNotAvailable(throwable.getMessage())); + } + + private void markBuilderAsNotAvailable(String errorMessage) { + latestBuilderAvailability.set(false); + eventLogger.executionBuilderIsOffline(errorMessage); + } } diff --git a/ethereum/executionlayer/src/main/java/tech/pegasys/teku/ethereum/executionlayer/ExecutionLayerManagerStub.java b/ethereum/executionlayer/src/main/java/tech/pegasys/teku/ethereum/executionlayer/ExecutionLayerManagerStub.java new file mode 100644 index 00000000000..f837fc35b29 --- /dev/null +++ b/ethereum/executionlayer/src/main/java/tech/pegasys/teku/ethereum/executionlayer/ExecutionLayerManagerStub.java @@ -0,0 +1,33 @@ +/* + * 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 tech.pegasys.teku.infrastructure.time.TimeProvider; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.Spec; +import tech.pegasys.teku.spec.executionlayer.ExecutionLayerChannelStub; + +public class ExecutionLayerManagerStub extends ExecutionLayerChannelStub + implements ExecutionLayerManager { + + public ExecutionLayerManagerStub( + Spec spec, TimeProvider timeProvider, boolean enableTransitionEmulation) { + super(spec, timeProvider, enableTransitionEmulation); + } + + @Override + public void onSlot(UInt64 slot) { + // NOOP + } +} diff --git a/ethereum/executionlayer/src/test/java/tech/pegasys/teku/ethereum/executionlayer/ExecutionLayerManagerImplTest.java b/ethereum/executionlayer/src/test/java/tech/pegasys/teku/ethereum/executionlayer/ExecutionLayerManagerImplTest.java new file mode 100644 index 00000000000..5c811394c5b --- /dev/null +++ b/ethereum/executionlayer/src/test/java/tech/pegasys/teku/ethereum/executionlayer/ExecutionLayerManagerImplTest.java @@ -0,0 +1,135 @@ +/* + * 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 static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import java.util.Optional; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import tech.pegasys.teku.ethereum.executionclient.ExecutionBuilderClient; +import tech.pegasys.teku.ethereum.executionclient.ExecutionEngineClient; +import tech.pegasys.teku.ethereum.executionclient.schema.GenericBuilderStatus; +import tech.pegasys.teku.ethereum.executionclient.schema.Response; +import tech.pegasys.teku.infrastructure.async.SafeFuture; +import tech.pegasys.teku.infrastructure.logging.EventLogger; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.Spec; +import tech.pegasys.teku.spec.TestSpecFactory; + +class ExecutionLayerManagerImplTest { + + private final ExecutionEngineClient executionEngineClient = + Mockito.mock(ExecutionEngineClient.class); + + private final ExecutionBuilderClient executionBuilderClient = + Mockito.mock(ExecutionBuilderClient.class); + + private final Spec spec = TestSpecFactory.createMinimalBellatrix(); + + private final EventLogger eventLogger = mock(EventLogger.class); + + private final ExecutionLayerManagerImpl underTest = createExecutionLayerChannelImpl(true); + + @Test + public void builderShouldNotBeAvailableWhenBuilderNotEnabled() { + ExecutionLayerManagerImpl noBuilderEnabled = createExecutionLayerChannelImpl(false); + + // trigger update of builder status (should not do anything since builder is not enabled) + noBuilderEnabled.onSlot(UInt64.ONE); + + assertThat(noBuilderEnabled.isBuilderAvailable()).isFalse(); + verifyNoInteractions(executionBuilderClient); + verifyNoInteractions(eventLogger); + } + + @Test + public void builderShouldBeAvailableWhenBuilderIsOperatingNormally() { + SafeFuture> builderClientResponse = + SafeFuture.completedFuture(new Response<>(GenericBuilderStatus.OK)); + + updateBuilderStatus(builderClientResponse); + + assertThat(underTest.isBuilderAvailable()).isTrue(); + verifyNoInteractions(eventLogger); + } + + @Test + public void builderShouldNotBeAvailableWhenBuilderIsNotOperatingNormally() { + SafeFuture> builderClientResponse = + SafeFuture.completedFuture(new Response<>("oops")); + + updateBuilderStatus(builderClientResponse); + + assertThat(underTest.isBuilderAvailable()).isFalse(); + verify(eventLogger).executionBuilderIsOffline("oops"); + } + + @Test + public void builderShouldNotBeAvailableWhenBuilderStatusCallFails() { + SafeFuture> builderClientResponse = + SafeFuture.failedFuture(new Throwable("oops")); + + updateBuilderStatus(builderClientResponse); + + assertThat(underTest.isBuilderAvailable()).isFalse(); + verify(eventLogger).executionBuilderIsOffline("oops"); + } + + @Test + public void builderAvailabilityIsUpdatedOnSlotEventAndLoggedAdequately() { + // Initially builder should be available + assertThat(underTest.isBuilderAvailable()).isTrue(); + + // Given builder status is ok + updateBuilderStatus(SafeFuture.completedFuture(new Response<>(GenericBuilderStatus.OK))); + + // Then + assertThat(underTest.isBuilderAvailable()).isTrue(); + verifyNoInteractions(eventLogger); + + // Given builder status is not ok + updateBuilderStatus(SafeFuture.completedFuture(new Response<>("oops"))); + + // Then + assertThat(underTest.isBuilderAvailable()).isFalse(); + verify(eventLogger).executionBuilderIsOffline("oops"); + + // Given builder status is back to being ok + updateBuilderStatus(SafeFuture.completedFuture(new Response<>(GenericBuilderStatus.OK))); + + // Then + assertThat(underTest.isBuilderAvailable()).isTrue(); + verify(eventLogger).executionBuilderIsBackOnline(); + } + + private ExecutionLayerManagerImpl createExecutionLayerChannelImpl(boolean builderEnabled) { + return new ExecutionLayerManagerImpl( + executionEngineClient, + builderEnabled ? Optional.of(executionBuilderClient) : Optional.empty(), + spec, + eventLogger); + } + + private void updateBuilderStatus( + SafeFuture> builderClientResponse) { + when(executionBuilderClient.status()).thenReturn(builderClientResponse); + // trigger update of the builder status + underTest.onSlot(UInt64.ONE); + } +} diff --git a/infrastructure/logging/src/main/java/tech/pegasys/teku/infrastructure/logging/EventLogger.java b/infrastructure/logging/src/main/java/tech/pegasys/teku/infrastructure/logging/EventLogger.java index e2225d7e3c6..3fe2641431e 100644 --- a/infrastructure/logging/src/main/java/tech/pegasys/teku/infrastructure/logging/EventLogger.java +++ b/infrastructure/logging/src/main/java/tech/pegasys/teku/infrastructure/logging/EventLogger.java @@ -137,6 +137,20 @@ public void executionClientIsOnline() { info("Execution Client is back online", Color.GREEN); } + public void executionBuilderIsOffline(String errorMessage) { + String executionBuilderOfflineEventLog = + String.format( + "The execution builder is offline: %s. Block production will fallback to the execution engine.", + errorMessage); + warn(executionBuilderOfflineEventLog, Color.YELLOW); + } + + public void executionBuilderIsBackOnline() { + String executionBuilderOnlineEventLog = + "The execution builder is back online. It will be used for block production."; + info(executionBuilderOnlineEventLog, Color.GREEN); + } + public void syncStart() { info("Syncing started", Color.YELLOW); } diff --git a/services/executionlayer/build.gradle b/services/executionlayer/build.gradle index f39252ca4b1..aea542bba3b 100644 --- a/services/executionlayer/build.gradle +++ b/services/executionlayer/build.gradle @@ -2,6 +2,7 @@ dependencies { implementation project(':beacon:pow') implementation project(':infrastructure:events') implementation project(':infrastructure:exceptions') + implementation project(':ethereum:events') implementation project(':ethereum:executionlayer') implementation project(':ethereum:executionclient') implementation project(':ethereum:networks') diff --git a/services/executionlayer/src/main/java/tech/pegasys/teku/services/executionlayer/ExecutionLayerService.java b/services/executionlayer/src/main/java/tech/pegasys/teku/services/executionlayer/ExecutionLayerService.java index 024505b95d7..8c76aecac64 100644 --- a/services/executionlayer/src/main/java/tech/pegasys/teku/services/executionlayer/ExecutionLayerService.java +++ b/services/executionlayer/src/main/java/tech/pegasys/teku/services/executionlayer/ExecutionLayerService.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 ConsenSys AG. + * 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 @@ -21,33 +21,28 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.hyperledger.besu.plugin.services.MetricsSystem; +import tech.pegasys.teku.ethereum.events.SlotEventsChannel; import tech.pegasys.teku.ethereum.executionclient.ExecutionWeb3jClientProvider; -import tech.pegasys.teku.ethereum.executionlayer.ExecutionLayerChannelImpl; +import tech.pegasys.teku.ethereum.executionlayer.ExecutionLayerManager; +import tech.pegasys.teku.ethereum.executionlayer.ExecutionLayerManagerImpl; +import tech.pegasys.teku.ethereum.executionlayer.ExecutionLayerManagerStub; import tech.pegasys.teku.infrastructure.async.SafeFuture; import tech.pegasys.teku.infrastructure.events.EventChannels; -import tech.pegasys.teku.infrastructure.time.TimeProvider; import tech.pegasys.teku.service.serviceutils.Service; import tech.pegasys.teku.service.serviceutils.ServiceConfig; import tech.pegasys.teku.spec.executionlayer.ExecutionLayerChannel; -import tech.pegasys.teku.spec.executionlayer.ExecutionLayerChannelStub; public class ExecutionLayerService extends Service { private static final Logger LOG = LogManager.getLogger(); private final EventChannels eventChannels; - private final ExecutionLayerConfiguration config; - private final MetricsSystem metricsSystem; private final ExecutionWeb3jClientProvider engineWeb3jClientProvider; - private final Optional builderWeb3jClientProvider; - private final TimeProvider timeProvider; + private final ExecutionLayerManager executionLayerManager; - public ExecutionLayerService( + public static ExecutionLayerService create( final ServiceConfig serviceConfig, final ExecutionLayerConfiguration config) { - this.eventChannels = serviceConfig.getEventChannels(); - this.metricsSystem = serviceConfig.getMetricsSystem(); - this.config = config; - this.engineWeb3jClientProvider = + final ExecutionWeb3jClientProvider engineWeb3jClientProvider = ExecutionWeb3jClientProvider.create( config.getEngineEndpoint(), serviceConfig.getTimeProvider(), @@ -55,7 +50,7 @@ public ExecutionLayerService( config.getEngineJwtSecretFile(), serviceConfig.getDataDirLayout().getBeaconDataDirectory()); - this.builderWeb3jClientProvider = + final Optional builderWeb3jClientProvider = config .getBuilderEndpoint() .map( @@ -73,27 +68,44 @@ public ExecutionLayerService( checkState( engineWeb3jClientProvider.isStub() == builderIsStub, "mixed configuration with stubbed and non-stubbed execution layer endpoints is not supported"); - this.timeProvider = serviceConfig.getTimeProvider(); - } - @Override - protected SafeFuture doStart() { final String endpoint = engineWeb3jClientProvider.getEndpoint(); LOG.info("Using execution engine at {}", endpoint); - final ExecutionLayerChannel executionLayerChannel; + + final ExecutionLayerManager executionLayerManager; if (engineWeb3jClientProvider.isStub()) { EVENT_LOG.executionLayerStubEnabled(); - executionLayerChannel = new ExecutionLayerChannelStub(config.getSpec(), timeProvider, true); + executionLayerManager = + new ExecutionLayerManagerStub(config.getSpec(), serviceConfig.getTimeProvider(), true); } else { - executionLayerChannel = - ExecutionLayerChannelImpl.create( + final MetricsSystem metricsSystem = serviceConfig.getMetricsSystem(); + executionLayerManager = + ExecutionLayerManagerImpl.create( engineWeb3jClientProvider.getWeb3JClient(), builderWeb3jClientProvider.map(ExecutionWeb3jClientProvider::getWeb3JClient), config.getEngineVersion(), config.getSpec(), metricsSystem); } - eventChannels.subscribe(ExecutionLayerChannel.class, executionLayerChannel); + + return new ExecutionLayerService( + serviceConfig.getEventChannels(), engineWeb3jClientProvider, executionLayerManager); + } + + ExecutionLayerService( + final EventChannels eventChannels, + final ExecutionWeb3jClientProvider engineWeb3jClientProvider, + final ExecutionLayerManager executionLayerManager) { + this.eventChannels = eventChannels; + this.engineWeb3jClientProvider = engineWeb3jClientProvider; + this.executionLayerManager = executionLayerManager; + } + + @Override + protected SafeFuture doStart() { + eventChannels + .subscribe(SlotEventsChannel.class, executionLayerManager) + .subscribe(ExecutionLayerChannel.class, executionLayerManager); return SafeFuture.COMPLETE; } diff --git a/services/executionlayer/src/test/java/tech/pegasys/teku/services/executionlayer/ExecutionLayerServiceTest.java b/services/executionlayer/src/test/java/tech/pegasys/teku/services/executionlayer/ExecutionLayerServiceTest.java new file mode 100644 index 00000000000..c8987a258ab --- /dev/null +++ b/services/executionlayer/src/test/java/tech/pegasys/teku/services/executionlayer/ExecutionLayerServiceTest.java @@ -0,0 +1,62 @@ +/* + * 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.services.executionlayer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.Test; +import tech.pegasys.teku.ethereum.events.SlotEventsChannel; +import tech.pegasys.teku.ethereum.executionclient.ExecutionWeb3jClientProvider; +import tech.pegasys.teku.ethereum.executionlayer.ExecutionLayerManager; +import tech.pegasys.teku.infrastructure.events.EventChannels; +import tech.pegasys.teku.spec.executionlayer.ExecutionLayerChannel; + +public class ExecutionLayerServiceTest { + + private final EventChannels eventChannels = mock(EventChannels.class); + private final ExecutionWeb3jClientProvider engineWeb3jClientProvider = + mock(ExecutionWeb3jClientProvider.class); + private final ExecutionLayerManager executionLayerManager = mock(ExecutionLayerManager.class); + + private final ExecutionLayerService underTest = + new ExecutionLayerService(eventChannels, engineWeb3jClientProvider, executionLayerManager); + + @Test + public void addsSubscribersToTheEventChannelOnServiceStart() { + when(eventChannels.subscribe(any(), any())).thenReturn(eventChannels); + + underTest.start().reportExceptions(); + + verify(eventChannels).subscribe(SlotEventsChannel.class, executionLayerManager); + verify(eventChannels).subscribe(ExecutionLayerChannel.class, executionLayerManager); + + underTest.stop().reportExceptions(); + } + + @Test + public void engineClientProviderIsEmptyIfStub() { + when(engineWeb3jClientProvider.isStub()).thenReturn(true); + assertThat(underTest.getEngineWeb3jClientProvider()).isEmpty(); + } + + @Test + public void engineClientProviderIsPresentIfNotStub() { + when(engineWeb3jClientProvider.isStub()).thenReturn(false); + assertThat(underTest.getEngineWeb3jClientProvider()).hasValue(engineWeb3jClientProvider); + } +} diff --git a/teku/src/main/java/tech/pegasys/teku/services/BeaconNodeServiceController.java b/teku/src/main/java/tech/pegasys/teku/services/BeaconNodeServiceController.java index adb45f9f35a..9923aa56989 100644 --- a/teku/src/main/java/tech/pegasys/teku/services/BeaconNodeServiceController.java +++ b/teku/src/main/java/tech/pegasys/teku/services/BeaconNodeServiceController.java @@ -34,7 +34,7 @@ public BeaconNodeServiceController( if (tekuConfig.executionLayer().isEnabled()) { // Need to make sure the execution engine is listening before starting the beacon chain ExecutionLayerService executionLayerService = - new ExecutionLayerService(serviceConfig, tekuConfig.executionLayer()); + ExecutionLayerService.create(serviceConfig, tekuConfig.executionLayer()); services.add(executionLayerService); maybeExecutionWeb3jClientProvider = executionLayerService.getEngineWeb3jClientProvider(); }