Skip to content

Commit

Permalink
Don't prune non-finalized blocks
Browse files Browse the repository at this point in the history
This requires two more db calls to get the finalized block and its header.
Find the min block height between finalized and the configured blocksToRetain, and only prune below that
Signed-off-by: Simon Dudley <[email protected]>
  • Loading branch information
siladu committed Nov 3, 2023
1 parent bd53bdf commit 9ac5efa
Show file tree
Hide file tree
Showing 2 changed files with 141 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -72,18 +73,18 @@ private void preloadCache() {
.setMessage("Loading first {} trie logs from database...")
.addArgument(loadingLimit)
.log();
final Stream<byte[]> trieLogs = rootWorldStateStorage.streamTrieLogKeys(loadingLimit);
final Stream<byte[]> 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<BlockHeader> 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);
Expand All @@ -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<Long> blockNumbersToRemove = new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}

Expand All @@ -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
Expand Down Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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};
}
}

0 comments on commit 9ac5efa

Please sign in to comment.