diff --git a/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/storage/StorageProvider.java b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/storage/StorageProvider.java index 5c66bdc75b..670ae864b9 100644 --- a/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/storage/StorageProvider.java +++ b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/storage/StorageProvider.java @@ -35,4 +35,6 @@ public interface StorageProvider extends Closeable { PrivateStateStorage createPrivateStateStorage(); KeyValueStorage createPruningStorage(); + + boolean isWorldStateIterable(); } diff --git a/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/storage/keyvalue/KeyValueStorageProvider.java b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/storage/keyvalue/KeyValueStorageProvider.java index da8d0f19e8..5448edfc9e 100644 --- a/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/storage/keyvalue/KeyValueStorageProvider.java +++ b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/storage/keyvalue/KeyValueStorageProvider.java @@ -34,6 +34,7 @@ public class KeyValueStorageProvider implements StorageProvider { private final KeyValueStorage privateTransactionStorage; private final KeyValueStorage privateStateStorage; private final KeyValueStorage pruningStorage; + private final boolean isWorldStateIterable; public KeyValueStorageProvider( final KeyValueStorage blockchainStorage, @@ -41,13 +42,15 @@ public KeyValueStorageProvider( final KeyValueStorage worldStatePreimageStorage, final KeyValueStorage privateTransactionStorage, final KeyValueStorage privateStateStorage, - final KeyValueStorage pruningStorage) { + final KeyValueStorage pruningStorage, + final boolean isWorldStateIterable) { this.blockchainStorage = blockchainStorage; this.worldStateStorage = worldStateStorage; this.worldStatePreimageStorage = worldStatePreimageStorage; this.privateTransactionStorage = privateTransactionStorage; this.privateStateStorage = privateStateStorage; this.pruningStorage = pruningStorage; + this.isWorldStateIterable = isWorldStateIterable; } @Override @@ -81,6 +84,11 @@ public KeyValueStorage createPruningStorage() { return pruningStorage; } + @Override + public boolean isWorldStateIterable() { + return isWorldStateIterable; + } + @Override public void close() throws IOException { blockchainStorage.close(); diff --git a/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/storage/keyvalue/RocksDbStorageProvider.java b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/storage/keyvalue/RocksDbStorageProvider.java index 62559d09ad..743990a4e7 100644 --- a/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/storage/keyvalue/RocksDbStorageProvider.java +++ b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/storage/keyvalue/RocksDbStorageProvider.java @@ -94,7 +94,7 @@ private static StorageProvider ofUnsegmented( final KeyValueStorage kv = RocksDbKeyValueStorage.create(rocksDbConfiguration, metricsSystem); final KeyValueStorage preimageKv = new LimitedInMemoryKeyValueStorage(worldStatePreimageCacheSize); - return new KeyValueStorageProvider(kv, kv, preimageKv, kv, kv, kv); + return new KeyValueStorageProvider(kv, kv, preimageKv, kv, kv, kv, false); } private static StorageProvider ofSegmented( @@ -114,7 +114,8 @@ private static StorageProvider ofSegmented( preimageStorage, new SegmentedKeyValueStorageAdapter<>(RocksDbSegment.PRIVATE_TRANSACTIONS, columnarStorage), new SegmentedKeyValueStorageAdapter<>(RocksDbSegment.PRIVATE_STATE, columnarStorage), - new SegmentedKeyValueStorageAdapter<>(RocksDbSegment.PRUNING_STATE, columnarStorage)); + new SegmentedKeyValueStorageAdapter<>(RocksDbSegment.PRUNING_STATE, columnarStorage), + true); } private enum RocksDbSegment implements Segment { diff --git a/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/worldstate/Pruner.java b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/worldstate/Pruner.java index bd1c32b39e..8f5d291e44 100644 --- a/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/worldstate/Pruner.java +++ b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/worldstate/Pruner.java @@ -30,29 +30,28 @@ public class Pruner { private final MarkSweepPruner pruningStrategy; private final Blockchain blockchain; private final ExecutorService executorService; - private final long retentionPeriodInBlocks; + private final long blocksRetained; private final AtomicReference state = new AtomicReference<>(State.IDLE); private volatile long markBlockNumber = 0; private volatile BlockHeader markedBlockHeader; - private long transientForkThreshold; + private long blockConfirmations; public Pruner( final MarkSweepPruner pruningStrategy, final Blockchain blockchain, final ExecutorService executorService, - final long transientForkThreshold, - final long retentionPeriodInBlocks) { + final PruningConfiguration pruningConfiguration) { this.pruningStrategy = pruningStrategy; this.executorService = executorService; this.blockchain = blockchain; - if (transientForkThreshold < 0 || retentionPeriodInBlocks < 0) { + this.blocksRetained = pruningConfiguration.getBlocksRetained(); + this.blockConfirmations = pruningConfiguration.getBlockConfirmations(); + if (blockConfirmations < 0 || blocksRetained < 0) { throw new IllegalArgumentException( String.format( - "TransientForkThreshold and RetentionPeriodInBlocks must be non-negative. transientForkThreshold=%d, retentionPeriodInBlocks=%d", - transientForkThreshold, retentionPeriodInBlocks)); + "blockConfirmations and blocksRetained must be non-negative. blockConfirmations=%d, blocksRetained=%d", + blockConfirmations, blocksRetained)); } - this.retentionPeriodInBlocks = retentionPeriodInBlocks; - this.transientForkThreshold = transientForkThreshold; } public void start() { @@ -70,14 +69,14 @@ private void handleNewBlock(final BlockAddedEvent event) { } final long blockNumber = event.getBlock().getHeader().getNumber(); - if (state.compareAndSet(State.IDLE, State.TRANSIENT_FORK_OUTLIVING)) { + if (state.compareAndSet(State.IDLE, State.MARK_BLOCK_CONFIRMATIONS_AWAITING)) { pruningStrategy.prepare(); markBlockNumber = blockNumber; - } else if (blockNumber >= markBlockNumber + transientForkThreshold - && state.compareAndSet(State.TRANSIENT_FORK_OUTLIVING, State.MARKING)) { + } else if (blockNumber >= markBlockNumber + blockConfirmations + && state.compareAndSet(State.MARK_BLOCK_CONFIRMATIONS_AWAITING, State.MARKING)) { markedBlockHeader = blockchain.getBlockHeader(markBlockNumber).get(); mark(markedBlockHeader); - } else if (blockNumber >= markBlockNumber + retentionPeriodInBlocks + } else if (blockNumber >= markBlockNumber + blocksRetained && blockchain.blockIsOnCanonicalChain(markedBlockHeader.getHash()) && state.compareAndSet(State.MARKING_COMPLETE, State.SWEEPING)) { sweep(); @@ -99,8 +98,7 @@ private void mark(final BlockHeader header) { } private void sweep() { - LOG.info( - "Begin sweeping unused nodes for pruning. Retention period: {}", retentionPeriodInBlocks); + LOG.info("Begin sweeping unused nodes for pruning. Retention period: {}", blocksRetained); execute( () -> { pruningStrategy.sweepBefore(markBlockNumber); @@ -119,7 +117,7 @@ private void execute(final Runnable action) { private enum State { IDLE, - TRANSIENT_FORK_OUTLIVING, + MARK_BLOCK_CONFIRMATIONS_AWAITING, MARKING, MARKING_COMPLETE, SWEEPING; diff --git a/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/worldstate/PruningConfiguration.java b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/worldstate/PruningConfiguration.java new file mode 100644 index 0000000000..650946ac47 --- /dev/null +++ b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/worldstate/PruningConfiguration.java @@ -0,0 +1,33 @@ +/* + * Copyright 2019 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.pantheon.ethereum.worldstate; + +public class PruningConfiguration { + + private final long blocksRetainedBeforeSweeping; + private final long blockConfirmationsBeforeMarking; + + public PruningConfiguration( + final long blockConfirmationsBeforeMarking, final long blocksRetainedBeforeSweeping) { + this.blockConfirmationsBeforeMarking = blockConfirmationsBeforeMarking; + this.blocksRetainedBeforeSweeping = blocksRetainedBeforeSweeping; + } + + public long getBlocksRetained() { + return blocksRetainedBeforeSweeping; + } + + public long getBlockConfirmations() { + return blockConfirmationsBeforeMarking; + } +} diff --git a/ethereum/core/src/test-support/java/tech/pegasys/pantheon/ethereum/core/InMemoryStorageProvider.java b/ethereum/core/src/test-support/java/tech/pegasys/pantheon/ethereum/core/InMemoryStorageProvider.java index 09f2b82264..ab3dc83ed1 100644 --- a/ethereum/core/src/test-support/java/tech/pegasys/pantheon/ethereum/core/InMemoryStorageProvider.java +++ b/ethereum/core/src/test-support/java/tech/pegasys/pantheon/ethereum/core/InMemoryStorageProvider.java @@ -92,6 +92,11 @@ public KeyValueStorage createPruningStorage() { return new InMemoryKeyValueStorage(); } + @Override + public boolean isWorldStateIterable() { + return true; + } + @Override public void close() {} } diff --git a/ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/worldstate/PrunerTest.java b/ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/worldstate/PrunerTest.java index 198b0d91c6..f631a76b6a 100644 --- a/ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/worldstate/PrunerTest.java +++ b/ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/worldstate/PrunerTest.java @@ -61,7 +61,9 @@ public void shouldMarkCorrectBlockAndSweep() throws InterruptedException { final MutableBlockchain blockchain = DefaultBlockchain.createMutable(genesisBlock, blockchainStorage, metricsSystem); - final Pruner pruner = new Pruner(markSweepPruner, blockchain, mockExecutorService, 0, 0); + final Pruner pruner = + new Pruner( + markSweepPruner, blockchain, mockExecutorService, new PruningConfiguration(0, 0)); pruner.start(); final Block block1 = appendBlockWithParent(blockchain, genesisBlock); @@ -74,7 +76,7 @@ public void shouldMarkCorrectBlockAndSweep() throws InterruptedException { } @Test - public void shouldOnlySweepAfterTransientForkPeriodAndRetentionPeriodEnds() + public void shouldOnlySweepAfterBlockConfirmationPeriodAndRetentionPeriodEnds() throws InterruptedException { final BlockchainStorage blockchainStorage = new KeyValueStoragePrefixedKeyBlockchainStorage( @@ -82,7 +84,9 @@ public void shouldOnlySweepAfterTransientForkPeriodAndRetentionPeriodEnds() final MutableBlockchain blockchain = DefaultBlockchain.createMutable(genesisBlock, blockchainStorage, metricsSystem); - final Pruner pruner = new Pruner(markSweepPruner, blockchain, mockExecutorService, 1, 2); + final Pruner pruner = + new Pruner( + markSweepPruner, blockchain, mockExecutorService, new PruningConfiguration(1, 2)); pruner.start(); final Hash markBlockStateRootHash = @@ -109,7 +113,9 @@ public void abortsPruningWhenFullyMarkedBlockNoLongerOnCanonicalChain() DefaultBlockchain.createMutable(genesisBlock, blockchainStorage, metricsSystem); // start pruner so it can start handling block added events - final Pruner pruner = new Pruner(markSweepPruner, blockchain, mockExecutorService, 0, 1); + final Pruner pruner = + new Pruner( + markSweepPruner, blockchain, mockExecutorService, new PruningConfiguration(0, 1)); pruner.start(); /* @@ -143,7 +149,13 @@ public void abortsPruningWhenFullyMarkedBlockNoLongerOnCanonicalChain() @Test public void shouldRejectInvalidArguments() { final Blockchain mockchain = mock(Blockchain.class); - assertThatThrownBy(() -> new Pruner(markSweepPruner, mockchain, mockExecutorService, -1, -2)) + assertThatThrownBy( + () -> + new Pruner( + markSweepPruner, + mockchain, + mockExecutorService, + new PruningConfiguration(-1, -2))) .isInstanceOf(IllegalArgumentException.class); } @@ -155,7 +167,9 @@ public void shouldCleanUpPruningStrategyOnShutdown() throws InterruptedException final MutableBlockchain blockchain = DefaultBlockchain.createMutable(genesisBlock, blockchainStorage, metricsSystem); - final Pruner pruner = new Pruner(markSweepPruner, blockchain, mockExecutorService, 0, 0); + final Pruner pruner = + new Pruner( + markSweepPruner, blockchain, mockExecutorService, new PruningConfiguration(0, 0)); pruner.start(); pruner.stop(); verify(markSweepPruner).cleanup(); diff --git a/ethereum/eth/src/main/java/tech/pegasys/pantheon/ethereum/eth/sync/DefaultSynchronizer.java b/ethereum/eth/src/main/java/tech/pegasys/pantheon/ethereum/eth/sync/DefaultSynchronizer.java index e53105d931..afb5986943 100644 --- a/ethereum/eth/src/main/java/tech/pegasys/pantheon/ethereum/eth/sync/DefaultSynchronizer.java +++ b/ethereum/eth/src/main/java/tech/pegasys/pantheon/ethereum/eth/sync/DefaultSynchronizer.java @@ -45,8 +45,7 @@ public class DefaultSynchronizer implements Synchronizer { private static final Logger LOG = LogManager.getLogger(); - private static final boolean PRUNING_ENABLED = false; - private final Pruner pruner; + private final Optional maybePruner; private final SyncState syncState; private final AtomicBoolean running = new AtomicBoolean(false); private final Subscribers syncStatusListeners = Subscribers.create(); @@ -60,13 +59,13 @@ public DefaultSynchronizer( final ProtocolContext protocolContext, final WorldStateStorage worldStateStorage, final BlockBroadcaster blockBroadcaster, - final Pruner pruner, + final Optional maybePruner, final EthContext ethContext, final SyncState syncState, final Path dataDirectory, final Clock clock, final MetricsSystem metricsSystem) { - this.pruner = pruner; + this.maybePruner = maybePruner; this.syncState = syncState; ChainHeadTracker.trackChainHeadForPeers( @@ -169,9 +168,7 @@ private void handleFastSyncResult(final FastSyncState result, final Throwable er private void startFullSync() { fullSyncDownloader.start(); - if (PRUNING_ENABLED) { - pruner.start(); - } + maybePruner.ifPresent(Pruner::start); } @Override diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/cli/DefaultCommandValues.java b/pantheon/src/main/java/tech/pegasys/pantheon/cli/DefaultCommandValues.java index 844ed1fc6e..dfbb52f84f 100644 --- a/pantheon/src/main/java/tech/pegasys/pantheon/cli/DefaultCommandValues.java +++ b/pantheon/src/main/java/tech/pegasys/pantheon/cli/DefaultCommandValues.java @@ -43,6 +43,8 @@ public interface DefaultCommandValues { String MANDATORY_NETWORK_FORMAT_HELP = ""; String MANDATORY_NODE_ID_FORMAT_HELP = ""; Wei DEFAULT_MIN_TRANSACTION_GAS_PRICE = Wei.of(1000); + long DEFAULT_PRUNING_BLOCKS_RETAINED = 1024; + long DEFAULT_PRUNING_BLOCK_CONFIRMATIONS = 10; BytesValue DEFAULT_EXTRA_DATA = BytesValue.EMPTY; long DEFAULT_MAX_REFRESH_DELAY = 3600000; long DEFAULT_MIN_REFRESH_DELAY = 1; diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java b/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java index 0105f35cf5..35eb8b50c0 100644 --- a/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java +++ b/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java @@ -81,6 +81,7 @@ import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfiguration; import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfigurationBuilder; import tech.pegasys.pantheon.ethereum.permissioning.SmartContractPermissioningConfiguration; +import tech.pegasys.pantheon.ethereum.worldstate.PruningConfiguration; import tech.pegasys.pantheon.metrics.MetricCategory; import tech.pegasys.pantheon.metrics.MetricsSystem; import tech.pegasys.pantheon.metrics.PantheonMetricCategory; @@ -547,6 +548,29 @@ void setBannedNodeIds(final List values) { arity = "1") private final BytesValue extraData = DEFAULT_EXTRA_DATA; + @Option( + names = {"--pruning-enabled"}, + hidden = true, + description = + "Enable pruning of world state of blocks older than the retention period (default: ${DEFAULT-VALUE})") + private final Boolean isPruningEnabled = false; + + @Option( + names = {"--pruning-blocks-retained"}, + hidden = true, + description = + "Number of recent blocks for which to keep entire world state (default: ${DEFAULT-VALUE})", + arity = "1") + private final Long pruningBlocksRetained = DEFAULT_PRUNING_BLOCKS_RETAINED; + + @Option( + names = {"--pruning-block-confirmations"}, + hidden = true, + description = + "Number of confirmations on a block before marking begins (default: ${DEFAULT-VALUE})", + arity = "1") + private final Long pruningBlockConfirmations = DEFAULT_PRUNING_BLOCK_CONFIRMATIONS; + @Option( names = {"--permissions-nodes-config-file-enabled"}, description = "Enable node level permissions (default: ${DEFAULT-VALUE})") @@ -829,6 +853,13 @@ private PantheonCommand checkOptions() { !SyncMode.FAST.equals(syncMode), singletonList("--fast-sync-min-peers")); + checkOptionDependencies( + logger, + commandLine, + "--pruning-enabled", + !isPruningEnabled, + asList("--pruning-block-confirmations", "--pruning-blocks-retained")); + // noinspection ConstantConditions if (isMiningEnabled && coinbase == null) { throw new ParameterException( @@ -911,7 +942,9 @@ public PantheonControllerBuilder getControllerBuilder() { .metricsSystem(metricsSystem.get()) .privacyParameters(privacyParameters()) .clock(Clock.systemUTC()) - .isRevertReasonEnabled(isRevertReasonEnabled); + .isRevertReasonEnabled(isRevertReasonEnabled) + .isPruningEnabled(isPruningEnabled) + .pruningConfiguration(buildPruningConfiguration()); } catch (IOException e) { throw new ExecutionException(this.commandLine, "Invalid path", e); } @@ -1181,6 +1214,10 @@ private TransactionPoolConfiguration buildTransactionPoolConfiguration() { .build(); } + private PruningConfiguration buildPruningConfiguration() { + return new PruningConfiguration(pruningBlockConfirmations, pruningBlocksRetained); + } + // Blockchain synchronisation from peers. private void synchronize( final PantheonController controller, diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/controller/PantheonControllerBuilder.java b/pantheon/src/main/java/tech/pegasys/pantheon/controller/PantheonControllerBuilder.java index 2d3053cf6f..406383d2f2 100644 --- a/pantheon/src/main/java/tech/pegasys/pantheon/controller/PantheonControllerBuilder.java +++ b/pantheon/src/main/java/tech/pegasys/pantheon/controller/PantheonControllerBuilder.java @@ -14,6 +14,7 @@ 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.pantheon.controller.KeyPairUtil.loadKeyPair; import tech.pegasys.pantheon.config.GenesisConfigFile; @@ -46,6 +47,7 @@ import tech.pegasys.pantheon.ethereum.storage.keyvalue.RocksDbStorageProvider; import tech.pegasys.pantheon.ethereum.worldstate.MarkSweepPruner; import tech.pegasys.pantheon.ethereum.worldstate.Pruner; +import tech.pegasys.pantheon.ethereum.worldstate.PruningConfiguration; import tech.pegasys.pantheon.ethereum.worldstate.WorldStateArchive; import tech.pegasys.pantheon.metrics.MetricsSystem; import tech.pegasys.pantheon.services.kvstore.RocksDbConfiguration; @@ -57,6 +59,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Optional; import java.util.OptionalLong; import java.util.concurrent.Executors; @@ -83,6 +86,8 @@ public abstract class PantheonControllerBuilder { private StorageProvider storageProvider; private final List shutdownActions = new ArrayList<>(); private RocksDbConfiguration rocksDbConfiguration; + private boolean isPruningEnabled; + private PruningConfiguration pruningConfiguration; public PantheonControllerBuilder rocksDbConfiguration( final RocksDbConfiguration rocksDbConfiguration) { @@ -164,6 +169,17 @@ public PantheonControllerBuilder isRevertReasonEnabled(final boolean isRevert return this; } + public PantheonControllerBuilder isPruningEnabled(final boolean pruningEnabled) { + this.isPruningEnabled = pruningEnabled; + return this; + } + + public PantheonControllerBuilder pruningConfiguration( + final PruningConfiguration pruningConfiguration) { + this.pruningConfiguration = pruningConfiguration; + return this; + } + public PantheonController build() throws IOException { checkNotNull(genesisConfig, "Missing genesis config"); checkNotNull(syncConfig, "Missing sync config"); @@ -202,30 +218,40 @@ public PantheonController build() throws IOException { final MutableBlockchain blockchain = protocolContext.getBlockchain(); - final Pruner pruner = - new Pruner( - new MarkSweepPruner( - protocolContext.getWorldStateArchive().getWorldStateStorage(), - blockchain, - storageProvider.createPruningStorage(), - metricsSystem), - blockchain, - Executors.newSingleThreadExecutor( - new ThreadFactoryBuilder() - .setDaemon(true) - .setPriority(Thread.MIN_PRIORITY) - .setNameFormat("StatePruning-%d") - .build()), - 10, - 1000); + Optional maybePruner = Optional.empty(); + if (isPruningEnabled) { + checkState( + storageProvider.isWorldStateIterable(), + "Cannot enable pruning with current database version. Resync to get the latest version."); + maybePruner = + Optional.of( + new Pruner( + new MarkSweepPruner( + protocolContext.getWorldStateArchive().getWorldStateStorage(), + blockchain, + storageProvider.createPruningStorage(), + metricsSystem), + blockchain, + Executors.newSingleThreadExecutor( + new ThreadFactoryBuilder() + .setDaemon(true) + .setPriority(Thread.MIN_PRIORITY) + .setNameFormat("StatePruning-%d") + .build()), + pruningConfiguration)); + } + + Optional finalMaybePruner = maybePruner; addShutdownAction( - () -> { - try { - pruner.stop(); - } catch (InterruptedException ie) { - throw new RuntimeException(ie); - } - }); + () -> + finalMaybePruner.ifPresent( + pruner -> { + try { + pruner.stop(); + } catch (InterruptedException ie) { + throw new RuntimeException(ie); + } + })); final boolean fastSyncEnabled = syncConfig.getSyncMode().equals(SyncMode.FAST); ethProtocolManager = createEthProtocolManager(protocolContext, fastSyncEnabled); @@ -238,7 +264,7 @@ public PantheonController build() throws IOException { protocolContext, protocolContext.getWorldStateArchive().getWorldStateStorage(), ethProtocolManager.getBlockBroadcaster(), - pruner, + maybePruner, ethProtocolManager.ethContext(), syncState, dataDirectory, diff --git a/pantheon/src/test/java/tech/pegasys/pantheon/cli/CommandTestAbstract.java b/pantheon/src/test/java/tech/pegasys/pantheon/cli/CommandTestAbstract.java index f974cee15c..6bb661749c 100644 --- a/pantheon/src/test/java/tech/pegasys/pantheon/cli/CommandTestAbstract.java +++ b/pantheon/src/test/java/tech/pegasys/pantheon/cli/CommandTestAbstract.java @@ -152,6 +152,8 @@ public void initMocks() throws Exception { when(mockControllerBuilder.privacyParameters(any())).thenReturn(mockControllerBuilder); when(mockControllerBuilder.clock(any())).thenReturn(mockControllerBuilder); when(mockControllerBuilder.isRevertReasonEnabled(false)).thenReturn(mockControllerBuilder); + when(mockControllerBuilder.isPruningEnabled(anyBoolean())).thenReturn(mockControllerBuilder); + when(mockControllerBuilder.pruningConfiguration(any())).thenReturn(mockControllerBuilder); // doReturn used because of generic PantheonController doReturn(mockController).when(mockControllerBuilder).build(); diff --git a/pantheon/src/test/java/tech/pegasys/pantheon/cli/PantheonCommandTest.java b/pantheon/src/test/java/tech/pegasys/pantheon/cli/PantheonCommandTest.java index da89e6f8e8..cb6bfb81dc 100644 --- a/pantheon/src/test/java/tech/pegasys/pantheon/cli/PantheonCommandTest.java +++ b/pantheon/src/test/java/tech/pegasys/pantheon/cli/PantheonCommandTest.java @@ -50,6 +50,7 @@ import tech.pegasys.pantheon.ethereum.permissioning.LocalPermissioningConfiguration; import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfiguration; import tech.pegasys.pantheon.ethereum.permissioning.SmartContractPermissioningConfiguration; +import tech.pegasys.pantheon.ethereum.worldstate.PruningConfiguration; import tech.pegasys.pantheon.metrics.StandardMetricCategory; import tech.pegasys.pantheon.metrics.prometheus.MetricsConfiguration; import tech.pegasys.pantheon.nat.NatMethod; @@ -2297,6 +2298,46 @@ public void miningParametersAreCaptured() throws Exception { .isEqualTo(BytesValue.fromHexString(extraDataString)); } + @Test + public void pruningIsEnabledWhenSpecified() throws Exception { + parseCommand("--pruning-enabled"); + + verify(mockControllerBuilder).isPruningEnabled(true); + verify(mockControllerBuilder).build(); + + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + + @Test + public void pruningOptionsRequiresServiceToBeEnabled() { + + parseCommand("--pruning-blocks-retained", "4", "--pruning-block-confirmations", "1"); + + verifyOptionsConstraintLoggerCall( + "--pruning-enabled", "--pruning-blocks-retained", "--pruning-block-confirmations"); + + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + + @Test + public void pruningParametersAreCaptured() throws Exception { + parseCommand( + "--pruning-enabled", "--pruning-blocks-retained=15", "--pruning-block-confirmations=4"); + + final ArgumentCaptor pruningArg = + ArgumentCaptor.forClass(PruningConfiguration.class); + + verify(mockControllerBuilder).pruningConfiguration(pruningArg.capture()); + verify(mockControllerBuilder).build(); + + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()).isEmpty(); + assertThat(pruningArg.getValue().getBlocksRetained()).isEqualTo(15); + assertThat(pruningArg.getValue().getBlockConfirmations()).isEqualTo(4); + } + @Test public void devModeOptionMustBeUsed() throws Exception { parseCommand("--network", "dev");