From cdf1047b6f9c17dae340f500c250fddd157fe792 Mon Sep 17 00:00:00 2001 From: wb Date: Fri, 17 Apr 2026 15:37:06 +0800 Subject: [PATCH] feat(net): optimize disconnectRandom by tracking block receive time per peer - Add blockRcvTime/blockRcvTimeCmp fields to PeerConnection to track when a peer last delivered a valid block - Set blockRcvTime in BlockMsgHandler after each block is received - Fix lastInteractiveTime update in InventoryMsgHandler: only update for block inventories above current head block num, preventing attackers from forging activity via stale block hashes - Add getRandomDisconnectionPeers() to ResilienceService: narrows the disconnect candidate pool to the oldest half by blockRcvTime, so peers that recently delivered blocks are protected from random eviction Co-Authored-By: Claude Sonnet 4.6 --- .../net/messagehandler/BlockMsgHandler.java | 1 + .../messagehandler/InventoryMsgHandler.java | 6 +- .../tron/core/net/peer/PeerConnection.java | 8 ++ .../service/effective/ResilienceService.java | 9 +- .../core/net/service/sync/SyncService.java | 1 + .../messagehandler/BlockMsgHandlerTest.java | 78 +++++++++++++++-- .../InventoryMsgHandlerTest.java | 85 +++++++++++++++++++ .../net/services/ResilienceServiceTest.java | 52 ++++++++++++ .../core/net/services/SyncServiceTest.java | 81 ++++++++++++++++++ 9 files changed, 313 insertions(+), 8 deletions(-) diff --git a/framework/src/main/java/org/tron/core/net/messagehandler/BlockMsgHandler.java b/framework/src/main/java/org/tron/core/net/messagehandler/BlockMsgHandler.java index dc886517476..3b9e86d4791 100644 --- a/framework/src/main/java/org/tron/core/net/messagehandler/BlockMsgHandler.java +++ b/framework/src/main/java/org/tron/core/net/messagehandler/BlockMsgHandler.java @@ -150,6 +150,7 @@ private void processBlock(PeerConnection peer, BlockCapsule block) throws P2pExc try { tronNetDelegate.processBlock(block, false); + peer.setBlockRcvTime(System.currentTimeMillis()); witnessProductBlockService.validWitnessProductTwoBlock(block); Item item = new Item(blockId, InventoryType.BLOCK); diff --git a/framework/src/main/java/org/tron/core/net/messagehandler/InventoryMsgHandler.java b/framework/src/main/java/org/tron/core/net/messagehandler/InventoryMsgHandler.java index e8783b25e95..c58f97fc26c 100644 --- a/framework/src/main/java/org/tron/core/net/messagehandler/InventoryMsgHandler.java +++ b/framework/src/main/java/org/tron/core/net/messagehandler/InventoryMsgHandler.java @@ -4,6 +4,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.tron.common.utils.Sha256Hash; +import org.tron.core.capsule.BlockCapsule.BlockId; import org.tron.core.config.args.Args; import org.tron.core.net.TronNetDelegate; import org.tron.core.net.message.TronMessage; @@ -40,7 +41,10 @@ public void processMessage(PeerConnection peer, TronMessage msg) { peer.getAdvInvReceive().put(item, System.currentTimeMillis()); advService.addInv(item); if (type.equals(InventoryType.BLOCK) && peer.getAdvInvSpread().getIfPresent(item) == null) { - peer.setLastInteractiveTime(System.currentTimeMillis()); + long headNum = tronNetDelegate.getHeadBlockId().getNum(); + if (new BlockId(id).getNum() > headNum) { + peer.setLastInteractiveTime(System.currentTimeMillis()); + } } } } 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 253502bc3a1..b79ff6deef9 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 @@ -88,6 +88,14 @@ public class PeerConnection { @Setter private volatile long lastInteractiveTime; + @Setter + @Getter + private volatile long blockRcvTime; + + @Setter + @Getter + private volatile long blockRcvTimeCmp; + @Getter @Setter private volatile TronState tronState = TronState.INIT; diff --git a/framework/src/main/java/org/tron/core/net/service/effective/ResilienceService.java b/framework/src/main/java/org/tron/core/net/service/effective/ResilienceService.java index b99b5b52bad..695362907f4 100644 --- a/framework/src/main/java/org/tron/core/net/service/effective/ResilienceService.java +++ b/framework/src/main/java/org/tron/core/net/service/effective/ResilienceService.java @@ -44,7 +44,7 @@ public class ResilienceService { @Autowired private ChainBaseManager chainBaseManager; - + public void init() { if (Args.getInstance().isOpenFullTcpDisconnect) { executor.scheduleWithFixedDelay(() -> { @@ -86,6 +86,7 @@ private void disconnectRandom() { .collect(Collectors.toList()); if (peers.size() >= minBroadcastPeerSize) { + peers = getRandomDisconnectionPeers(peers); long now = System.currentTimeMillis(); Map weights = new HashMap<>(); peers.forEach(peer -> { @@ -121,6 +122,12 @@ private void disconnectRandom() { } + private List getRandomDisconnectionPeers(List peers) { + peers.forEach(p -> p.setBlockRcvTimeCmp(p.getBlockRcvTime())); + peers.sort(Comparator.comparingLong(PeerConnection::getBlockRcvTimeCmp)); + return peers.subList(0, peers.size() / 2); + } + private void disconnectLan() { if (!isLanNode()) { return; diff --git a/framework/src/main/java/org/tron/core/net/service/sync/SyncService.java b/framework/src/main/java/org/tron/core/net/service/sync/SyncService.java index 75349bd4c19..8e43a28e10a 100644 --- a/framework/src/main/java/org/tron/core/net/service/sync/SyncService.java +++ b/framework/src/main/java/org/tron/core/net/service/sync/SyncService.java @@ -305,6 +305,7 @@ private void processSyncBlock(BlockCapsule block, PeerConnection peerConnection) try { tronNetDelegate.validSignature(block); tronNetDelegate.processBlock(block, true); + peerConnection.setBlockRcvTime(System.currentTimeMillis()); pbftDataSyncHandler.processPBFTCommitData(block); } catch (P2pException p2pException) { logger.error("Process sync block {} failed, type: {}", diff --git a/framework/src/test/java/org/tron/core/net/messagehandler/BlockMsgHandlerTest.java b/framework/src/test/java/org/tron/core/net/messagehandler/BlockMsgHandlerTest.java index 82ea2b6cb57..41c3ba08b49 100644 --- a/framework/src/test/java/org/tron/core/net/messagehandler/BlockMsgHandlerTest.java +++ b/framework/src/test/java/org/tron/core/net/messagehandler/BlockMsgHandlerTest.java @@ -132,12 +132,12 @@ public void testProcessMessage() { } @Test - public void testProcessBlock() { + public void testProcessBlock() throws Exception { TronNetDelegate tronNetDelegate = Mockito.mock(TronNetDelegate.class); - + Field field = handler.getClass().getDeclaredField("tronNetDelegate"); + field.setAccessible(true); + Object origin = field.get(handler); try { - Field field = handler.getClass().getDeclaredField("tronNetDelegate"); - field.setAccessible(true); field.set(handler, tronNetDelegate); BlockCapsule blockCapsule0 = new BlockCapsule(1, @@ -164,8 +164,74 @@ public void testProcessBlock() { .getDeclaredMethod("processBlock", PeerConnection.class, BlockCapsule.class); method.setAccessible(true); method.invoke(handler, peer, blockCapsule0); - } catch (Exception e) { - Assert.fail(); + } finally { + field.set(handler, origin); + } + } + + @Test + public void testBlockRcvTimeSetAfterProcessBlockSuccess() throws Exception { + TronNetDelegate tronNetDelegate = Mockito.mock(TronNetDelegate.class); + Field field = handler.getClass().getDeclaredField("tronNetDelegate"); + field.setAccessible(true); + Object origin = field.get(handler); + try { + field.set(handler, tronNetDelegate); + + BlockCapsule blockCapsule = new BlockCapsule(1, + Sha256Hash.wrap(ByteString.copyFrom(ByteArray.fromHexString( + "9938a342238077182498b464ac0292229938a342238077182498b464ac029222"))), + 1234, ByteString.copyFrom("1234567".getBytes())); + + Mockito.doReturn(true).when(tronNetDelegate).validBlock(any(BlockCapsule.class)); + Mockito.doReturn(true).when(tronNetDelegate).containBlock(any(BlockId.class)); + Mockito.doReturn(blockCapsule.getBlockId()).when(tronNetDelegate).getHeadBlockId(); + Mockito.doNothing().when(tronNetDelegate).processBlock(any(BlockCapsule.class), anyBoolean()); + Mockito.doReturn(new ArrayList()).when(tronNetDelegate).getActivePeer(); + + peer.setBlockRcvTime(0L); + Method method = handler.getClass() + .getDeclaredMethod("processBlock", PeerConnection.class, BlockCapsule.class); + method.setAccessible(true); + + long before = System.currentTimeMillis(); + method.invoke(handler, peer, blockCapsule); + long after = System.currentTimeMillis(); + + Assert.assertTrue("blockRcvTime should be set after successful processBlock", + peer.getBlockRcvTime() >= before && peer.getBlockRcvTime() <= after); + } finally { + field.set(handler, origin); + } + } + + @Test + public void testBlockRcvTimeNotSetWhenValidationFails() throws Exception { + TronNetDelegate tronNetDelegate = Mockito.mock(TronNetDelegate.class); + Field field = handler.getClass().getDeclaredField("tronNetDelegate"); + field.setAccessible(true); + Object origin = field.get(handler); + try { + field.set(handler, tronNetDelegate); + + BlockCapsule blockCapsule = new BlockCapsule(1, + Sha256Hash.wrap(ByteString.copyFrom(ByteArray.fromHexString( + "9938a342238077182498b464ac0292229938a342238077182498b464ac029222"))), + 1234, ByteString.copyFrom("1234567".getBytes())); + + // validBlock returns false → processBlock short-circuits before setBlockRcvTime + Mockito.doReturn(false).when(tronNetDelegate).validBlock(any(BlockCapsule.class)); + + peer.setBlockRcvTime(0L); + Method method = handler.getClass() + .getDeclaredMethod("processBlock", PeerConnection.class, BlockCapsule.class); + method.setAccessible(true); + method.invoke(handler, peer, blockCapsule); + + Assert.assertEquals("blockRcvTime must stay 0 when block fails validation", + 0L, peer.getBlockRcvTime()); + } finally { + field.set(handler, origin); } } } diff --git a/framework/src/test/java/org/tron/core/net/messagehandler/InventoryMsgHandlerTest.java b/framework/src/test/java/org/tron/core/net/messagehandler/InventoryMsgHandlerTest.java index 338b44e6699..24443735889 100644 --- a/framework/src/test/java/org/tron/core/net/messagehandler/InventoryMsgHandlerTest.java +++ b/framework/src/test/java/org/tron/core/net/messagehandler/InventoryMsgHandlerTest.java @@ -1,18 +1,24 @@ package org.tron.core.net.messagehandler; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import java.lang.reflect.Field; import java.net.InetAddress; import java.net.InetSocketAddress; import java.util.ArrayList; +import java.util.Arrays; +import org.junit.Assert; import org.junit.Test; import org.mockito.Mockito; import org.tron.common.TestConstants; +import org.tron.common.utils.Sha256Hash; +import org.tron.core.capsule.BlockCapsule.BlockId; import org.tron.core.config.args.Args; import org.tron.core.net.TronNetDelegate; import org.tron.core.net.message.adv.InventoryMessage; import org.tron.core.net.peer.PeerConnection; +import org.tron.core.net.service.adv.AdvService; import org.tron.p2p.connection.Channel; import org.tron.protos.Protocol.Inventory.InventoryType; @@ -51,6 +57,85 @@ public void testProcessMessage() throws Exception { handler.processMessage(peer, msg); } + @Test + public void testLastInteractiveTimeNotUpdatedForSolidifiedBlock() throws Exception { + InventoryMsgHandler handler = new InventoryMsgHandler(); + Args.setParam(new String[]{}, TestConstants.TEST_CONF); + + TronNetDelegate tronNetDelegate = mock(TronNetDelegate.class); + AdvService advService = mock(AdvService.class); + Mockito.when(advService.addInv(any())).thenReturn(true); + // block num 100 is at head boundary — should NOT update + Mockito.when(tronNetDelegate.getHeadBlockId()) + .thenReturn(new BlockId(Sha256Hash.ZERO_HASH, 100L)); + + Field delegateField = handler.getClass().getDeclaredField("tronNetDelegate"); + delegateField.setAccessible(true); + delegateField.set(handler, tronNetDelegate); + Field advField = handler.getClass().getDeclaredField("advService"); + advField.setAccessible(true); + advField.set(handler, advService); + + PeerConnection peer = new PeerConnection(); + peer.setChannel(getChannel("1.0.0.4", 1001)); + peer.setNeedSyncFromPeer(false); + peer.setNeedSyncFromUs(false); + peer.setLastInteractiveTime(0L); + + // Block hash encodes num=100 (at solidified boundary — should NOT update) + Sha256Hash blockHash = new BlockId(Sha256Hash.ZERO_HASH, 100L); + InventoryMessage msg = new InventoryMessage(Arrays.asList(blockHash), InventoryType.BLOCK); + handler.processMessage(peer, msg); + + Assert.assertEquals("lastInteractiveTime should NOT be updated for solidified block", + 0L, peer.getLastInteractiveTime()); + } + + @Test + public void testLastInteractiveTimeUpdatedForBothPeersWithSameAboveSolidifiedBlock() + throws Exception { + InventoryMsgHandler handler = new InventoryMsgHandler(); + Args.setParam(new String[]{}, TestConstants.TEST_CONF); + + TronNetDelegate tronNetDelegate = mock(TronNetDelegate.class); + AdvService advService = mock(AdvService.class); + // First call returns true (peer1), second call returns false (peer2 — already in cache) + Mockito.when(advService.addInv(any())).thenReturn(true).thenReturn(false); + Mockito.when(tronNetDelegate.getHeadBlockId()) + .thenReturn(new BlockId(Sha256Hash.ZERO_HASH, 99L)); + + Field delegateField = handler.getClass().getDeclaredField("tronNetDelegate"); + delegateField.setAccessible(true); + delegateField.set(handler, tronNetDelegate); + Field advField = handler.getClass().getDeclaredField("advService"); + advField.setAccessible(true); + advField.set(handler, advService); + + PeerConnection peer1 = new PeerConnection(); + peer1.setChannel(getChannel("1.0.0.5", 1002)); + peer1.setNeedSyncFromPeer(false); + peer1.setNeedSyncFromUs(false); + peer1.setLastInteractiveTime(0L); + + PeerConnection peer2 = new PeerConnection(); + peer2.setChannel(getChannel("1.0.0.6", 1003)); + peer2.setNeedSyncFromPeer(false); + peer2.setNeedSyncFromUs(false); + peer2.setLastInteractiveTime(0L); + + // block num 100 > solidified 99 — both peers should update + Sha256Hash blockHash = new BlockId(Sha256Hash.ZERO_HASH, 100L); + InventoryMessage msg = new InventoryMessage(Arrays.asList(blockHash), InventoryType.BLOCK); + + handler.processMessage(peer1, msg); + handler.processMessage(peer2, msg); + + Assert.assertTrue("peer1 lastInteractiveTime should be updated", + peer1.getLastInteractiveTime() > 0L); + Assert.assertTrue("peer2 lastInteractiveTime should be updated even when addInv returns false", + peer2.getLastInteractiveTime() > 0L); + } + private Channel getChannel(String host, int port) throws Exception { Channel channel = new Channel(); InetSocketAddress inetSocketAddress = new InetSocketAddress(host, port); diff --git a/framework/src/test/java/org/tron/core/net/services/ResilienceServiceTest.java b/framework/src/test/java/org/tron/core/net/services/ResilienceServiceTest.java index 792fb82c2c6..c8c4d974d8e 100644 --- a/framework/src/test/java/org/tron/core/net/services/ResilienceServiceTest.java +++ b/framework/src/test/java/org/tron/core/net/services/ResilienceServiceTest.java @@ -7,6 +7,7 @@ import java.io.IOException; import java.net.InetSocketAddress; import java.util.HashSet; +import java.util.List; import java.util.Set; import javax.annotation.Resource; import org.junit.After; @@ -97,6 +98,57 @@ public void testDisconnectRandom() { Assert.assertEquals(maxConnection - 1, PeerManager.getPeers().size()); } + @Test + public void testDisconnectRandomPreservesRecentBlockRcvTimePeer() { + int maxConnection = 30; + Assert.assertEquals(0, PeerManager.getPeers().size()); + + ApplicationContext ctx = (ApplicationContext) ReflectUtils.getFieldObject(p2pEventHandler, + "ctx"); + + // Create maxConnection + 1 peers (triggers disconnectRandom) + for (int i = 0; i < maxConnection + 1; i++) { + InetSocketAddress inetSocketAddress = new InetSocketAddress("202.0.0." + i, 10001); + Channel c1 = spy(Channel.class); + ReflectUtils.setFieldValue(c1, "inetSocketAddress", inetSocketAddress); + ReflectUtils.setFieldValue(c1, "inetAddress", inetSocketAddress.getAddress()); + ReflectUtils.setFieldValue(c1, "ctx", spy(ChannelHandlerContext.class)); + Mockito.doNothing().when(c1).send((byte[]) any()); + PeerManager.add(ctx, c1); + } + + // Set first minBroadcastPeerSize peers as broadcast-state + List peers = PeerManager.getPeers(); + for (PeerConnection peer : peers.subList(0, ResilienceService.minBroadcastPeerSize)) { + peer.setNeedSyncFromPeer(false); + peer.setNeedSyncFromUs(false); + peer.setLastInteractiveTime(System.currentTimeMillis() - 1000); + } + for (PeerConnection peer : peers.subList(ResilienceService.minBroadcastPeerSize, + maxConnection + 1)) { + peer.setNeedSyncFromPeer(false); + peer.setNeedSyncFromUs(true); + } + + // Give the LAST broadcast peer a very recent blockRcvTime — it must NOT be disconnected + PeerConnection bestPeer = peers.stream() + .filter(p -> !p.isNeedSyncFromUs() && !p.isNeedSyncFromPeer()) + .reduce((a, b) -> b) // last broadcast peer + .orElseThrow(() -> new AssertionError("no broadcast peer")); + bestPeer.setBlockRcvTime(System.currentTimeMillis()); + + InetSocketAddress bestPeerAddress = bestPeer.getChannel().getInetSocketAddress(); + + // With minBroadcastPeerSize=3 broadcast peers, getRandomDisconnectionPeers returns + // the 1 peer with oldest blockRcvTime (0). bestPeer has most recent time → exempt. + ReflectUtils.invokeMethod(service, "disconnectRandom"); + + boolean bestPeerStillConnected = PeerManager.getPeers().stream() + .anyMatch(p -> p.getChannel().getInetSocketAddress().equals(bestPeerAddress)); + Assert.assertTrue("Peer with most recent blockRcvTime should not be disconnected", + bestPeerStillConnected); + } + @Test public void testDisconnectLan() { int minConnection = 8; diff --git a/framework/src/test/java/org/tron/core/net/services/SyncServiceTest.java b/framework/src/test/java/org/tron/core/net/services/SyncServiceTest.java index 45ecbe866ab..a91afd02fde 100644 --- a/framework/src/test/java/org/tron/core/net/services/SyncServiceTest.java +++ b/framework/src/test/java/org/tron/core/net/services/SyncServiceTest.java @@ -1,5 +1,7 @@ package org.tron.core.net.services; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.Mockito.mock; import com.google.common.cache.Cache; @@ -18,7 +20,9 @@ import org.tron.common.utils.Sha256Hash; import org.tron.core.capsule.BlockCapsule; import org.tron.core.net.P2pEventHandlerImpl; +import org.tron.core.net.TronNetDelegate; import org.tron.core.net.message.adv.BlockMessage; +import org.tron.core.net.messagehandler.PbftDataSyncHandler; import org.tron.core.net.peer.PeerConnection; import org.tron.core.net.peer.PeerManager; import org.tron.core.net.peer.TronState; @@ -171,6 +175,83 @@ public void testStartFetchSyncBlock() throws Exception { Assert.assertTrue(peer.getSyncBlockRequested().get(blockId) == null); } + @Test + public void testProcessSyncBlockSetsBlockRcvTime() throws Exception { + TronNetDelegate mockDelegate = mock(TronNetDelegate.class); + PbftDataSyncHandler mockPbft = mock(PbftDataSyncHandler.class); + Mockito.doNothing().when(mockDelegate).validSignature(any(BlockCapsule.class)); + Mockito.doNothing().when(mockDelegate).processBlock(any(BlockCapsule.class), anyBoolean()); + Mockito.doReturn(new ArrayList()).when(mockDelegate).getActivePeer(); + Mockito.doNothing().when(mockPbft).processPBFTCommitData(any(BlockCapsule.class)); + + Object originDelegate = ReflectUtils.getFieldObject(service, "tronNetDelegate"); + Object originPbft = ReflectUtils.getFieldObject(service, "pbftDataSyncHandler"); + try { + ReflectUtils.setFieldValue(service, "tronNetDelegate", mockDelegate); + ReflectUtils.setFieldValue(service, "pbftDataSyncHandler", mockPbft); + + peer = context.getBean(PeerConnection.class); + Channel c1 = new Channel(); + ReflectUtils.setFieldValue(c1, "inetSocketAddress", inetSocketAddress); + ReflectUtils.setFieldValue(c1, "inetAddress", inetSocketAddress.getAddress()); + peer.setChannel(c1); + peer.setBlockRcvTime(0L); + + BlockCapsule blockCapsule = new BlockCapsule(Protocol.Block.newBuilder().build()); + + Method method = service.getClass() + .getDeclaredMethod("processSyncBlock", BlockCapsule.class, PeerConnection.class); + method.setAccessible(true); + + long before = System.currentTimeMillis(); + method.invoke(service, blockCapsule, peer); + long after = System.currentTimeMillis(); + + Assert.assertTrue("blockRcvTime should be set after successful processSyncBlock", + peer.getBlockRcvTime() >= before && peer.getBlockRcvTime() <= after); + } finally { + ReflectUtils.setFieldValue(service, "tronNetDelegate", originDelegate); + ReflectUtils.setFieldValue(service, "pbftDataSyncHandler", originPbft); + } + } + + @Test + public void testProcessSyncBlockDoesNotSetBlockRcvTimeOnFailure() throws Exception { + TronNetDelegate mockDelegate = mock(TronNetDelegate.class); + PbftDataSyncHandler mockPbft = mock(PbftDataSyncHandler.class); + Mockito.doNothing().when(mockDelegate).validSignature(any(BlockCapsule.class)); + Mockito.doThrow(new RuntimeException("process failed")) + .when(mockDelegate).processBlock(any(BlockCapsule.class), anyBoolean()); + Mockito.doReturn(new ArrayList()).when(mockDelegate).getActivePeer(); + + Object originDelegate = ReflectUtils.getFieldObject(service, "tronNetDelegate"); + Object originPbft = ReflectUtils.getFieldObject(service, "pbftDataSyncHandler"); + try { + ReflectUtils.setFieldValue(service, "tronNetDelegate", mockDelegate); + ReflectUtils.setFieldValue(service, "pbftDataSyncHandler", mockPbft); + + peer = context.getBean(PeerConnection.class); + Channel c1 = new Channel(); + ReflectUtils.setFieldValue(c1, "inetSocketAddress", inetSocketAddress); + ReflectUtils.setFieldValue(c1, "inetAddress", inetSocketAddress.getAddress()); + peer.setChannel(c1); + peer.setBlockRcvTime(0L); + + BlockCapsule blockCapsule = new BlockCapsule(Protocol.Block.newBuilder().build()); + + Method method = service.getClass() + .getDeclaredMethod("processSyncBlock", BlockCapsule.class, PeerConnection.class); + method.setAccessible(true); + method.invoke(service, blockCapsule, peer); + + Assert.assertEquals("blockRcvTime must stay 0 when processBlock throws", + 0L, peer.getBlockRcvTime()); + } finally { + ReflectUtils.setFieldValue(service, "tronNetDelegate", originDelegate); + ReflectUtils.setFieldValue(service, "pbftDataSyncHandler", originPbft); + } + } + @Test public void testHandleSyncBlock() throws Exception {