diff --git a/ethereum/executionclient/build.gradle b/ethereum/executionclient/build.gradle index 81ffaa3c283..c3fe7dd344d 100644 --- a/ethereum/executionclient/build.gradle +++ b/ethereum/executionclient/build.gradle @@ -17,6 +17,7 @@ dependencies { testImplementation testFixtures(project(':infrastructure:async')) testImplementation testFixtures(project(':infrastructure:time')) + testImplementation testFixtures(project(':infrastructure:metrics')) testImplementation testFixtures(project(':ethereum:spec')) integrationTestImplementation testFixtures(project(':infrastructure:json')) diff --git a/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/ThrottlingExecutionEngineClient.java b/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/ThrottlingExecutionEngineClient.java index 4300d185a7d..c1a40f454e0 100644 --- a/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/ThrottlingExecutionEngineClient.java +++ b/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/ThrottlingExecutionEngineClient.java @@ -53,7 +53,7 @@ public SafeFuture> getPowBlock(final Bytes32 blockHash) { @Override public SafeFuture getPowChainHead() { - return taskQueue.queueTask(() -> delegate.getPowChainHead()); + return taskQueue.queueTask(delegate::getPowChainHead); } @Override diff --git a/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/metrics/MetricRecordingExecutionBuilderClient.java b/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/metrics/MetricRecordingExecutionBuilderClient.java new file mode 100644 index 00000000000..42d1cc19eb5 --- /dev/null +++ b/ethereum/executionclient/src/main/java/tech/pegasys/teku/ethereum/executionclient/metrics/MetricRecordingExecutionBuilderClient.java @@ -0,0 +1,132 @@ +/* + * 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.executionclient.metrics; + +import static tech.pegasys.teku.ethereum.executionclient.metrics.MetricRecordingExecutionBuilderClient.RequestOutcome.ERROR; +import static tech.pegasys.teku.ethereum.executionclient.metrics.MetricRecordingExecutionBuilderClient.RequestOutcome.SUCCESS; + +import java.util.List; +import java.util.Map; +import org.apache.tuweni.bytes.Bytes32; +import org.hyperledger.besu.plugin.services.MetricsSystem; +import tech.pegasys.teku.bls.BLSPublicKey; +import tech.pegasys.teku.ethereum.executionclient.ExecutionBuilderClient; +import tech.pegasys.teku.ethereum.executionclient.schema.Response; +import tech.pegasys.teku.infrastructure.async.SafeFuture; +import tech.pegasys.teku.infrastructure.metrics.MetricsCountersByIntervals; +import tech.pegasys.teku.infrastructure.metrics.TekuMetricCategory; +import tech.pegasys.teku.infrastructure.ssz.SszList; +import tech.pegasys.teku.infrastructure.time.TimeProvider; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.datastructures.blocks.SignedBeaconBlock; +import tech.pegasys.teku.spec.datastructures.execution.ExecutionPayload; +import tech.pegasys.teku.spec.datastructures.execution.SignedBuilderBid; +import tech.pegasys.teku.spec.datastructures.execution.SignedValidatorRegistration; + +public class MetricRecordingExecutionBuilderClient implements ExecutionBuilderClient { + + public static final String BUILDER_REQUESTS_COUNTER_NAME = "builder_requests_total"; + + public static final String STATUS_METHOD = "status"; + public static final String REGISTER_VALIDATORS_METHOD = "register_validators"; + public static final String GET_HEADER_METHOD = "get_header"; + public static final String GET_PAYLOAD_METHOD = "get_payload"; + + private final ExecutionBuilderClient delegate; + private final TimeProvider timeProvider; + + private final MetricsCountersByIntervals builderRequestsCountersByIntervals; + + public MetricRecordingExecutionBuilderClient( + final ExecutionBuilderClient delegate, + final TimeProvider timeProvider, + final MetricsSystem metricsSystem) { + this.delegate = delegate; + this.timeProvider = timeProvider; + + builderRequestsCountersByIntervals = + MetricsCountersByIntervals.create( + TekuMetricCategory.BEACON, + metricsSystem, + BUILDER_REQUESTS_COUNTER_NAME, + "Counter recording the number of requests made to the builder by method, outcome and execution time interval", + List.of("method", "outcome"), + Map.of(List.of(), List.of(100L, 300L, 500L, 1000L, 2000L, 3000L, 5000L))); + } + + @Override + public SafeFuture> status() { + return countRequest(delegate::status, STATUS_METHOD); + } + + @Override + public SafeFuture> registerValidators( + final UInt64 slot, final SszList signedValidatorRegistrations) { + return countRequest( + () -> delegate.registerValidators(slot, signedValidatorRegistrations), + REGISTER_VALIDATORS_METHOD); + } + + @Override + public SafeFuture> getHeader( + final UInt64 slot, final BLSPublicKey pubKey, final Bytes32 parentHash) { + return countRequest(() -> delegate.getHeader(slot, pubKey, parentHash), GET_HEADER_METHOD); + } + + @Override + public SafeFuture> getPayload( + final SignedBeaconBlock signedBlindedBeaconBlock) { + return countRequest(() -> delegate.getPayload(signedBlindedBeaconBlock), GET_PAYLOAD_METHOD); + } + + private SafeFuture> countRequest( + final RequestRunner requestRunner, final String method) { + final UInt64 startTime = timeProvider.getTimeInMillis(); + return requestRunner + .run() + .catchAndRethrow(__ -> recordRequestError(startTime, method)) + .thenPeek( + response -> { + if (response.isFailure()) { + recordRequestError(startTime, method); + } else { + recordRequestSuccess(startTime, method); + } + }); + } + + private void recordRequestSuccess(final UInt64 startTime, final String method) { + recordRequest(startTime, method, SUCCESS); + } + + private void recordRequestError(final UInt64 startTime, final String method) { + recordRequest(startTime, method, ERROR); + } + + private void recordRequest( + final UInt64 startTime, final String method, final RequestOutcome requestOutcome) { + final UInt64 duration = timeProvider.getTimeInMillis().minusMinZero(startTime); + builderRequestsCountersByIntervals.recordValue(duration, method, requestOutcome.name()); + } + + @FunctionalInterface + private interface RequestRunner { + SafeFuture> run(); + } + + enum RequestOutcome { + SUCCESS, + ERROR; + } +} diff --git a/ethereum/executionclient/src/test/java/tech/pegasys/teku/ethereum/executionclient/metrics/MetricRecordingExecutionBuilderClientTest.java b/ethereum/executionclient/src/test/java/tech/pegasys/teku/ethereum/executionclient/metrics/MetricRecordingExecutionBuilderClientTest.java new file mode 100644 index 00000000000..70b2bcc7842 --- /dev/null +++ b/ethereum/executionclient/src/test/java/tech/pegasys/teku/ethereum/executionclient/metrics/MetricRecordingExecutionBuilderClientTest.java @@ -0,0 +1,177 @@ +/* + * 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.executionclient.metrics; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static tech.pegasys.teku.ethereum.executionclient.metrics.MetricRecordingExecutionBuilderClient.BUILDER_REQUESTS_COUNTER_NAME; + +import java.util.function.Function; +import java.util.stream.Stream; +import org.apache.tuweni.bytes.Bytes32; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Named; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import tech.pegasys.teku.bls.BLSPublicKey; +import tech.pegasys.teku.ethereum.executionclient.ExecutionBuilderClient; +import tech.pegasys.teku.ethereum.executionclient.metrics.MetricRecordingExecutionBuilderClient.RequestOutcome; +import tech.pegasys.teku.ethereum.executionclient.schema.Response; +import tech.pegasys.teku.infrastructure.async.SafeFuture; +import tech.pegasys.teku.infrastructure.metrics.StubMetricsSystem; +import tech.pegasys.teku.infrastructure.metrics.TekuMetricCategory; +import tech.pegasys.teku.infrastructure.ssz.SszList; +import tech.pegasys.teku.infrastructure.time.StubTimeProvider; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.TestSpecFactory; +import tech.pegasys.teku.spec.datastructures.blocks.SignedBeaconBlock; +import tech.pegasys.teku.spec.datastructures.execution.ExecutionPayload; +import tech.pegasys.teku.spec.datastructures.execution.SignedBuilderBid; +import tech.pegasys.teku.spec.datastructures.execution.SignedValidatorRegistration; +import tech.pegasys.teku.spec.util.DataStructureUtil; + +class MetricRecordingExecutionBuilderClientTest { + + private static final long RESPONSE_DELAY = 1300; + private static final String EXPECTED_TIME_INTERVAL = "[1000,2000)"; + + private final ExecutionBuilderClient delegate = mock(ExecutionBuilderClient.class); + private final StubMetricsSystem metricsSystem = new StubMetricsSystem(); + + private StubTimeProvider stubTimeProvider; + private MetricRecordingExecutionBuilderClient executionBuilderClient; + + @BeforeEach + void setup() { + stubTimeProvider = StubTimeProvider.withTimeInMillis(0); + executionBuilderClient = + new MetricRecordingExecutionBuilderClient(delegate, stubTimeProvider, metricsSystem); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("getRequestArguments") + public void shouldCountSuccessfulRequest( + final Function> requestRunner, + final String method, + final Object value) { + setupResponse(requestRunner, SafeFuture.completedFuture(value)); + + final SafeFuture result = requestRunner.apply(executionBuilderClient); + + assertThat(result).isCompletedWithValue(value); + + assertThat(getCounterValue(method, RequestOutcome.SUCCESS)).isOne(); + assertThat(getCounterValue(method, RequestOutcome.ERROR)).isZero(); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("getRequestArguments") + public void shouldCountRequestWithFailedFutureResponse( + final Function> requestRunner, + final String method) { + final RuntimeException exception = new RuntimeException("Nope"); + setupResponse(requestRunner, SafeFuture.failedFuture(exception)); + + final SafeFuture result = requestRunner.apply(executionBuilderClient); + assertThat(result).isCompletedExceptionally(); + assertThatThrownBy(result::join).hasRootCause(exception); + + assertThat(getCounterValue(method, RequestOutcome.ERROR)).isOne(); + assertThat(getCounterValue(method, RequestOutcome.SUCCESS)).isZero(); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("getRequestWithResponseFailureArguments") + public void shouldCountRequestWithResponseFailure( + final Function> requestRunner, + final String method, + final Object value) { + setupResponse(requestRunner, SafeFuture.completedFuture(value)); + + final SafeFuture result = requestRunner.apply(executionBuilderClient); + + assertThat(result).isCompletedWithValue(value); + + assertThat(getCounterValue(method, RequestOutcome.ERROR)).isOne(); + assertThat(getCounterValue(method, RequestOutcome.SUCCESS)).isZero(); + } + + private void setupResponse( + final Function> requestRunner, + final SafeFuture response) { + when(requestRunner.apply(delegate)) + .thenAnswer( + __ -> { + stubTimeProvider.advanceTimeByMillis(RESPONSE_DELAY); + return response; + }); + } + + private long getCounterValue(final String method, final RequestOutcome requestOutcome) { + return metricsSystem + .getCounter(TekuMetricCategory.BEACON, BUILDER_REQUESTS_COUNTER_NAME) + .getValue(method, requestOutcome.name(), EXPECTED_TIME_INTERVAL); + } + + public static Stream getRequestWithResponseFailureArguments() { + return getRequestArguments() + .peek(arguments -> arguments.get()[2] = Response.withErrorMessage("oopsy")); + } + + public static Stream getRequestArguments() { + final DataStructureUtil dataStructureUtil = + new DataStructureUtil(TestSpecFactory.createMinimalBellatrix()); + final UInt64 slot = dataStructureUtil.randomUInt64(); + final SszList validatorRegistrations = + dataStructureUtil.randomSignedValidatorRegistrations(3); + final BLSPublicKey publicKey = dataStructureUtil.randomPublicKey(); + final Bytes32 parentHash = dataStructureUtil.randomBytes32(); + final SignedBuilderBid builderBid = dataStructureUtil.randomSignedBuilderBid(); + final SignedBeaconBlock beaconBlock = dataStructureUtil.randomSignedBlindedBeaconBlock(); + final ExecutionPayload executionPayload = dataStructureUtil.randomExecutionPayload(); + + return Stream.of( + getArguments( + "status", + ExecutionBuilderClient::status, + MetricRecordingExecutionBuilderClient.STATUS_METHOD, + Response.withNullPayload()), + getArguments( + "registerValidators", + client -> client.registerValidators(slot, validatorRegistrations), + MetricRecordingExecutionBuilderClient.REGISTER_VALIDATORS_METHOD, + Response.withNullPayload()), + getArguments( + "getHeader", + client -> client.getHeader(slot, publicKey, parentHash), + MetricRecordingExecutionBuilderClient.GET_HEADER_METHOD, + new Response<>(builderBid)), + getArguments( + "getPayload", + client -> client.getPayload(beaconBlock), + MetricRecordingExecutionBuilderClient.GET_PAYLOAD_METHOD, + new Response<>(executionPayload))); + } + + private static Arguments getArguments( + final String name, + final Function> method, + final String counterName, + final T value) { + return Arguments.of(Named.of(name, method), counterName, value); + } +} diff --git a/ethereum/executionlayer/build.gradle b/ethereum/executionlayer/build.gradle index 36fa4726f0f..594d90fd647 100644 --- a/ethereum/executionlayer/build.gradle +++ b/ethereum/executionlayer/build.gradle @@ -12,6 +12,7 @@ dependencies { testImplementation testFixtures(project(':infrastructure:async')) testImplementation testFixtures(project(':infrastructure:bls')) + testImplementation testFixtures(project(':infrastructure:metrics')) testImplementation testFixtures(project(':ethereum:spec')) } diff --git a/ethereum/executionlayer/src/main/java/tech/pegasys/teku/ethereum/executionlayer/ExecutionLayerManagerImpl.java b/ethereum/executionlayer/src/main/java/tech/pegasys/teku/ethereum/executionlayer/ExecutionLayerManagerImpl.java index 84bbe303730..5fed1024d9c 100644 --- a/ethereum/executionlayer/src/main/java/tech/pegasys/teku/ethereum/executionlayer/ExecutionLayerManagerImpl.java +++ b/ethereum/executionlayer/src/main/java/tech/pegasys/teku/ethereum/executionlayer/ExecutionLayerManagerImpl.java @@ -27,11 +27,14 @@ import org.apache.logging.log4j.Logger; import org.apache.tuweni.bytes.Bytes32; import org.hyperledger.besu.plugin.services.MetricsSystem; +import org.hyperledger.besu.plugin.services.metrics.Counter; +import org.hyperledger.besu.plugin.services.metrics.LabelledMetric; import tech.pegasys.teku.bls.BLSPublicKey; import tech.pegasys.teku.ethereum.executionclient.ExecutionBuilderClient; import tech.pegasys.teku.ethereum.executionclient.ExecutionEngineClient; import tech.pegasys.teku.ethereum.executionclient.ThrottlingExecutionBuilderClient; import tech.pegasys.teku.ethereum.executionclient.ThrottlingExecutionEngineClient; +import tech.pegasys.teku.ethereum.executionclient.metrics.MetricRecordingExecutionBuilderClient; import tech.pegasys.teku.ethereum.executionclient.rest.RestClient; import tech.pegasys.teku.ethereum.executionclient.rest.RestExecutionBuilderClient; import tech.pegasys.teku.ethereum.executionclient.schema.ExecutionPayloadV1; @@ -46,7 +49,9 @@ 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.metrics.TekuMetricCategory; import tech.pegasys.teku.infrastructure.ssz.SszList; +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.SpecMilestone; @@ -65,13 +70,13 @@ import tech.pegasys.teku.spec.schemas.SchemaDefinitionsBellatrix; public class ExecutionLayerManagerImpl implements ExecutionLayerManager { + private static final Logger LOG = LogManager.getLogger(); private static final UInt64 FALLBACK_DATA_RETENTION_SLOTS = UInt64.valueOf(2); - private final ExecutionEngineClient executionEngineClient; - private final Optional executionBuilderClient; - - private final AtomicBoolean latestBuilderAvailability; + static final String LOCAL_EL_SOURCE = "local_el"; + static final String BUILDER_SOURCE = "builder"; + static final String BUILDER_LOCAL_EL_FALLBACK_SOURCE = "builder_local_el_fallback"; /** * slotToLocalElFallbackPayload usage: @@ -85,26 +90,31 @@ public class ExecutionLayerManagerImpl implements ExecutionLayerManager { private final NavigableMap> slotToLocalElFallbackPayload = new ConcurrentSkipListMap<>(); + private final ExecutionEngineClient executionEngineClient; + private final Optional executionBuilderClient; + private final AtomicBoolean latestBuilderAvailability; private final Spec spec; - private final EventLogger eventLogger; private final BuilderBidValidator builderBidValidator; + private final LabelledMetric executionPayloadSourceCounter; public static ExecutionLayerManagerImpl create( final Web3JClient engineWeb3JClient, final Optional builderRestClient, final Version version, final Spec spec, + final TimeProvider timeProvider, final MetricsSystem metricsSystem, final BuilderBidValidator builderBidValidator) { checkNotNull(version); return new ExecutionLayerManagerImpl( createEngineClient(version, engineWeb3JClient, metricsSystem), - createBuilderClient(builderRestClient, spec, metricsSystem), + createBuilderClient(builderRestClient, spec, timeProvider, metricsSystem), spec, EVENT_LOG, - builderBidValidator); + builderBidValidator, + metricsSystem); } private static ExecutionEngineClient createEngineClient( @@ -120,13 +130,18 @@ private static ExecutionEngineClient createEngineClient( private static Optional createBuilderClient( final Optional builderRestClient, final Spec spec, + final TimeProvider timeProvider, final MetricsSystem metricsSystem) { return builderRestClient.map( - client -> - new ThrottlingExecutionBuilderClient( - new RestExecutionBuilderClient(client, spec), - MAXIMUM_CONCURRENT_EB_REQUESTS, - metricsSystem)); + client -> { + final RestExecutionBuilderClient restBuilderClient = + new RestExecutionBuilderClient(client, spec); + final MetricRecordingExecutionBuilderClient metricRecordingBuilderClient = + new MetricRecordingExecutionBuilderClient( + restBuilderClient, timeProvider, metricsSystem); + return new ThrottlingExecutionBuilderClient( + metricRecordingBuilderClient, MAXIMUM_CONCURRENT_EB_REQUESTS, metricsSystem); + }); } ExecutionLayerManagerImpl( @@ -134,13 +149,20 @@ private static Optional createBuilderClient( final Optional executionBuilderClient, final Spec spec, final EventLogger eventLogger, - final BuilderBidValidator builderBidValidator) { + final BuilderBidValidator builderBidValidator, + final MetricsSystem metricsSystem) { this.executionEngineClient = executionEngineClient; this.executionBuilderClient = executionBuilderClient; this.latestBuilderAvailability = new AtomicBoolean(executionBuilderClient.isPresent()); this.spec = spec; this.eventLogger = eventLogger; this.builderBidValidator = builderBidValidator; + executionPayloadSourceCounter = + metricsSystem.createLabelledCounter( + TekuMetricCategory.BEACON, + "execution_payload_source", + "Counter recording the source of the execution payload during block production", + "source"); } @Override @@ -227,12 +249,16 @@ && isBuilderAvailable() .getExecutionPayloadSchema()), ExecutionPayloadV1::asInternalExecutionPayload) .thenPeek( - executionPayload -> - LOG.trace( - "engineGetPayload(payloadId={}, slot={}) -> {}", - executionPayloadContext.getPayloadId(), - slot, - executionPayload)); + executionPayload -> { + if (!isFallbackCall) { + recordExecutionPayloadSource(LOCAL_EL_SOURCE); + } + LOG.trace( + "engineGetPayload(payloadId={}, slot={}) -> {}", + executionPayloadContext.getPayloadId(), + slot, + executionPayload); + }); } @Override @@ -375,6 +401,8 @@ public SafeFuture builderGetPayload( // note: we don't do any particular consistency check here. // the header/payload compatibility check is done by SignedBeaconBlockUnblinder + recordExecutionPayloadSource(BUILDER_LOCAL_EL_FALLBACK_SOURCE); + return SafeFuture.completedFuture(maybeLocalElFallbackPayload.get()); } @@ -404,11 +432,13 @@ private SafeFuture getPayloadFromBuilder( .getPayload(signedBlindedBeaconBlock) .thenApply(ExecutionLayerManagerImpl::unwrapResponseOrThrow) .thenPeek( - executionPayload -> - LOG.trace( - "builderGetPayload(signedBlindedBeaconBlock={}) -> {}", - signedBlindedBeaconBlock, - executionPayload)); + executionPayload -> { + recordExecutionPayloadSource(BUILDER_SOURCE); + LOG.trace( + "builderGetPayload(signedBlindedBeaconBlock={}) -> {}", + signedBlindedBeaconBlock, + executionPayload); + }); } boolean isBuilderAvailable() { @@ -455,4 +485,8 @@ private void logReceivedBuilderBid(final BuilderBid builderBid) { payloadHeader.getGasLimit(), payloadHeader.getGasUsed()); } + + private void recordExecutionPayloadSource(final String source) { + executionPayloadSourceCounter.labels(source).inc(); + } } 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 index ecfc6a8636f..db938bdea08 100644 --- 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 @@ -20,6 +20,9 @@ import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; +import static tech.pegasys.teku.ethereum.executionlayer.ExecutionLayerManagerImpl.BUILDER_LOCAL_EL_FALLBACK_SOURCE; +import static tech.pegasys.teku.ethereum.executionlayer.ExecutionLayerManagerImpl.BUILDER_SOURCE; +import static tech.pegasys.teku.ethereum.executionlayer.ExecutionLayerManagerImpl.LOCAL_EL_SOURCE; import java.util.Optional; import java.util.stream.IntStream; @@ -31,6 +34,8 @@ 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.metrics.StubMetricsSystem; +import tech.pegasys.teku.infrastructure.metrics.TekuMetricCategory; import tech.pegasys.teku.infrastructure.unsigned.UInt64; import tech.pegasys.teku.spec.Spec; import tech.pegasys.teku.spec.TestSpecFactory; @@ -51,8 +56,11 @@ class ExecutionLayerManagerImplTest { Mockito.mock(ExecutionBuilderClient.class); private final Spec spec = TestSpecFactory.createMinimalBellatrix(); + private final DataStructureUtil dataStructureUtil = new DataStructureUtil(spec); + private final StubMetricsSystem stubMetricsSystem = new StubMetricsSystem(); + private final EventLogger eventLogger = mock(EventLogger.class); private ExecutionLayerManagerImpl executionLayerManager = @@ -72,7 +80,7 @@ public void builderShouldNotBeAvailableWhenBuilderNotEnabled() { @Test public void builderShouldBeAvailableWhenBuilderIsOperatingNormally() { - SafeFuture> builderClientResponse = + final SafeFuture> builderClientResponse = SafeFuture.completedFuture(Response.withNullPayload()); updateBuilderStatus(builderClientResponse); @@ -83,7 +91,7 @@ public void builderShouldBeAvailableWhenBuilderIsOperatingNormally() { @Test public void builderShouldNotBeAvailableWhenBuilderIsNotOperatingNormally() { - SafeFuture> builderClientResponse = + final SafeFuture> builderClientResponse = SafeFuture.completedFuture(Response.withErrorMessage("oops")); updateBuilderStatus(builderClientResponse); @@ -94,7 +102,7 @@ public void builderShouldNotBeAvailableWhenBuilderIsNotOperatingNormally() { @Test public void builderShouldNotBeAvailableWhenBuilderStatusCallFails() { - SafeFuture> builderClientResponse = + final SafeFuture> builderClientResponse = SafeFuture.failedFuture(new Throwable("oops")); updateBuilderStatus(builderClientResponse); @@ -130,6 +138,23 @@ public void builderAvailabilityIsUpdatedOnSlotEventAndLoggedAdequately() { verify(eventLogger).executionBuilderIsBackOnline(); } + @Test + public void engineGetPayload_shouldReturnPayloadViaEngine() { + final ExecutionPayloadContext executionPayloadContext = + dataStructureUtil.randomPayloadExecutionContext(false, true); + final UInt64 slot = executionPayloadContext.getForkChoiceState().getHeadBlockSlot(); + + final ExecutionPayload payload = prepareEngineGetPayloadResponse(executionPayloadContext); + + assertThat(executionLayerManager.engineGetPayload(executionPayloadContext, slot)) + .isCompletedWithValue(payload); + + // we expect no calls to builder + verifyNoInteractions(executionBuilderClient); + + verifySourceCounter(LOCAL_EL_SOURCE); + } + @Test public void builderGetHeaderGetPayload_shouldReturnHeaderAndPayloadViaBuilder() { setBuilderOnline(); @@ -169,6 +194,8 @@ public void builderGetHeaderGetPayload_shouldReturnHeaderAndPayloadViaBuilder() // we expect both builder and local engine have been called verify(executionBuilderClient).getPayload(signedBlindedBeaconBlock); verifyNoMoreInteractions(executionEngineClient); + + verifySourceCounter(BUILDER_SOURCE); } @Test @@ -216,6 +243,8 @@ public void builderGetHeaderGetPayload_shouldReturnHeaderAndPayloadViaEngineOnBu // we expect no additional calls verifyNoMoreInteractions(executionBuilderClient); verifyNoMoreInteractions(executionEngineClient); + + verifySourceCounter(BUILDER_LOCAL_EL_FALLBACK_SOURCE); } @Test @@ -265,6 +294,8 @@ public void builderGetHeaderGetPayload_shouldReturnHeaderAndPayloadViaEngineOnBu // we expect no additional calls verifyNoMoreInteractions(executionBuilderClient); verifyNoMoreInteractions(executionEngineClient); + + verifySourceCounter(BUILDER_LOCAL_EL_FALLBACK_SOURCE); } @Test @@ -304,6 +335,8 @@ public void builderGetHeaderGetPayload_shouldReturnHeaderAndPayloadViaEngineIfBu // we expect no additional calls verifyNoMoreInteractions(executionBuilderClient); verifyNoMoreInteractions(executionEngineClient); + + verifySourceCounter(BUILDER_LOCAL_EL_FALLBACK_SOURCE); } @Test @@ -344,6 +377,8 @@ public void builderGetHeaderGetPayload_shouldReturnHeaderAndPayloadViaEngineIfBu // we expect no additional calls verifyNoMoreInteractions(executionBuilderClient); verifyNoMoreInteractions(executionEngineClient); + + verifySourceCounter(BUILDER_LOCAL_EL_FALLBACK_SOURCE); } @Test @@ -384,6 +419,8 @@ public void builderGetHeaderGetPayload_shouldReturnHeaderAndPayloadViaEngineIfBu // we expect no additional calls verifyNoMoreInteractions(executionBuilderClient); verifyNoMoreInteractions(executionEngineClient); + + verifySourceCounter(BUILDER_LOCAL_EL_FALLBACK_SOURCE); } @Test @@ -430,7 +467,7 @@ private ExecutionPayloadHeader prepareBuilderGetHeaderResponse( final ExecutionPayloadContext executionPayloadContext) { final UInt64 slot = executionPayloadContext.getForkChoiceState().getHeadBlockSlot(); - SignedBuilderBid signedBuilderBid = dataStructureUtil.randomSignedBuilderBid(); + final SignedBuilderBid signedBuilderBid = dataStructureUtil.randomSignedBuilderBid(); when(executionBuilderClient.getHeader( slot, @@ -483,7 +520,7 @@ private ExecutionPayload prepareEngineGetPayloadResponse( } private ExecutionLayerManagerImpl createExecutionLayerChannelImpl( - boolean builderEnabled, boolean builderValidatorEnabled) { + final boolean builderEnabled, final boolean builderValidatorEnabled) { return new ExecutionLayerManagerImpl( executionEngineClient, builderEnabled ? Optional.of(executionBuilderClient) : Optional.empty(), @@ -491,10 +528,11 @@ private ExecutionLayerManagerImpl createExecutionLayerChannelImpl( eventLogger, builderValidatorEnabled ? new BuilderBidValidatorImpl(eventLogger) - : BuilderBidValidator.NOOP); + : BuilderBidValidator.NOOP, + stubMetricsSystem); } - private void updateBuilderStatus(SafeFuture> builderClientResponse) { + private void updateBuilderStatus(final SafeFuture> builderClientResponse) { updateBuilderStatus(builderClientResponse, UInt64.ONE); } @@ -508,19 +546,23 @@ private void setBuilderOffline() { setBuilderOffline(UInt64.ONE); } - private void setBuilderOffline(UInt64 slot) { + private void setBuilderOffline(final UInt64 slot) { updateBuilderStatus(SafeFuture.completedFuture(Response.withErrorMessage("oops")), slot); reset(executionBuilderClient); assertThat(executionLayerManager.isBuilderAvailable()).isFalse(); } private void setBuilderOnline() { - setBuilderOnline(UInt64.ONE); - } - - private void setBuilderOnline(UInt64 slot) { - updateBuilderStatus(SafeFuture.completedFuture(Response.withNullPayload()), slot); + updateBuilderStatus(SafeFuture.completedFuture(Response.withNullPayload()), UInt64.ONE); reset(executionBuilderClient); assertThat(executionLayerManager.isBuilderAvailable()).isTrue(); } + + private void verifySourceCounter(final String source) { + final long actualCount = + stubMetricsSystem + .getCounter(TekuMetricCategory.BEACON, "execution_payload_source") + .getValue(source); + assertThat(actualCount).isOne(); + } } 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 842c67d4e9d..9dfca5f1afd 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 @@ -95,6 +95,7 @@ public static ExecutionLayerService create( builderRestClientProvider.map(RestClientProvider::getRestClient), config.getEngineVersion(), config.getSpec(), + timeProvider, metricsSystem, new BuilderBidValidatorImpl(EVENT_LOG)); }