From 5549eded38ec45dcea4ad5d363ff19d593b645a5 Mon Sep 17 00:00:00 2001 From: catbref Date: Thu, 5 Nov 2020 09:34:57 +0000 Subject: [PATCH] Improve/fix use of latest block cache, for more cache hits, faster chain-tip response, etc. --- .../org/qortal/controller/Controller.java | 28 +++- src/test/java/org/qortal/test/BlockTests.java | 127 ++++++++++++++++++ 2 files changed, 153 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/qortal/controller/Controller.java b/src/main/java/org/qortal/controller/Controller.java index fc72475d..77f20caf 100644 --- a/src/main/java/org/qortal/controller/Controller.java +++ b/src/main/java/org/qortal/controller/Controller.java @@ -1225,12 +1225,24 @@ public class Controller extends Thread { final byte[] parentSignature = getBlockSummariesMessage.getParentSignature(); this.stats.getBlockSummariesStats.requests.incrementAndGet(); + // If peer's parent signature matches our latest block signature + // then we can short-circuit with an empty response + BlockData chainTip = getChainTip(); + if (chainTip != null && Arrays.equals(parentSignature, chainTip.getSignature())) { + Message blockSummariesMessage = new BlockSummariesMessage(Collections.emptyList()); + blockSummariesMessage.setId(message.getId()); + if (!peer.sendMessage(blockSummariesMessage)) + peer.disconnect("failed to send block summaries"); + + return; + } + List blockSummaries = new ArrayList<>(); // Attempt to serve from our cache of latest blocks synchronized (this.latestBlocks) { blockSummaries = this.latestBlocks.stream() - .dropWhile(cachedBlockData -> Arrays.equals(cachedBlockData.getSignature(), parentSignature)) + .dropWhile(cachedBlockData -> !Arrays.equals(cachedBlockData.getReference(), parentSignature)) .map(BlockSummaryData::new) .collect(Collectors.toList()); } @@ -1268,12 +1280,24 @@ public class Controller extends Thread { final byte[] parentSignature = getSignaturesMessage.getParentSignature(); this.stats.getBlockSignaturesV2Stats.requests.incrementAndGet(); + // If peer's parent signature matches our latest block signature + // then we can short-circuit with an empty response + BlockData chainTip = getChainTip(); + if (chainTip != null && Arrays.equals(parentSignature, chainTip.getSignature())) { + Message signaturesMessage = new SignaturesMessage(Collections.emptyList()); + signaturesMessage.setId(message.getId()); + if (!peer.sendMessage(signaturesMessage)) + peer.disconnect("failed to send signatures (v2)"); + + return; + } + List signatures = new ArrayList<>(); // Attempt to serve from our cache of latest blocks synchronized (this.latestBlocks) { signatures = this.latestBlocks.stream() - .dropWhile(cachedBlockData -> Arrays.equals(cachedBlockData.getSignature(), parentSignature)) + .dropWhile(cachedBlockData -> !Arrays.equals(cachedBlockData.getReference(), parentSignature)) .map(BlockData::getSignature) .collect(Collectors.toList()); } diff --git a/src/test/java/org/qortal/test/BlockTests.java b/src/test/java/org/qortal/test/BlockTests.java index 3e3d0ada..b6d4429d 100644 --- a/src/test/java/org/qortal/test/BlockTests.java +++ b/src/test/java/org/qortal/test/BlockTests.java @@ -1,6 +1,10 @@ package org.qortal.test; +import java.util.Arrays; +import java.util.Deque; +import java.util.LinkedList; import java.util.List; +import java.util.stream.Collectors; import org.junit.Before; import org.junit.Test; @@ -133,6 +137,129 @@ public class BlockTests extends Common { } } + @Test + public void testLatestBlockCacheWithLatestBlock() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + Deque latestBlockCache = buildLatestBlockCache(repository, 20); + + BlockData latestBlock = repository.getBlockRepository().getLastBlock(); + byte[] parentSignature = latestBlock.getSignature(); + + List childBlocks = findCachedChildBlocks(latestBlockCache, parentSignature); + + assertEquals(true, childBlocks.isEmpty()); + } + } + + @Test + public void testLatestBlockCacheWithPenultimateBlock() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + Deque latestBlockCache = buildLatestBlockCache(repository, 20); + + BlockData latestBlock = repository.getBlockRepository().getLastBlock(); + BlockData penultimateBlock = repository.getBlockRepository().fromHeight(latestBlock.getHeight() - 1); + byte[] parentSignature = penultimateBlock.getSignature(); + + List childBlocks = findCachedChildBlocks(latestBlockCache, parentSignature); + + assertEquals(false, childBlocks.isEmpty()); + assertEquals(1, childBlocks.size()); + + BlockData expectedBlock = latestBlock; + BlockData actualBlock = childBlocks.get(0); + assertArrayEquals(expectedBlock.getSignature(), actualBlock.getSignature()); + } + } + + @Test + public void testLatestBlockCacheWithMiddleBlock() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + Deque latestBlockCache = buildLatestBlockCache(repository, 20); + + int tipOffset = 5; + + BlockData latestBlock = repository.getBlockRepository().getLastBlock(); + BlockData parentBlock = repository.getBlockRepository().fromHeight(latestBlock.getHeight() - tipOffset); + byte[] parentSignature = parentBlock.getSignature(); + + List childBlocks = findCachedChildBlocks(latestBlockCache, parentSignature); + + assertEquals(false, childBlocks.isEmpty()); + assertEquals(tipOffset, childBlocks.size()); + + BlockData expectedFirstBlock = repository.getBlockRepository().fromHeight(parentBlock.getHeight() + 1); + BlockData actualFirstBlock = childBlocks.get(0); + assertArrayEquals(expectedFirstBlock.getSignature(), actualFirstBlock.getSignature()); + + BlockData expectedLastBlock = latestBlock; + BlockData actualLastBlock = childBlocks.get(childBlocks.size() - 1); + assertArrayEquals(expectedLastBlock.getSignature(), actualLastBlock.getSignature()); + } + } + + @Test + public void testLatestBlockCacheWithFirstBlock() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + Deque latestBlockCache = buildLatestBlockCache(repository, 20); + + int tipOffset = latestBlockCache.size(); + + BlockData latestBlock = repository.getBlockRepository().getLastBlock(); + BlockData parentBlock = repository.getBlockRepository().fromHeight(latestBlock.getHeight() - tipOffset); + byte[] parentSignature = parentBlock.getSignature(); + + List childBlocks = findCachedChildBlocks(latestBlockCache, parentSignature); + + assertEquals(false, childBlocks.isEmpty()); + assertEquals(tipOffset, childBlocks.size()); + + BlockData expectedFirstBlock = repository.getBlockRepository().fromHeight(parentBlock.getHeight() + 1); + BlockData actualFirstBlock = childBlocks.get(0); + assertArrayEquals(expectedFirstBlock.getSignature(), actualFirstBlock.getSignature()); + + BlockData expectedLastBlock = latestBlock; + BlockData actualLastBlock = childBlocks.get(childBlocks.size() - 1); + assertArrayEquals(expectedLastBlock.getSignature(), actualLastBlock.getSignature()); + } + } + + @Test + public void testLatestBlockCacheWithNoncachedBlock() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + Deque latestBlockCache = buildLatestBlockCache(repository, 20); + + int tipOffset = latestBlockCache.size() + 1; // outside of cache + + BlockData latestBlock = repository.getBlockRepository().getLastBlock(); + BlockData parentBlock = repository.getBlockRepository().fromHeight(latestBlock.getHeight() - tipOffset); + byte[] parentSignature = parentBlock.getSignature(); + + List childBlocks = findCachedChildBlocks(latestBlockCache, parentSignature); + + assertEquals(true, childBlocks.isEmpty()); + } + } + + private Deque buildLatestBlockCache(Repository repository, int count) throws DataException { + Deque latestBlockCache = new LinkedList<>(); + + // Mint some blocks + for (int h = 0; h < count; ++h) + latestBlockCache.addLast(BlockUtils.mintBlock(repository).getBlockData()); + + // Reduce cache down to latest 10 blocks + while (latestBlockCache.size() > 10) + latestBlockCache.removeFirst(); + + return latestBlockCache; + } + + private List findCachedChildBlocks(Deque latestBlockCache, byte[] parentSignature) { + return latestBlockCache.stream() + .dropWhile(cachedBlockData -> !Arrays.equals(cachedBlockData.getReference(), parentSignature)) + .collect(Collectors.toList()); + } + @Test public void testCommonBlockSearch() { // Given a list of block summaries, trim all trailing summaries after common block