From cd6f6e77c37e79e163aa3116b862c2f71b1645f5 Mon Sep 17 00:00:00 2001 From: wb Date: Fri, 18 Sep 2026 14:57:36 +0800 Subject: [PATCH 1/2] fix(net): harden sync message resource controls Reject negative remain counts and overflowed declared heights when sync height validation is enabled, and apply sync-chain rate limiting to every request. Use a single-segment block ID cache with capacity for the full 4001-height fetch window. Cover complete-window retention, out-of-order requests, and duplicate-request rejection with regression tests. --- .../ChainInventoryMsgHandler.java | 14 +++- .../SyncBlockChainMsgHandler.java | 3 +- .../tron/core/net/peer/PeerConnection.java | 3 +- .../ChainInventoryMsgHandlerTest.java | 55 +++++++++++++++- .../FetchInvDataMsgHandlerTest.java | 66 +++++++++++++++++++ .../SyncBlockChainMsgHandlerTest.java | 22 +++++++ 6 files changed, 156 insertions(+), 7 deletions(-) diff --git a/framework/src/main/java/org/tron/core/net/messagehandler/ChainInventoryMsgHandler.java b/framework/src/main/java/org/tron/core/net/messagehandler/ChainInventoryMsgHandler.java index bd2e428418c..ecff89fcf67 100644 --- a/framework/src/main/java/org/tron/core/net/messagehandler/ChainInventoryMsgHandler.java +++ b/framework/src/main/java/org/tron/core/net/messagehandler/ChainInventoryMsgHandler.java @@ -108,6 +108,11 @@ private void check(PeerConnection peer, ChainInventoryMessage msg) throws P2pExc throw new P2pException(TypeEnum.BAD_MESSAGE, "blockIds is empty"); } + if (msg.getRemainNum() < 0) { + throw new P2pException(TypeEnum.BAD_MESSAGE, + "remainNum is negative: " + msg.getRemainNum()); + } + if (blockIds.size() > NetConstants.SYNC_FETCH_BATCH_NUM + 1) { throw new P2pException(TypeEnum.BAD_MESSAGE, "big blockIds size: " + blockIds.size()); } @@ -137,9 +142,12 @@ private void check(PeerConnection peer, ChainInventoryMessage msg) throws P2pExc long maxFutureNum = maxRemainTime / BLOCK_PRODUCED_INTERVAL + tronNetDelegate.getSolidBlockId().getNum(); long lastNum = blockIds.get(blockIds.size() - 1).getNum(); - if (lastNum + msg.getRemainNum() > maxFutureNum) { - throw new P2pException(TypeEnum.BAD_MESSAGE, "lastNum: " + lastNum + " + remainNum: " - + msg.getRemainNum() + " > futureMaxNum: " + maxFutureNum); + long declaredHighestNum = lastNum + msg.getRemainNum(); + if (declaredHighestNum < 0 || declaredHighestNum > maxFutureNum) { + throw new P2pException(TypeEnum.BAD_MESSAGE, + "Invalid declared highest block number: " + declaredHighestNum + + ", lastNum: " + lastNum + ", remainNum: " + msg.getRemainNum() + + ", futureMaxNum: " + maxFutureNum); } } } diff --git a/framework/src/main/java/org/tron/core/net/messagehandler/SyncBlockChainMsgHandler.java b/framework/src/main/java/org/tron/core/net/messagehandler/SyncBlockChainMsgHandler.java index 5c18e014978..dd9d5e47ebe 100644 --- a/framework/src/main/java/org/tron/core/net/messagehandler/SyncBlockChainMsgHandler.java +++ b/framework/src/main/java/org/tron/core/net/messagehandler/SyncBlockChainMsgHandler.java @@ -58,8 +58,7 @@ public void processMessage(PeerConnection peer, TronMessage msg) throws P2pExcep } private boolean check(PeerConnection peer, SyncBlockChainMessage msg) throws P2pException { - if (peer.getRemainNum() > 0 - && !peer.getP2pRateLimiter().tryAcquire(msg.getType().asByte())) { + if (!peer.getP2pRateLimiter().tryAcquire(msg.getType().asByte())) { // Discard messages that exceed the rate limit logger.warn("{} message from peer {} exceeds the rate limit", msg.getType(), peer.getInetSocketAddress()); diff --git a/framework/src/main/java/org/tron/core/net/peer/PeerConnection.java b/framework/src/main/java/org/tron/core/net/peer/PeerConnection.java index 7d7457cf2fc..839ccd26400 100644 --- a/framework/src/main/java/org/tron/core/net/peer/PeerConnection.java +++ b/framework/src/main/java/org/tron/core/net/peer/PeerConnection.java @@ -146,7 +146,8 @@ public class PeerConnection { private volatile long remainNum; @Getter private Cache syncBlockIdCache = CacheBuilder.newBuilder() - .maximumSize(2 * NetConstants.SYNC_FETCH_BATCH_NUM).recordStats().build(); + .concurrencyLevel(1) + .maximumSize(2 * NetConstants.SYNC_FETCH_BATCH_NUM + 1).recordStats().build(); @Setter @Getter private Deque syncBlockToFetch = new ConcurrentLinkedDeque<>(); diff --git a/framework/src/test/java/org/tron/core/net/messagehandler/ChainInventoryMsgHandlerTest.java b/framework/src/test/java/org/tron/core/net/messagehandler/ChainInventoryMsgHandlerTest.java index 56853c3dbb7..aab20891f75 100644 --- a/framework/src/test/java/org/tron/core/net/messagehandler/ChainInventoryMsgHandlerTest.java +++ b/framework/src/test/java/org/tron/core/net/messagehandler/ChainInventoryMsgHandlerTest.java @@ -1,5 +1,7 @@ package org.tron.core.net.messagehandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; @@ -7,13 +9,16 @@ import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; +import org.mockito.Mockito; import org.tron.common.TestConstants; import org.tron.common.utils.Pair; +import org.tron.common.utils.ReflectUtils; +import org.tron.common.utils.Sha256Hash; import org.tron.core.capsule.BlockCapsule.BlockId; import org.tron.core.config.Parameter.NetConstants; import org.tron.core.config.args.Args; import org.tron.core.exception.P2pException; -import org.tron.core.net.message.keepalive.PingMessage; +import org.tron.core.net.TronNetDelegate; import org.tron.core.net.message.sync.ChainInventoryMessage; import org.tron.core.net.peer.PeerConnection; @@ -78,4 +83,52 @@ public void testProcessMessage() throws Exception { Assert.assertNull(msg.getAnswerMessage()); } + @Test + public void testNegativeRemainNumRejected() throws Exception { + assertCheckRejects(createContinuousBlockIds(0L), -1L); + } + + @Test + public void testRemainNumOverflowRejected() throws Exception { + assertCheckRejects(createContinuousBlockIds( + Long.MAX_VALUE - NetConstants.SYNC_FETCH_BATCH_NUM + 1), 1L); + } + + private void assertCheckRejects(List ids, long remainNum) throws Exception { + ChainInventoryMsgHandler messageHandler = new ChainInventoryMsgHandler(); + TronNetDelegate tronNetDelegate = Mockito.mock(TronNetDelegate.class); + ReflectUtils.setFieldValue(messageHandler, "tronNetDelegate", tronNetDelegate); + Mockito.when(tronNetDelegate.getHeadBlockId()).thenReturn( + new BlockId(Sha256Hash.ZERO_HASH, 1L)); + Mockito.when(tronNetDelegate.getSolidBlockId()).thenReturn( + new BlockId(Sha256Hash.ZERO_HASH, 1L)); + Mockito.when(tronNetDelegate.getBlockTime(Mockito.any())).thenReturn(0L); + + PeerConnection connection = Mockito.mock(PeerConnection.class); + LinkedList requestedIds = new LinkedList<>(); + requestedIds.add(ids.get(0)); + Mockito.when(connection.getSyncChainRequested()).thenReturn( + new Pair<>(requestedIds, System.currentTimeMillis())); + + Method check = ChainInventoryMsgHandler.class.getDeclaredMethod( + "check", PeerConnection.class, ChainInventoryMessage.class); + check.setAccessible(true); + try { + check.invoke(messageHandler, connection, new ChainInventoryMessage(ids, remainNum)); + Assert.fail("Expected invalid remainNum to be rejected"); + } catch (InvocationTargetException e) { + Assert.assertTrue(e.getCause() instanceof P2pException); + Assert.assertEquals(P2pException.TypeEnum.BAD_MESSAGE, + ((P2pException) e.getCause()).getType()); + } + } + + private List createContinuousBlockIds(long firstNum) { + List ids = new ArrayList<>(); + for (int i = 0; i < NetConstants.SYNC_FETCH_BATCH_NUM; i++) { + ids.add(new BlockId(Sha256Hash.ZERO_HASH, firstNum + i)); + } + return ids; + } + } diff --git a/framework/src/test/java/org/tron/core/net/messagehandler/FetchInvDataMsgHandlerTest.java b/framework/src/test/java/org/tron/core/net/messagehandler/FetchInvDataMsgHandlerTest.java index 7ea6337443d..a7fc2a45f73 100644 --- a/framework/src/test/java/org/tron/core/net/messagehandler/FetchInvDataMsgHandlerTest.java +++ b/framework/src/test/java/org/tron/core/net/messagehandler/FetchInvDataMsgHandlerTest.java @@ -5,8 +5,13 @@ import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; import java.util.LinkedList; import java.util.List; +import java.util.Random; import java.util.concurrent.TimeUnit; import org.junit.Assert; import org.junit.Test; @@ -68,6 +73,67 @@ public void testProcessMessage() throws Exception { Assert.assertNotNull(syncBlockIdCache.getIfPresent(blockId)); } + @Test + public void testSyncBlockIdCacheRetainsFullWindowAndRejectsDuplicates() throws Exception { + assertFullWindowRetainedAndDuplicatesRejected(false); + } + + @Test + public void testSyncBlockIdCacheRetainsShuffledWindowAndRejectsDuplicates() throws Exception { + assertFullWindowRetainedAndDuplicatesRejected(true); + } + + private void assertFullWindowRetainedAndDuplicatesRejected(boolean shuffled) throws Exception { + PeerConnection peer = new PeerConnection(); + int windowSize = 2 * (int) Parameter.NetConstants.SYNC_FETCH_BATCH_NUM + 1; + long lastHeight = 10000L; + peer.setNeedSyncFromUs(true); + peer.setLastSyncBlockId(new BlockCapsule.BlockId(Sha256Hash.ZERO_HASH, lastHeight)); + + FetchInvDataMsgHandler handler = new FetchInvDataMsgHandler(); + Method check = FetchInvDataMsgHandler.class.getDeclaredMethod( + "check", PeerConnection.class, FetchInvDataMessage.class, boolean.class); + check.setAccessible(true); + List hashes = new ArrayList<>(); + for (int i = 0; i < windowSize; i++) { + hashes.add(new BlockCapsule.BlockId(createHash(i), lastHeight - windowSize + 1 + i)); + } + if (shuffled) { + Collections.shuffle(hashes, new Random(6966L)); + } + + for (Sha256Hash hash : hashes) { + check.invoke(handler, peer, new FetchInvDataMessage(Collections.singletonList(hash), + Protocol.Inventory.InventoryType.BLOCK), false); + } + peer.getSyncBlockIdCache().cleanUp(); + + for (Sha256Hash hash : hashes) { + Assert.assertNotNull("Missing block " + new BlockCapsule.BlockId(hash).getNum(), + peer.getSyncBlockIdCache().getIfPresent(hash)); + } + for (Sha256Hash hash : hashes) { + FetchInvDataMessage message = new FetchInvDataMessage(Collections.singletonList(hash), + Protocol.Inventory.InventoryType.BLOCK); + InvocationTargetException exception = Assert.assertThrows(InvocationTargetException.class, + () -> check.invoke(handler, peer, message, false)); + Assert.assertTrue(exception.getCause() instanceof P2pException); + P2pException cause = (P2pException) exception.getCause(); + Assert.assertEquals(P2pException.TypeEnum.BAD_MESSAGE, cause.getType()); + Assert.assertEquals(new BlockCapsule.BlockId(hash).getString() + " is exist", + cause.getMessage()); + } + } + + private Sha256Hash createHash(int value) { + byte[] bytes = new byte[Sha256Hash.LENGTH]; + bytes[28] = (byte) (value >>> 24); + bytes[29] = (byte) (value >>> 16); + bytes[30] = (byte) (value >>> 8); + bytes[31] = (byte) value; + return Sha256Hash.wrap(bytes); + } + @Test public void testIsAdvInv() { FetchInvDataMsgHandler fetchInvDataMsgHandler = new FetchInvDataMsgHandler(); diff --git a/framework/src/test/java/org/tron/core/net/messagehandler/SyncBlockChainMsgHandlerTest.java b/framework/src/test/java/org/tron/core/net/messagehandler/SyncBlockChainMsgHandlerTest.java index 08c5484880f..f8edf5f7bc8 100644 --- a/framework/src/test/java/org/tron/core/net/messagehandler/SyncBlockChainMsgHandlerTest.java +++ b/framework/src/test/java/org/tron/core/net/messagehandler/SyncBlockChainMsgHandlerTest.java @@ -1,5 +1,7 @@ package org.tron.core.net.messagehandler; +import static org.tron.core.net.message.MessageTypes.SYNC_BLOCK_CHAIN; + import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; @@ -14,6 +16,7 @@ import org.junit.ClassRule; import org.junit.Test; import org.junit.rules.TemporaryFolder; +import org.mockito.Mockito; import org.tron.common.TestConstants; import org.tron.common.application.TronApplicationContext; import org.tron.common.utils.Sha256Hash; @@ -22,6 +25,7 @@ import org.tron.core.config.DefaultConfig; import org.tron.core.config.args.Args; import org.tron.core.exception.P2pException; +import org.tron.core.net.P2pRateLimiter; import org.tron.core.net.TronNetDelegate; import org.tron.core.net.message.sync.BlockInventoryMessage; import org.tron.core.net.message.sync.SyncBlockChainMessage; @@ -160,6 +164,24 @@ public void testBlockIdsAtLimit() throws Exception { } } + @Test + public void testRemainNumZeroStillConsumesSyncBlockChainRateLimit() throws Exception { + PeerConnection rateLimitedPeer = Mockito.mock(PeerConnection.class); + P2pRateLimiter rateLimiter = new P2pRateLimiter(); + rateLimiter.register(SYNC_BLOCK_CHAIN.asByte(), 0.0001D); + Mockito.when(rateLimitedPeer.getP2pRateLimiter()).thenReturn(rateLimiter); + + BlockId genesis = context.getBean(TronNetDelegate.class).getGenesisBlockId(); + SyncBlockChainMessage message = new SyncBlockChainMessage( + java.util.Collections.singletonList(genesis)); + Method checkMethod = SyncBlockChainMsgHandler.class + .getDeclaredMethod("check", PeerConnection.class, SyncBlockChainMessage.class); + checkMethod.setAccessible(true); + + Assert.assertTrue((boolean) checkMethod.invoke(handler, rateLimitedPeer, message)); + Assert.assertFalse((boolean) checkMethod.invoke(handler, rateLimitedPeer, message)); + } + @AfterClass public static void destroy() { for (PeerConnection p : PeerManager.getPeers()) { From a3cabdf5ebc8d5db83ba3b644b8b8f4fb1c1438e Mon Sep 17 00:00:00 2001 From: wb Date: Thu, 15 Oct 2026 18:31:41 +0800 Subject: [PATCH 2/2] fix(net): restrict sync fetches to canonical blocks Check each sync block hash against the canonical height index before adding it to the deduplication cache. This prevents retained fork blocks from evicting entries within a fixed canonical sync window. Treat a noncanonical request as a sync failure so a chain reorganization does not trigger a bad-protocol ban. Preserve advertised block fetching. --- .../FetchInvDataMsgHandler.java | 8 +- .../FetchInvDataMsgHandlerTest.java | 77 ++++++++++++++++++- 2 files changed, 82 insertions(+), 3 deletions(-) diff --git a/framework/src/main/java/org/tron/core/net/messagehandler/FetchInvDataMsgHandler.java b/framework/src/main/java/org/tron/core/net/messagehandler/FetchInvDataMsgHandler.java index 2d6d47d1dd1..2bbba25c925 100644 --- a/framework/src/main/java/org/tron/core/net/messagehandler/FetchInvDataMsgHandler.java +++ b/framework/src/main/java/org/tron/core/net/messagehandler/FetchInvDataMsgHandler.java @@ -206,6 +206,12 @@ private void check(PeerConnection peer, FetchInvDataMessage fetchInvDataMsg, throw new P2pException(TypeEnum.BAD_MESSAGE, "maxBlockNum: " + peer.getLastSyncBlockId().getNum() + ", blockNum: " + blockNum); } + if (!hash.equals(tronNetDelegate.getBlockIdByNum(blockNum))) { + // The main chain may have changed since we sent the sync inventory. + // Fail synchronization without treating a stale request as bad protocol. + throw new P2pException(TypeEnum.SYNC_FAILED, + "Requested sync block is not in the main chain: " + new BlockId(hash).getString()); + } if (peer.getSyncBlockIdCache().getIfPresent(hash) != null) { throw new P2pException(TypeEnum.BAD_MESSAGE, new BlockId(hash).getString() + " is exist"); @@ -214,4 +220,4 @@ private void check(PeerConnection peer, FetchInvDataMessage fetchInvDataMsg, } } } -} \ No newline at end of file +} diff --git a/framework/src/test/java/org/tron/core/net/messagehandler/FetchInvDataMsgHandlerTest.java b/framework/src/test/java/org/tron/core/net/messagehandler/FetchInvDataMsgHandlerTest.java index a7fc2a45f73..8cfc999116e 100644 --- a/framework/src/test/java/org/tron/core/net/messagehandler/FetchInvDataMsgHandlerTest.java +++ b/framework/src/test/java/org/tron/core/net/messagehandler/FetchInvDataMsgHandlerTest.java @@ -4,13 +4,16 @@ import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; +import com.google.protobuf.ByteString; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Random; import java.util.concurrent.TimeUnit; import org.junit.Assert; @@ -46,6 +49,7 @@ public void testProcessMessage() throws Exception { Mockito.when(tronNetDelegate.allowPBFT()).thenReturn(false); BlockCapsule.BlockId blockId = new BlockCapsule.BlockId(); + Mockito.when(tronNetDelegate.getBlockIdByNum(blockId.getNum())).thenReturn(blockId); List blockIds = new LinkedList<>(); blockIds.add(blockId); @@ -84,20 +88,30 @@ public void testSyncBlockIdCacheRetainsShuffledWindowAndRejectsDuplicates() thro } private void assertFullWindowRetainedAndDuplicatesRejected(boolean shuffled) throws Exception { - PeerConnection peer = new PeerConnection(); + PeerConnection peer = Mockito.spy(new PeerConnection()); + Mockito.doNothing().when(peer).sendMessage(Mockito.any()); int windowSize = 2 * (int) Parameter.NetConstants.SYNC_FETCH_BATCH_NUM + 1; long lastHeight = 10000L; peer.setNeedSyncFromUs(true); peer.setLastSyncBlockId(new BlockCapsule.BlockId(Sha256Hash.ZERO_HASH, lastHeight)); FetchInvDataMsgHandler handler = new FetchInvDataMsgHandler(); + TronNetDelegate delegate = Mockito.mock(TronNetDelegate.class); + ReflectUtils.setFieldValue(handler, "tronNetDelegate", delegate); + ReflectUtils.setFieldValue(handler, "advService", Mockito.mock(AdvService.class)); Method check = FetchInvDataMsgHandler.class.getDeclaredMethod( "check", PeerConnection.class, FetchInvDataMessage.class, boolean.class); check.setAccessible(true); List hashes = new ArrayList<>(); + Map mainChain = new HashMap<>(); for (int i = 0; i < windowSize; i++) { - hashes.add(new BlockCapsule.BlockId(createHash(i), lastHeight - windowSize + 1 + i)); + BlockCapsule.BlockId id = + new BlockCapsule.BlockId(createHash(i), lastHeight - windowSize + 1 + i); + hashes.add(id); + mainChain.put(id.getNum(), id); } + Mockito.when(delegate.getBlockIdByNum(Mockito.anyLong())) + .thenAnswer(invocation -> mainChain.get(invocation.getArgument(0))); if (shuffled) { Collections.shuffle(hashes, new Random(6966L)); } @@ -108,6 +122,19 @@ private void assertFullWindowRetainedAndDuplicatesRejected(boolean shuffled) thr } peer.getSyncBlockIdCache().cleanUp(); + // A retained fork at the same height must not become a 4,002nd cache key. + BlockCapsule fork = new BlockCapsule(lastHeight, Sha256Hash.ZERO_HASH, 1, + ByteString.EMPTY); + Mockito.when(delegate.getData(fork.getBlockId(), Protocol.Inventory.InventoryType.BLOCK)) + .thenReturn(new BlockMessage(fork)); + P2pException forkException = Assert.assertThrows(P2pException.class, + () -> handler.processMessage(peer, new FetchInvDataMessage( + Collections.singletonList(fork.getBlockId()), Protocol.Inventory.InventoryType.BLOCK))); + Assert.assertEquals(P2pException.TypeEnum.SYNC_FAILED, forkException.getType()); + Assert.assertNull(peer.getSyncBlockIdCache().getIfPresent(fork.getBlockId())); + Mockito.verify(delegate, Mockito.never()).getData(Mockito.any(), Mockito.any()); + Mockito.verify(peer, Mockito.never()).sendMessage(Mockito.any()); + for (Sha256Hash hash : hashes) { Assert.assertNotNull("Missing block " + new BlockCapsule.BlockId(hash).getNum(), peer.getSyncBlockIdCache().getIfPresent(hash)); @@ -125,6 +152,52 @@ private void assertFullWindowRetainedAndDuplicatesRejected(boolean shuffled) thr } } + @Test + public void testPreviouslyFetchedBlockAfterReorgIsSyncFailure() throws Exception { + FetchInvDataMsgHandler handler = new FetchInvDataMsgHandler(); + TronNetDelegate delegate = Mockito.mock(TronNetDelegate.class); + ReflectUtils.setFieldValue(handler, "tronNetDelegate", delegate); + PeerConnection peer = new PeerConnection(); + BlockCapsule.BlockId oldId = new BlockCapsule.BlockId(createHash(1), 10000L); + BlockCapsule.BlockId newId = new BlockCapsule.BlockId(createHash(2), 10000L); + peer.setNeedSyncFromUs(true); + peer.setLastSyncBlockId(oldId); + peer.getSyncBlockIdCache().put(oldId, 1L); + Mockito.when(delegate.getBlockIdByNum(10000L)).thenReturn(newId); + + P2pException exception = Assert.assertThrows(P2pException.class, + () -> handler.processMessage(peer, new FetchInvDataMessage( + Collections.singletonList(oldId), Protocol.Inventory.InventoryType.BLOCK))); + + Assert.assertEquals(P2pException.TypeEnum.SYNC_FAILED, exception.getType()); + Mockito.verify(delegate, Mockito.never()).getData(Mockito.any(), Mockito.any()); + } + + @Test + public void testAdvertisedForkBlockCanStillBeFetched() throws Exception { + FetchInvDataMsgHandler handler = new FetchInvDataMsgHandler(); + TronNetDelegate delegate = Mockito.mock(TronNetDelegate.class); + ReflectUtils.setFieldValue(handler, "tronNetDelegate", delegate); + ReflectUtils.setFieldValue(handler, "advService", Mockito.mock(AdvService.class)); + PeerConnection peer = Mockito.spy(new PeerConnection()); + Mockito.doNothing().when(peer).sendMessage(Mockito.any()); + BlockCapsule fork = new BlockCapsule(10000L, Sha256Hash.ZERO_HASH, 1, + ByteString.EMPTY); + Item item = new Item(fork.getBlockId(), Protocol.Inventory.InventoryType.BLOCK); + peer.getAdvInvSpread().put(item, 1L); + BlockMessage message = new BlockMessage(fork); + Mockito.when(delegate.getData(fork.getBlockId(), Protocol.Inventory.InventoryType.BLOCK)) + .thenReturn(message); + + handler.processMessage(peer, new FetchInvDataMessage( + Collections.singletonList(fork.getBlockId()), Protocol.Inventory.InventoryType.BLOCK)); + + Mockito.verify(peer).sendMessage(message); + Mockito.verify(delegate, Mockito.never()).getBlockIdByNum(Mockito.anyLong()); + Assert.assertNull(peer.getAdvInvSpread().getIfPresent(item)); + Assert.assertEquals(0, peer.getSyncBlockIdCache().size()); + } + private Sha256Hash createHash(int value) { byte[] bytes = new byte[Sha256Hash.LENGTH]; bytes[28] = (byte) (value >>> 24);