diff --git a/besu/build.gradle b/besu/build.gradle index 73977d81ba7..b3c6d0644e9 100644 --- a/besu/build.gradle +++ b/besu/build.gradle @@ -77,6 +77,7 @@ dependencies { implementation 'org.springframework.security:spring-security-crypto' implementation 'org.xerial.snappy:snappy-java' implementation 'tech.pegasys:jc-kzg-4844' + implementation 'org.rocksdb:rocksdbjni' runtimeOnly 'org.apache.logging.log4j:log4j-jul' runtimeOnly 'com.splunk.logging:splunk-library-javalogging' diff --git a/besu/src/main/java/org/hyperledger/besu/cli/subcommands/operator/OperatorSubCommand.java b/besu/src/main/java/org/hyperledger/besu/cli/subcommands/operator/OperatorSubCommand.java index 705b8fd9e5e..db5c7cf893f 100644 --- a/besu/src/main/java/org/hyperledger/besu/cli/subcommands/operator/OperatorSubCommand.java +++ b/besu/src/main/java/org/hyperledger/besu/cli/subcommands/operator/OperatorSubCommand.java @@ -36,7 +36,9 @@ GenerateBlockchainConfig.class, GenerateLogBloomCache.class, BackupState.class, - RestoreState.class + RestoreState.class, + TrieLogSubCommand.class, + RocksDbSubCommand.class }) public class OperatorSubCommand implements Runnable { diff --git a/besu/src/main/java/org/hyperledger/besu/cli/subcommands/operator/RocksDbSubCommand.java b/besu/src/main/java/org/hyperledger/besu/cli/subcommands/operator/RocksDbSubCommand.java new file mode 100644 index 00000000000..511fcd24cd4 --- /dev/null +++ b/besu/src/main/java/org/hyperledger/besu/cli/subcommands/operator/RocksDbSubCommand.java @@ -0,0 +1,112 @@ +/* + * Copyright Hyperledger Besu Contributors. + * + * 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.subcommands.operator; + +import static org.hyperledger.besu.controller.BesuController.DATABASE_PATH; + +import org.hyperledger.besu.cli.util.VersionProvider; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; + +import org.rocksdb.ColumnFamilyDescriptor; +import org.rocksdb.ColumnFamilyHandle; +import org.rocksdb.Options; +import org.rocksdb.RocksDB; +import org.rocksdb.RocksDBException; +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.ParentCommand; + +/** The RocksDB subcommand. */ +@Command( + name = "x-rocksdb", + description = "Print RocksDB information", + mixinStandardHelpOptions = true, + versionProvider = VersionProvider.class, + subcommands = {RocksDbSubCommand.RocksDbUsage.class}) +public class RocksDbSubCommand implements Runnable { + + @ParentCommand private OperatorSubCommand parentCommand; + + @SuppressWarnings("unused") + @CommandLine.Spec + private CommandLine.Model.CommandSpec spec; + + @Override + public void run() { + spec.commandLine().usage(System.out); + } + + @Command( + name = "usage", + description = "Prints disk usage", + mixinStandardHelpOptions = true, + versionProvider = VersionProvider.class) + static class RocksDbUsage implements Runnable { + + @SuppressWarnings("unused") + @CommandLine.Spec + private CommandLine.Model.CommandSpec spec; + + @SuppressWarnings("unused") + @ParentCommand + private RocksDbSubCommand parentCommand; + + @Override + public void run() { + + final PrintWriter out = spec.commandLine().getOut(); + + final String dbPath = + parentCommand + .parentCommand + .parentCommand + .dataDir() + .toString() + .concat("/") + .concat(DATABASE_PATH); + + RocksDB.loadLibrary(); + Options options = new Options(); + options.setCreateIfMissing(true); + + // Open the RocksDB database with multiple column families + List cfNames; + try { + cfNames = RocksDB.listColumnFamilies(options, dbPath); + } catch (RocksDBException e) { + throw new RuntimeException(e); + } + List cfHandles = new ArrayList<>(); + List cfDescriptors = new ArrayList<>(); + for (byte[] cfName : cfNames) { + cfDescriptors.add(new ColumnFamilyDescriptor(cfName)); + } + try (final RocksDB rocksdb = RocksDB.openReadOnly(dbPath, cfDescriptors, cfHandles)) { + for (ColumnFamilyHandle cfHandle : cfHandles) { + RocksDbUsageHelper.printUsageForColumnFamily(rocksdb, cfHandle, out); + } + } catch (RocksDBException e) { + throw new RuntimeException(e); + } finally { + for (ColumnFamilyHandle cfHandle : cfHandles) { + cfHandle.close(); + } + } + } + } +} diff --git a/besu/src/main/java/org/hyperledger/besu/cli/subcommands/operator/RocksDbUsageHelper.java b/besu/src/main/java/org/hyperledger/besu/cli/subcommands/operator/RocksDbUsageHelper.java new file mode 100644 index 00000000000..955b9a8ce02 --- /dev/null +++ b/besu/src/main/java/org/hyperledger/besu/cli/subcommands/operator/RocksDbUsageHelper.java @@ -0,0 +1,96 @@ +/* + * 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.subcommands.operator; + +import org.hyperledger.besu.ethereum.storage.keyvalue.KeyValueSegmentIdentifier; + +import java.io.PrintWriter; + +import org.bouncycastle.util.Arrays; +import org.rocksdb.ColumnFamilyHandle; +import org.rocksdb.ColumnFamilyMetaData; +import org.rocksdb.RocksDB; +import org.rocksdb.RocksDBException; + +public class RocksDbUsageHelper { + + static void printUsageForColumnFamily( + final RocksDB rocksdb, final ColumnFamilyHandle cfHandle, final PrintWriter out) + throws RocksDBException { + String size = rocksdb.getProperty(cfHandle, "rocksdb.estimate-live-data-size"); + boolean emptyColumnFamily = false; + if (!size.isEmpty() && !size.isBlank()) { + long sizeLong = Long.parseLong(size); + if (sizeLong == 0) emptyColumnFamily = true; + if (!emptyColumnFamily) { + out.println( + "****** Column family '" + + getNameById(cfHandle.getName()) + + "' size: " + + formatOutputSize(sizeLong) + + " ******"); + // System.out.println("SST table : "+ rocksdb.getProperty(cfHandle, + // "rocksdb.sstables")); + + out.println( + "Number of live snapshots : " + rocksdb.getProperty(cfHandle, "rocksdb.num-snapshots")); + out.println( + "Number of keys : " + rocksdb.getProperty(cfHandle, "rocksdb.estimate-num-keys")); + + String totolSstFilesSize = rocksdb.getProperty(cfHandle, "rocksdb.total-sst-files-size"); + if (!totolSstFilesSize.isEmpty() && !totolSstFilesSize.isBlank()) { + out.println( + "Total size of SST Files : " + formatOutputSize(Long.parseLong(totolSstFilesSize))); + } + String liveSstFilesSize = rocksdb.getProperty(cfHandle, "rocksdb.live-sst-files-size"); + if (!liveSstFilesSize.isEmpty() && !liveSstFilesSize.isBlank()) { + out.println( + "Size of live SST Filess : " + formatOutputSize(Long.parseLong(liveSstFilesSize))); + } + + ColumnFamilyMetaData columnFamilyMetaData = rocksdb.getColumnFamilyMetaData(cfHandle); + long sizeBytes = columnFamilyMetaData.size(); + out.println( + "Column family size (with getColumnFamilyMetaData) : " + formatOutputSize(sizeBytes)); + out.println(""); + } + } + } + + private static String formatOutputSize(final long size) { + if (size > (1024 * 1024 * 1024)) { + long sizeInGiB = size / (1024 * 1024 * 1024); + return sizeInGiB + " GiB"; + } else if (size > (1024 * 1024)) { + long sizeInMiB = size / (1024 * 1024); + return sizeInMiB + " MiB"; + } else if (size > 1024) { + long sizeInKiB = size / 1024; + return sizeInKiB + " KiB"; + } else { + return size + " B"; + } + } + + public static String getNameById(final byte[] id) { + for (KeyValueSegmentIdentifier segment : KeyValueSegmentIdentifier.values()) { + if (Arrays.areEqual(segment.getId(), id)) { + return segment.getName(); + } + } + return null; // id not found + } +} diff --git a/besu/src/main/java/org/hyperledger/besu/cli/subcommands/operator/TrieLogSubCommand.java b/besu/src/main/java/org/hyperledger/besu/cli/subcommands/operator/TrieLogSubCommand.java new file mode 100644 index 00000000000..26c2688eeaa --- /dev/null +++ b/besu/src/main/java/org/hyperledger/besu/cli/subcommands/operator/TrieLogSubCommand.java @@ -0,0 +1,519 @@ +/* + * Copyright Hyperledger Besu Contributors. + * + * 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.subcommands.operator; + +import static com.google.common.base.Preconditions.checkNotNull; +import static org.hyperledger.besu.cli.DefaultCommandValues.MANDATORY_LONG_FORMAT_HELP; +import static org.hyperledger.besu.controller.BesuController.DATABASE_PATH; +import static org.hyperledger.besu.ethereum.bonsai.trielog.AbstractTrieLogManager.LOG_RANGE_LIMIT; + +import org.hyperledger.besu.cli.util.VersionProvider; +import org.hyperledger.besu.controller.BesuController; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.ethereum.bonsai.BonsaiWorldStateProvider; +import org.hyperledger.besu.ethereum.bonsai.trielog.AbstractTrieLogManager; +import org.hyperledger.besu.ethereum.chain.MutableBlockchain; +import org.hyperledger.besu.ethereum.core.BlockHeader; +import org.hyperledger.besu.ethereum.storage.keyvalue.KeyValueSegmentIdentifier; +import org.hyperledger.besu.ethereum.worldstate.WorldStateArchive; +import org.hyperledger.besu.plugin.services.storage.KeyValueStorage; +import org.hyperledger.besu.plugin.services.trielogs.TrieLog; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Predicate; +import java.util.stream.LongStream; +import java.util.stream.Stream; + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.rocksdb.ColumnFamilyDescriptor; +import org.rocksdb.ColumnFamilyHandle; +import org.rocksdb.Options; +import org.rocksdb.RocksDB; +import org.rocksdb.RocksDBException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.ParentCommand; + +/** The Trie Log subcommand. */ +@Command( + name = "x-trie-log", + description = "Manipulate trie logs", + mixinStandardHelpOptions = true, + versionProvider = VersionProvider.class, + subcommands = { + TrieLogSubCommand.CountTrieLog.class, + TrieLogSubCommand.ListTrieLog.class, + TrieLogSubCommand.DeleteTrieLog.class, + TrieLogSubCommand.PruneTrieLog.class + }) +public class TrieLogSubCommand implements Runnable { + + private static final Logger LOG = LoggerFactory.getLogger(TrieLogSubCommand.class); + + @Option( + names = "--block", + paramLabel = MANDATORY_LONG_FORMAT_HELP, + description = "The block", + arity = "0..1") + private String targetBlockHash = ""; + + @Option( + names = {"--from", "--from-block-number"}, + paramLabel = MANDATORY_LONG_FORMAT_HELP, + description = "Start of the block number range", + arity = "0..1") + private Long fromBlockNumber; + + @Option( + names = {"--to", "--to-block-number"}, + paramLabel = MANDATORY_LONG_FORMAT_HELP, + description = "End of the block number range", + arity = "0..1") + private Long toBlockNumber; + + @ParentCommand private OperatorSubCommand parentCommand; + + @SuppressWarnings("unused") + @CommandLine.Spec + private CommandLine.Model.CommandSpec spec; // Picocli injects reference to command spec + + private BesuController besuController; + + @Override + public void run() { + try { + final PrintWriter out = spec.commandLine().getOut(); + + besuController = createBesuController(); + + // TODO SLD use TrieLogProvider instead? + // parentCommand.parentCommand.parentCommand.besuPluginService + + /* TODO SLD CacheWorldStorageManager vs BonsaiWorldStateProvider? + this.trieLogManager = + new CachedWorldStorageManager( + this, + blockchain, + worldStateStorage, + metricsSystem, + maxLayersToLoad.orElse(RETAINED_LAYERS), + pluginContext); + */ + if (besuController.getProtocolContext().getWorldStateArchive() + instanceof BonsaiWorldStateProvider) { + Optional trieLogLayer = + ((BonsaiWorldStateProvider) besuController.getProtocolContext().getWorldStateArchive()) + .getTrieLogManager() + .getTrieLogLayer(Hash.fromHexString(targetBlockHash)); + if (trieLogLayer.isPresent()) { + out.printf("result: %s\n", trieLogLayer.get()); + } else { + out.printf("No trie log found for block hash %s\n", targetBlockHash); + } + } else { + out.println("Subcommand only works with Bonsai"); + } + + // KeyValueStorage trieLogStorage = + // + // besuController.getStorageProvider().getStorageBySegmentIdentifier(KeyValueSegmentIdentifier.TRIE_LOG_STORAGE); + // Optional bytes = + // trieLogStorage.get(Bytes.fromHexString(targetBlockHash.toString()).toArrayUnsafe()); + // LOG.atInfo().setMessage("result: {}") + // .addArgument(HexFormat.of().formatHex(bytes.orElse(new byte[0]))) + // .log(); + } catch (final Exception e) { + LOG.error("TODO SLD", e); + spec.commandLine().usage(System.out); + } + } + + private BesuController createBesuController() { + return parentCommand.parentCommand.buildController(); + } + + private Stream rangeAsStream(final long fromBlockNumber, final long toBlockNumber) { + if (Math.abs(toBlockNumber - fromBlockNumber) > LOG_RANGE_LIMIT) { + throw new IllegalArgumentException("Requested Range too large"); + } + long left = Math.min(fromBlockNumber, toBlockNumber); + long right = Math.max(fromBlockNumber, toBlockNumber); + return LongStream.range(left, right).boxed(); + } + + @Command( + name = "count", + description = "This command counts all the trie logs", + mixinStandardHelpOptions = true, + versionProvider = VersionProvider.class) + static class CountTrieLog implements Runnable { + + @SuppressWarnings("unused") + @ParentCommand + private TrieLogSubCommand parentCommand; + + @SuppressWarnings("unused") + @CommandLine.Spec + private CommandLine.Model.CommandSpec spec; // Picocli injects reference to command spec + + private BesuController besuController; + + @Override + public void run() { + checkNotNull(parentCommand); + + final PrintWriter out = spec.commandLine().getOut(); + + besuController = parentCommand.createBesuController(); + + WorldStateArchive worldStateArchive = + besuController.getProtocolContext().getWorldStateArchive(); + final MutableBlockchain blockchain = besuController.getProtocolContext().getBlockchain(); + + if (worldStateArchive instanceof BonsaiWorldStateProvider) { + final KeyValueStorage trieLogStorage = + besuController + .getStorageProvider() + .getStorageBySegmentIdentifier(KeyValueSegmentIdentifier.TRIE_LOG_STORAGE); + final long totalCount = trieLogStorage.stream().count(); + + final long canonicalCount = + trieLogStorage + .streamKeys() + .map(Bytes32::wrap) + .map(Bytes::toHexString) + .map(Hash::fromHexString) + .map(blockchain::getBlockHeader) + .filter(Optional::isPresent) + .count(); + out.printf("trieLog total count: %d; blockchain count: %d\n", totalCount, canonicalCount); + } else { + out.println("Subcommand only works with Bonsai"); + } + } + } + + @Command( + name = "list", + description = "This command lists all the trie logs", + mixinStandardHelpOptions = true, + versionProvider = VersionProvider.class) + static class ListTrieLog implements Runnable { + + @SuppressWarnings("unused") + @ParentCommand + private TrieLogSubCommand parentCommand; + + @SuppressWarnings("unused") + @CommandLine.Spec + private CommandLine.Model.CommandSpec spec; // Picocli injects reference to command spec + + private BesuController besuController; + + @Override + public void run() { + checkNotNull(parentCommand); + + final PrintWriter out = spec.commandLine().getOut(); + + besuController = parentCommand.createBesuController(); + + WorldStateArchive worldStateArchive = + besuController.getProtocolContext().getWorldStateArchive(); + final MutableBlockchain blockchain = besuController.getProtocolContext().getBlockchain(); + + if (worldStateArchive instanceof BonsaiWorldStateProvider) { + final KeyValueStorage trieLogStorage = + besuController + .getStorageProvider() + .getStorageBySegmentIdentifier(KeyValueSegmentIdentifier.TRIE_LOG_STORAGE); + out.println("list of trie logs:"); + out.println( + " block hash (key) | block number"); + out.println( + "___________________________________________________________________|__________________"); + + Predicate rangeFilter = (_ignore) -> true; + if (parentCommand.fromBlockNumber != null && parentCommand.toBlockNumber != null) { + + final List blockHashRange = + parentCommand + .rangeAsStream(parentCommand.fromBlockNumber, parentCommand.toBlockNumber) + .map(blockchain::getBlockHeader) + .flatMap(Optional::stream) + .map(BlockHeader::getHash) + .toList(); + + rangeFilter = blockHashRange::contains; + } + + final AtomicInteger count = new AtomicInteger(); + final AtomicInteger canonicalCount = new AtomicInteger(); + final AtomicInteger forkCount = new AtomicInteger(); + final AtomicInteger orphanCount = new AtomicInteger(); + trieLogStorage + .streamKeys() + .map(Bytes32::wrap) + .map(Bytes::toHexString) + .map(Hash::fromHexString) + .filter(rangeFilter) + .forEach( + hash -> { + count.getAndIncrement(); + out.printf( + "%s | %s\n", + hash, + blockchain + .getBlockHeader(hash) + .map( + (header) -> { + long number = header.getNumber(); + final Optional headerByNumber = + blockchain.getBlockHeader(number); + if (headerByNumber.isPresent() + && headerByNumber.get().getHash().equals(hash)) { + canonicalCount.getAndIncrement(); + return String.valueOf(number); + } else { + forkCount.getAndIncrement(); + return "fork of " + number; + } + }) + .orElseGet( + () -> { + orphanCount.getAndIncrement(); + return "not in blockchain"; + })); + }); + + out.printf( + "trieLog count: %s\n - canonical count: %s\n - fork count: %s\n - orphaned count: %s\n", + count, canonicalCount, forkCount, orphanCount); + } else { + out.println("Subcommand only works with Bonsai"); + } + } + } + + @Command( + name = "delete", + description = "Deletes the trie logs stored under the specified hash or block number range.", + mixinStandardHelpOptions = true, + versionProvider = VersionProvider.class) + static class DeleteTrieLog implements Runnable { + + @SuppressWarnings("unused") + @ParentCommand + private TrieLogSubCommand parentCommand; + + @SuppressWarnings("unused") + @CommandLine.Spec + private CommandLine.Model.CommandSpec spec; // Picocli injects reference to command spec + + private BesuController besuController; + + @Override + public void run() { + final PrintWriter out = spec.commandLine().getOut(); + + checkNotNull(parentCommand); + + besuController = parentCommand.createBesuController(); + final MutableBlockchain blockchain = besuController.getProtocolContext().getBlockchain(); + + WorldStateArchive worldStateArchive = + besuController.getProtocolContext().getWorldStateArchive(); + + if (worldStateArchive instanceof BonsaiWorldStateProvider) { + final String targetBlockHash = parentCommand.targetBlockHash; + final Long fromBlockNumber = parentCommand.fromBlockNumber; + final Long toBlockNumber = parentCommand.toBlockNumber; + + boolean success = false; + if (!targetBlockHash.isEmpty()) { + success = + ((BonsaiWorldStateProvider) worldStateArchive) + .getTrieLogManager() + .deleteTrieLogLayer(Hash.fromHexString(targetBlockHash)); + } else if (fromBlockNumber != null && toBlockNumber != null) { + + final Stream toDelete = + parentCommand + .rangeAsStream(fromBlockNumber, toBlockNumber) + .map(blockchain::getBlockHeader) + .flatMap(Optional::stream) + .map(BlockHeader::getHash); + + success = + toDelete.allMatch( + h -> + ((BonsaiWorldStateProvider) worldStateArchive) + .getTrieLogManager() + .deleteTrieLogLayer(h)); + + } else { + out.println("Please specify either --block or --fromBlockNumber and --toBlockNumber"); + } + out.printf("success? %s\n", success); + } else { + out.println("Subcommand only works with Bonsai"); + } + } + } + + @Command( + name = "prune", + description = + "This command prunes all trie log layers below the specified block number, including orphaned trie logs.", + mixinStandardHelpOptions = true, + versionProvider = VersionProvider.class) + static class PruneTrieLog implements Runnable { + + @SuppressWarnings("unused") + @ParentCommand + private TrieLogSubCommand parentCommand; + + @SuppressWarnings("unused") + @CommandLine.Spec + private CommandLine.Model.CommandSpec spec; // Picocli injects reference to command spec + + private BesuController besuController; + + @Option( + names = {"--retain", "--layers-to-retain"}, + paramLabel = MANDATORY_LONG_FORMAT_HELP, + description = + "Number of trie log layers to retain starting at head, prune below this number", + arity = "0..1") + private Long layersToRetain = AbstractTrieLogManager.RETAINED_LAYERS; + + @Override + public void run() { + checkNotNull(parentCommand); + + final PrintWriter out = spec.commandLine().getOut(); + besuController = parentCommand.createBesuController(); + final MutableBlockchain blockchain = besuController.getProtocolContext().getBlockchain(); + + out.println("Current trie log disk usage:"); + printTrieLogDiskUsage(out); + + out.printf("Current head block number: %d\n", blockchain.getChainHead().getHeight()); + out.printf("Trie log layers to retain: %d\n", layersToRetain); + final long deleteBelowHere = blockchain.getChainHead().getHeight() - layersToRetain + 1; + out.printf( + "Attempting trie log layer prune below block %s = (%s - %s + 1)...\n", + deleteBelowHere, blockchain.getChainHead().getHeight(), layersToRetain); + + WorldStateArchive worldStateArchive = + besuController.getProtocolContext().getWorldStateArchive(); + + if (worldStateArchive instanceof BonsaiWorldStateProvider) { + if (layersToRetain != null) { + + final KeyValueStorage trieLogStorage = + besuController + .getStorageProvider() + .getStorageBySegmentIdentifier(KeyValueSegmentIdentifier.TRIE_LOG_STORAGE); + + final AtomicInteger prunedCount = new AtomicInteger(); + trieLogStorage + .streamKeys() + .forEach( + hashAsBytes -> { + Hash hash = Hash.wrap(Bytes32.wrap(hashAsBytes)); + + final Optional header = blockchain.getBlockHeader(hash); + if (header.isEmpty()) { // TODO SLD what if we're still producing this block? + // Orphaned trie logs are neither in the canonical blockchain nor forks. + // Likely created during block production + recordResult(trieLogStorage.tryDelete(hashAsBytes), prunedCount, hash); + } else { + if (header.get().getNumber() < deleteBelowHere) { + // Prune canonical and fork trie logs below the block number + recordResult(trieLogStorage.tryDelete(hashAsBytes), prunedCount, hash); + } + // else { + // LOG.atInfo().setMessage("Retain + // {}").addArgument(hash::toHexString).log(); + // } + } + }); + out.printf("Pruned %d trie logs\n", prunedCount.get()); + + out.println("New trie log disk usage:"); + printTrieLogDiskUsage(out); + } else { + out.println("Please specify --belowBlockNumber"); + } + } else { + out.println("Subcommand only works with Bonsai"); + } + } + + private void printTrieLogDiskUsage(final PrintWriter out) { + + final String dbPath = + parentCommand + .parentCommand + .parentCommand + .dataDir() + .toString() + .concat("/") + .concat(DATABASE_PATH); + + RocksDB.loadLibrary(); + Options options = new Options(); + options.setCreateIfMissing(true); + + List cfHandles = new ArrayList<>(); + List cfDescriptors = new ArrayList<>(); + cfDescriptors.add(new ColumnFamilyDescriptor(RocksDB.DEFAULT_COLUMN_FAMILY)); + cfDescriptors.add( + new ColumnFamilyDescriptor(KeyValueSegmentIdentifier.TRIE_LOG_STORAGE.getId())); + try (final RocksDB rocksdb = RocksDB.openReadOnly(dbPath, cfDescriptors, cfHandles)) { + for (ColumnFamilyHandle cfHandle : cfHandles) { + RocksDbUsageHelper.printUsageForColumnFamily(rocksdb, cfHandle, out); + } + } catch (RocksDBException e) { + LOG.error("TODO SLD ", e); + e.printStackTrace(); + throw new RuntimeException(e); + } finally { + for (ColumnFamilyHandle cfHandle : cfHandles) { + cfHandle.close(); + } + } + } + + private void recordResult( + final boolean success, final AtomicInteger prunedCount, final Hash hash) { + if (success) { + prunedCount.getAndIncrement(); + // LOG.atInfo().setMessage("Pruned {}").addArgument(hash::toHexString).log(); + } else { + LOG.atInfo().setMessage("Failed to prune {}").addArgument(hash::toHexString).log(); + } + } + } +} diff --git a/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/KeyPairUtil.java b/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/KeyPairUtil.java index f20b1b37103..013b385c3c2 100644 --- a/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/KeyPairUtil.java +++ b/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/KeyPairUtil.java @@ -84,6 +84,7 @@ public static KeyPair loadKeyPair(final File keyFile) { final KeyPair key; if (keyFile.exists()) { + LOG.info("Attempting to load public key from {}", keyFile.getAbsolutePath()); key = load(keyFile); LOG.info( "Loaded public key {} from {}", key.getPublicKey().toString(), keyFile.getAbsolutePath()); 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 299b826369c..0cc9162ae91 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 @@ -153,6 +153,12 @@ public boolean isWorldStateAvailable(final Hash rootHash, final Hash blockHash) || worldStateStorage.isWorldStateAvailable(rootHash, blockHash); } + // TODO SLD protection against pruned trie logs when rolling back/forward etc so we're not relying + // on 512 max layers config. + + // TODO SLD during reorg, prune trie logs, then another reorg -> roll forward - could be a problem + // with roll forward? + @Override public Optional getMutable( final BlockHeader blockHeader, final boolean shouldPersistState) { 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..9fab5b6ac3c 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 @@ -335,6 +335,10 @@ public long prune(final Predicate inUseCheck) { throw new RuntimeException("Bonsai Tries do not work with pruning."); } + public boolean deleteTrieLog(final Hash blockHash) { + return trieLogStorage.tryDelete(blockHash.toArrayUnsafe()); + } + @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/AbstractTrieLogManager.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/trielog/AbstractTrieLogManager.java index 90aff386e83..6db2d0cbba5 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/trielog/AbstractTrieLogManager.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/trielog/AbstractTrieLogManager.java @@ -95,6 +95,11 @@ public synchronized void saveTrieLog( } } + @Override + public boolean deleteTrieLogLayer(final Hash blockHash) { + return rootWorldStateStorage.deleteTrieLog(blockHash); + } + @VisibleForTesting TrieLog prepareTrieLog( final BlockHeader blockHeader, final BonsaiWorldStateUpdateAccumulator localUpdater) { @@ -149,6 +154,7 @@ public long getMaxLayersToLoad() { @Override public Optional getTrieLogLayer(final Hash blockHash) { + // TODO SLD check CACHE1 first? return rootWorldStateStorage.getTrieLog(blockHash).map(trieLogFactory::deserialize); } diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/trielog/TrieLogLayer.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/trielog/TrieLogLayer.java index 7d61e501266..ee4e4bdeb84 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/trielog/TrieLogLayer.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/trielog/TrieLogLayer.java @@ -264,4 +264,22 @@ public int hashCode() { .append(storage) .toHashCode(); } + + @Override + public String toString() { + return "TrieLogLayer{" + + "blockHash=" + + blockHash + + ", blockNumber=" + + blockNumber + + ", accounts=" + + accounts + + ", code=" + + code + + ", storage=" + + storage + + ", frozen=" + + frozen + + '}'; + } } 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 5da83180a1c..7bd38ac0263 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 @@ -33,6 +33,8 @@ void saveTrieLog( final BlockHeader forBlockHeader, final BonsaiWorldState forWorldState); + boolean deleteTrieLogLayer(final Hash blockHash); + void addCachedLayer( BlockHeader blockHeader, Hash worldStateRootHash, BonsaiWorldState forWorldState); diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/worldview/BonsaiWorldState.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/worldview/BonsaiWorldState.java index ed81c90f439..4015caf5399 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/worldview/BonsaiWorldState.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/worldview/BonsaiWorldState.java @@ -393,6 +393,8 @@ public void persist(final BlockHeader blockHeader) { verifyWorldStateRoot(newWorldStateRootHash, blockHeader); saveTrieLog = () -> { + // TODO SLD if isFrozen pushTo CACHE1 else persist ? To avoid persisting when + // producing block (cache to keep optimisation between build and newPayload import) trieLogManager.saveTrieLog(localCopy, newWorldStateRootHash, blockHeader, this); // not save a frozen state in the cache if (!isFrozen) { 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 0e23f5406af..6bdd7a6cc9a 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 @@ -140,6 +140,11 @@ public void saveTrieLog( trieLogObservers.forEach(o -> o.onTrieLogAdded(new TrieLogAddedEvent(trieLog))); } + @Override + public boolean deleteTrieLogLayer(final Hash blockHash) { + return false; + } + @Override public void addCachedLayer( final BlockHeader blockHeader,