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 index 03cccd22a68..643ea687664 100644 --- 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 @@ -19,6 +19,7 @@ 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.ArrayList; import java.util.Arrays; @@ -72,18 +73,18 @@ private void preloadCache() { .setMessage("Loading first {} trie logs from database...") .addArgument(loadingLimit) .log(); - final Stream trieLogs = rootWorldStateStorage.streamTrieLogKeys(loadingLimit); + final Stream trieLogKeys = rootWorldStateStorage.streamTrieLogKeys(loadingLimit); final AtomicLong count = new AtomicLong(); - trieLogs.forEach( - hashAsBytes -> { - Hash hash = Hash.wrap(Bytes32.wrap(hashAsBytes)); + trieLogKeys.forEach( + blockHashAsBytes -> { + Hash hash = Hash.wrap(Bytes32.wrap(blockHashAsBytes)); final Optional header = blockchain.getBlockHeader(hash); if (header.isPresent()) { - knownTrieLogKeysByDescendingBlockNumber.put(header.get().getNumber(), hashAsBytes); + knownTrieLogKeysByDescendingBlockNumber.put(header.get().getNumber(), blockHashAsBytes); count.getAndIncrement(); } else { // prune orphaned blocks (sometimes created during block production) - rootWorldStateStorage.pruneTrieLog(hashAsBytes); + rootWorldStateStorage.pruneTrieLog(blockHashAsBytes); } }); LOG.atInfo().log("Loaded {} trie logs from database", count); @@ -99,17 +100,34 @@ void cacheForLaterPruning(final long blockNumber, final byte[] trieLogKey) { } void pruneFromCache() { + final long retainAboveThisBlock = blockchain.getChainHeadBlockNumber() - numBlocksToRetain; + final long retainAboveThisBlockOrFinalized = + blockchain + .getFinalized() + .flatMap(blockchain::getBlockHeader) + .map(ProcessableBlockHeader::getNumber) + .map(finalizedBlock -> Math.min(finalizedBlock, retainAboveThisBlock)) + .orElse(retainAboveThisBlock); + LOG.atTrace() - .setMessage("(chainHeadNumber: {} - numBlocksToRetain: {}) = retainAboveThisBlock: {}") + .setMessage( + "min((chainHeadNumber: {} - numBlocksToRetain: {}), finalized: {})) = retainAboveThisBlockOrFinalized: {}") .addArgument(blockchain.getChainHeadBlockNumber()) .addArgument(numBlocksToRetain) - .addArgument(retainAboveThisBlock) + .addArgument( + () -> + blockchain + .getFinalized() + .flatMap(blockchain::getBlockHeader) + .map(ProcessableBlockHeader::getNumber) + .orElse(null)) + .addArgument(retainAboveThisBlockOrFinalized) .log(); final var pruneWindowEntries = knownTrieLogKeysByDescendingBlockNumber.asMap().entrySet().stream() - .dropWhile((e) -> e.getKey() > retainAboveThisBlock) + .dropWhile((e) -> e.getKey() > retainAboveThisBlockOrFinalized) .limit(pruningLimit); final List blockNumbersToRemove = new ArrayList<>(); 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 index 95d64ee28d8..abbef430e2e 100644 --- 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 @@ -15,6 +15,7 @@ package org.hyperledger.besu.ethereum.bonsai.trielog; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -37,12 +38,12 @@ public class TrieLogPrunerTest { - private BonsaiWorldStateKeyValueStorage rootWorldStateStorage; + private BonsaiWorldStateKeyValueStorage worldState; private Blockchain blockchain; @BeforeEach public void setup() { - rootWorldStateStorage = Mockito.mock(BonsaiWorldStateKeyValueStorage.class); + worldState = Mockito.mock(BonsaiWorldStateKeyValueStorage.class); blockchain = Mockito.mock(Blockchain.class); } @@ -57,7 +58,7 @@ public void trieLogs_pruned_in_reverse_order_within_pruning_window() { final long blocksToRetain = 3; final int pruningWindowSize = 2; TrieLogPruner trieLogPruner = - new TrieLogPruner(rootWorldStateStorage, blockchain, blocksToRetain, pruningWindowSize); + new TrieLogPruner(worldState, blockchain, blocksToRetain, pruningWindowSize); final byte[] key0 = new byte[] {1, 2, 3}; // older block outside the prune window final byte[] key1 = new byte[] {1, 2, 3}; // block inside the prune window @@ -87,10 +88,10 @@ public void trieLogs_pruned_in_reverse_order_within_pruning_window() { trieLogPruner.pruneFromCache(); // Then - InOrder inOrder = Mockito.inOrder(rootWorldStateStorage); - inOrder.verify(rootWorldStateStorage, times(1)).pruneTrieLog(key3); - inOrder.verify(rootWorldStateStorage, times(1)).pruneTrieLog(key1); - inOrder.verify(rootWorldStateStorage, times(1)).pruneTrieLog(key2); + InOrder inOrder = Mockito.inOrder(worldState); + inOrder.verify(worldState, times(1)).pruneTrieLog(key3); + inOrder.verify(worldState, times(1)).pruneTrieLog(key1); + inOrder.verify(worldState, times(1)).pruneTrieLog(key2); // Subsequent run should add one more block, then prune two oldest remaining keys long block6 = 1006L; @@ -99,8 +100,104 @@ public void trieLogs_pruned_in_reverse_order_within_pruning_window() { trieLogPruner.pruneFromCache(); - inOrder.verify(rootWorldStateStorage, times(1)).pruneTrieLog(key4); - inOrder.verify(rootWorldStateStorage, times(1)).pruneTrieLog(key0); + inOrder.verify(worldState, times(1)).pruneTrieLog(key4); + inOrder.verify(worldState, times(1)).pruneTrieLog(key0); + } + + @SuppressWarnings("BannedMethod") + @Test + public void retain_non_finalized_blocks() { + Configurator.setLevel(LogManager.getLogger(TrieLogPruner.class).getName(), Level.TRACE); + + // Given + // finalizedBlockHeight < configuredRetainHeight + final long finalizedBlockHeight = 1; + final long configuredRetainHeight = 3; + TrieLogPruner trieLogPruner = + setupPrunerAndFinalizedBlock(configuredRetainHeight, finalizedBlockHeight); + + // When + trieLogPruner.pruneFromCache(); + + // Then + 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) + } + + @SuppressWarnings("BannedMethod") + @Test + public void boundary_test_when_configured_retain_equals_finalized_block() { + Configurator.setLevel(LogManager.getLogger(TrieLogPruner.class).getName(), Level.TRACE); + + // Given + // finalizedBlockHeight == configuredRetainHeight + final long finalizedBlockHeight = 2; + final long configuredRetainHeight = 2; + + TrieLogPruner trieLogPruner = + setupPrunerAndFinalizedBlock(configuredRetainHeight, finalizedBlockHeight); + + // When + trieLogPruner.pruneFromCache(); + + // Then + 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) + } + + @SuppressWarnings("BannedMethod") + @Test + public void use_configured_retain_when_finalized_block_is_higher() { + Configurator.setLevel(LogManager.getLogger(TrieLogPruner.class).getName(), Level.TRACE); + + // Given + // finalizedBlockHeight > configuredRetainHeight + final long finalizedBlockHeight = 4; + final long configuredRetainHeight = 3; + + final TrieLogPruner trieLogPruner = + setupPrunerAndFinalizedBlock(configuredRetainHeight, finalizedBlockHeight); + + // When + trieLogPruner.pruneFromCache(); + + // Then + 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) + } + + 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); + + trieLogPruner.cacheForLaterPruning(1, key(1)); + trieLogPruner.cacheForLaterPruning(2, key(2)); + trieLogPruner.cacheForLaterPruning(3, key(3)); + trieLogPruner.cacheForLaterPruning(4, key(4)); + trieLogPruner.cacheForLaterPruning(5, key(5)); + + return trieLogPruner; } @Test @@ -110,18 +207,21 @@ public void initialize_preloads_cache_and_prunes_orphaned_blocks() { final BlockDataGenerator generator = new BlockDataGenerator(); final BlockHeader header1 = generator.header(1); final BlockHeader header2 = generator.header(2); - when(rootWorldStateStorage.streamTrieLogKeys(loadingLimit)) + 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(rootWorldStateStorage, blockchain, 3, loadingLimit); + TrieLogPruner trieLogPruner = new TrieLogPruner(worldState, blockchain, 3, loadingLimit); trieLogPruner.initialize(); // Then - verify(rootWorldStateStorage, times(1)).streamTrieLogKeys(2); - verify(rootWorldStateStorage, times(1)).pruneTrieLog(header2.getBlockHash().toArray()); + verify(worldState, times(1)).streamTrieLogKeys(2); + verify(worldState, times(1)).pruneTrieLog(header2.getBlockHash().toArray()); + } + + private byte[] key(final int k) { + return new byte[] {(byte) k}; } }