diff --git a/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java b/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java index 470a72c16a0..40d6eb989dd 100644 --- a/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java +++ b/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java @@ -294,7 +294,7 @@ public class BesuCommand implements DefaultCommandValues, Runnable { private final ChainPruningOptions unstableChainPruningOptions = ChainPruningOptions.create(); // stable CLI options - private final DataStorageOptions dataStorageOptions = DataStorageOptions.create(); + final DataStorageOptions dataStorageOptions = DataStorageOptions.create(); private final EthstatsOptions ethstatsOptions = EthstatsOptions.create(); private final NodePrivateKeyFileOption nodePrivateKeyFileOption = NodePrivateKeyFileOption.create(); @@ -1785,6 +1785,7 @@ private void validateOptions() { validateChainDataPruningParams(); validatePostMergeCheckpointBlockRequirements(); validateTransactionPoolOptions(); + validateDataStorageOptions(); p2pTLSConfigOptions.checkP2PTLSOptionsDependencies(logger, commandLine); pkiBlockCreationOptions.checkPkiBlockCreationOptionsDependencies(logger, commandLine); } @@ -1793,6 +1794,10 @@ private void validateTransactionPoolOptions() { transactionPoolOptions.validate(commandLine, getActualGenesisConfigOptions()); } + private void validateDataStorageOptions() { + dataStorageOptions.validate(commandLine); + } + private void validateRequiredOptions() { commandLine .getCommandSpec() @@ -3466,6 +3471,14 @@ private String generateConfigurationOverview() { builder.setHighSpecEnabled(); } + if (dataStorageOptions.toDomainObject().getUnstable().getBonsaiTrieLogPruningEnabled()) { + builder.setTrieLogPruningEnabled(); + builder.setTrieLogRetentionThreshold( + dataStorageOptions.toDomainObject().getUnstable().getBonsaiTrieLogRetentionThreshold()); + builder.setTrieLogPruningLimit( + dataStorageOptions.toDomainObject().getUnstable().getBonsaiTrieLogPruningLimit()); + } + builder.setTxPoolImplementation(buildTransactionPoolConfiguration().getTxPoolImplementation()); builder.setWorldStateUpdateMode(unstableEvmOptions.toDomainObject().worldUpdaterMode()); diff --git a/besu/src/main/java/org/hyperledger/besu/cli/ConfigurationOverviewBuilder.java b/besu/src/main/java/org/hyperledger/besu/cli/ConfigurationOverviewBuilder.java index 8b25f6377eb..5ed209ca311 100644 --- a/besu/src/main/java/org/hyperledger/besu/cli/ConfigurationOverviewBuilder.java +++ b/besu/src/main/java/org/hyperledger/besu/cli/ConfigurationOverviewBuilder.java @@ -50,6 +50,9 @@ public class ConfigurationOverviewBuilder { private Collection engineApis; private String engineJwtFilePath; private boolean isHighSpec = false; + private boolean isTrieLogPruningEnabled = false; + private long trieLogRetentionThreshold = 0; + private Integer trieLogPruningLimit = null; private TransactionPoolConfiguration.Implementation txPoolImplementation; private EvmConfiguration.WorldUpdaterMode worldStateUpdateMode; private Map environment; @@ -171,6 +174,38 @@ public ConfigurationOverviewBuilder setHighSpecEnabled() { return this; } + /** + * Sets trie log pruning enabled + * + * @return the builder + */ + public ConfigurationOverviewBuilder setTrieLogPruningEnabled() { + isTrieLogPruningEnabled = true; + return this; + } + + /** + * Sets trie log retention threshold + * + * @param threshold the number of blocks to retain trie logs for + * @return the builder + */ + public ConfigurationOverviewBuilder setTrieLogRetentionThreshold(final long threshold) { + trieLogRetentionThreshold = threshold; + return this; + } + + /** + * Sets trie log pruning limit + * + * @param limit the max number of blocks to load and prune trie logs for at startup + * @return the builder + */ + public ConfigurationOverviewBuilder setTrieLogPruningLimit(final int limit) { + trieLogPruningLimit = limit; + return this; + } + /** * Sets the txpool implementation in use. * @@ -266,13 +301,25 @@ public String build() { lines.add("Engine JWT: " + engineJwtFilePath); } + lines.add("Using " + txPoolImplementation + " transaction pool implementation"); + if (isHighSpec) { lines.add("Experimental high spec configuration enabled"); } - lines.add("Using " + txPoolImplementation + " transaction pool implementation"); lines.add("Using " + worldStateUpdateMode + " worldstate update mode"); + if (isTrieLogPruningEnabled) { + final StringBuilder trieLogPruningString = new StringBuilder(); + trieLogPruningString + .append("Trie log pruning enabled: retention: ") + .append(trieLogRetentionThreshold); + if (trieLogPruningLimit != null) { + trieLogPruningString.append("; prune limit: ").append(trieLogPruningLimit); + } + lines.add(trieLogPruningString.toString()); + } + lines.add(""); lines.add("Host:"); diff --git a/besu/src/main/java/org/hyperledger/besu/cli/options/stable/DataStorageOptions.java b/besu/src/main/java/org/hyperledger/besu/cli/options/stable/DataStorageOptions.java index c93761fdae1..e0b19735683 100644 --- a/besu/src/main/java/org/hyperledger/besu/cli/options/stable/DataStorageOptions.java +++ b/besu/src/main/java/org/hyperledger/besu/cli/options/stable/DataStorageOptions.java @@ -17,8 +17,13 @@ package org.hyperledger.besu.cli.options.stable; import static org.hyperledger.besu.ethereum.worldstate.DataStorageConfiguration.DEFAULT_BONSAI_MAX_LAYERS_TO_LOAD; +import static org.hyperledger.besu.ethereum.worldstate.DataStorageConfiguration.Unstable.DEFAULT_BONSAI_TRIE_LOG_PRUNING_ENABLED; +import static org.hyperledger.besu.ethereum.worldstate.DataStorageConfiguration.Unstable.DEFAULT_BONSAI_TRIE_LOG_PRUNING_LIMIT; +import static org.hyperledger.besu.ethereum.worldstate.DataStorageConfiguration.Unstable.DEFAULT_BONSAI_TRIE_LOG_RETENTION_THRESHOLD; +import static org.hyperledger.besu.ethereum.worldstate.DataStorageConfiguration.Unstable.MINIMUM_BONSAI_TRIE_LOG_RETENTION_THRESHOLD; import org.hyperledger.besu.cli.options.CLIOptions; +import org.hyperledger.besu.cli.util.CommandLineUtils; import org.hyperledger.besu.ethereum.worldstate.DataStorageConfiguration; import org.hyperledger.besu.ethereum.worldstate.DataStorageFormat; import org.hyperledger.besu.ethereum.worldstate.ImmutableDataStorageConfiguration; @@ -26,6 +31,7 @@ import java.util.List; import org.apache.commons.lang3.StringUtils; +import picocli.CommandLine; import picocli.CommandLine.Option; /** The Data storage CLI options. */ @@ -42,7 +48,7 @@ public class DataStorageOptions implements CLIOptions description = "Format to store trie data in. Either FOREST or BONSAI (default: ${DEFAULT-VALUE}).", arity = "1") - private final DataStorageFormat dataStorageFormat = DataStorageFormat.FOREST; + private DataStorageFormat dataStorageFormat = DataStorageFormat.FOREST; @Option( names = {BONSAI_STORAGE_FORMAT_MAX_LAYERS_TO_LOAD, "--bonsai-maximum-back-layers-to-load"}, @@ -50,8 +56,33 @@ public class DataStorageOptions implements CLIOptions description = "Limit of historical layers that can be loaded with BONSAI (default: ${DEFAULT-VALUE}).", arity = "1") - private final Long bonsaiMaxLayersToLoad = DEFAULT_BONSAI_MAX_LAYERS_TO_LOAD; + private Long bonsaiMaxLayersToLoad = DEFAULT_BONSAI_MAX_LAYERS_TO_LOAD; + @CommandLine.ArgGroup(validate = false) + private final DataStorageOptions.Unstable unstableOptions = new Unstable(); + + static class Unstable { + + @CommandLine.Option( + hidden = true, + names = {"--Xbonsai-trie-log-pruning-enabled"}, + description = "Enable trie log pruning. (default: ${DEFAULT-VALUE})") + private boolean bonsaiTrieLogPruningEnabled = DEFAULT_BONSAI_TRIE_LOG_PRUNING_ENABLED; + + @CommandLine.Option( + hidden = true, + names = {"--Xbonsai-trie-log-retention-threshold"}, + description = + "The number of blocks for which to retain trie logs. (default: ${DEFAULT-VALUE})") + private long bonsaiTrieLogRetentionThreshold = DEFAULT_BONSAI_TRIE_LOG_RETENTION_THRESHOLD; + + @CommandLine.Option( + hidden = true, + names = {"--Xbonsai-trie-log-pruning-limit"}, + description = + "The max number of blocks to load and prune trie logs for at startup. (default: ${DEFAULT-VALUE})") + private int bonsaiTrieLogPruningLimit = DEFAULT_BONSAI_TRIE_LOG_PRUNING_LIMIT; + } /** * Create data storage options. * @@ -61,21 +92,62 @@ public static DataStorageOptions create() { return new DataStorageOptions(); } + /** + * Validates the data storage options + * + * @param commandLine the full commandLine to check all the options specified by the user + */ + public void validate(final CommandLine commandLine) { + if (unstableOptions.bonsaiTrieLogPruningEnabled) { + if (unstableOptions.bonsaiTrieLogRetentionThreshold + < MINIMUM_BONSAI_TRIE_LOG_RETENTION_THRESHOLD) { + throw new CommandLine.ParameterException( + commandLine, + String.format( + "--Xbonsai-trie-log-retention-threshold minimum value is %d", + MINIMUM_BONSAI_TRIE_LOG_RETENTION_THRESHOLD)); + } + if (unstableOptions.bonsaiTrieLogPruningLimit <= 0) { + throw new CommandLine.ParameterException( + commandLine, + String.format( + "--Xbonsai-trie-log-pruning-limit=%d must be greater than 0", + unstableOptions.bonsaiTrieLogPruningLimit)); + } + } + } + + static DataStorageOptions fromConfig(final DataStorageConfiguration domainObject) { + final DataStorageOptions dataStorageOptions = DataStorageOptions.create(); + dataStorageOptions.dataStorageFormat = domainObject.getDataStorageFormat(); + dataStorageOptions.bonsaiMaxLayersToLoad = domainObject.getBonsaiMaxLayersToLoad(); + dataStorageOptions.unstableOptions.bonsaiTrieLogPruningEnabled = + domainObject.getUnstable().getBonsaiTrieLogPruningEnabled(); + dataStorageOptions.unstableOptions.bonsaiTrieLogRetentionThreshold = + domainObject.getUnstable().getBonsaiTrieLogRetentionThreshold(); + dataStorageOptions.unstableOptions.bonsaiTrieLogPruningLimit = + domainObject.getUnstable().getBonsaiTrieLogPruningLimit(); + + return dataStorageOptions; + } + @Override public DataStorageConfiguration toDomainObject() { return ImmutableDataStorageConfiguration.builder() .dataStorageFormat(dataStorageFormat) .bonsaiMaxLayersToLoad(bonsaiMaxLayersToLoad) + .unstable( + ImmutableDataStorageConfiguration.Unstable.builder() + .bonsaiTrieLogPruningEnabled(unstableOptions.bonsaiTrieLogPruningEnabled) + .bonsaiTrieLogRetentionThreshold(unstableOptions.bonsaiTrieLogRetentionThreshold) + .bonsaiTrieLogPruningLimit(unstableOptions.bonsaiTrieLogPruningLimit) + .build()) .build(); } @Override public List getCLIOptions() { - return List.of( - DATA_STORAGE_FORMAT, - dataStorageFormat.toString(), - BONSAI_STORAGE_FORMAT_MAX_LAYERS_TO_LOAD, - bonsaiMaxLayersToLoad.toString()); + return CommandLineUtils.getCLIOptions(this, new DataStorageOptions()); } /** diff --git a/besu/src/main/java/org/hyperledger/besu/controller/BesuControllerBuilder.java b/besu/src/main/java/org/hyperledger/besu/controller/BesuControllerBuilder.java index ab05e7d9dfa..2c975bccb92 100644 --- a/besu/src/main/java/org/hyperledger/besu/controller/BesuControllerBuilder.java +++ b/besu/src/main/java/org/hyperledger/besu/controller/BesuControllerBuilder.java @@ -34,6 +34,7 @@ import org.hyperledger.besu.ethereum.bonsai.BonsaiWorldStateProvider; import org.hyperledger.besu.ethereum.bonsai.cache.CachedMerkleTrieLoader; import org.hyperledger.besu.ethereum.bonsai.storage.BonsaiWorldStateKeyValueStorage; +import org.hyperledger.besu.ethereum.bonsai.trielog.TrieLogPruner; import org.hyperledger.besu.ethereum.chain.Blockchain; import org.hyperledger.besu.ethereum.chain.BlockchainStorage; import org.hyperledger.besu.ethereum.chain.ChainDataPruner; @@ -1066,14 +1067,30 @@ WorldStateArchive createWorldStateArchive( final Blockchain blockchain, final CachedMerkleTrieLoader cachedMerkleTrieLoader) { return switch (dataStorageConfiguration.getDataStorageFormat()) { - case BONSAI -> new BonsaiWorldStateProvider( - (BonsaiWorldStateKeyValueStorage) worldStateStorage, - blockchain, - Optional.of(dataStorageConfiguration.getBonsaiMaxLayersToLoad()), - cachedMerkleTrieLoader, - metricsSystem, - besuComponent.map(BesuComponent::getBesuPluginContext).orElse(null), - evmConfiguration); + case BONSAI -> { + final GenesisConfigOptions genesisConfigOptions = configOptionsSupplier.get(); + final boolean isProofOfStake = + genesisConfigOptions.getTerminalTotalDifficulty().isPresent(); + final TrieLogPruner trieLogPruner = + dataStorageConfiguration.getUnstable().getBonsaiTrieLogPruningEnabled() + ? new TrieLogPruner( + (BonsaiWorldStateKeyValueStorage) worldStateStorage, + blockchain, + dataStorageConfiguration.getUnstable().getBonsaiTrieLogRetentionThreshold(), + dataStorageConfiguration.getUnstable().getBonsaiTrieLogPruningLimit(), + isProofOfStake) + : TrieLogPruner.noOpTrieLogPruner(); + trieLogPruner.initialize(); + yield new BonsaiWorldStateProvider( + (BonsaiWorldStateKeyValueStorage) worldStateStorage, + blockchain, + Optional.of(dataStorageConfiguration.getBonsaiMaxLayersToLoad()), + cachedMerkleTrieLoader, + metricsSystem, + besuComponent.map(BesuComponent::getBesuPluginContext).orElse(null), + evmConfiguration, + trieLogPruner); + } case FOREST -> { final WorldStatePreimageStorage preimageStorage = storageProvider.createWorldStatePreimageStorage(); diff --git a/besu/src/test/java/org/hyperledger/besu/cli/CommandTestAbstract.java b/besu/src/test/java/org/hyperledger/besu/cli/CommandTestAbstract.java index a2c793296dd..23e458552d9 100644 --- a/besu/src/test/java/org/hyperledger/besu/cli/CommandTestAbstract.java +++ b/besu/src/test/java/org/hyperledger/besu/cli/CommandTestAbstract.java @@ -33,6 +33,7 @@ import org.hyperledger.besu.cli.config.EthNetworkConfig; import org.hyperledger.besu.cli.options.MiningOptions; import org.hyperledger.besu.cli.options.TransactionPoolOptions; +import org.hyperledger.besu.cli.options.stable.DataStorageOptions; import org.hyperledger.besu.cli.options.stable.EthstatsOptions; import org.hyperledger.besu.cli.options.unstable.EthProtocolOptions; import org.hyperledger.besu.cli.options.unstable.MetricsCLIOptions; @@ -568,6 +569,10 @@ public TransactionPoolOptions getTransactionPoolOptions() { return transactionPoolOptions; } + public DataStorageOptions getDataStorageOptions() { + return dataStorageOptions; + } + public MetricsCLIOptions getMetricsCLIOptions() { return unstableMetricsCLIOptions; } diff --git a/besu/src/test/java/org/hyperledger/besu/cli/ConfigurationOverviewBuilderTest.java b/besu/src/test/java/org/hyperledger/besu/cli/ConfigurationOverviewBuilderTest.java index 7642e14c946..44718e96c67 100644 --- a/besu/src/test/java/org/hyperledger/besu/cli/ConfigurationOverviewBuilderTest.java +++ b/besu/src/test/java/org/hyperledger/besu/cli/ConfigurationOverviewBuilderTest.java @@ -148,6 +148,24 @@ void setHighSpecEnabled() { assertThat(highSpecEnabled).contains("Experimental high spec configuration enabled"); } + @Test + void setTrieLogPruningEnabled() { + final String noTrieLogRetentionThresholdSet = builder.build(); + assertThat(noTrieLogRetentionThresholdSet).doesNotContain("Trie log pruning enabled"); + + builder.setTrieLogPruningEnabled(); + builder.setTrieLogRetentionThreshold(42); + String trieLogRetentionThresholdSet = builder.build(); + assertThat(trieLogRetentionThresholdSet) + .contains("Trie log pruning enabled") + .contains("retention: 42"); + assertThat(trieLogRetentionThresholdSet).doesNotContain("prune limit"); + + builder.setTrieLogPruningLimit(1000); + trieLogRetentionThresholdSet = builder.build(); + assertThat(trieLogRetentionThresholdSet).contains("prune limit: 1000"); + } + @Test void setTxPoolImplementationLayered() { builder.setTxPoolImplementation(LAYERED); diff --git a/besu/src/test/java/org/hyperledger/besu/cli/options/stable/DataStorageOptionsTest.java b/besu/src/test/java/org/hyperledger/besu/cli/options/stable/DataStorageOptionsTest.java new file mode 100644 index 00000000000..5b4e0228550 --- /dev/null +++ b/besu/src/test/java/org/hyperledger/besu/cli/options/stable/DataStorageOptionsTest.java @@ -0,0 +1,111 @@ +/* + * Copyright contributors to Hyperledger Besu. + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hyperledger.besu.cli.options.stable; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hyperledger.besu.ethereum.worldstate.DataStorageConfiguration.Unstable.MINIMUM_BONSAI_TRIE_LOG_RETENTION_THRESHOLD; + +import org.hyperledger.besu.cli.options.AbstractCLIOptionsTest; +import org.hyperledger.besu.ethereum.worldstate.DataStorageConfiguration; +import org.hyperledger.besu.ethereum.worldstate.DataStorageFormat; +import org.hyperledger.besu.ethereum.worldstate.ImmutableDataStorageConfiguration; + +import org.junit.Test; + +public class DataStorageOptionsTest + extends AbstractCLIOptionsTest { + + @Test + public void bonsaiTrieLogPruningLimitOption() { + internalTestSuccess( + dataStorageConfiguration -> + assertThat(dataStorageConfiguration.getUnstable().getBonsaiTrieLogPruningLimit()) + .isEqualTo(1), + "--Xbonsai-trie-log-pruning-enabled", + "--Xbonsai-trie-log-pruning-limit", + "1"); + } + + @Test + public void bonsaiTrieLogPruningLimitShouldBePositive() { + internalTestFailure( + "--Xbonsai-trie-log-pruning-limit=0 must be greater than 0", + "--Xbonsai-trie-log-pruning-enabled", + "--Xbonsai-trie-log-pruning-limit", + "0"); + } + + @Test + public void bonsaiTrieLogRetentionThresholdOption() { + internalTestSuccess( + dataStorageConfiguration -> + assertThat(dataStorageConfiguration.getUnstable().getBonsaiTrieLogRetentionThreshold()) + .isEqualTo(MINIMUM_BONSAI_TRIE_LOG_RETENTION_THRESHOLD + 1), + "--Xbonsai-trie-log-pruning-enabled", + "--Xbonsai-trie-log-retention-threshold", + "513"); + } + + @Test + public void bonsaiTrieLogRetentionThresholdOption_boundaryTest() { + internalTestSuccess( + dataStorageConfiguration -> + assertThat(dataStorageConfiguration.getUnstable().getBonsaiTrieLogRetentionThreshold()) + .isEqualTo(MINIMUM_BONSAI_TRIE_LOG_RETENTION_THRESHOLD), + "--Xbonsai-trie-log-pruning-enabled", + "--Xbonsai-trie-log-retention-threshold", + "512"); + } + + @Test + public void bonsaiTrieLogRetentionThresholdShouldBeAboveMinimum() { + internalTestFailure( + "--Xbonsai-trie-log-retention-threshold minimum value is 512", + "--Xbonsai-trie-log-pruning-enabled", + "--Xbonsai-trie-log-retention-threshold", + "511"); + } + + @Override + protected DataStorageConfiguration createDefaultDomainObject() { + return DataStorageConfiguration.DEFAULT_CONFIG; + } + + @Override + protected DataStorageConfiguration createCustomizedDomainObject() { + return ImmutableDataStorageConfiguration.builder() + .dataStorageFormat(DataStorageFormat.BONSAI) + .bonsaiMaxLayersToLoad(100L) + .unstable( + ImmutableDataStorageConfiguration.Unstable.builder() + .bonsaiTrieLogPruningEnabled(true) + .bonsaiTrieLogRetentionThreshold(1000L) + .bonsaiTrieLogPruningLimit(20) + .build()) + .build(); + } + + @Override + protected DataStorageOptions optionsFromDomainObject( + final DataStorageConfiguration domainObject) { + return DataStorageOptions.fromConfig(domainObject); + } + + @Override + protected DataStorageOptions getOptionsFromBesuCommand(final TestBesuCommand besuCommand) { + return besuCommand.getDataStorageOptions(); + } +} diff --git a/consensus/merge/src/main/java/org/hyperledger/besu/consensus/merge/blockcreation/MergeCoordinator.java b/consensus/merge/src/main/java/org/hyperledger/besu/consensus/merge/blockcreation/MergeCoordinator.java index aa3910d2b5b..ab7fd05ea21 100644 --- a/consensus/merge/src/main/java/org/hyperledger/besu/consensus/merge/blockcreation/MergeCoordinator.java +++ b/consensus/merge/src/main/java/org/hyperledger/besu/consensus/merge/blockcreation/MergeCoordinator.java @@ -491,7 +491,7 @@ public Optional getOrSyncHeadByHash(final Hash headHash, final Hash if (maybeHeadHeader.isPresent()) { LOG.atDebug() - .setMessage("BlockHeader {} is already present") + .setMessage("BlockHeader {} is already present in blockchain") .addArgument(maybeHeadHeader.get()::toLogString) .log(); } else { diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/BonsaiWorldStateProvider.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/BonsaiWorldStateProvider.java index f0869333ba9..5313977c037 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/BonsaiWorldStateProvider.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/BonsaiWorldStateProvider.java @@ -24,6 +24,7 @@ import org.hyperledger.besu.ethereum.bonsai.cache.CachedWorldStorageManager; import org.hyperledger.besu.ethereum.bonsai.storage.BonsaiWorldStateKeyValueStorage; import org.hyperledger.besu.ethereum.bonsai.trielog.TrieLogManager; +import org.hyperledger.besu.ethereum.bonsai.trielog.TrieLogPruner; import org.hyperledger.besu.ethereum.bonsai.worldview.BonsaiWorldState; import org.hyperledger.besu.ethereum.bonsai.worldview.BonsaiWorldStateUpdateAccumulator; import org.hyperledger.besu.ethereum.chain.Blockchain; @@ -74,14 +75,19 @@ public BonsaiWorldStateProvider( final CachedMerkleTrieLoader cachedMerkleTrieLoader, final ObservableMetricsSystem metricsSystem, final BesuContext pluginContext, - final EvmConfiguration evmConfiguration) { + final EvmConfiguration evmConfiguration, + final TrieLogPruner trieLogPruner) { this.cachedWorldStorageManager = new CachedWorldStorageManager(this, worldStateStorage, metricsSystem); // TODO: de-dup constructors this.trieLogManager = new TrieLogManager( - blockchain, worldStateStorage, maxLayersToLoad.orElse(RETAINED_LAYERS), pluginContext); + blockchain, + worldStateStorage, + maxLayersToLoad.orElse(RETAINED_LAYERS), + pluginContext, + trieLogPruner); this.blockchain = blockchain; this.worldStateStorage = worldStateStorage; this.cachedMerkleTrieLoader = cachedMerkleTrieLoader; diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/storage/BonsaiWorldStateKeyValueStorage.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/storage/BonsaiWorldStateKeyValueStorage.java index 9756b280e45..28e2c5bd7aa 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/storage/BonsaiWorldStateKeyValueStorage.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/storage/BonsaiWorldStateKeyValueStorage.java @@ -47,6 +47,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Predicate; import java.util.function.Supplier; +import java.util.stream.Stream; import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes32; @@ -203,6 +204,10 @@ public Optional getTrieLog(final Hash blockHash) { return trieLogStorage.get(blockHash.toArrayUnsafe()); } + public Stream streamTrieLogKeys(final int limit) { + return trieLogStorage.streamKeys().limit(limit); + } + public Optional getStateTrieNode(final Bytes location) { return composedWorldStateStorage .get(TRIE_BRANCH_STORAGE, location.toArrayUnsafe()) @@ -335,6 +340,15 @@ public long prune(final Predicate inUseCheck) { throw new RuntimeException("Bonsai Tries do not work with pruning."); } + public boolean pruneTrieLog(final Hash blockHash) { + try { + return trieLogStorage.tryDelete(blockHash.toArrayUnsafe()); + } catch (Exception e) { + LOG.error("Error pruning trie log for block hash {}", blockHash, e); + return false; + } + } + @Override public long addNodeAddedListener(final NodesAddedListener listener) { throw new RuntimeException("addNodeAddedListener not available"); diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/trielog/TrieLogManager.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/trielog/TrieLogManager.java index cabd5a2d300..3a874de6b67 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/trielog/TrieLogManager.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/trielog/TrieLogManager.java @@ -47,16 +47,19 @@ public class TrieLogManager { protected final Subscribers trieLogObservers = Subscribers.create(); protected final TrieLogFactory trieLogFactory; + private final TrieLogPruner trieLogPruner; public TrieLogManager( final Blockchain blockchain, final BonsaiWorldStateKeyValueStorage worldStateStorage, final long maxLayersToLoad, - final BesuContext pluginContext) { + final BesuContext pluginContext, + final TrieLogPruner trieLogPruner) { this.blockchain = blockchain; this.rootWorldStateStorage = worldStateStorage; this.maxLayersToLoad = maxLayersToLoad; this.trieLogFactory = setupTrieLogFactory(pluginContext); + this.trieLogPruner = trieLogPruner; } public synchronized void saveTrieLog( @@ -82,6 +85,8 @@ public synchronized void saveTrieLog( } finally { if (success) { stateUpdater.commit(); + trieLogPruner.addToPruneQueue(forBlockHeader.getNumber(), forBlockHeader.getBlockHash()); + trieLogPruner.pruneFromQueue(); } else { stateUpdater.rollback(); } diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/trielog/TrieLogPruner.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/trielog/TrieLogPruner.java new file mode 100644 index 00000000000..6ba88170742 --- /dev/null +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/trielog/TrieLogPruner.java @@ -0,0 +1,194 @@ +/* + * Copyright contributors to Hyperledger Besu. + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hyperledger.besu.ethereum.bonsai.trielog; + +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.ethereum.bonsai.storage.BonsaiWorldStateKeyValueStorage; +import org.hyperledger.besu.ethereum.chain.Blockchain; +import org.hyperledger.besu.ethereum.core.BlockHeader; +import org.hyperledger.besu.ethereum.core.ProcessableBlockHeader; + +import java.util.Comparator; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Stream; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Multimap; +import com.google.common.collect.TreeMultimap; +import org.apache.tuweni.bytes.Bytes32; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class TrieLogPruner { + + private static final Logger LOG = LoggerFactory.getLogger(TrieLogPruner.class); + + private final int pruningLimit; + private final int loadingLimit; + private final BonsaiWorldStateKeyValueStorage rootWorldStateStorage; + private final Blockchain blockchain; + private final long numBlocksToRetain; + private final boolean requireFinalizedBlock; + + private final Multimap trieLogBlocksAndForksByDescendingBlockNumber = + TreeMultimap.create(Comparator.reverseOrder(), Comparator.naturalOrder()); + + public TrieLogPruner( + final BonsaiWorldStateKeyValueStorage rootWorldStateStorage, + final Blockchain blockchain, + final long numBlocksToRetain, + final int pruningLimit, + final boolean requireFinalizedBlock) { + this.rootWorldStateStorage = rootWorldStateStorage; + this.blockchain = blockchain; + this.numBlocksToRetain = numBlocksToRetain; + this.pruningLimit = pruningLimit; + this.loadingLimit = pruningLimit; // same as pruningLimit for now + this.requireFinalizedBlock = requireFinalizedBlock; + } + + public void initialize() { + preloadQueue(); + } + + private void preloadQueue() { + LOG.atInfo() + .setMessage("Loading first {} trie logs from database...") + .addArgument(loadingLimit) + .log(); + try (final Stream trieLogKeys = rootWorldStateStorage.streamTrieLogKeys(loadingLimit)) { + final AtomicLong count = new AtomicLong(); + trieLogKeys.forEach( + blockHashAsBytes -> { + final Hash blockHash = Hash.wrap(Bytes32.wrap(blockHashAsBytes)); + final Optional header = blockchain.getBlockHeader(blockHash); + if (header.isPresent()) { + trieLogBlocksAndForksByDescendingBlockNumber.put(header.get().getNumber(), blockHash); + count.getAndIncrement(); + } else { + // prune orphaned blocks (sometimes created during block production) + rootWorldStateStorage.pruneTrieLog(blockHash); + } + }); + LOG.atInfo().log("Loaded {} trie logs from database", count); + pruneFromQueue(); + } catch (Exception e) { + LOG.error("Error loading trie logs from database, nothing pruned", e); + } + } + + void addToPruneQueue(final long blockNumber, final Hash blockHash) { + LOG.atTrace() + .setMessage("adding trie log to queue for later pruning blockNumber {}; blockHash {}") + .addArgument(blockNumber) + .addArgument(blockHash) + .log(); + trieLogBlocksAndForksByDescendingBlockNumber.put(blockNumber, blockHash); + } + + int pruneFromQueue() { + final long retainAboveThisBlock = blockchain.getChainHeadBlockNumber() - numBlocksToRetain; + final Optional finalized = blockchain.getFinalized(); + if (requireFinalizedBlock && finalized.isEmpty()) { + LOG.debug("No finalized block present, skipping pruning"); + return 0; + } + + final long retainAboveThisBlockOrFinalized = + finalized + .flatMap(blockchain::getBlockHeader) + .map(ProcessableBlockHeader::getNumber) + .map(finalizedBlock -> Math.min(finalizedBlock, retainAboveThisBlock)) + .orElse(retainAboveThisBlock); + + LOG.atTrace() + .setMessage( + "min((chainHeadNumber: {} - numBlocksToRetain: {}) = {}, finalized: {})) = retainAboveThisBlockOrFinalized: {}") + .addArgument(blockchain::getChainHeadBlockNumber) + .addArgument(numBlocksToRetain) + .addArgument(retainAboveThisBlock) + .addArgument( + () -> + finalized + .flatMap(blockchain::getBlockHeader) + .map(ProcessableBlockHeader::getNumber) + .orElse(null)) + .addArgument(retainAboveThisBlockOrFinalized) + .log(); + + final var pruneWindowEntries = + trieLogBlocksAndForksByDescendingBlockNumber.asMap().entrySet().stream() + .dropWhile((e) -> e.getKey() > retainAboveThisBlockOrFinalized) + .limit(pruningLimit); + + final Multimap wasPruned = ArrayListMultimap.create(); + + pruneWindowEntries.forEach( + (e) -> { + for (Hash blockHash : e.getValue()) { + if (rootWorldStateStorage.pruneTrieLog(blockHash)) { + wasPruned.put(e.getKey(), blockHash); + } + } + }); + + wasPruned.keySet().forEach(trieLogBlocksAndForksByDescendingBlockNumber::removeAll); + + LOG.atTrace() + .setMessage("pruned {} trie logs for blocks {}") + .addArgument(wasPruned::size) + .addArgument(wasPruned) + .log(); + LOG.atDebug() + .setMessage("pruned {} trie logs from {} blocks") + .addArgument(wasPruned::size) + .addArgument(() -> wasPruned.keySet().size()) + .log(); + + return wasPruned.size(); + } + + public static TrieLogPruner noOpTrieLogPruner() { + return new NoOpTrieLogPruner(null, null, 0, 0); + } + + public static class NoOpTrieLogPruner extends TrieLogPruner { + private NoOpTrieLogPruner( + final BonsaiWorldStateKeyValueStorage rootWorldStateStorage, + final Blockchain blockchain, + final long numBlocksToRetain, + final int pruningLimit) { + super(rootWorldStateStorage, blockchain, numBlocksToRetain, pruningLimit, true); + } + + @Override + public void initialize() { + // no-op + } + + @Override + void addToPruneQueue(final long blockNumber, final Hash blockHash) { + // no-op + } + + @Override + int pruneFromQueue() { + // no-op + return -1; + } + } +} diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/worldstate/DataStorageConfiguration.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/worldstate/DataStorageConfiguration.java index 93b22807ee9..a36b150337a 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/worldstate/DataStorageConfiguration.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/worldstate/DataStorageConfiguration.java @@ -19,6 +19,7 @@ import org.immutables.value.Value; @Value.Immutable +@Value.Enclosing public interface DataStorageConfiguration { long DEFAULT_BONSAI_MAX_LAYERS_TO_LOAD = 512; @@ -27,9 +28,42 @@ public interface DataStorageConfiguration { ImmutableDataStorageConfiguration.builder() .dataStorageFormat(DataStorageFormat.FOREST) .bonsaiMaxLayersToLoad(DEFAULT_BONSAI_MAX_LAYERS_TO_LOAD) + .unstable(Unstable.DEFAULT) .build(); DataStorageFormat getDataStorageFormat(); Long getBonsaiMaxLayersToLoad(); + + @Value.Default + default Unstable getUnstable() { + return Unstable.DEFAULT; + } + + @Value.Immutable + interface Unstable { + + boolean DEFAULT_BONSAI_TRIE_LOG_PRUNING_ENABLED = false; + long DEFAULT_BONSAI_TRIE_LOG_RETENTION_THRESHOLD = 512L; + long MINIMUM_BONSAI_TRIE_LOG_RETENTION_THRESHOLD = DEFAULT_BONSAI_TRIE_LOG_RETENTION_THRESHOLD; + int DEFAULT_BONSAI_TRIE_LOG_PRUNING_LIMIT = 30_000; + + DataStorageConfiguration.Unstable DEFAULT = + ImmutableDataStorageConfiguration.Unstable.builder().build(); + + @Value.Default + default boolean getBonsaiTrieLogPruningEnabled() { + return DEFAULT_BONSAI_TRIE_LOG_PRUNING_ENABLED; + } + + @Value.Default + default long getBonsaiTrieLogRetentionThreshold() { + return DEFAULT_BONSAI_TRIE_LOG_RETENTION_THRESHOLD; + } + + @Value.Default + default int getBonsaiTrieLogPruningLimit() { + return DEFAULT_BONSAI_TRIE_LOG_PRUNING_LIMIT; + } + } } diff --git a/ethereum/core/src/test-support/java/org/hyperledger/besu/ethereum/core/InMemoryKeyValueStorageProvider.java b/ethereum/core/src/test-support/java/org/hyperledger/besu/ethereum/core/InMemoryKeyValueStorageProvider.java index e54cd96a30d..fc8bfe77fba 100644 --- a/ethereum/core/src/test-support/java/org/hyperledger/besu/ethereum/core/InMemoryKeyValueStorageProvider.java +++ b/ethereum/core/src/test-support/java/org/hyperledger/besu/ethereum/core/InMemoryKeyValueStorageProvider.java @@ -17,6 +17,7 @@ import org.hyperledger.besu.ethereum.bonsai.BonsaiWorldStateProvider; import org.hyperledger.besu.ethereum.bonsai.cache.CachedMerkleTrieLoader; import org.hyperledger.besu.ethereum.bonsai.storage.BonsaiWorldStateKeyValueStorage; +import org.hyperledger.besu.ethereum.bonsai.trielog.TrieLogPruner; import org.hyperledger.besu.ethereum.chain.Blockchain; import org.hyperledger.besu.ethereum.chain.DefaultBlockchain; import org.hyperledger.besu.ethereum.chain.MutableBlockchain; @@ -103,7 +104,8 @@ public static BonsaiWorldStateProvider createBonsaiInMemoryWorldStateArchive( cachedMerkleTrieLoader, new NoOpMetricsSystem(), null, - evmConfiguration); + evmConfiguration, + TrieLogPruner.noOpTrieLogPruner()); } public static MutableWorldState createInMemoryWorldState() { diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/AbstractIsolationTests.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/AbstractIsolationTests.java index 5fe3b434046..8cd26eb17b8 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/AbstractIsolationTests.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/AbstractIsolationTests.java @@ -34,6 +34,7 @@ import org.hyperledger.besu.ethereum.blockcreation.AbstractBlockCreator; import org.hyperledger.besu.ethereum.bonsai.cache.CachedMerkleTrieLoader; import org.hyperledger.besu.ethereum.bonsai.storage.BonsaiWorldStateKeyValueStorage; +import org.hyperledger.besu.ethereum.bonsai.trielog.TrieLogPruner; import org.hyperledger.besu.ethereum.chain.GenesisState; import org.hyperledger.besu.ethereum.chain.MutableBlockchain; import org.hyperledger.besu.ethereum.core.Block; @@ -155,7 +156,8 @@ public void createStorage() { new CachedMerkleTrieLoader(new NoOpMetricsSystem()), new NoOpMetricsSystem(), null, - EvmConfiguration.DEFAULT); + EvmConfiguration.DEFAULT, + TrieLogPruner.noOpTrieLogPruner()); var ws = archive.getMutable(); genesisState.writeStateTo(ws); protocolContext = new ProtocolContext(blockchain, archive, null, Optional.empty()); diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/BonsaiWorldStateArchiveTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/BonsaiWorldStateArchiveTest.java index 0c664f77e02..bfe69546295 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/BonsaiWorldStateArchiveTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/BonsaiWorldStateArchiveTest.java @@ -37,6 +37,7 @@ import org.hyperledger.besu.ethereum.bonsai.trielog.TrieLogFactoryImpl; import org.hyperledger.besu.ethereum.bonsai.trielog.TrieLogLayer; import org.hyperledger.besu.ethereum.bonsai.trielog.TrieLogManager; +import org.hyperledger.besu.ethereum.bonsai.trielog.TrieLogPruner; import org.hyperledger.besu.ethereum.bonsai.worldview.BonsaiWorldState; import org.hyperledger.besu.ethereum.chain.Blockchain; import org.hyperledger.besu.ethereum.core.BlockHeader; @@ -124,7 +125,8 @@ void testGetMutableReturnEmptyWhenLoadMoreThanLimitLayersBack() { new CachedMerkleTrieLoader(new NoOpMetricsSystem()), new NoOpMetricsSystem(), null, - EvmConfiguration.DEFAULT); + EvmConfiguration.DEFAULT, + TrieLogPruner.noOpTrieLogPruner()); final BlockHeader blockHeader = blockBuilder.number(0).buildHeader(); final BlockHeader chainHead = blockBuilder.number(512).buildHeader(); when(blockchain.getChainHeadHeader()).thenReturn(chainHead); diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/storage/BonsaiWorldStateKeyValueStorageTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/storage/BonsaiWorldStateKeyValueStorageTest.java index 32a8272237f..13dbe22a403 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/storage/BonsaiWorldStateKeyValueStorageTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/storage/BonsaiWorldStateKeyValueStorageTest.java @@ -19,9 +19,11 @@ import static org.hyperledger.besu.ethereum.storage.keyvalue.KeyValueSegmentIdentifier.TRIE_BRANCH_STORAGE; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import org.hyperledger.besu.datatypes.Address; import org.hyperledger.besu.datatypes.Hash; @@ -29,12 +31,16 @@ import org.hyperledger.besu.ethereum.core.InMemoryKeyValueStorageProvider; import org.hyperledger.besu.ethereum.core.TrieGenerator; import org.hyperledger.besu.ethereum.rlp.RLP; +import org.hyperledger.besu.ethereum.storage.StorageProvider; +import org.hyperledger.besu.ethereum.storage.keyvalue.KeyValueSegmentIdentifier; import org.hyperledger.besu.ethereum.trie.MerkleTrie; import org.hyperledger.besu.ethereum.trie.StorageEntriesCollector; import org.hyperledger.besu.ethereum.trie.patricia.StoredMerklePatriciaTrie; import org.hyperledger.besu.ethereum.worldstate.FlatDbMode; import org.hyperledger.besu.ethereum.worldstate.StateTrieAccountValue; import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem; +import org.hyperledger.besu.plugin.services.storage.KeyValueStorage; +import org.hyperledger.besu.plugin.services.storage.SegmentedKeyValueStorage; import java.util.Arrays; import java.util.Collection; @@ -45,6 +51,7 @@ import org.apache.tuweni.bytes.Bytes32; import org.apache.tuweni.units.bigints.UInt256; import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mockito; @@ -447,4 +454,39 @@ private BonsaiWorldStateKeyValueStorage emptyStorage() { return new BonsaiWorldStateKeyValueStorage( new InMemoryKeyValueStorageProvider(), new NoOpMetricsSystem()); } + + @Test + void successfulPruneReturnsTrue() { + final KeyValueStorage mockTrieLogStorage = mock(KeyValueStorage.class); + when(mockTrieLogStorage.tryDelete(any())).thenReturn(true); + final BonsaiWorldStateKeyValueStorage storage = setupMockStorage(mockTrieLogStorage); + assertThat(storage.pruneTrieLog(Hash.ZERO)).isTrue(); + } + + @Test + void failedPruneReturnsFalse() { + final KeyValueStorage mockTrieLogStorage = mock(KeyValueStorage.class); + when(mockTrieLogStorage.tryDelete(any())).thenReturn(false); + final BonsaiWorldStateKeyValueStorage storage = setupMockStorage(mockTrieLogStorage); + assertThat(storage.pruneTrieLog(Hash.ZERO)).isFalse(); + } + + @Test + void exceptionalPruneReturnsFalse() { + final KeyValueStorage mockTrieLogStorage = mock(KeyValueStorage.class); + when(mockTrieLogStorage.tryDelete(any())).thenThrow(new RuntimeException("test exception")); + final BonsaiWorldStateKeyValueStorage storage = setupMockStorage(mockTrieLogStorage); + assertThat(storage.pruneTrieLog(Hash.ZERO)).isFalse(); + } + + private BonsaiWorldStateKeyValueStorage setupMockStorage( + final KeyValueStorage mockTrieLogStorage) { + final StorageProvider mockStorageProvider = mock(StorageProvider.class); + when(mockStorageProvider.getStorageBySegmentIdentifier( + KeyValueSegmentIdentifier.TRIE_LOG_STORAGE)) + .thenReturn(mockTrieLogStorage); + when(mockStorageProvider.getStorageBySegmentIdentifiers(any())) + .thenReturn(mock(SegmentedKeyValueStorage.class)); + return new BonsaiWorldStateKeyValueStorage(mockStorageProvider, new NoOpMetricsSystem()); + } } diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/trielog/TrieLogManagerTests.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/trielog/TrieLogManagerTests.java index 9dfe5fac2db..f2fb2224be6 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/trielog/TrieLogManagerTests.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/trielog/TrieLogManagerTests.java @@ -16,6 +16,7 @@ package org.hyperledger.besu.ethereum.bonsai.trielog; import static org.assertj.core.api.Assertions.assertThat; +import static org.hyperledger.besu.ethereum.bonsai.trielog.TrieLogPruner.noOpTrieLogPruner; import static org.mockito.Mockito.spy; import org.hyperledger.besu.datatypes.Hash; @@ -56,7 +57,9 @@ class TrieLogManagerTests { @BeforeEach public void setup() { - trieLogManager = new TrieLogManager(blockchain, bonsaiWorldStateKeyValueStorage, 512, null); + trieLogManager = + new TrieLogManager( + blockchain, bonsaiWorldStateKeyValueStorage, 512, null, noOpTrieLogPruner()); } @Test diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/trielog/TrieLogPrunerTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/trielog/TrieLogPrunerTest.java new file mode 100644 index 00000000000..8538edda378 --- /dev/null +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/trielog/TrieLogPrunerTest.java @@ -0,0 +1,258 @@ +/* + * Copyright contributors to Hyperledger Besu. + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hyperledger.besu.ethereum.bonsai.trielog; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.ethereum.bonsai.storage.BonsaiWorldStateKeyValueStorage; +import org.hyperledger.besu.ethereum.chain.Blockchain; +import org.hyperledger.besu.ethereum.core.BlockDataGenerator; +import org.hyperledger.besu.ethereum.core.BlockHeader; + +import java.util.Optional; +import java.util.stream.Stream; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.config.Configurator; +import org.apache.tuweni.bytes.Bytes; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InOrder; +import org.mockito.Mockito; + +public class TrieLogPrunerTest { + + private BonsaiWorldStateKeyValueStorage worldState; + private Blockchain blockchain; + + @SuppressWarnings("BannedMethod") + @BeforeEach + public void setup() { + Configurator.setLevel(LogManager.getLogger(TrieLogPruner.class).getName(), Level.TRACE); + worldState = Mockito.mock(BonsaiWorldStateKeyValueStorage.class); + blockchain = Mockito.mock(Blockchain.class); + when(worldState.pruneTrieLog(any(Hash.class))).thenReturn(true); + } + + @Test + public void initialize_preloads_queue_and_prunes_orphaned_blocks() { + // Given + int loadingLimit = 2; + final BlockDataGenerator generator = new BlockDataGenerator(); + final BlockHeader header1 = generator.header(1); + final BlockHeader header2 = generator.header(2); + when(worldState.streamTrieLogKeys(loadingLimit)) + .thenReturn(Stream.of(header1.getBlockHash().toArray(), header2.getBlockHash().toArray())); + when(blockchain.getBlockHeader(header1.getBlockHash())).thenReturn(Optional.of(header1)); + when(blockchain.getBlockHeader(header2.getBlockHash())).thenReturn(Optional.empty()); + + // When + TrieLogPruner trieLogPruner = new TrieLogPruner(worldState, blockchain, 3, loadingLimit, false); + trieLogPruner.initialize(); + + // Then + verify(worldState, times(1)).streamTrieLogKeys(2); + verify(worldState, times(1)).pruneTrieLog(header2.getBlockHash()); + } + + @Test + public void trieLogs_pruned_in_reverse_order_within_pruning_window() { + // Given + + // pruning window is below numBlocksToRetain and inside the pruningWindowSize offset. + final long blocksToRetain = 3; + final int pruningWindowSize = 2; + when(blockchain.getChainHeadBlockNumber()).thenReturn(5L); + when(worldState.pruneTrieLog(any(Hash.class))).thenReturn(true); + // requireFinalizedBlock = false means this is not a PoS chain + TrieLogPruner trieLogPruner = + new TrieLogPruner(worldState, blockchain, blocksToRetain, pruningWindowSize, false); + + trieLogPruner.addToPruneQueue(0, key(0)); // older block outside prune window + trieLogPruner.addToPruneQueue(1, key(1)); // block inside the prune window + trieLogPruner.addToPruneQueue(1, key(2)); // same block number (fork) + trieLogPruner.addToPruneQueue(2, key(3)); // different block inside prune window + trieLogPruner.addToPruneQueue(3, key(4)); // retained block + trieLogPruner.addToPruneQueue(4, key(5)); // different retained block + trieLogPruner.addToPruneQueue(5, key(6)); // another retained block + + // When + int wasPruned = trieLogPruner.pruneFromQueue(); + + // Then + assertThat(wasPruned).isEqualTo(3); + InOrder inOrder = Mockito.inOrder(worldState); + inOrder.verify(worldState, times(1)).pruneTrieLog(key(3)); + inOrder.verify(worldState, times(1)).pruneTrieLog(key(1)); // forks in order + inOrder.verify(worldState, times(1)).pruneTrieLog(key(2)); + + // Subsequent run should add one more block, then prune two oldest remaining keys + trieLogPruner.addToPruneQueue(6, key(6)); + when(blockchain.getChainHeadBlockNumber()).thenReturn(6L); + + wasPruned = trieLogPruner.pruneFromQueue(); + + assertThat(wasPruned).isEqualTo(2); + inOrder.verify(worldState, times(1)).pruneTrieLog(key(4)); + inOrder.verify(worldState, times(1)).pruneTrieLog(key(0)); + } + + @Test + public void retain_non_finalized_blocks() { + // Given + // finalizedBlockHeight < configuredRetainHeight + final long finalizedBlockHeight = 1; + final long configuredRetainHeight = 3; + TrieLogPruner trieLogPruner = + setupPrunerAndFinalizedBlock(configuredRetainHeight, finalizedBlockHeight); + + // When + final int wasPruned = trieLogPruner.pruneFromQueue(); + + // Then + assertThat(wasPruned).isEqualTo(1); + verify(worldState, times(1)).pruneTrieLog(key(1)); // should prune (finalized) + verify(worldState, never()).pruneTrieLog(key(2)); // would prune but (NOT finalized) + verify(worldState, never()).pruneTrieLog(key(3)); // would prune but (NOT finalized) + verify(worldState, never()).pruneTrieLog(key(4)); // retained block (NOT finalized) + verify(worldState, never()).pruneTrieLog(key(5)); // chain height (NOT finalized) + } + + @Test + public void boundary_test_when_configured_retain_equals_finalized_block() { + // Given + // finalizedBlockHeight == configuredRetainHeight + final long finalizedBlockHeight = 2; + final long configuredRetainHeight = 2; + TrieLogPruner trieLogPruner = + setupPrunerAndFinalizedBlock(configuredRetainHeight, finalizedBlockHeight); + + // When + final int wasPruned = trieLogPruner.pruneFromQueue(); + + // Then + assertThat(wasPruned).isEqualTo(1); + verify(worldState, times(1)).pruneTrieLog(key(1)); // should prune (finalized) + verify(worldState, never()).pruneTrieLog(key(2)); // retained block (finalized) + verify(worldState, never()).pruneTrieLog(key(3)); // retained block (NOT finalized) + verify(worldState, never()).pruneTrieLog(key(4)); // retained block (NOT finalized) + verify(worldState, never()).pruneTrieLog(key(5)); // chain height (NOT finalized) + } + + @Test + public void use_configured_retain_when_finalized_block_is_higher() { + // Given + // finalizedBlockHeight > configuredRetainHeight + final long finalizedBlockHeight = 4; + final long configuredRetainHeight = 3; + final TrieLogPruner trieLogPruner = + setupPrunerAndFinalizedBlock(configuredRetainHeight, finalizedBlockHeight); + + // When + final int wasPruned = trieLogPruner.pruneFromQueue(); + + // Then + assertThat(wasPruned).isEqualTo(2); + final InOrder inOrder = Mockito.inOrder(worldState); + inOrder.verify(worldState, times(1)).pruneTrieLog(key(2)); // should prune (finalized) + inOrder.verify(worldState, times(1)).pruneTrieLog(key(1)); // should prune (finalized) + verify(worldState, never()).pruneTrieLog(key(3)); // retained block (finalized) + verify(worldState, never()).pruneTrieLog(key(4)); // retained block (finalized) + verify(worldState, never()).pruneTrieLog(key(5)); // chain height (NOT finalized) + } + + @Test + public void skip_pruning_when_finalized_block_required_but_not_present() { + // This can occur at the start of PoS chains + + // Given + when(blockchain.getFinalized()).thenReturn(Optional.empty()); + final long configuredRetainHeight = 2; + final long chainHeight = 2; + final long configuredRetainAboveHeight = configuredRetainHeight - 1; + final long blocksToRetain = chainHeight - configuredRetainAboveHeight; + final int pruningWindowSize = (int) chainHeight; + when(blockchain.getChainHeadBlockNumber()).thenReturn(chainHeight); + TrieLogPruner trieLogPruner = + new TrieLogPruner(worldState, blockchain, blocksToRetain, pruningWindowSize, true); + + trieLogPruner.addToPruneQueue(1, key(1)); + trieLogPruner.addToPruneQueue(2, key(2)); + + // When + final int wasPruned = trieLogPruner.pruneFromQueue(); + + // Then + assertThat(wasPruned).isEqualTo(0); + verify(worldState, never()).pruneTrieLog(key(1)); // not finalized + verify(worldState, never()).pruneTrieLog(key(2)); // not finalized + } + + @Test + public void do_not_count_trieLog_when_prune_fails_first_attempt() { + // Given + when(worldState.pruneTrieLog(key(2))).thenReturn(false); + final long finalizedBlockHeight = 4; + final long configuredRetainHeight = 4; + final TrieLogPruner trieLogPruner = + setupPrunerAndFinalizedBlock(configuredRetainHeight, finalizedBlockHeight); + + // When + final int wasPruned = trieLogPruner.pruneFromQueue(); + + // Then + assertThat(wasPruned).isEqualTo(2); + + // Subsequent run should prune previously skipped trieLog + when(worldState.pruneTrieLog(key(2))).thenReturn(true); + assertThat(trieLogPruner.pruneFromQueue()).isEqualTo(1); + } + + private TrieLogPruner setupPrunerAndFinalizedBlock( + final long configuredRetainHeight, final long finalizedBlockHeight) { + final long chainHeight = 5; + final long configuredRetainAboveHeight = configuredRetainHeight - 1; + final long blocksToRetain = chainHeight - configuredRetainAboveHeight; + final int pruningWindowSize = (int) chainHeight; + + final BlockHeader finalizedHeader = new BlockDataGenerator().header(finalizedBlockHeight); + when(blockchain.getFinalized()).thenReturn(Optional.of(finalizedHeader.getBlockHash())); + when(blockchain.getBlockHeader(finalizedHeader.getBlockHash())) + .thenReturn(Optional.of(finalizedHeader)); + when(blockchain.getChainHeadBlockNumber()).thenReturn(chainHeight); + TrieLogPruner trieLogPruner = + new TrieLogPruner(worldState, blockchain, blocksToRetain, pruningWindowSize, true); + + trieLogPruner.addToPruneQueue(1, key(1)); + trieLogPruner.addToPruneQueue(2, key(2)); + trieLogPruner.addToPruneQueue(3, key(3)); + trieLogPruner.addToPruneQueue(4, key(4)); + trieLogPruner.addToPruneQueue(5, key(5)); + + return trieLogPruner; + } + + private Hash key(final int k) { + return Hash.hash(Bytes.of(k)); + } +} diff --git a/ethereum/referencetests/src/main/java/org/hyperledger/besu/ethereum/referencetests/BonsaiReferenceTestWorldState.java b/ethereum/referencetests/src/main/java/org/hyperledger/besu/ethereum/referencetests/BonsaiReferenceTestWorldState.java index c46536592cd..e56d5c858f8 100644 --- a/ethereum/referencetests/src/main/java/org/hyperledger/besu/ethereum/referencetests/BonsaiReferenceTestWorldState.java +++ b/ethereum/referencetests/src/main/java/org/hyperledger/besu/ethereum/referencetests/BonsaiReferenceTestWorldState.java @@ -22,6 +22,7 @@ import org.hyperledger.besu.ethereum.bonsai.storage.BonsaiWorldStateKeyValueStorage; import org.hyperledger.besu.ethereum.bonsai.trielog.TrieLogAddedEvent; import org.hyperledger.besu.ethereum.bonsai.trielog.TrieLogManager; +import org.hyperledger.besu.ethereum.bonsai.trielog.TrieLogPruner; import org.hyperledger.besu.ethereum.bonsai.worldview.BonsaiWorldState; import org.hyperledger.besu.ethereum.bonsai.worldview.BonsaiWorldStateUpdateAccumulator; import org.hyperledger.besu.ethereum.core.BlockHeader; @@ -197,7 +198,7 @@ public void reset() { static class NoOpTrieLogManager extends TrieLogManager { public NoOpTrieLogManager() { - super(null, null, 0, null); + super(null, null, 0, null, TrieLogPruner.noOpTrieLogPruner()); } @SuppressWarnings({"UnsynchronizedOverridesSynchronized", "squid:S3551"})