diff --git a/.gitignore b/.gitignore index 6ef5a3e2d79..ae80a2e96a4 100644 --- a/.gitignore +++ b/.gitignore @@ -29,7 +29,6 @@ target/ tmp/ build/ out/ -ganache-cli/ pow/src/main/resources/keys.json !validator_test_data.json /interopDepositsAndKeys.json diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 842f2823bf6..00000000000 --- a/.gitmodules +++ /dev/null @@ -1,4 +0,0 @@ -[submodule "ganache-cli"] - path = ganache-cli - url = https://github.com/trufflesuite/ganache-cli.git - diff --git a/CHANGELOG.md b/CHANGELOG.md index 978da50de66..f5b456b13ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,6 @@ For information on changes in released versions of Teku, see the [releases page] ### Additions and Improvements -Introduce `--exchange-capabilities-monitoring-enabled` parameter. If enabled, EL will be queried periodically for the Engine API methods it supports. If incompatibility is detected, there will be a warning raised in the logs. The default is `true`. +- Introduce `--exchange-capabilities-monitoring-enabled` parameter. If enabled, EL will be queried periodically for the Engine API methods it supports. If incompatibility is detected, a warning is raised in the logs. The default is `true`. ### Bug Fixes diff --git a/data/provider/src/main/java/tech/pegasys/teku/api/AbstractSelectorFactory.java b/data/provider/src/main/java/tech/pegasys/teku/api/AbstractSelectorFactory.java index 709c107e713..228192d1d48 100644 --- a/data/provider/src/main/java/tech/pegasys/teku/api/AbstractSelectorFactory.java +++ b/data/provider/src/main/java/tech/pegasys/teku/api/AbstractSelectorFactory.java @@ -93,17 +93,13 @@ private boolean isHexString(final String identifier) { } private T createSelectorForKeywordOrSlot(final String identifier) { - switch (identifier) { - case HEAD: - return headSelector(); - case GENESIS: - return genesisSelector(); - case FINALIZED: - return finalizedSelector(); - case JUSTIFIED: - return justifiedSelector(); - } - return slotSelector(UInt64.valueOf(identifier)); + return switch (identifier) { + case HEAD -> headSelector(); + case GENESIS -> genesisSelector(); + case FINALIZED -> finalizedSelector(); + case JUSTIFIED -> justifiedSelector(); + default -> slotSelector(UInt64.valueOf(identifier)); + }; } private BadRequestException badRequestException(final String type, final String identifier) { diff --git a/gradle/versions.gradle b/gradle/versions.gradle index 4bc27c5c60a..3cf936c86b4 100644 --- a/gradle/versions.gradle +++ b/gradle/versions.gradle @@ -87,7 +87,6 @@ dependencyManagement { dependency 'org.apache.commons:commons-text:1.10.0' dependency 'org.apache.commons:commons-lang3:3.12.0' dependency 'commons-io:commons-io:2.13.0' - dependency 'org.apache.commons:commons-compress:1.21' dependency 'org.commonjava.mimeparse:mimeparse:0.1.3.3' dependencySet(group: 'org.apache.logging.log4j', version: '2.20.0') { diff --git a/infrastructure/ssz/src/main/java/tech/pegasys/teku/infrastructure/ssz/primitive/SszUInt64.java b/infrastructure/ssz/src/main/java/tech/pegasys/teku/infrastructure/ssz/primitive/SszUInt64.java index cb901e99fe8..bb7cf523a7e 100644 --- a/infrastructure/ssz/src/main/java/tech/pegasys/teku/infrastructure/ssz/primitive/SszUInt64.java +++ b/infrastructure/ssz/src/main/java/tech/pegasys/teku/infrastructure/ssz/primitive/SszUInt64.java @@ -19,13 +19,24 @@ public class SszUInt64 extends AbstractSszPrimitive { - public static final SszUInt64 ZERO = SszUInt64.of(UInt64.ZERO); + public static final SszUInt64 ZERO = new SszUInt64(UInt64.ZERO); + public static final SszUInt64 THIRTY_TWO_ETH = new SszUInt64(UInt64.THIRTY_TWO_ETH); + public static final SszUInt64 MAX_VALUE = new SszUInt64(UInt64.MAX_VALUE); - public static SszUInt64 of(UInt64 val) { + public static SszUInt64 of(final UInt64 val) { + if (val.isZero()) { + return ZERO; + } + if (val.isThirtyTwoEth()) { + return THIRTY_TWO_ETH; + } + if (val.isMaxValue()) { + return MAX_VALUE; + } return new SszUInt64(val); } - private SszUInt64(UInt64 val) { + private SszUInt64(final UInt64 val) { super(val, SszPrimitiveSchemas.UINT64_SCHEMA); } diff --git a/infrastructure/unsigned/src/main/java/tech/pegasys/teku/infrastructure/unsigned/UInt64.java b/infrastructure/unsigned/src/main/java/tech/pegasys/teku/infrastructure/unsigned/UInt64.java index 84d4b22aa5a..ca4a8803a2e 100644 --- a/infrastructure/unsigned/src/main/java/tech/pegasys/teku/infrastructure/unsigned/UInt64.java +++ b/infrastructure/unsigned/src/main/java/tech/pegasys/teku/infrastructure/unsigned/UInt64.java @@ -28,6 +28,7 @@ public final class UInt64 implements Comparable { public static final UInt64 ZERO = new UInt64(0); public static final UInt64 ONE = new UInt64(1); + public static final UInt64 THIRTY_TWO_ETH = new UInt64(32_000_000_000L); public static final UInt64 MAX_VALUE = new UInt64(-1L); private final long value; @@ -82,7 +83,16 @@ public static UInt64 valueOf(final BigInteger value) { * @return the created UInt64. */ public static UInt64 fromLongBits(final long value) { - return value == 0 ? ZERO : new UInt64(value); + if (value == 0L) { + return ZERO; + } + if (value == 32_000_000_000L) { + return THIRTY_TWO_ETH; + } + if (value == -1L) { + return MAX_VALUE; + } + return new UInt64(value); } /** @@ -365,6 +375,19 @@ public boolean isZero() { return value == 0; } + /** + * Returns true if the value is 32_000_000_000L + * + * @return true if the value is 32_000_000_000L + */ + public boolean isThirtyTwoEth() { + return value == 32_000_000_000L; + } + + public boolean isMaxValue() { + return value == -1L; + } + /** * Returns true if this value is strictly greater than the specified value. * diff --git a/infrastructure/unsigned/src/test/java/tech/pegasys/teku/infrastructure/unsigned/UInt64Test.java b/infrastructure/unsigned/src/test/java/tech/pegasys/teku/infrastructure/unsigned/UInt64Test.java index ca7b2a9d4db..a377fc00c29 100644 --- a/infrastructure/unsigned/src/test/java/tech/pegasys/teku/infrastructure/unsigned/UInt64Test.java +++ b/infrastructure/unsigned/src/test/java/tech/pegasys/teku/infrastructure/unsigned/UInt64Test.java @@ -290,6 +290,7 @@ void lessThanOrEqualTo_shouldReturnTrueWhenNumberIsLessThanOrEqual( @Test void constants_shouldHaveExpectedValues() { assertThat(UInt64.ZERO).isEqualTo(UInt64.valueOf(0)); + assertThat(UInt64.THIRTY_TWO_ETH).isEqualTo(UInt64.valueOf(32_000_000_000L)); assertThat(UInt64.ONE).isEqualTo(UInt64.valueOf(1)); assertThat(UInt64.MAX_VALUE).isEqualTo(UInt64.fromLongBits(-1)); } diff --git a/services/chainstorage/build.gradle b/services/chainstorage/build.gradle index 6f261b50900..c338497010e 100644 --- a/services/chainstorage/build.gradle +++ b/services/chainstorage/build.gradle @@ -5,6 +5,7 @@ dependencies { implementation project(':infrastructure:async') implementation project(':infrastructure:exceptions') implementation project(':infrastructure:logging') + implementation project(':infrastructure:metrics') implementation project(':infrastructure:serviceutils') implementation project(':storage') implementation project(':storage:api') diff --git a/services/chainstorage/src/main/java/tech/pegasys/teku/services/chainstorage/StorageService.java b/services/chainstorage/src/main/java/tech/pegasys/teku/services/chainstorage/StorageService.java index 6768912d72f..0f228fde49d 100644 --- a/services/chainstorage/src/main/java/tech/pegasys/teku/services/chainstorage/StorageService.java +++ b/services/chainstorage/src/main/java/tech/pegasys/teku/services/chainstorage/StorageService.java @@ -22,6 +22,8 @@ import tech.pegasys.teku.infrastructure.async.SafeFuture; import tech.pegasys.teku.infrastructure.async.eventthread.AsyncRunnerEventThread; import tech.pegasys.teku.infrastructure.events.EventChannels; +import tech.pegasys.teku.infrastructure.metrics.SettableLabelledGauge; +import tech.pegasys.teku.infrastructure.metrics.TekuMetricCategory; import tech.pegasys.teku.service.serviceutils.Service; import tech.pegasys.teku.service.serviceutils.ServiceConfig; import tech.pegasys.teku.spec.SpecMilestone; @@ -80,6 +82,22 @@ protected SafeFuture doStart() { database.migrate(); + final SettableLabelledGauge pruningTimingsLabelledGauge = + SettableLabelledGauge.create( + serviceConfig.getMetricsSystem(), + TekuMetricCategory.STORAGE, + "pruning_time", + "Tracks last pruning duration in milliseconds", + "type"); + + final SettableLabelledGauge pruningActiveLabelledGauge = + SettableLabelledGauge.create( + serviceConfig.getMetricsSystem(), + TekuMetricCategory.STORAGE, + "pruning_active", + "Tracks when pruner is active", + "type"); + if (!config.getDataStorageMode().storesAllBlocks()) { blockPruner = Optional.of( @@ -87,7 +105,10 @@ protected SafeFuture doStart() { config.getSpec(), database, storagePrunerAsyncRunner, - config.getBlockPruningInterval())); + config.getBlockPruningInterval(), + "block", + pruningTimingsLabelledGauge, + pruningActiveLabelledGauge)); } if (config.getSpec().isMilestoneSupported(SpecMilestone.DENEB)) { blobsPruner = @@ -101,6 +122,9 @@ protected SafeFuture doStart() { config.getBlobsPruningInterval(), config.getBlobsPruningLimit(), blobSidecarsStorageCountersEnabled, + "blob_sidecar", + pruningTimingsLabelledGauge, + pruningActiveLabelledGauge, config.isStoreNonCanonicalBlocksEnabled())); } final EventChannels eventChannels = serviceConfig.getEventChannels(); diff --git a/storage/src/main/java/tech/pegasys/teku/storage/server/pruner/BlobSidecarPruner.java b/storage/src/main/java/tech/pegasys/teku/storage/server/pruner/BlobSidecarPruner.java index 9a971ca98d3..af146083afb 100644 --- a/storage/src/main/java/tech/pegasys/teku/storage/server/pruner/BlobSidecarPruner.java +++ b/storage/src/main/java/tech/pegasys/teku/storage/server/pruner/BlobSidecarPruner.java @@ -24,6 +24,7 @@ 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.metrics.SettableLabelledGauge; import tech.pegasys.teku.infrastructure.metrics.TekuMetricCategory; import tech.pegasys.teku.infrastructure.time.TimeProvider; import tech.pegasys.teku.infrastructure.unsigned.UInt64; @@ -44,6 +45,9 @@ public class BlobSidecarPruner extends Service { private final int pruneLimit; private final TimeProvider timeProvider; private final boolean blobSidecarsStorageCountersEnabled; + private final String pruningMetricsType; + private final SettableLabelledGauge pruningTimingsLabelledGauge; + private final SettableLabelledGauge pruningActiveLabelledGauge; private Optional scheduledPruner = Optional.empty(); private Optional genesisTime = Optional.empty(); @@ -61,6 +65,9 @@ public BlobSidecarPruner( final Duration pruneInterval, final int pruneLimit, final boolean blobSidecarsStorageCountersEnabled, + final String pruningMetricsType, + final SettableLabelledGauge pruningTimingsLabelledGauge, + final SettableLabelledGauge pruningActiveLabelledGauge, final boolean storeNonCanonicalBlobSidecars) { this.spec = spec; this.database = database; @@ -69,6 +76,9 @@ public BlobSidecarPruner( this.pruneLimit = pruneLimit; this.timeProvider = timeProvider; this.blobSidecarsStorageCountersEnabled = blobSidecarsStorageCountersEnabled; + this.pruningMetricsType = pruningMetricsType; + this.pruningTimingsLabelledGauge = pruningTimingsLabelledGauge; + this.pruningActiveLabelledGauge = pruningActiveLabelledGauge; this.storeNonCanonicalBlobSidecars = storeNonCanonicalBlobSidecars; if (blobSidecarsStorageCountersEnabled) { @@ -103,8 +113,14 @@ protected synchronized SafeFuture doStop() { } private void pruneBlobs() { + pruningActiveLabelledGauge.set(1, pruningMetricsType); + final long start = System.currentTimeMillis(); + pruneBlobsPriorToAvailabilityWindow(); + pruningTimingsLabelledGauge.set(System.currentTimeMillis() - start, pruningMetricsType); + pruningActiveLabelledGauge.set(0, pruningMetricsType); + if (blobSidecarsStorageCountersEnabled) { blobColumnSize.set(database.getBlobSidecarColumnCount()); earliestBlobSidecarSlot.set( @@ -147,6 +163,8 @@ private void pruneBlobsPriorToAvailabilityWindow() { nonCanonicalBlobsPruningStart, nonCanonicalBlobsLimitReached); } + final boolean limitReached = database.pruneOldestBlobSidecars(latestPrunableSlot, pruneLimit); + LOG.debug("Blobs pruning finished. Limit reached: {}", () -> limitReached); } catch (ShuttingDownException | RejectedExecutionException ex) { LOG.debug("Shutting down", ex); } diff --git a/storage/src/main/java/tech/pegasys/teku/storage/server/pruner/BlockPruner.java b/storage/src/main/java/tech/pegasys/teku/storage/server/pruner/BlockPruner.java index 2e1a9abc6c9..c5b12d87c99 100644 --- a/storage/src/main/java/tech/pegasys/teku/storage/server/pruner/BlockPruner.java +++ b/storage/src/main/java/tech/pegasys/teku/storage/server/pruner/BlockPruner.java @@ -21,6 +21,7 @@ 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.metrics.SettableLabelledGauge; import tech.pegasys.teku.infrastructure.unsigned.UInt64; import tech.pegasys.teku.service.serviceutils.Service; import tech.pegasys.teku.spec.Spec; @@ -35,6 +36,9 @@ public class BlockPruner extends Service { private final Database database; private final AsyncRunner asyncRunner; private final Duration pruneInterval; + private final SettableLabelledGauge pruningTimingsLabelledGauge; + private final SettableLabelledGauge pruningActiveLabelledGauge; + private final String pruningMetricsType; private Optional scheduledPruner = Optional.empty(); @@ -42,11 +46,17 @@ public BlockPruner( final Spec spec, final Database database, final AsyncRunner asyncRunner, - final Duration pruneInterval) { + final Duration pruneInterval, + final String pruningMetricsType, + final SettableLabelledGauge pruningTimingsLabelledGauge, + final SettableLabelledGauge pruningActiveLabelledGauge) { this.spec = spec; this.database = database; this.asyncRunner = asyncRunner; this.pruneInterval = pruneInterval; + this.pruningMetricsType = pruningMetricsType; + this.pruningTimingsLabelledGauge = pruningTimingsLabelledGauge; + this.pruningActiveLabelledGauge = pruningActiveLabelledGauge; } @Override @@ -54,7 +64,14 @@ protected synchronized SafeFuture doStart() { scheduledPruner = Optional.of( asyncRunner.runWithFixedDelay( - this::pruneBlocks, + () -> { + pruningActiveLabelledGauge.set(1, pruningMetricsType); + final long start = System.currentTimeMillis(); + pruneBlocks(); + pruningTimingsLabelledGauge.set( + System.currentTimeMillis() - start, pruningMetricsType); + pruningActiveLabelledGauge.set(0, pruningMetricsType); + }, Duration.ZERO, pruneInterval, error -> LOG.error("Failed to prune old blocks", error))); diff --git a/storage/src/test/java/tech/pegasys/teku/storage/server/pruner/BlobSidecarPrunerTest.java b/storage/src/test/java/tech/pegasys/teku/storage/server/pruner/BlobSidecarPrunerTest.java index 48da7309b27..7ad1b9145cd 100644 --- a/storage/src/test/java/tech/pegasys/teku/storage/server/pruner/BlobSidecarPrunerTest.java +++ b/storage/src/test/java/tech/pegasys/teku/storage/server/pruner/BlobSidecarPrunerTest.java @@ -28,6 +28,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import tech.pegasys.teku.infrastructure.async.StubAsyncRunner; +import tech.pegasys.teku.infrastructure.metrics.SettableLabelledGauge; import tech.pegasys.teku.infrastructure.metrics.StubMetricsSystem; import tech.pegasys.teku.infrastructure.time.StubTimeProvider; import tech.pegasys.teku.infrastructure.unsigned.UInt64; @@ -64,6 +65,9 @@ public class BlobSidecarPrunerTest { PRUNE_INTERVAL, PRUNE_LIMIT, false, + "test", + mock(SettableLabelledGauge.class), + mock(SettableLabelledGauge.class), false); @BeforeEach @@ -149,6 +153,9 @@ void shouldUseEpochsStoreBlobs() { PRUNE_INTERVAL, PRUNE_LIMIT, false, + "test", + mock(SettableLabelledGauge.class), + mock(SettableLabelledGauge.class), false); when(databaseOverride.getGenesisTime()).thenReturn(Optional.of(genesisTime)); assertThat(blobsPrunerOverride.start()).isCompleted(); @@ -201,6 +208,9 @@ void shouldNotPruneWhenEpochsStoreBlobsIsMax() { PRUNE_INTERVAL, PRUNE_LIMIT, false, + "test", + mock(SettableLabelledGauge.class), + mock(SettableLabelledGauge.class), false); when(databaseOverride.getGenesisTime()).thenReturn(Optional.of(genesisTime)); assertThat(blobsPrunerOverride.start()).isCompleted(); diff --git a/storage/src/test/java/tech/pegasys/teku/storage/server/pruner/BlockPrunerTest.java b/storage/src/test/java/tech/pegasys/teku/storage/server/pruner/BlockPrunerTest.java index 02b887f3cdd..24a93fd069b 100644 --- a/storage/src/test/java/tech/pegasys/teku/storage/server/pruner/BlockPrunerTest.java +++ b/storage/src/test/java/tech/pegasys/teku/storage/server/pruner/BlockPrunerTest.java @@ -25,6 +25,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import tech.pegasys.teku.infrastructure.async.StubAsyncRunner; +import tech.pegasys.teku.infrastructure.metrics.SettableLabelledGauge; import tech.pegasys.teku.infrastructure.time.StubTimeProvider; import tech.pegasys.teku.infrastructure.unsigned.UInt64; import tech.pegasys.teku.spec.Spec; @@ -51,7 +52,15 @@ class BlockPrunerTest { private final StubAsyncRunner asyncRunner = new StubAsyncRunner(timeProvider); private final Database database = mock(Database.class); - private final BlockPruner pruner = new BlockPruner(spec, database, asyncRunner, PRUNE_INTERVAL); + private final BlockPruner pruner = + new BlockPruner( + spec, + database, + asyncRunner, + PRUNE_INTERVAL, + "test", + mock(SettableLabelledGauge.class), + mock(SettableLabelledGauge.class)); @BeforeEach void setUp() { diff --git a/validator/client/src/main/java/tech/pegasys/teku/validator/client/VoluntaryExitDataProvider.java b/validator/client/src/main/java/tech/pegasys/teku/validator/client/VoluntaryExitDataProvider.java new file mode 100644 index 00000000000..56e963e961e --- /dev/null +++ b/validator/client/src/main/java/tech/pegasys/teku/validator/client/VoluntaryExitDataProvider.java @@ -0,0 +1,72 @@ +/* + * 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.validator.client; + +import java.util.Optional; +import tech.pegasys.teku.api.exceptions.BadRequestException; +import tech.pegasys.teku.bls.BLSPublicKey; +import tech.pegasys.teku.bls.BLSSignature; +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.SpecVersion; +import tech.pegasys.teku.spec.datastructures.operations.SignedVoluntaryExit; +import tech.pegasys.teku.spec.datastructures.operations.VoluntaryExit; +import tech.pegasys.teku.spec.datastructures.state.ForkInfo; + +public class VoluntaryExitDataProvider { + private final Spec spec; + private final KeyManager keyManager; + private final TimeProvider timeProvider; + + VoluntaryExitDataProvider( + final Spec spec, final KeyManager keyManager, final TimeProvider timeProvider) { + this.spec = spec; + this.keyManager = keyManager; + this.timeProvider = timeProvider; + } + + UInt64 calculateCurrentEpoch(final UInt64 genesisTime) { + final SpecVersion genesisSpec = spec.getGenesisSpec(); + final UInt64 currentTime = timeProvider.getTimeInSeconds(); + final UInt64 slot = genesisSpec.miscHelpers().computeSlotAtTime(genesisTime, currentTime); + return spec.computeEpochAtSlot(slot); + } + + SignedVoluntaryExit createSignedVoluntaryExit( + final int validatorIndex, + final BLSPublicKey publicKey, + final UInt64 epoch, + final ForkInfo forkInfo) { + final Validator validator = + keyManager.getActiveValidatorKeys().stream() + .filter(v -> v.getPublicKey().equals(publicKey)) + .findFirst() + .orElseThrow( + () -> + new BadRequestException( + String.format( + "Validator %s is not in the list of keys managed by this service.", + publicKey))); + final VoluntaryExit message = new VoluntaryExit(epoch, UInt64.valueOf(validatorIndex)); + final BLSSignature signature = + Optional.ofNullable(validator) + .orElseThrow() + .getSigner() + .signVoluntaryExit(message, forkInfo) + .join(); + + return new SignedVoluntaryExit(message, signature); + } +} diff --git a/validator/client/src/test/java/tech/pegasys/teku/validator/client/VoluntaryExitDataProviderTest.java b/validator/client/src/test/java/tech/pegasys/teku/validator/client/VoluntaryExitDataProviderTest.java new file mode 100644 index 00000000000..5c6e151c3a7 --- /dev/null +++ b/validator/client/src/test/java/tech/pegasys/teku/validator/client/VoluntaryExitDataProviderTest.java @@ -0,0 +1,101 @@ +/* + * 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.validator.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import tech.pegasys.teku.api.exceptions.BadRequestException; +import tech.pegasys.teku.bls.BLSKeyPair; +import tech.pegasys.teku.bls.BLSSignature; +import tech.pegasys.teku.bls.BLSTestUtil; +import tech.pegasys.teku.infrastructure.async.SafeFuture; +import tech.pegasys.teku.infrastructure.time.StubTimeProvider; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.Spec; +import tech.pegasys.teku.spec.SpecMilestone; +import tech.pegasys.teku.spec.TestSpecFactory; +import tech.pegasys.teku.spec.datastructures.operations.SignedVoluntaryExit; +import tech.pegasys.teku.spec.datastructures.operations.VoluntaryExit; +import tech.pegasys.teku.spec.datastructures.state.ForkInfo; +import tech.pegasys.teku.spec.signatures.Signer; +import tech.pegasys.teku.spec.util.DataStructureUtil; + +class VoluntaryExitDataProviderTest { + private final Spec spec = TestSpecFactory.createMinimal(SpecMilestone.CAPELLA); + private final KeyManager keyManager = mock(KeyManager.class); + private final StubTimeProvider timeProvider = StubTimeProvider.withTimeInSeconds(100000); + private final DataStructureUtil dataStructureUtil = new DataStructureUtil(spec); + private VoluntaryExitDataProvider provider; + + @BeforeEach + void setUp() { + provider = new VoluntaryExitDataProvider(spec, keyManager, timeProvider); + } + + @Test + void calculateCurrentEpoch_shouldReturnEpoch() { + final UInt64 epoch = provider.calculateCurrentEpoch(UInt64.ZERO); + final UInt64 expectedEpoch = UInt64.valueOf(2083); + assertThat(epoch).isEqualTo(expectedEpoch); + } + + @Test + void createSignedVoluntaryExit_shouldReturnSignedVoluntaryExit() { + final BLSSignature signature = dataStructureUtil.randomSignature(); + final Validator activeValidator = getValidator(signature); + final int validatorIndex = 0; + when(keyManager.getActiveValidatorKeys()).thenReturn(List.of(activeValidator)); + + final UInt64 epoch = dataStructureUtil.randomEpoch(); + final ForkInfo forkInfo = dataStructureUtil.randomForkInfo(); + + final SignedVoluntaryExit signedVoluntaryExit = + provider.createSignedVoluntaryExit( + validatorIndex, activeValidator.getPublicKey(), epoch, forkInfo); + + final SignedVoluntaryExit expected = + new SignedVoluntaryExit( + new VoluntaryExit(epoch, UInt64.valueOf(validatorIndex)), signature); + assertThat(signedVoluntaryExit).isEqualTo(expected); + } + + @Test + void createSignedVoluntaryExit_throwsExceptionWhenValidatorNotInList() { + when(keyManager.getActiveValidatorKeys()).thenReturn(List.of()); + final UInt64 epoch = dataStructureUtil.randomEpoch(); + final ForkInfo forkInfo = dataStructureUtil.randomForkInfo(); + + assertThatThrownBy( + () -> + provider.createSignedVoluntaryExit( + 0, dataStructureUtil.randomPublicKey(), epoch, forkInfo)) + .isInstanceOf(BadRequestException.class) + .hasMessageMatching("Validator (.*) is not in the list of keys managed by this service."); + } + + private Validator getValidator(final BLSSignature signature) { + BLSKeyPair keyPair1 = BLSTestUtil.randomKeyPair(1); + Signer signer = mock(Signer.class); + when(signer.signVoluntaryExit(any(), any())).thenReturn(SafeFuture.completedFuture(signature)); + return new Validator(keyPair1.getPublicKey(), signer, Optional::empty, true); + } +} diff --git a/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/RemoteValidatorApiHandler.java b/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/RemoteValidatorApiHandler.java index 5784f1b32cd..e56b31b1b8e 100644 --- a/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/RemoteValidatorApiHandler.java +++ b/validator/remote/src/main/java/tech/pegasys/teku/validator/remote/RemoteValidatorApiHandler.java @@ -446,15 +446,15 @@ public SafeFuture registerValidators( @Override public SafeFuture>> getValidatorsLiveness( - List validatorIndices, UInt64 epoch) { + final List validatorIndices, final UInt64 epoch) { return sendRequest( () -> apiClient .sendValidatorsLiveness(epoch, validatorIndices) - .map(this::responseToDoppelgangerDetectionResult)); + .map(this::responseToValidatorsLivenessResult)); } - private List responseToDoppelgangerDetectionResult( + private List responseToValidatorsLivenessResult( final PostValidatorLivenessResponse response) { return response.data.stream() .map(