From 9b6221e0e13aedd69a8ce9b7209f7a1a0e3333ac Mon Sep 17 00:00:00 2001 From: wb Date: Tue, 20 Oct 2026 14:09:56 +0800 Subject: [PATCH 1/6] fix(net): restore transaction revalidation and sanitize relay data Track permission-update owners when blocks are rolled back so cached transaction verification is invalidated. Clear transaction results before caching relay messages and omit full hash lists when transaction inventory is rate limited. --- .../main/java/org/tron/core/db/Manager.java | 8 +++-- .../tron/core/net/P2pEventHandlerImpl.java | 3 -- .../tron/core/net/service/adv/AdvService.java | 2 ++ .../java/org/tron/core/db/ManagerTest.java | 33 +++++++++++++++++++ .../TransactionsMsgHandlerTest.java | 31 +++++++++++++++++ .../core/net/services/AdvServiceTest.java | 13 ++++++-- 6 files changed, 83 insertions(+), 7 deletions(-) diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index 0efa48dc20a..ea1102e0a46 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -999,8 +999,12 @@ public void eraseBlock() { khaosDb.pop(); revokingStore.fastPop(); logger.info("End to erase block: {}.", oldHeadBlock); - oldHeadBlock.getTransactions().forEach(tc -> - poppedTransactions.add(new TransactionCapsule(tc.getInstance()))); + oldHeadBlock.getTransactions().forEach(tc -> { + if (isMultiSignTransaction(tc.getInstance())) { + ownerAddressSet.add(ByteArray.toHexString(tc.getOwnerAddress())); + } + poppedTransactions.add(new TransactionCapsule(tc.getInstance())); + }); Metrics.gaugeInc(MetricKeys.Gauge.MANAGER_QUEUE, oldHeadBlock.getTransactions().size(), MetricLabels.Gauge.QUEUE_POPPED); diff --git a/framework/src/main/java/org/tron/core/net/P2pEventHandlerImpl.java b/framework/src/main/java/org/tron/core/net/P2pEventHandlerImpl.java index 67751230367..50fcafbf16a 100644 --- a/framework/src/main/java/org/tron/core/net/P2pEventHandlerImpl.java +++ b/framework/src/main/java/org/tron/core/net/P2pEventHandlerImpl.java @@ -233,9 +233,6 @@ private boolean checkInvRateLimit(PeerConnection peer, InventoryMessage msg) if (count + currentSize > maxCountIn10s) { logger.warn("Drop TRX inv from {}, window:{}, cur:{}, max:{}", peer.getInetAddress(), count, currentSize, maxCountIn10s); - if (Args.getInstance().isOpenPrintLog()) { - logger.warn("[overload] Drop tx list: {}", msg.getHashList()); - } return false; } } else if (invType == InventoryType.BLOCK) { diff --git a/framework/src/main/java/org/tron/core/net/service/adv/AdvService.java b/framework/src/main/java/org/tron/core/net/service/adv/AdvService.java index 2241ce8d1ce..f3de5607a17 100644 --- a/framework/src/main/java/org/tron/core/net/service/adv/AdvService.java +++ b/framework/src/main/java/org/tron/core/net/service/adv/AdvService.java @@ -169,6 +169,7 @@ public int fastBroadcastTransaction(TransactionMessage msg) { Item item = new Item(msg.getMessageId(), InventoryType.TRX); trxCount.add(); + msg.getTransactionCapsule().resetResult(); trxCache.put(item, new TransactionMessage(msg.getTransactionCapsule().getInstance())); List list = new ArrayList<>(); @@ -216,6 +217,7 @@ public void broadcast(Message msg) { TransactionMessage trxMsg = (TransactionMessage) msg; item = new Item(trxMsg.getMessageId(), InventoryType.TRX); trxCount.add(); + trxMsg.getTransactionCapsule().resetResult(); trxCache.put(item, new TransactionMessage(trxMsg.getTransactionCapsule().getInstance())); } else { logger.error("Adv item is neither block nor trx, type: {}", msg.getType()); diff --git a/framework/src/test/java/org/tron/core/db/ManagerTest.java b/framework/src/test/java/org/tron/core/db/ManagerTest.java index 804a29bab63..be6b4cab750 100755 --- a/framework/src/test/java/org/tron/core/db/ManagerTest.java +++ b/framework/src/test/java/org/tron/core/db/ManagerTest.java @@ -928,6 +928,39 @@ public void getVerifyTxsSkipsBlockWhenPermissionTxAlreadyConsumed() throws Excep } } + @Test + public void eraseBlockMarksPermissionUpdateOwnerForVerification() throws Exception { + byte[] owner = ByteArray.fromHexString(Wallet.getAddressPreFixString() + + "548794500882809695A8A687866E76D4271A1ABC"); + AccountContract.AccountPermissionUpdateContract contract = + AccountContract.AccountPermissionUpdateContract.newBuilder() + .setOwnerAddress(ByteString.copyFrom(owner)) + .build(); + TransactionCapsule permissionUpdate = new TransactionCapsule(contract, + ContractType.AccountPermissionUpdateContract); + BlockCapsule rolledBackBlock = new BlockCapsule(1, Sha256Hash.ZERO_HASH.getByteString(), 0, + Arrays.asList(permissionUpdate.getInstance())); + rolledBackBlock.setMerkleRoot(); + + Manager manager = new Manager(); + ChainBaseManager mockedChainManager = mock(ChainBaseManager.class); + DynamicPropertiesStore mockedDynamicPropertiesStore = mock(DynamicPropertiesStore.class); + when(mockedChainManager.getDynamicPropertiesStore()).thenReturn(mockedDynamicPropertiesStore); + when(mockedDynamicPropertiesStore.getLatestBlockHeaderHash()) + .thenReturn(Sha256Hash.wrap(new byte[32])); + when(mockedChainManager.getBlockById(any(Sha256Hash.class))).thenReturn(rolledBackBlock); + ReflectUtils.setFieldValue(manager, "chainBaseManager", mockedChainManager); + ReflectUtils.setFieldValue(manager, "khaosDb", mock(KhaosDatabase.class)); + ReflectUtils.setFieldValue(manager, "revokingStore", mock(RevokingDatabase.class)); + + manager.eraseBlock(); + + @SuppressWarnings("unchecked") + Set ownerAddressSet = + (Set) ReflectUtils.getFieldObject(manager, "ownerAddressSet"); + Assert.assertTrue(ownerAddressSet.contains(ByteArray.toHexString(owner))); + } + @Test public void doNotSwitch() throws ValidateSignatureException, ContractValidateException, ContractExeException, diff --git a/framework/src/test/java/org/tron/core/net/messagehandler/TransactionsMsgHandlerTest.java b/framework/src/test/java/org/tron/core/net/messagehandler/TransactionsMsgHandlerTest.java index d7a6155d968..a694e8c7aed 100644 --- a/framework/src/test/java/org/tron/core/net/messagehandler/TransactionsMsgHandlerTest.java +++ b/framework/src/test/java/org/tron/core/net/messagehandler/TransactionsMsgHandlerTest.java @@ -188,6 +188,37 @@ public void testProcessMessageAfterClose() throws Exception { } } + @Test + public void testResultClearedBeforeSmartContractQueued() throws Exception { + TransactionsMsgHandler handler = new TransactionsMsgHandler(); + try { + PeerConnection peer = Mockito.mock(PeerConnection.class); + Protocol.Transaction trx = TvmTestUtils.generateTriggerSmartContractAndGetTransaction( + ByteArray.fromHexString("121212a9cf"), + ByteArray.fromHexString("121212a9cf"), + ByteArray.fromHexString("123456"), + 100, 100000000, 0, 0) + .toBuilder() + .addRet(Protocol.Transaction.Result.newBuilder().setFee(123L).build()) + .build(); + TransactionsMessage msg = new TransactionsMessage(Collections.singletonList(trx)); + stubAdvInvRequest(peer, msg); + + handler.processMessage(peer, msg); + + Field queueField = TransactionsMsgHandler.class.getDeclaredField("smartContractQueue"); + queueField.setAccessible(true); + BlockingQueue smartContractQueue = (BlockingQueue) queueField.get(handler); + TransactionsMsgHandler.TrxEvent event = + (TransactionsMsgHandler.TrxEvent) smartContractQueue.poll(); + Assert.assertNotNull(event); + Assert.assertEquals(0, + event.getMsg().getTransactionCapsule().getInstance().getRetCount()); + } finally { + handler.close(); + } + } + @Test public void testRejectedExecution() throws Exception { TransactionsMsgHandler handler = new TransactionsMsgHandler(); diff --git a/framework/src/test/java/org/tron/core/net/services/AdvServiceTest.java b/framework/src/test/java/org/tron/core/net/services/AdvServiceTest.java index 808c5eb09e0..856f9fb01cf 100644 --- a/framework/src/test/java/org/tron/core/net/services/AdvServiceTest.java +++ b/framework/src/test/java/org/tron/core/net/services/AdvServiceTest.java @@ -128,12 +128,21 @@ private void testTrxBroadcast() { .setRawData( Protocol.Transaction.raw.newBuilder() .setRefBlockNum(1) - .setExpiration(System.currentTimeMillis() + 3000).build()).build(); + .setExpiration(System.currentTimeMillis() + 3000).build()) + .addRet(Protocol.Transaction.Result.newBuilder().setFee(123L).build()) + .build(); CommonParameter.getInstance().setValidContractProtoThreadNum(1); TransactionMessage msg = new TransactionMessage(trx); service.broadcast(msg); Item item = new Item(msg.getMessageId(), InventoryType.TRX); - Assert.assertNotNull(service.getMessage(item)); + TransactionMessage cached = (TransactionMessage) service.getMessage(item); + Assert.assertNotNull(cached); + Assert.assertEquals(0, msg.getTransactionCapsule().getInstance().getRetCount()); + Assert.assertEquals(0, cached.getTransactionCapsule().getInstance().getRetCount()); + + TransactionMessage fastMsg = new TransactionMessage(trx); + service.fastBroadcastTransaction(fastMsg); + Assert.assertEquals(0, fastMsg.getTransactionCapsule().getInstance().getRetCount()); Protocol.Transaction expiredTrx = Protocol.Transaction.newBuilder() .setRawData( From 9126dfdfb83f14473b64029ac214ccd957e2e9f1 Mon Sep 17 00:00:00 2001 From: wb Date: Tue, 20 Oct 2026 18:17:01 +0800 Subject: [PATCH 2/6] fix(net): recover sync requests after block processing failures Release the global sync request on every block processing failure so another peer can supply the block without waiting for cache expiration. Disconnect only the sender and preserve other peers' queued blocks and sync progress. Reject advertised blocks whose witness is outside the active set instead of silently dropping them. Keep advertisement recovery on the sync path and retain log-only handling for failures during advertised block application. Unify block application errors as BAD_BLOCK while retaining their causes. Log P2P failure details without a stack trace and keep stack traces for unexpected sync failures. --- .../org/tron/core/net/TronNetDelegate.java | 15 +- .../net/messagehandler/BlockMsgHandler.java | 9 +- .../core/net/service/sync/SyncService.java | 29 +- .../tron/core/net/TronNetDelegateTest.java | 58 ++++ .../BlockAlreadyKnownCounterTest.java | 3 - .../messagehandler/BlockMsgHandlerTest.java | 1 - .../net/services/BlockFetchFailureTest.java | 292 ++++++++++++++++++ 7 files changed, 367 insertions(+), 40 deletions(-) create mode 100644 framework/src/test/java/org/tron/core/net/services/BlockFetchFailureTest.java diff --git a/framework/src/main/java/org/tron/core/net/TronNetDelegate.java b/framework/src/main/java/org/tron/core/net/TronNetDelegate.java index 9d15886f3ab..1d7f6a86294 100644 --- a/framework/src/main/java/org/tron/core/net/TronNetDelegate.java +++ b/framework/src/main/java/org/tron/core/net/TronNetDelegate.java @@ -1,7 +1,6 @@ package org.tron.core.net; import static org.tron.core.config.Parameter.ChainConstant.BLOCK_PRODUCED_INTERVAL; -import static org.tron.core.exception.BadBlockException.TypeEnum.CALC_MERKLE_ROOT_FAILED; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; @@ -312,12 +311,7 @@ public void processBlock(BlockCapsule block, boolean isSync) throws P2pException | EventBloomException e) { metricsService.failProcessBlock(block.getNum(), e.getMessage()); logger.error("Process block failed, {}, reason: {}", blockId.getString(), e.getMessage()); - if (e instanceof BadBlockException - && ((BadBlockException) e).getType().equals(CALC_MERKLE_ROOT_FAILED)) { - throw new P2pException(TypeEnum.BLOCK_MERKLE_INVALID, e); - } else { - throw new P2pException(TypeEnum.BAD_BLOCK, e); - } + throw new P2pException(TypeEnum.BAD_BLOCK, e); } } } @@ -356,7 +350,7 @@ public void validSignature(BlockCapsule block) throws P2pException { } } - public boolean validBlock(BlockCapsule block) throws P2pException { + public void validBlock(BlockCapsule block) throws P2pException { long time = System.currentTimeMillis(); if (block.getTimeStamp() - time > timeout) { throw new P2pException(TypeEnum.BAD_BLOCK, @@ -368,7 +362,10 @@ public boolean validBlock(BlockCapsule block) throws P2pException { throw new P2pException(TypeEnum.BLOCK_MERKLE_INVALID, e.getMessage()); } validSignature(block); - return witnessScheduleStore.getActiveWitnesses().contains(block.getWitnessAddress()); + if (!witnessScheduleStore.getActiveWitnesses().contains(block.getWitnessAddress())) { + throw new P2pException(TypeEnum.BAD_BLOCK, + "Block witness is not active: " + block.getBlockId().getString()); + } } public PbftSignCapsule getBlockPbftCommitData(long blockNum) { 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 b1631979913..b92765ff815 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 @@ -134,14 +134,7 @@ private void check(PeerConnection peer, BlockMessage msg) throws P2pException { private void processBlock(PeerConnection peer, BlockCapsule block) throws P2pException { BlockId blockId = block.getBlockId(); - boolean flag = tronNetDelegate.validBlock(block); - if (!flag) { - logger.warn("Receive a bad block from {}, {}, {}", - peer.getInetSocketAddress(), blockId.getString(), - Hex.toHexString(block.getWitnessAddress().toByteArray())); - return; - } - + tronNetDelegate.validBlock(block); if (!tronNetDelegate.containBlock(block.getParentBlockId())) { logger.warn("Get unlink block {} from {}, head is {}", blockId.getString(), peer.getInetAddress(), tronNetDelegate.getHeadBlockId().getString()); 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 75d1ec9f4b5..b8ccb6daf59 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 @@ -339,26 +339,21 @@ private synchronized void handleSyncBlock() { } private void processSyncBlock(BlockCapsule block, PeerConnection peerConnection) { - boolean flag = true; - boolean attackFlag = false; BlockId blockId = block.getBlockId(); 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: {}", - blockId.getString(), p2pException.getType()); - attackFlag = p2pException.getType().equals(TypeEnum.BLOCK_SIGN_INVALID) - || p2pException.getType().equals(TypeEnum.BLOCK_MERKLE_INVALID); - flag = false; } catch (Exception e) { - logger.error("Process sync block {} failed", blockId.getString(), e); - flag = false; - } - - if (attackFlag) { + if (e instanceof P2pException) { + logger.error("Process sync block {} from {} failed, type: {}, reason: {}", + blockId.getString(), peerConnection.getInetSocketAddress(), + ((P2pException) e).getType(), e.getMessage()); + } else { + logger.error("Process sync block {} from {} failed", + blockId.getString(), peerConnection.getInetSocketAddress(), e); + } invalid(blockId, peerConnection); peerConnection.disconnect(ReasonCode.BAD_BLOCK); return; @@ -369,12 +364,8 @@ private void processSyncBlock(BlockCapsule block, PeerConnection peerConnection) if (blockId.equals(bid)) { peer.setBlockBothHave(blockId); peer.getSyncBlockToFetch().remove(bid); - if (flag) { - if (peer.getSyncBlockToFetch().isEmpty() && peer.isFetchAble()) { - syncNext(peer); - } - } else { - peer.disconnect(ReasonCode.BAD_BLOCK); + if (peer.getSyncBlockToFetch().isEmpty() && peer.isFetchAble()) { + syncNext(peer); } } } diff --git a/framework/src/test/java/org/tron/core/net/TronNetDelegateTest.java b/framework/src/test/java/org/tron/core/net/TronNetDelegateTest.java index 38738f78a6d..9cf78f88204 100644 --- a/framework/src/test/java/org/tron/core/net/TronNetDelegateTest.java +++ b/framework/src/test/java/org/tron/core/net/TronNetDelegateTest.java @@ -4,6 +4,7 @@ import com.google.protobuf.ByteString; import java.lang.reflect.Field; +import java.util.Collections; import org.junit.Assert; import org.junit.Test; import org.mockito.Mockito; @@ -17,9 +18,13 @@ import org.tron.core.capsule.TransactionCapsule; import org.tron.core.config.args.Args; import org.tron.core.db.Manager; +import org.tron.core.exception.BadBlockException; import org.tron.core.exception.P2pException; import org.tron.core.exception.P2pException.TypeEnum; +import org.tron.core.exception.ValidateScheduleException; +import org.tron.core.metrics.MetricsService; import org.tron.core.store.DynamicPropertiesStore; +import org.tron.core.store.WitnessScheduleStore; import org.tron.protos.Protocol.Transaction.Contract.ContractType; import org.tron.protos.contract.BalanceContract.TransferContract; @@ -186,4 +191,57 @@ public void testValidBlockMerkleRoot() throws Exception { Assert.assertEquals(TypeEnum.BLOCK_MERKLE_INVALID, e.getType()); } } + + @Test + public void testValidBlockRejectsInactiveWitness() throws Exception { + TronNetDelegate delegate = Mockito.spy(new TronNetDelegate()); + WitnessScheduleStore schedule = mock(WitnessScheduleStore.class); + setField(delegate, "witnessScheduleStore", schedule); + BlockCapsule block = new BlockCapsule(1, Sha256Hash.ZERO_HASH, 0, + ByteString.copyFromUtf8("witness")); + block.setMerkleRoot(); + Mockito.doNothing().when(delegate).validSignature(block); + Mockito.when(schedule.getActiveWitnesses()).thenReturn(Collections.emptyList()); + try { + delegate.validBlock(block); + Assert.fail("An inactive witness must be rejected instead of silently dropped"); + } catch (P2pException e) { + Assert.assertEquals(TypeEnum.BAD_BLOCK, e.getType()); + } + Mockito.when(schedule.getActiveWitnesses()) + .thenReturn(Collections.singletonList(block.getWitnessAddress())); + delegate.validBlock(block); + } + + @Test + public void testProcessBlockWrapsScheduleFailureWithCause() throws Exception { + assertProcessBlockFailure(new ValidateScheduleException("invalid scheduled witness")); + } + + @Test + public void testProcessBlockWrapsMerkleFailureWithCause() throws Exception { + assertProcessBlockFailure(new BadBlockException( + BadBlockException.TypeEnum.CALC_MERKLE_ROOT_FAILED, "invalid merkle root")); + } + + private void assertProcessBlockFailure(Exception failure) throws Exception { + TronNetDelegate delegate = new TronNetDelegate(); + Manager manager = mock(Manager.class); + ChainBaseManager chainManager = mock(ChainBaseManager.class); + setField(delegate, "dbManager", manager); + setField(delegate, "chainBaseManager", chainManager); + setField(delegate, "metricsService", mock(MetricsService.class)); + Mockito.when(manager.getBlockedTimer()).thenReturn(new ThreadLocal<>()); + Mockito.when(chainManager.getHeadBlockId()) + .thenReturn(new BlockCapsule.BlockId(Sha256Hash.ZERO_HASH, 0)); + BlockCapsule block = new BlockCapsule(1, Sha256Hash.ZERO_HASH, 0, ByteString.EMPTY); + Mockito.doThrow(failure).when(manager).pushBlock(block); + try { + delegate.processBlock(block, true); + Assert.fail("Expected block validation failure"); + } catch (P2pException e) { + Assert.assertEquals(TypeEnum.BAD_BLOCK, e.getType()); + Assert.assertSame(failure, e.getCause()); + } + } } diff --git a/framework/src/test/java/org/tron/core/net/messagehandler/BlockAlreadyKnownCounterTest.java b/framework/src/test/java/org/tron/core/net/messagehandler/BlockAlreadyKnownCounterTest.java index 64e77d77bba..b2afbafda7d 100644 --- a/framework/src/test/java/org/tron/core/net/messagehandler/BlockAlreadyKnownCounterTest.java +++ b/framework/src/test/java/org/tron/core/net/messagehandler/BlockAlreadyKnownCounterTest.java @@ -90,7 +90,6 @@ public void tearDown() { @Test public void testMatchedRequestAlreadyKnownBlockIncrements() throws P2pException { when(delegate.containBlock(any(BlockId.class))).thenReturn(true); - when(delegate.validBlock(any(BlockCapsule.class))).thenReturn(true); when(delegate.getHeadBlockId()).thenReturn(new BlockId(Sha256Hash.ZERO_HASH, HEAD_NUM)); BlockMessage msg = newBlockMessage(1); @@ -116,7 +115,6 @@ public void testMatchedRequestUnknownForkBelowHeadNotIncremented() throws P2pExc assertNotEquals(msg.getBlockId(), msg.getBlockCapsule().getParentBlockId()); when(delegate.containBlock(msg.getBlockId())).thenReturn(false); when(delegate.containBlock(msg.getBlockCapsule().getParentBlockId())).thenReturn(true); - when(delegate.validBlock(any(BlockCapsule.class))).thenReturn(true); when(delegate.getHeadBlockId()).thenReturn(new BlockId(Sha256Hash.ZERO_HASH, HEAD_NUM)); double before = sample(); @@ -150,7 +148,6 @@ public void testMatchedRequestHeadDuplicateIncrements() throws P2pException { // re-delivery of the current head block id counts: the id is exactly known even // though it is not below head. when(delegate.containBlock(any(BlockId.class))).thenReturn(true); - when(delegate.validBlock(any(BlockCapsule.class))).thenReturn(true); when(delegate.getHeadBlockId()).thenReturn(new BlockId(Sha256Hash.ZERO_HASH, HEAD_NUM)); when(delegate.getActivePeer()).thenReturn(Collections.emptyList()); 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..65397e84da1 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 @@ -152,7 +152,6 @@ public void testProcessBlock() { .put(new Item(blockCapsule0.getBlockId(), InventoryType.BLOCK), System.currentTimeMillis()); - Mockito.doReturn(true).when(tronNetDelegate).validBlock(any(BlockCapsule.class)); Mockito.doReturn(true).when(tronNetDelegate).containBlock(any(BlockId.class)); Mockito.doReturn(blockCapsule0.getBlockId()).when(tronNetDelegate).getHeadBlockId(); Mockito.doNothing().when(tronNetDelegate).processBlock(any(BlockCapsule.class), anyBoolean()); diff --git a/framework/src/test/java/org/tron/core/net/services/BlockFetchFailureTest.java b/framework/src/test/java/org/tron/core/net/services/BlockFetchFailureTest.java new file mode 100644 index 00000000000..1ef73de9a99 --- /dev/null +++ b/framework/src/test/java/org/tron/core/net/services/BlockFetchFailureTest.java @@ -0,0 +1,292 @@ +package org.tron.core.net.services; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.common.cache.Cache; +import com.google.protobuf.ByteString; +import java.lang.reflect.Method; +import java.net.InetSocketAddress; +import java.util.Arrays; +import java.util.Collections; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tron.common.TestConstants; +import org.tron.common.utils.ReflectUtils; +import org.tron.common.utils.Sha256Hash; +import org.tron.core.capsule.BlockCapsule; +import org.tron.core.capsule.BlockCapsule.BlockId; +import org.tron.core.config.args.Args; +import org.tron.core.exception.BadBlockException; +import org.tron.core.exception.P2pException; +import org.tron.core.exception.P2pException.TypeEnum; +import org.tron.core.exception.ValidateScheduleException; +import org.tron.core.net.P2pEventHandlerImpl; +import org.tron.core.net.TronNetDelegate; +import org.tron.core.net.message.TronMessage; +import org.tron.core.net.message.adv.BlockMessage; +import org.tron.core.net.message.adv.FetchInvDataMessage; +import org.tron.core.net.messagehandler.BlockMsgHandler; +import org.tron.core.net.peer.Item; +import org.tron.core.net.peer.PeerConnection; +import org.tron.core.net.service.adv.AdvService; +import org.tron.core.net.service.fetchblock.FetchBlockService; +import org.tron.core.net.service.sync.SyncService; +import org.tron.p2p.connection.Channel; +import org.tron.protos.Protocol.Inventory.InventoryType; +import org.tron.protos.Protocol.ReasonCode; + +public class BlockFetchFailureTest { + + @ClassRule + public static final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @BeforeClass + public static void init() throws Exception { + Args.setParam(new String[]{"-d", temporaryFolder.newFolder().toString()}, + TestConstants.TEST_CONF); + } + + @AfterClass + public static void destroy() { + Args.clearParam(); + } + + @Test + public void invalidAdvertisedSignatureDoesNotRetryAnotherPeer() throws Exception { + assertAdvertisedBlockDoesNotRetry(TypeEnum.BLOCK_SIGN_INVALID); + } + + @Test + public void invalidAdvertisedMerkleRootDoesNotRetryAnotherPeer() throws Exception { + assertAdvertisedBlockDoesNotRetry(TypeEnum.BLOCK_MERKLE_INVALID); + } + + @Test + public void inactiveAdvertisedWitnessIsBannedWithoutRetryingAnotherPeer() throws Exception { + assertAdvertisedBlockDoesNotRetry(TypeEnum.BAD_BLOCK); + } + + @Test + public void advertisedScheduleFailureDoesNotDisconnectRelayingPeers() throws Exception { + TronNetDelegate delegate = mock(TronNetDelegate.class); + FetchBlockService fetch = mock(FetchBlockService.class); + AdvService adv = new AdvService(); + ReflectUtils.setFieldValue(adv, "tronNetDelegate", delegate); + ReflectUtils.setFieldValue(adv, "fetchBlockService", fetch); + BlockMsgHandler handler = new BlockMsgHandler(); + ReflectUtils.setFieldValue(handler, "tronNetDelegate", delegate); + ReflectUtils.setFieldValue(handler, "fetchBlockService", fetch); + ReflectUtils.setFieldValue(handler, "advService", adv); + PeerConnection sender = newPeer(10001); + PeerConnection other = newPeer(10002); + BlockCapsule block = newBlock(); + Item item = new Item(block.getBlockId(), InventoryType.BLOCK); + when(delegate.getHeadBlockId()).thenReturn(new BlockId(Sha256Hash.ZERO_HASH, 0)); + when(delegate.containBlock(block.getParentBlockId())).thenReturn(true); + when(delegate.getActivePeer()).thenReturn(Collections.singletonList(sender)); + sender.getAdvInvReceive().put(item, System.currentTimeMillis()); + other.getAdvInvReceive().put(item, System.currentTimeMillis()); + P2pException failure = new P2pException(TypeEnum.BAD_BLOCK, + new ValidateScheduleException("wrong slot")); + doThrow(failure).when(delegate).processBlock(any(BlockCapsule.class), anyBoolean()); + try { + Assert.assertTrue(adv.addInv(item)); + when(delegate.getActivePeer()).thenReturn(Arrays.asList(sender, other)); + handler.processMessage(sender, new BlockMessage(block)); + verify(delegate).processBlock(block, false); + verify(sender, never()).disconnect(any()); + Assert.assertFalse(sender.getAdvInvRequest().containsKey(item)); + verify(other, never()).disconnect(any()); + Assert.assertNotEquals(block.getBlockId(), other.getBlockBothHave()); + Assert.assertFalse(other.getAdvInvRequest().containsKey(item)); + verify(other, never()).sendMessage(any(FetchInvDataMessage.class)); + } finally { + adv.close(); + } + } + + private void assertAdvertisedBlockDoesNotRetry(TypeEnum type) throws Exception { + TronNetDelegate delegate = mock(TronNetDelegate.class); + FetchBlockService fetch = mock(FetchBlockService.class); + AdvService adv = new AdvService(); + ReflectUtils.setFieldValue(adv, "tronNetDelegate", delegate); + ReflectUtils.setFieldValue(adv, "fetchBlockService", fetch); + BlockMsgHandler handler = new BlockMsgHandler(); + ReflectUtils.setFieldValue(handler, "tronNetDelegate", delegate); + ReflectUtils.setFieldValue(handler, "fetchBlockService", fetch); + PeerConnection sender = newPeer(10001); + PeerConnection other = newPeer(10002); + BlockCapsule block = newBlock(); + Item item = new Item(block.getBlockId(), InventoryType.BLOCK); + when(delegate.getHeadBlockId()).thenReturn(new BlockId(Sha256Hash.ZERO_HASH, 0)); + when(delegate.getActivePeer()).thenReturn(Collections.singletonList(sender)); + sender.getAdvInvReceive().put(item, System.currentTimeMillis()); + other.getAdvInvReceive().put(item, System.currentTimeMillis()); + P2pException failure = new P2pException(type, "invalid block"); + doThrow(failure).when(delegate).validBlock(any(BlockCapsule.class)); + try { + Assert.assertTrue(adv.addInv(item)); + Assert.assertTrue(sender.getAdvInvRequest().containsKey(item)); + try { + handler.processMessage(sender, new BlockMessage(block)); + Assert.fail("Invalid block must be rejected"); + } catch (P2pException e) { + Assert.assertSame(failure, e); + Method exceptionHandler = P2pEventHandlerImpl.class.getDeclaredMethod( + "processException", PeerConnection.class, TronMessage.class, Exception.class); + exceptionHandler.setAccessible(true); + exceptionHandler.invoke(new P2pEventHandlerImpl(), sender, new BlockMessage(block), e); + verify(sender).disconnect(ReasonCode.BAD_BLOCK); + } + Assert.assertFalse("The failed advertisement must not be retried on disconnect", + sender.getAdvInvRequest().containsKey(item)); + when(delegate.getActivePeer()).thenReturn(Collections.singletonList(other)); + adv.onDisconnect(sender); + Assert.assertFalse(other.getAdvInvRequest().containsKey(item)); + verify(other, never()).sendMessage(any(FetchInvDataMessage.class)); + verify(other, never()).disconnect(any()); + Assert.assertFalse("Leave recovery to sync instead of accepting the same INV again", + adv.addInv(item)); + } finally { + adv.close(); + } + } + + @Test + public void scheduleFailureReleasesSyncRequestWithoutDisconnectingOtherPeers() throws Exception { + assertSyncFailure(new P2pException(TypeEnum.BAD_BLOCK, + new ValidateScheduleException("validateWitnessSchedule error")), ReasonCode.BAD_BLOCK); + } + + @Test + public void invalidSignatureReleasesSyncRequestWithoutDisconnectingOtherPeers() throws Exception { + assertSyncFailure(new P2pException(TypeEnum.BLOCK_SIGN_INVALID, "signature"), + ReasonCode.BAD_BLOCK); + } + + @Test + public void invalidMerkleRootReleasesSyncRequestAndPreservesOtherPeers() throws Exception { + assertSyncFailure(new P2pException(TypeEnum.BAD_BLOCK, new BadBlockException( + BadBlockException.TypeEnum.CALC_MERKLE_ROOT_FAILED, "merkle")), + ReasonCode.BAD_BLOCK); + } + + @Test + public void invalidBlockReleasesSyncRequestWithoutDisconnectingOtherPeers() throws Exception { + assertSyncFailure(new P2pException(TypeEnum.BAD_BLOCK, "invalid transaction"), + ReasonCode.BAD_BLOCK); + } + + @Test + public void localFailureReleasesSyncRequestAndDisconnectsOnlySender() throws Exception { + assertSyncFailure(new IllegalStateException("local processing failure"), ReasonCode.BAD_BLOCK); + } + + @Test + public void unlinkedBlockReleasesSyncRequestAndDisconnectsOnlySender() throws Exception { + assertSyncFailure(new P2pException(TypeEnum.UNLINK_BLOCK, "missing parent"), + ReasonCode.BAD_BLOCK); + } + + @Test + public void syncResponseDoesNotCheckCurrentActiveWitnesses() throws Exception { + BlockMsgHandler handler = new BlockMsgHandler(); + TronNetDelegate delegate = mock(TronNetDelegate.class); + SyncService sync = mock(SyncService.class); + ReflectUtils.setFieldValue(handler, "tronNetDelegate", delegate); + ReflectUtils.setFieldValue(handler, "syncService", sync); + PeerConnection sender = newPeer(10001); + BlockMessage message = new BlockMessage(newBlock()); + sender.getSyncBlockRequested().put(message.getBlockId(), System.currentTimeMillis()); + + handler.processMessage(sender, message); + + verify(delegate, never()).validBlock(any()); + verify(sync).processBlock(sender, message); + Assert.assertFalse(sender.getSyncBlockRequested().containsKey(message.getBlockId())); + Assert.assertTrue(sender.getSyncBlockInProcess().contains(message.getBlockId())); + } + + @SuppressWarnings("unchecked") + private void assertSyncFailure(Exception failure, ReasonCode reason) throws Exception { + SyncService sync = new SyncService(); + TronNetDelegate delegate = mock(TronNetDelegate.class); + ReflectUtils.setFieldValue(sync, "tronNetDelegate", delegate); + PeerConnection sender = newPeer(10001); + PeerConnection other = newPeer(10002); + BlockCapsule block = newBlock(); + BlockId id = block.getBlockId(); + BlockId previous = new BlockId(Sha256Hash.ZERO_HASH, 0); + sender.getSyncBlockToFetch().add(id); + other.getSyncBlockToFetch().add(id); + sender.setBlockBothHave(previous); + other.setBlockBothHave(previous); + sender.getSyncBlockInProcess().add(id); + other.setFetchAble(true); + when(delegate.getActivePeer()).thenReturn(Arrays.asList(sender, other)); + when(delegate.getBlockLock()).thenReturn(new Object()); + when(delegate.getSolidBlockId()).thenReturn(previous); + if (failure instanceof P2pException + && ((P2pException) failure).getType() == TypeEnum.BLOCK_SIGN_INVALID) { + doThrow(failure).when(delegate).validSignature(any(BlockCapsule.class)); + } else { + doThrow(failure).when(delegate).processBlock(any(BlockCapsule.class), anyBoolean()); + } + Cache requests = + (Cache) ReflectUtils.getFieldObject(sync, "requestBlockIds"); + requests.put(id, sender); + try { + sync.processBlock(sender, new BlockMessage(block)); + invoke(sync, "handleSyncBlock"); + sync.onDisconnect(sender); + Assert.assertNull("A failed response must release the global request", + requests.getIfPresent(id)); + Assert.assertEquals(previous, other.getBlockBothHave()); + Assert.assertEquals(id, other.getSyncBlockToFetch().peek()); + Assert.assertFalse(sender.getSyncBlockInProcess().contains(id)); + verify(sender).disconnect(reason); + verify(other, never()).disconnect(any()); + when(delegate.getActivePeer()).thenReturn(Collections.singletonList(other)); + invoke(sync, "startFetchSyncBlock"); + Assert.assertSame(other, requests.getIfPresent(id)); + Assert.assertTrue(other.getSyncBlockRequested().containsKey(id)); + verify(other).sendMessage(any(FetchInvDataMessage.class)); + } finally { + sync.close(); + } + } + + private static BlockCapsule newBlock() { + return new BlockCapsule(1, Sha256Hash.ZERO_HASH, 0, ByteString.EMPTY); + } + + private static PeerConnection newPeer(int port) { + PeerConnection peer = spy(new PeerConnection()); + Channel channel = mock(Channel.class); + InetSocketAddress address = new InetSocketAddress("127.0.0.1", port); + when(channel.getInetSocketAddress()).thenReturn(address); + when(channel.getInetAddress()).thenReturn(address.getAddress()); + peer.setChannel(channel); + doNothing().when(peer).sendMessage(any()); + doNothing().when(peer).disconnect(any()); + return peer; + } + + private static void invoke(SyncService service, String name) throws Exception { + Method method = SyncService.class.getDeclaredMethod(name); + method.setAccessible(true); + method.invoke(service); + } +} From fdf2ccc4953c94ac4218b4f7c41289216bc46b2f Mon Sep 17 00:00:00 2001 From: wb Date: Wed, 21 Oct 2026 15:50:28 +0800 Subject: [PATCH 3/6] fix(net): preserve pending transaction results during relay Clear results only on the transaction copy cached for relay. Keep the original capsule unchanged so broadcasting cannot erase pending execution results or race with block assembly. --- .../tron/core/net/service/adv/AdvService.java | 8 +++--- .../core/net/services/AdvServiceTest.java | 25 +++++++++++++++---- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/framework/src/main/java/org/tron/core/net/service/adv/AdvService.java b/framework/src/main/java/org/tron/core/net/service/adv/AdvService.java index f3de5607a17..ba3538f673f 100644 --- a/framework/src/main/java/org/tron/core/net/service/adv/AdvService.java +++ b/framework/src/main/java/org/tron/core/net/service/adv/AdvService.java @@ -169,8 +169,8 @@ public int fastBroadcastTransaction(TransactionMessage msg) { Item item = new Item(msg.getMessageId(), InventoryType.TRX); trxCount.add(); - msg.getTransactionCapsule().resetResult(); - trxCache.put(item, new TransactionMessage(msg.getTransactionCapsule().getInstance())); + trxCache.put(item, new TransactionMessage( + msg.getTransactionCapsule().getInstance().toBuilder().clearRet().build())); List list = new ArrayList<>(); list.add(msg.getMessageId()); @@ -217,8 +217,8 @@ public void broadcast(Message msg) { TransactionMessage trxMsg = (TransactionMessage) msg; item = new Item(trxMsg.getMessageId(), InventoryType.TRX); trxCount.add(); - trxMsg.getTransactionCapsule().resetResult(); - trxCache.put(item, new TransactionMessage(trxMsg.getTransactionCapsule().getInstance())); + trxCache.put(item, new TransactionMessage( + trxMsg.getTransactionCapsule().getInstance().toBuilder().clearRet().build())); } else { logger.error("Adv item is neither block nor trx, type: {}", msg.getType()); return; diff --git a/framework/src/test/java/org/tron/core/net/services/AdvServiceTest.java b/framework/src/test/java/org/tron/core/net/services/AdvServiceTest.java index 856f9fb01cf..26864a806b9 100644 --- a/framework/src/test/java/org/tron/core/net/services/AdvServiceTest.java +++ b/framework/src/test/java/org/tron/core/net/services/AdvServiceTest.java @@ -69,7 +69,7 @@ public static void after() { } @Test - public void test() { + public void test() throws IOException { testAddInv(); testBroadcast(); testTrxBroadcast(); @@ -123,7 +123,7 @@ private void testBroadcast() { Assert.assertNotNull(peer.getAdvInvSpread().getIfPresent(item)); } - private void testTrxBroadcast() { + private void testTrxBroadcast() throws IOException { Protocol.Transaction trx = Protocol.Transaction.newBuilder() .setRawData( Protocol.Transaction.raw.newBuilder() @@ -137,12 +137,13 @@ private void testTrxBroadcast() { Item item = new Item(msg.getMessageId(), InventoryType.TRX); TransactionMessage cached = (TransactionMessage) service.getMessage(item); Assert.assertNotNull(cached); - Assert.assertEquals(0, msg.getTransactionCapsule().getInstance().getRetCount()); - Assert.assertEquals(0, cached.getTransactionCapsule().getInstance().getRetCount()); + assertRelayPreservesOriginal(trx, msg, cached); TransactionMessage fastMsg = new TransactionMessage(trx); service.fastBroadcastTransaction(fastMsg); - Assert.assertEquals(0, fastMsg.getTransactionCapsule().getInstance().getRetCount()); + TransactionMessage fastCached = (TransactionMessage) service.getMessage(item); + Assert.assertNotSame(cached, fastCached); + assertRelayPreservesOriginal(trx, fastMsg, fastCached); Protocol.Transaction expiredTrx = Protocol.Transaction.newBuilder() .setRawData( @@ -157,6 +158,20 @@ private void testTrxBroadcast() { Assert.assertNull(service.getMessage(item1)); } + private void assertRelayPreservesOriginal(Protocol.Transaction original, + TransactionMessage message, TransactionMessage cached) throws IOException { + Assert.assertNotNull(cached); + Assert.assertEquals(original, message.getTransactionCapsule().getInstance()); + Assert.assertArrayEquals(original.toByteArray(), message.getData()); + Assert.assertNotSame(message.getTransactionCapsule(), cached.getTransactionCapsule()); + Protocol.Transaction relay = Protocol.Transaction.parseFrom(cached.getData()); + Assert.assertEquals(0, relay.getRetCount()); + Assert.assertEquals(original.getRawData(), relay.getRawData()); + Assert.assertEquals(original.getSignatureList(), relay.getSignatureList()); + Assert.assertEquals(relay, cached.getTransactionCapsule().getInstance()); + Assert.assertEquals(message.getMessageId(), cached.getMessageId()); + } + @Test public void sameAddressPeersReceiveBroadcastAndDisconnectRetryIndependently() { InetSocketAddress address = new InetSocketAddress("127.0.0.2", 10001); From 312f1e1a9eed1bc24c081e9dc677a0dc337578f5 Mon Sep 17 00:00:00 2001 From: wb Date: Mon, 26 Oct 2026 17:30:22 +0800 Subject: [PATCH 4/6] fix(net): tolerate isolated inactive-witness blocks Avoid disconnecting a peer for a single inactive-witness block. Separate basic block validation from advertised-block witness checks, record the last inactive-witness block time per connection, and disconnect only on a repeat within half the current maintenance interval. Log every inactive-witness occurrence before applying the disconnect condition. Keep timestamp, Merkle-root and signature validation ahead of the witness check and allow isolated occurrences to continue processing. --- .../org/tron/core/net/TronNetDelegate.java | 16 +- .../net/messagehandler/BlockMsgHandler.java | 2 +- .../tron/core/net/peer/PeerConnection.java | 4 + .../tron/core/net/TronNetDelegateTest.java | 50 +++++- .../net/services/BlockFetchFailureTest.java | 151 +++++++++++++++++- 5 files changed, 209 insertions(+), 14 deletions(-) diff --git a/framework/src/main/java/org/tron/core/net/TronNetDelegate.java b/framework/src/main/java/org/tron/core/net/TronNetDelegate.java index 1d7f6a86294..e130a3f36c2 100644 --- a/framework/src/main/java/org/tron/core/net/TronNetDelegate.java +++ b/framework/src/main/java/org/tron/core/net/TronNetDelegate.java @@ -23,6 +23,7 @@ import org.tron.common.prometheus.MetricLabels; import org.tron.common.prometheus.Metrics; import org.tron.common.utils.Sha256Hash; +import org.tron.common.utils.Time; import org.tron.core.ChainBaseManager; import org.tron.core.capsule.BlockCapsule; import org.tron.core.capsule.BlockCapsule.BlockId; @@ -362,9 +363,20 @@ public void validBlock(BlockCapsule block) throws P2pException { throw new P2pException(TypeEnum.BLOCK_MERKLE_INVALID, e.getMessage()); } validSignature(block); + } + + public void validAdvBlock(PeerConnection peer, BlockCapsule block) throws P2pException { + validBlock(block); if (!witnessScheduleStore.getActiveWitnesses().contains(block.getWitnessAddress())) { - throw new P2pException(TypeEnum.BAD_BLOCK, - "Block witness is not active: " + block.getBlockId().getString()); + logger.warn("Block witness is not active, block: {}, peer: {}", + block.getBlockId().getString(), peer.getInetSocketAddress()); + long now = Time.getCurrentMillis(); + long lastTime = peer.getLastInactiveWitnessBlockTime(); + peer.setLastInactiveWitnessBlockTime(now); + if (now - lastTime < getMaintenanceTimeInterval() / 2) { + throw new P2pException(TypeEnum.BAD_BLOCK, + "Repeated block from an inactive witness: " + block.getBlockId().getString()); + } } } 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 b92765ff815..9d4fc1ae74a 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 @@ -134,7 +134,7 @@ private void check(PeerConnection peer, BlockMessage msg) throws P2pException { private void processBlock(PeerConnection peer, BlockCapsule block) throws P2pException { BlockId blockId = block.getBlockId(); - tronNetDelegate.validBlock(block); + tronNetDelegate.validAdvBlock(peer, block); if (!tronNetDelegate.containBlock(block.getParentBlockId())) { logger.warn("Get unlink block {} from {}, head is {}", blockId.getString(), peer.getInetAddress(), tronNetDelegate.getHeadBlockId().getString()); 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 149ae69b124..a466e2bd6cc 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 @@ -93,6 +93,10 @@ public class PeerConnection { @Getter private volatile long blockRcvTime; + @Getter + @Setter + private volatile long lastInactiveWitnessBlockTime; + /** * Sliding factor of the fetch latency Exponentially Weighted Moving Average (EWMA): * each new sample contributes diff --git a/framework/src/test/java/org/tron/core/net/TronNetDelegateTest.java b/framework/src/test/java/org/tron/core/net/TronNetDelegateTest.java index 9cf78f88204..e1d5c6107b0 100644 --- a/framework/src/test/java/org/tron/core/net/TronNetDelegateTest.java +++ b/framework/src/test/java/org/tron/core/net/TronNetDelegateTest.java @@ -23,6 +23,7 @@ import org.tron.core.exception.P2pException.TypeEnum; import org.tron.core.exception.ValidateScheduleException; import org.tron.core.metrics.MetricsService; +import org.tron.core.net.peer.PeerConnection; import org.tron.core.store.DynamicPropertiesStore; import org.tron.core.store.WitnessScheduleStore; import org.tron.protos.Protocol.Transaction.Contract.ContractType; @@ -190,10 +191,18 @@ public void testValidBlockMerkleRoot() throws Exception { } catch (P2pException e) { Assert.assertEquals(TypeEnum.BLOCK_MERKLE_INVALID, e.getType()); } + PeerConnection peer = new PeerConnection(); + try { + tronNetDelegate.validAdvBlock(peer, tampered); + Assert.fail("Invalid Merkle root must not reach witness activity handling"); + } catch (P2pException e) { + Assert.assertEquals(TypeEnum.BLOCK_MERKLE_INVALID, e.getType()); + } + Assert.assertEquals(0, peer.getLastInactiveWitnessBlockTime()); } @Test - public void testValidBlockRejectsInactiveWitness() throws Exception { + public void testValidBlockDoesNotCheckWitnessActivity() throws Exception { TronNetDelegate delegate = Mockito.spy(new TronNetDelegate()); WitnessScheduleStore schedule = mock(WitnessScheduleStore.class); setField(delegate, "witnessScheduleStore", schedule); @@ -201,16 +210,45 @@ public void testValidBlockRejectsInactiveWitness() throws Exception { ByteString.copyFromUtf8("witness")); block.setMerkleRoot(); Mockito.doNothing().when(delegate).validSignature(block); - Mockito.when(schedule.getActiveWitnesses()).thenReturn(Collections.emptyList()); + delegate.validBlock(block); + Mockito.verify(schedule, Mockito.never()).getActiveWitnesses(); + } + + @Test + public void testValidAdvBlockChecksSignatureBeforeWitnessActivity() throws Exception { + TronNetDelegate delegate = Mockito.spy(new TronNetDelegate()); + BlockCapsule block = new BlockCapsule(1, Sha256Hash.ZERO_HASH, 0, ByteString.EMPTY); + block.setMerkleRoot(); + P2pException failure = new P2pException(TypeEnum.BLOCK_SIGN_INVALID, "invalid signature"); + Mockito.doThrow(failure).when(delegate).validSignature(block); + PeerConnection peer = new PeerConnection(); try { - delegate.validBlock(block); - Assert.fail("An inactive witness must be rejected instead of silently dropped"); + delegate.validAdvBlock(peer, block); + Assert.fail("Invalid signature must not reach witness activity handling"); } catch (P2pException e) { - Assert.assertEquals(TypeEnum.BAD_BLOCK, e.getType()); + Assert.assertSame(failure, e); } + Assert.assertEquals(0, peer.getLastInactiveWitnessBlockTime()); + } + + @Test + public void testValidAdvBlockAcceptsActiveWitnessWithoutChangingHistory() throws Exception { + TronNetDelegate delegate = Mockito.spy(new TronNetDelegate()); + WitnessScheduleStore schedule = mock(WitnessScheduleStore.class); + setField(delegate, "witnessScheduleStore", schedule); + BlockCapsule block = new BlockCapsule(1, Sha256Hash.ZERO_HASH, 0, + ByteString.copyFromUtf8("witness")); + block.setMerkleRoot(); + Mockito.doNothing().when(delegate).validSignature(block); Mockito.when(schedule.getActiveWitnesses()) .thenReturn(Collections.singletonList(block.getWitnessAddress())); - delegate.validBlock(block); + PeerConnection peer = new PeerConnection(); + peer.setLastInactiveWitnessBlockTime(86_400_000L); + + delegate.validAdvBlock(peer, block); + + Mockito.verify(delegate).validSignature(block); + Assert.assertEquals(86_400_000L, peer.getLastInactiveWitnessBlockTime()); } @Test diff --git a/framework/src/test/java/org/tron/core/net/services/BlockFetchFailureTest.java b/framework/src/test/java/org/tron/core/net/services/BlockFetchFailureTest.java index 1ef73de9a99..3d16e324e83 100644 --- a/framework/src/test/java/org/tron/core/net/services/BlockFetchFailureTest.java +++ b/framework/src/test/java/org/tron/core/net/services/BlockFetchFailureTest.java @@ -3,10 +3,13 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -22,9 +25,11 @@ import org.junit.ClassRule; import org.junit.Test; import org.junit.rules.TemporaryFolder; +import org.mockito.MockedStatic; import org.tron.common.TestConstants; import org.tron.common.utils.ReflectUtils; import org.tron.common.utils.Sha256Hash; +import org.tron.common.utils.Time; import org.tron.core.capsule.BlockCapsule; import org.tron.core.capsule.BlockCapsule.BlockId; import org.tron.core.config.args.Args; @@ -43,6 +48,7 @@ import org.tron.core.net.service.adv.AdvService; import org.tron.core.net.service.fetchblock.FetchBlockService; import org.tron.core.net.service.sync.SyncService; +import org.tron.core.store.WitnessScheduleStore; import org.tron.p2p.connection.Channel; import org.tron.protos.Protocol.Inventory.InventoryType; import org.tron.protos.Protocol.ReasonCode; @@ -74,8 +80,128 @@ public void invalidAdvertisedMerkleRootDoesNotRetryAnotherPeer() throws Exceptio } @Test - public void inactiveAdvertisedWitnessIsBannedWithoutRetryingAnotherPeer() throws Exception { - assertAdvertisedBlockDoesNotRetry(TypeEnum.BAD_BLOCK); + public void firstInactiveAdvertisedWitnessContinuesWithoutDisconnecting() throws Exception { + TronNetDelegate delegate = inactiveWitnessDelegate(21_600_000L); + BlockMsgHandler handler = new BlockMsgHandler(); + ReflectUtils.setFieldValue(handler, "tronNetDelegate", delegate); + ReflectUtils.setFieldValue(handler, "fetchBlockService", mock(FetchBlockService.class)); + SyncService sync = mock(SyncService.class); + ReflectUtils.setFieldValue(handler, "syncService", sync); + PeerConnection sender = newPeer(10001); + BlockCapsule block = newBlock(); + block.setMerkleRoot(); + sender.getAdvInvRequest().put(new Item(block.getBlockId(), InventoryType.BLOCK), + System.currentTimeMillis()); + + handler.processMessage(sender, new BlockMessage(block)); + + verify(sender, never()).disconnect(any()); + verify(sync).startSync(sender); + verify(delegate, never()).processBlock(any(BlockCapsule.class), anyBoolean()); + } + + @Test + public void inactiveWitnessWithinHalfMaintenanceDisconnects() throws Exception { + assertInactiveWitnessTiming(new long[]{86_400_000L, 97_199_999L}, + new boolean[]{false, true}); + } + + @Test + public void inactiveWitnessAtHalfMaintenanceStartsNewWindow() throws Exception { + assertInactiveWitnessTiming(new long[]{86_400_000L, 97_200_000L, 97_200_001L}, + new boolean[]{false, false, true}); + } + + @Test + public void inactiveWitnessAfterHalfMaintenanceStartsNewWindow() throws Exception { + assertInactiveWitnessTiming(new long[]{86_400_000L, 97_200_001L, 97_200_002L}, + new boolean[]{false, false, true}); + } + + @Test + public void inactiveWitnessClockRollbackIsTreatedAsRepeat() throws Exception { + assertInactiveWitnessTiming(new long[]{86_400_000L, 86_399_999L}, + new boolean[]{false, true}); + } + + @Test + public void inactiveWitnessBelowTruncatedHalfMaintenanceDisconnects() throws Exception { + assertInactiveWitnessTiming(new long[]{86_400_000L, 86_440_499L}, + new boolean[]{false, true}, 81_001L); + } + + @Test + public void inactiveWitnessAtTruncatedHalfMaintenanceStartsNewWindow() throws Exception { + assertInactiveWitnessTiming(new long[]{86_400_000L, 86_440_500L}, + new boolean[]{false, false}, 81_001L); + } + + @Test + public void inactiveWitnessTimesAreIndependentForEachPeer() throws Exception { + TronNetDelegate delegate = inactiveWitnessDelegate(21_600_000L); + BlockMsgHandler handler = new BlockMsgHandler(); + ReflectUtils.setFieldValue(handler, "tronNetDelegate", delegate); + ReflectUtils.setFieldValue(handler, "fetchBlockService", mock(FetchBlockService.class)); + ReflectUtils.setFieldValue(handler, "syncService", mock(SyncService.class)); + PeerConnection first = newPeer(10001); + PeerConnection second = newPeer(10002); + BlockMessage message = new BlockMessage(newBlock()); + try (MockedStatic