diff --git a/chainbase/src/main/java/org/tron/core/capsule/BlockCapsule.java b/chainbase/src/main/java/org/tron/core/capsule/BlockCapsule.java index 096f295493a..471f4bc27a1 100755 --- a/chainbase/src/main/java/org/tron/core/capsule/BlockCapsule.java +++ b/chainbase/src/main/java/org/tron/core/capsule/BlockCapsule.java @@ -53,7 +53,6 @@ public class BlockCapsule implements ProtoCapsule { public boolean generatedByMyself = false; - private volatile boolean merkleValidated = false; @Getter @Setter private TransactionRetCapsule result; @@ -233,16 +232,12 @@ public Sha256Hash calcMerkleRoot() { } public void validateMerkleRoot() throws BadBlockException { - if (merkleValidated) { - return; - } Sha256Hash actual = calcMerkleRoot(); if (!actual.equals(getMerkleRoot())) { throw new BadBlockException(CALC_MERKLE_ROOT_FAILED, String.format("merkle root mismatch for block %d: expected %s, actual %s", getNum(), getMerkleRoot(), actual)); } - merkleValidated = true; } public void setMerkleRoot() { 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 20fb38a15b5..d476ae73cfd 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -1223,7 +1223,8 @@ public List getVerifyTxs(BlockCapsule block) { } /** - * save a block. + * Save a block. Untrusted callers must first use network prevalidation to check its timestamp, + * transaction structure, Merkle root and block signature. */ public void pushBlock(final BlockCapsule block) throws ValidateSignatureException, ContractValidateException, ContractExeException, @@ -1265,7 +1266,6 @@ public void pushBlock(final BlockCapsule block) try (PendingManager pm = new PendingManager(this)) { if (!block.generatedByMyself) { - block.validateMerkleRoot(); consensus.receiveBlock(block); } 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 7574305268b..2948862b181 100644 --- a/framework/src/main/java/org/tron/core/net/TronNetDelegate.java +++ b/framework/src/main/java/org/tron/core/net/TronNetDelegate.java @@ -62,11 +62,15 @@ import org.tron.core.net.peer.PeerConnection; import org.tron.core.store.WitnessScheduleStore; import org.tron.protos.Protocol.Inventory.InventoryType; +import org.tron.protos.Protocol.Transaction; @Slf4j(topic = "net") @Component public class TronNetDelegate { + // Keep Merkle leaf encodings larger than the 64-byte internal-node input. + private static final int MIN_TRANSACTION_SIZE = 80; + @Autowired private Manager dbManager; @@ -89,8 +93,6 @@ public class TronNetDelegate { private int blockIdCacheSize = 100; - private long timeout = 1000; - @Getter // for test private volatile boolean hitDown = false; @@ -352,10 +354,19 @@ public void validSignature(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, - "time:" + time + ",block time:" + block.getTimeStamp()); + for (Transaction transaction : block.getInstance().getTransactionsList()) { + int contractCount = transaction.getRawData().getContractCount(); + if (contractCount != 1) { + throw new P2pException(TypeEnum.BAD_BLOCK, + "Invalid contract count: " + contractCount + + ", block: " + block.getBlockId().getString()); + } + int transactionSize = transaction.getSerializedSize(); + if (transactionSize < MIN_TRANSACTION_SIZE) { + throw new P2pException(TypeEnum.BAD_BLOCK, + "Invalid transaction size: " + transactionSize + + ", block: " + block.getBlockId().getString()); + } } try { block.validateMerkleRoot(); 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 b8ccb6daf59..8ccfb1e6886 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 @@ -341,7 +341,7 @@ private synchronized void handleSyncBlock() { private void processSyncBlock(BlockCapsule block, PeerConnection peerConnection) { BlockId blockId = block.getBlockId(); try { - tronNetDelegate.validSignature(block); + tronNetDelegate.validBlock(block); tronNetDelegate.processBlock(block, true); peerConnection.setBlockRcvTime(System.currentTimeMillis()); pbftDataSyncHandler.processPBFTCommitData(block); diff --git a/framework/src/test/java/org/tron/core/capsule/BlockCapsuleTest.java b/framework/src/test/java/org/tron/core/capsule/BlockCapsuleTest.java index 2f46fcccb9d..c06e9d1ef83 100644 --- a/framework/src/test/java/org/tron/core/capsule/BlockCapsuleTest.java +++ b/framework/src/test/java/org/tron/core/capsule/BlockCapsuleTest.java @@ -106,11 +106,13 @@ public void testValidateMerkleRoot() throws Exception { 1234, ByteString.copyFrom("1234567".getBytes())); + local.addTransaction(new TransactionCapsule( + TransferContract.newBuilder().setAmount(1).build(), ContractType.TransferContract)); + // valid block: setMerkleRoot then validate — should not throw local.setMerkleRoot(); local.validateMerkleRoot(); // no exception - // flag is set — second call must be a no-op (no recomputation) local.validateMerkleRoot(); // still no exception // tamper with a transaction to break merkle @@ -124,14 +126,29 @@ public void testValidateMerkleRoot() throws Exception { new TransactionCapsule(transferContract, ContractType.TransferContract)); // merkle root was set before adding the tx, so it is now stale/invalid - BlockCapsule tampered = new BlockCapsule(local.getInstance()); - // tampered has no merkleValidated flag set try { - tampered.validateMerkleRoot(); + local.validateMerkleRoot(); Assert.fail("Expected BadBlockException for merkle mismatch"); } catch (BadBlockException e) { Assert.assertTrue(e.getMessage().contains("merkle")); } + local.setMerkleRoot(); + local.validateMerkleRoot(); + } + + @Test + public void testAddAllTransactionsMakesMerkleRootStale() throws Exception { + BlockCapsule block = new BlockCapsule(1, Sha256Hash.ZERO_HASH, 1234, ByteString.EMPTY); + block.setMerkleRoot(); + block.validateMerkleRoot(); + TransactionCapsule transaction = new TransactionCapsule( + TransferContract.newBuilder().setAmount(999).build(), ContractType.TransferContract); + + block.addAllTransactions(Arrays.asList(transaction)); + + Assert.assertThrows(BadBlockException.class, block::validateMerkleRoot); + block.setMerkleRoot(); + block.validateMerkleRoot(); } /* @Test 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 4741c13640f..18ede91c0e4 100755 --- a/framework/src/test/java/org/tron/core/db/ManagerTest.java +++ b/framework/src/test/java/org/tron/core/db/ManagerTest.java @@ -8,13 +8,13 @@ import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.tron.common.utils.Commons.adjustAssetBalanceV2; import static org.tron.common.utils.Commons.adjustTotalShieldedPoolValue; import static org.tron.common.utils.Commons.getExchangeStoreFinal; import static org.tron.common.utils.StringUtil.encode58Check; -import static org.tron.core.exception.BadBlockException.TypeEnum.CALC_MERKLE_ROOT_FAILED; import static org.tron.protos.Protocol.Transaction.Result.contractResult.SUCCESS; import com.beust.jcommander.internal.Lists; @@ -103,6 +103,7 @@ import org.tron.core.exception.ValidateScheduleException; import org.tron.core.exception.ValidateSignatureException; import org.tron.core.exception.ZksnarkException; +import org.tron.core.net.TronNetDelegate; import org.tron.core.store.AccountStore; import org.tron.core.store.CodeStore; import org.tron.core.store.DynamicPropertiesStore; @@ -503,23 +504,21 @@ public void adjustAssetBalanceV2Test() { } @Test - public void pushBlockInvalidMerkelRoot() { - Transaction trx = Transaction.newBuilder().build(); - TransactionCapsule moreTrans = new TransactionCapsule(trx); - blockCapsule2.addTransaction(moreTrans); // add one more transaction will change merkroot - blockCapsule2.sign(ByteArray.fromHexString(Args.getLocalWitnesses() - .getPrivateKey())); - try { - dbManager.pushBlock(blockCapsule2); - Assert.assertTrue(false); - } catch (BadBlockException e) { - Assert.assertTrue(e instanceof BadBlockException); - Assert.assertTrue(e.getType().equals(CALC_MERKLE_ROOT_FAILED)); - Assert.assertTrue(e.getMessage().startsWith( - "merkle root mismatch for block " + blockCapsule2.getNum() + ":")); - } catch (Exception e) { - Assert.assertFalse(e instanceof Exception); - } + public void pushPrevalidatedBlockDoesNotRepeatMerkleValidation() throws Exception { + byte[] witnessKey = ByteArray.fromHexString(Args.getLocalWitnesses().getPrivateKey()); + BlockCapsule block = new BlockCapsule(1, chainManager.getGenesisBlockId(), 1, + ByteString.copyFrom(ECKey.fromPrivate(witnessKey).getAddress())); + block.setMerkleRoot(); + block.sign(witnessKey); + BlockCapsule received = spy(block); + + context.getBean(TronNetDelegate.class).validBlock(received); + dbManager.pushBlock(received); + + Assert.assertEquals(received.getBlockId(), chainManager.getHeadBlockId()); + Assert.assertEquals(received.getInstance(), + chainManager.getBlockStore().get(received.getBlockId().getBytes()).getInstance()); + verify(received, times(1)).validateMerkleRoot(); } @Test 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 b43a2d06992..38c0b38f04b 100644 --- a/framework/src/test/java/org/tron/core/net/TronNetDelegateTest.java +++ b/framework/src/test/java/org/tron/core/net/TronNetDelegateTest.java @@ -23,12 +23,16 @@ import org.tron.core.exception.P2pException; import org.tron.core.exception.P2pException.TypeEnum; import org.tron.core.exception.ValidateScheduleException; +import org.tron.core.exception.ValidateSignatureException; import org.tron.core.metrics.MetricsService; import org.tron.core.net.peer.PeerConnection; +import org.tron.core.store.AccountStore; import org.tron.core.store.DynamicPropertiesStore; import org.tron.core.store.WitnessScheduleStore; +import org.tron.protos.Protocol.Transaction; import org.tron.protos.Protocol.Transaction.Contract.ContractType; import org.tron.protos.contract.BalanceContract.TransferContract; +import org.tron.protos.contract.ShieldContract.ShieldedTransferContract; public class TronNetDelegateTest { @@ -182,7 +186,6 @@ public void testValidBlockMerkleRoot() throws Exception { block.addTransaction( new TransactionCapsule(transferContract, ContractType.TransferContract)); - // Wrap in a fresh BlockCapsule so the merkleValidated flag is reset. BlockCapsule tampered = new BlockCapsule(block.getInstance()); TronNetDelegate tronNetDelegate = new TronNetDelegate(); @@ -284,6 +287,206 @@ public void testValidAdvBlockAcceptsActiveWitnessWithoutChangingHistory() throws Assert.assertEquals(86_400_000L, peer.getLastInactiveWitnessBlockTime()); } + @Test + public void testValidBlockRejectsTransactionsBelow80Bytes() throws Exception { + for (int size : new int[]{64, 79}) { + for (boolean advertised : new boolean[]{false, true}) { + TronNetDelegate delegate = signatureDelegate(); + BlockCapsule block = blockWithTransaction(sizedTransaction(size)); + WitnessScheduleStore schedule = mock(WitnessScheduleStore.class); + Mockito.doReturn(Collections.singletonList(block.getWitnessAddress())) + .when(schedule).getActiveWitnesses(); + setField(delegate, "witnessScheduleStore", schedule); + + P2pException error = Assert.assertThrows(P2pException.class, () -> { + if (advertised) { + delegate.validAdvBlock(new PeerConnection(), block); + } else { + delegate.validBlock(block); + } + }); + + Assert.assertEquals(TypeEnum.BAD_BLOCK, error.getType()); + Assert.assertTrue(error.getMessage().contains("size: " + size)); + Mockito.verify(block, Mockito.never()).validateMerkleRoot(); + Mockito.verify(block, Mockito.never()).validateSignature(Mockito.any(), Mockito.any()); + } + } + } + + @Test + public void testValidBlockAccepts80ByteBoundary() throws Exception { + for (int size : new int[]{80, 81}) { + BlockCapsule block = blockWithTransaction(sizedTransaction(size)); + signatureDelegate().validBlock(block); + Mockito.verify(block).validateMerkleRoot(); + Mockito.verify(block).validateSignature(Mockito.any(), Mockito.any()); + } + } + + @Test + public void testValidBlockSizeIncludesSignaturesAndResults() throws Exception { + Transaction small = sizedTransaction(78); + for (Transaction transaction : new Transaction[]{ + small.toBuilder().addSignature(ByteString.copyFrom(new byte[1])).build(), + small.toBuilder().addRet(Transaction.Result.newBuilder().setFee(1)).build()}) { + Assert.assertTrue(transaction.getRawData().getSerializedSize() < 80); + Assert.assertTrue(transaction.getSerializedSize() >= 80); + signatureDelegate().validBlock(blockWithTransaction(transaction)); + } + } + + @Test + public void testValidBlockAcceptsShieldedTransactionWithoutOuterSignature() throws Exception { + Transaction transaction = new TransactionCapsule(ShieldedTransferContract.newBuilder() + .setBindingSignature(ByteString.copyFrom(new byte[64])).build(), + ContractType.ShieldedTransferContract).getInstance(); + Assert.assertEquals(0, transaction.getSignatureCount()); + Assert.assertTrue(transaction.getSerializedSize() >= 80); + signatureDelegate().validBlock(blockWithTransaction(transaction)); + } + + private Transaction sizedTransaction(int size) { + Transaction transaction = Transaction.newBuilder().setRawData(Transaction.raw.newBuilder() + .addContract(Transaction.Contract.newBuilder().setType(ContractType.TransferContract)) + .setData(ByteString.copyFrom(new byte[size - 8]))).build(); + Assert.assertEquals(size, transaction.getSerializedSize()); + Assert.assertEquals(size, transaction.toByteArray().length); + return transaction; + } + + private BlockCapsule blockWithTransaction(Transaction transaction) throws Exception { + BlockCapsule block = Mockito.spy(new BlockCapsule(1, Sha256Hash.ZERO_HASH, 0, + ByteString.copyFromUtf8("witness"))); + block.addTransaction(new TransactionCapsule(transaction)); + block.setMerkleRoot(); + Mockito.doReturn(true).when(block).validateSignature(Mockito.any(), Mockito.any()); + return block; + } + + @Test + public void testValidBlockRejectsInvalidContractCounts() throws Exception { + for (int count : new int[]{0, 2}) { + for (boolean laterTransaction : new boolean[]{false, true}) { + TronNetDelegate delegate = signatureDelegate(); + BlockCapsule block = Mockito.spy(new BlockCapsule(1, Sha256Hash.ZERO_HASH, 0, + ByteString.copyFromUtf8("witness"))); + if (laterTransaction) { + block.addTransaction(contractCountTransaction(1)); + } + block.addTransaction(contractCountTransaction(count)); + block.setMerkleRoot(); + Mockito.doReturn(true).when(block).validateSignature(Mockito.any(), Mockito.any()); + WitnessScheduleStore schedule = mock(WitnessScheduleStore.class); + Mockito.doReturn(Collections.singletonList(block.getWitnessAddress())) + .when(schedule).getActiveWitnesses(); + setField(delegate, "witnessScheduleStore", schedule); + + P2pException error = Assert.assertThrows(P2pException.class, + () -> delegate.validBlock(block)); + + Assert.assertEquals(TypeEnum.BAD_BLOCK, error.getType()); + Assert.assertTrue(error.getMessage().contains(block.getBlockId().getString())); + Assert.assertTrue(error.getMessage().contains("count: " + count)); + Mockito.verify(block, Mockito.never()).validateSignature(Mockito.any(), Mockito.any()); + } + } + } + + @Test + public void testValidBlockAcceptsEmptyAndSingleContractTransactions() throws Exception { + for (int transactions : new int[]{0, 1, 2}) { + TronNetDelegate delegate = signatureDelegate(); + BlockCapsule block = Mockito.spy(new BlockCapsule(1, Sha256Hash.ZERO_HASH, 0, + ByteString.copyFromUtf8("witness"))); + for (int i = 0; i < transactions; i++) { + block.addTransaction(contractCountTransaction(1)); + } + block.setMerkleRoot(); + Mockito.doReturn(true).when(block).validateSignature(Mockito.any(), Mockito.any()); + WitnessScheduleStore schedule = mock(WitnessScheduleStore.class); + Mockito.doReturn(Collections.singletonList(block.getWitnessAddress())) + .when(schedule).getActiveWitnesses(); + setField(delegate, "witnessScheduleStore", schedule); + + delegate.validBlock(block); + + Mockito.verify(block).validateSignature(Mockito.any(), Mockito.any()); + } + } + + private TransactionCapsule contractCountTransaction(int count) { + Transaction.raw.Builder raw = Transaction.raw.newBuilder() + .setData(ByteString.copyFrom(new byte[80])); + for (int i = 0; i < count; i++) { + raw.addContract(Transaction.Contract.newBuilder().setType(ContractType.TransferContract)); + } + return new TransactionCapsule(Transaction.newBuilder().setRawData(raw).build()); + } + + @Test + public void testCommonAndAdvertisedValidationDoNotRepeatIngressClockCheck() throws Exception { + TronNetDelegate delegate = signatureDelegate(); + BlockCapsule block = Mockito.spy(new BlockCapsule(1, Sha256Hash.ZERO_HASH, + System.currentTimeMillis() + 60000, ByteString.copyFromUtf8("witness"))); + block.setMerkleRoot(); + Mockito.doReturn(true).when(block).validateSignature(Mockito.any(), Mockito.any()); + WitnessScheduleStore schedule = mock(WitnessScheduleStore.class); + Mockito.doReturn(Collections.singletonList(block.getWitnessAddress())) + .when(schedule).getActiveWitnesses(); + setField(delegate, "witnessScheduleStore", schedule); + + delegate.validBlock(block); + Assert.assertTrue(delegate.validAdvBlock(new PeerConnection(), block)); + + Mockito.verify(block, Mockito.times(2)).validateMerkleRoot(); + Mockito.verify(block, Mockito.times(2)).validateSignature(Mockito.any(), Mockito.any()); + } + + @Test + public void testValidBlockRejectsFalseSignature() throws Exception { + TronNetDelegate delegate = signatureDelegate(); + BlockCapsule block = Mockito.spy(new BlockCapsule(1, Sha256Hash.ZERO_HASH, 0, + ByteString.copyFromUtf8("witness"))); + block.setMerkleRoot(); + Mockito.doReturn(false).when(block).validateSignature(Mockito.any(), Mockito.any()); + + P2pException error = Assert.assertThrows(P2pException.class, + () -> delegate.validBlock(block)); + + Assert.assertEquals(TypeEnum.BLOCK_SIGN_INVALID, error.getType()); + Assert.assertEquals("valid signature failed.", error.getMessage()); + } + + @Test + public void testValidBlockWrapsSignatureFailureWithCause() throws Exception { + for (Exception failure : new Exception[]{new ValidateSignatureException("invalid signature"), + new IllegalStateException("signature validation unavailable")}) { + TronNetDelegate delegate = signatureDelegate(); + BlockCapsule block = Mockito.spy(new BlockCapsule(1, Sha256Hash.ZERO_HASH, 0, + ByteString.copyFromUtf8("witness"))); + block.setMerkleRoot(); + Mockito.doThrow(failure).when(block).validateSignature(Mockito.any(), Mockito.any()); + + P2pException error = Assert.assertThrows(P2pException.class, + () -> delegate.validBlock(block)); + + Assert.assertEquals(TypeEnum.BLOCK_SIGN_INVALID, error.getType()); + Assert.assertSame(failure, error.getCause()); + Assert.assertEquals(failure.toString(), error.getMessage()); + } + } + + private TronNetDelegate signatureDelegate() throws Exception { + Manager manager = mock(Manager.class); + Mockito.when(manager.getDynamicPropertiesStore()) + .thenReturn(mock(DynamicPropertiesStore.class)); + Mockito.when(manager.getAccountStore()).thenReturn(mock(AccountStore.class)); + TronNetDelegate delegate = Mockito.spy(new TronNetDelegate()); + setField(delegate, "dbManager", manager); + return delegate; + } + @Test public void testProcessBlockWrapsScheduleFailureWithCause() throws Exception { assertProcessBlockFailure(new ValidateScheduleException("invalid scheduled witness")); 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 eda03cd6842..78521764e9a 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 @@ -42,6 +42,7 @@ 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.relay.RelayService; import org.tron.core.net.service.sync.SyncService; import org.tron.core.services.WitnessProductBlockService; import org.tron.core.store.AccountStore; @@ -236,6 +237,149 @@ public void testInvalidWitnessIngress() throws Exception { } } + @Test + public void testFutureBlockRejectedBeforeAdvertisedOrSyncProcessing() throws Exception { + for (boolean syncResponse : new boolean[]{false, true}) { + BlockCapsule block = new BlockCapsule(1, Sha256Hash.ZERO_HASH, + System.currentTimeMillis() + 60000, ByteString.EMPTY); + BlockMessage message = new BlockMessage(block); + PeerConnection sender = witnessPeer(); + if (syncResponse) { + sender.getSyncBlockRequested().put(message.getBlockId(), 0L); + } else { + sender.getAdvInvRequest().put(new Item(message.getBlockId(), InventoryType.BLOCK), 0L); + } + TronNetDelegate delegate = Mockito.mock(TronNetDelegate.class); + AdvService adv = Mockito.mock(AdvService.class); + SyncService sync = Mockito.mock(SyncService.class); + BlockMsgHandler ingress = witnessHandler(delegate, adv, sync); + + P2pException error = Assert.assertThrows(P2pException.class, + () -> ingress.processMessage(sender, message)); + + Assert.assertEquals(P2pException.TypeEnum.BAD_MESSAGE, error.getType()); + Assert.assertEquals("block time error", error.getMessage()); + Mockito.verifyNoInteractions(delegate, adv, sync); + } + } + + @Test + public void testInvalidContractCountRejectedBeforeBroadcast() throws Exception { + for (boolean fastForward : new boolean[]{false, true}) { + for (int count : new int[]{0, 2}) { + ECKey key = ECKey.fromPrivate(BigInteger.TEN); + BlockCapsule block = new BlockCapsule(1, Sha256Hash.ZERO_HASH, 1234, + ByteString.copyFrom(key.getAddress())); + block.addTransaction(new TransactionCapsule(Transaction.newBuilder() + .setRawData(Transaction.raw.newBuilder() + .setData(ByteString.copyFrom(new byte[80])).addContract( + Transaction.Contract.newBuilder() + .setType(Transaction.Contract.ContractType.TransferContract))).build())); + Transaction.raw.Builder raw = Transaction.raw.newBuilder().setTimestamp(1235); + for (int i = 0; i < count; i++) { + raw.addContract(Transaction.Contract.newBuilder() + .setType(Transaction.Contract.ContractType.TransferContract)); + } + block.addTransaction(new TransactionCapsule( + Transaction.newBuilder().setRawData(raw).build())); + block.setMerkleRoot(); + block.sign(key.getPrivKeyBytes()); + TronNetDelegate delegate = witnessDelegate(false, block.getWitnessAddress()); + Manager manager = (Manager) ReflectUtils.getFieldObject(delegate, "dbManager"); + Assert.assertTrue(block.validateSignature(manager.getDynamicPropertiesStore(), + manager.getAccountStore())); + AdvService adv = Mockito.mock(AdvService.class); + RelayService relay = Mockito.mock(RelayService.class); + BlockMsgHandler ingress = witnessHandler(delegate, adv, Mockito.mock(SyncService.class)); + ReflectUtils.setFieldValue(ingress, "fastForward", fastForward); + ReflectUtils.setFieldValue(ingress, "relayService", relay); + BlockMessage message = new BlockMessage(block); + PeerConnection sender = witnessPeer(); + sender.getAdvInvRequest().put(new Item(message.getBlockId(), InventoryType.BLOCK), 0L); + + P2pException error = Assert.assertThrows(P2pException.class, + () -> ingress.processMessage(sender, message)); + + Assert.assertEquals(P2pException.TypeEnum.BAD_BLOCK, error.getType()); + Assert.assertTrue(error.getMessage().contains("count: " + count)); + Mockito.verify(adv, Mockito.never()).broadcast(any()); + Mockito.verify(relay, Mockito.never()).broadcast(any()); + Mockito.verify(delegate, Mockito.never()).processBlock(any(BlockCapsule.class), + anyBoolean()); + } + } + } + + @Test + public void testUndersizedLaterTransactionRejectedBeforeBroadcast() throws Exception { + for (boolean fastForward : new boolean[]{false, true}) { + for (int size : new int[]{64, 79}) { + BlockCapsule block = signedWitnessBlock(); + Transaction transaction = Transaction.newBuilder() + .setRawData(Transaction.raw.newBuilder() + .setData(ByteString.copyFrom(new byte[size - 8])).addContract( + Transaction.Contract.newBuilder() + .setType(Transaction.Contract.ContractType.TransferContract))).build(); + Assert.assertEquals(size, transaction.getSerializedSize()); + block.addTransaction(new TransactionCapsule(transaction)); + block.setMerkleRoot(); + block.sign(ECKey.fromPrivate(BigInteger.TEN).getPrivKeyBytes()); + TronNetDelegate delegate = witnessDelegate(false, block.getWitnessAddress()); + AdvService adv = Mockito.mock(AdvService.class); + RelayService relay = Mockito.mock(RelayService.class); + BlockMsgHandler ingress = witnessHandler(delegate, adv, Mockito.mock(SyncService.class)); + ReflectUtils.setFieldValue(ingress, "fastForward", fastForward); + ReflectUtils.setFieldValue(ingress, "relayService", relay); + BlockMessage message = new BlockMessage(block); + PeerConnection sender = witnessPeer(); + sender.getAdvInvRequest().put(new Item(message.getBlockId(), InventoryType.BLOCK), 0L); + + P2pException error = Assert.assertThrows(P2pException.class, + () -> ingress.processMessage(sender, message)); + + Assert.assertEquals(P2pException.TypeEnum.BAD_BLOCK, error.getType()); + Assert.assertTrue(error.getMessage().contains("size: " + size)); + Mockito.verify(adv, Mockito.never()).broadcast(any()); + Mockito.verify(relay, Mockito.never()).broadcast(any()); + Mockito.verify(delegate, Mockito.never()).processBlock(any(BlockCapsule.class), + anyBoolean()); + } + } + } + + @Test + public void testInvalidMerkleRootRejectedBeforeBroadcast() throws Exception { + for (boolean fastForward : new boolean[]{false, true}) { + BlockCapsule block = signedWitnessBlock(); + block.addTransaction(new TransactionCapsule(Transaction.newBuilder() + .setRawData(Transaction.raw.newBuilder().setTimestamp(1235) + .setData(ByteString.copyFrom(new byte[80])).addContract( + Transaction.Contract.newBuilder() + .setType(Transaction.Contract.ContractType.TransferContract))).build())); + TronNetDelegate delegate = witnessDelegate(false, block.getWitnessAddress()); + Manager manager = (Manager) ReflectUtils.getFieldObject(delegate, "dbManager"); + Assert.assertTrue(block.validateSignature(manager.getDynamicPropertiesStore(), + manager.getAccountStore())); + AdvService adv = Mockito.mock(AdvService.class); + RelayService relay = Mockito.mock(RelayService.class); + BlockMsgHandler ingress = witnessHandler(delegate, adv, Mockito.mock(SyncService.class)); + ReflectUtils.setFieldValue(ingress, "fastForward", fastForward); + ReflectUtils.setFieldValue(ingress, "relayService", relay); + BlockMessage message = new BlockMessage(block); + PeerConnection sender = witnessPeer(); + sender.getAdvInvRequest().put(new Item(message.getBlockId(), InventoryType.BLOCK), 0L); + + P2pException error = Assert.assertThrows(P2pException.class, + () -> ingress.processMessage(sender, message)); + + Assert.assertEquals(P2pException.TypeEnum.BLOCK_MERKLE_INVALID, error.getType()); + Mockito.verify(adv, Mockito.never()).broadcast(any()); + Mockito.verify(relay, Mockito.never()).broadcast(any()); + Mockito.verify(delegate, Mockito.never()).processBlock(any(BlockCapsule.class), + anyBoolean()); + } + } + @Test public void testPaddedWitnessSyncHandoff() throws Exception { BlockCapsule canonical = signedWitnessBlock(); @@ -264,7 +408,10 @@ private BlockCapsule signedWitnessBlock() { ByteString.copyFrom(key.getAddress())); // A padded transaction signature must survive witness normalization, including sync handoff. Transaction transaction = Transaction.newBuilder() - .setRawData(Transaction.raw.newBuilder().setTimestamp(1234)) + .setRawData(Transaction.raw.newBuilder().setTimestamp(1234) + .setData(ByteString.copyFrom(new byte[80])) + .addContract(Transaction.Contract.newBuilder() + .setType(Transaction.Contract.ContractType.TransferContract))) .addSignature(ByteString.copyFrom(new byte[68])).build(); block.addTransaction(new TransactionCapsule(transaction)); block.setMerkleRoot(); 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 e93e3f285a7..85216d805c0 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 @@ -2,6 +2,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; @@ -16,6 +17,7 @@ import com.google.common.cache.Cache; import com.google.protobuf.ByteString; import java.lang.reflect.Method; +import java.math.BigInteger; import java.net.InetSocketAddress; import java.util.Arrays; import java.util.Collections; @@ -27,13 +29,15 @@ import org.junit.rules.TemporaryFolder; import org.mockito.MockedStatic; import org.tron.common.TestConstants; +import org.tron.common.crypto.ECKey; 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.capsule.TransactionCapsule; import org.tron.core.config.args.Args; -import org.tron.core.exception.BadBlockException; +import org.tron.core.db.Manager; import org.tron.core.exception.P2pException; import org.tron.core.exception.P2pException.TypeEnum; import org.tron.core.exception.ValidateScheduleException; @@ -43,6 +47,7 @@ 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.messagehandler.PbftDataSyncHandler; import org.tron.core.net.peer.Item; import org.tron.core.net.peer.PeerConnection; import org.tron.core.net.service.adv.AdvService; @@ -50,10 +55,13 @@ import org.tron.core.net.service.relay.RelayService; import org.tron.core.net.service.sync.SyncService; import org.tron.core.services.WitnessProductBlockService; +import org.tron.core.store.AccountStore; +import org.tron.core.store.DynamicPropertiesStore; 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; +import org.tron.protos.Protocol.Transaction; public class BlockFetchFailureTest { @@ -329,6 +337,8 @@ private void assertAdvertisedBlockDoesNotRetry(TypeEnum type) throws Exception { Assert.fail("Invalid block must be rejected"); } catch (P2pException e) { Assert.assertSame(failure, e); + verify(delegate).validAdvBlock(sender, block); + verify(delegate, never()).processBlock(any(), anyBoolean()); Method exceptionHandler = P2pEventHandlerImpl.class.getDeclaredMethod( "processException", PeerConnection.class, TronMessage.class, Exception.class); exceptionHandler.setAccessible(true); @@ -358,14 +368,13 @@ public void scheduleFailureReleasesSyncRequestWithoutDisconnectingOtherPeers() t @Test public void invalidSignatureReleasesSyncRequestWithoutDisconnectingOtherPeers() throws Exception { assertSyncFailure(new P2pException(TypeEnum.BLOCK_SIGN_INVALID, "signature"), - ReasonCode.BAD_BLOCK); + ReasonCode.BAD_BLOCK, true); } @Test public void invalidMerkleRootReleasesSyncRequestAndPreservesOtherPeers() throws Exception { - assertSyncFailure(new P2pException(TypeEnum.BAD_BLOCK, new BadBlockException( - BadBlockException.TypeEnum.CALC_MERKLE_ROOT_FAILED, "merkle")), - ReasonCode.BAD_BLOCK); + assertSyncFailure(new P2pException(TypeEnum.BLOCK_MERKLE_INVALID, "merkle"), + ReasonCode.BAD_BLOCK, true); } @Test @@ -398,17 +407,24 @@ public void syncResponseDoesNotCheckCurrentActiveWitnesses() throws Exception { handler.processMessage(sender, message); + verify(delegate, never()).validBlock(any()); verify(delegate, never()).validAdvBlock(any(), 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 { + assertSyncFailure(failure, reason, false); + } + + @SuppressWarnings("unchecked") + private void assertSyncFailure(Exception failure, ReasonCode reason, boolean prevalidation) + throws Exception { SyncService sync = new SyncService(); TronNetDelegate delegate = mock(TronNetDelegate.class); ReflectUtils.setFieldValue(sync, "tronNetDelegate", delegate); + ReflectUtils.setFieldValue(sync, "pbftDataSyncHandler", mock(PbftDataSyncHandler.class)); PeerConnection sender = newPeer(10001); PeerConnection other = newPeer(10002); BlockCapsule block = newBlock(); @@ -423,9 +439,8 @@ private void assertSyncFailure(Exception failure, ReasonCode reason) throws Exce 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)); + if (prevalidation) { + doThrow(failure).when(delegate).validBlock(any(BlockCapsule.class)); } else { doThrow(failure).when(delegate).processBlock(any(BlockCapsule.class), anyBoolean()); } @@ -435,6 +450,13 @@ private void assertSyncFailure(Exception failure, ReasonCode reason) throws Exce try { sync.processBlock(sender, new BlockMessage(block)); invoke(sync, "handleSyncBlock"); + verify(delegate).validBlock(any(BlockCapsule.class)); + verify(delegate, never()).validAdvBlock(any(), any()); + if (prevalidation) { + verify(delegate, never()).processBlock(any(), anyBoolean()); + } else { + verify(delegate).processBlock(any(BlockCapsule.class), eq(true)); + } sync.onDisconnect(sender); Assert.assertNull("A failed response must release the global request", requests.getIfPresent(id)); @@ -453,6 +475,104 @@ private void assertSyncFailure(Exception failure, ReasonCode reason) throws Exce } } + @Test + public void syncRejectsInvalidContractCountsBeforeProcessing() throws Exception { + for (int count : new int[]{0, 2}) { + assertRealSyncValidation(count, false); + } + } + + @Test + public void syncAcceptsHistoricalWitnessOutsideCurrentSchedule() throws Exception { + assertRealSyncValidation(1, true); + } + + @Test + public void syncRejectsInvalidMerkleRootBeforeProcessing() throws Exception { + assertRealSyncValidation(1, false, true); + } + + @Test + public void syncRejectsUndersizedTransactionsBeforeProcessing() throws Exception { + for (int dataSize : new int[]{56, 71}) { + assertRealSyncValidation(1, false, false, dataSize); + } + } + + private void assertRealSyncValidation(int count, boolean accepted) throws Exception { + assertRealSyncValidation(count, accepted, false); + } + + private void assertRealSyncValidation(int count, boolean accepted, boolean staleRoot) + throws Exception { + assertRealSyncValidation(count, accepted, staleRoot, 80); + } + + private void assertRealSyncValidation(int count, boolean accepted, boolean staleRoot, + int dataSize) throws Exception { + ECKey key = ECKey.fromPrivate(BigInteger.TEN); + BlockCapsule block = new BlockCapsule(1, Sha256Hash.ZERO_HASH, 1234, + ByteString.copyFrom(key.getAddress())); + Transaction.raw.Builder raw = Transaction.raw.newBuilder() + .setData(ByteString.copyFrom(new byte[dataSize])); + for (int i = 0; i < count; i++) { + raw.addContract(Transaction.Contract.newBuilder() + .setType(Transaction.Contract.ContractType.TransferContract)); + } + block.addTransaction(new TransactionCapsule(Transaction.newBuilder().setRawData(raw).build())); + block.setMerkleRoot(); + if (staleRoot) { + block.addTransaction(new TransactionCapsule(Transaction.newBuilder() + .setRawData(Transaction.raw.newBuilder().setTimestamp(1) + .setData(ByteString.copyFrom(new byte[80])).addContract( + Transaction.Contract.newBuilder() + .setType(Transaction.Contract.ContractType.TransferContract))).build())); + } + block.sign(key.getPrivKeyBytes()); + block = spy(block); + DynamicPropertiesStore properties = mock(DynamicPropertiesStore.class); + Manager manager = mock(Manager.class); + when(manager.getDynamicPropertiesStore()).thenReturn(properties); + when(manager.getAccountStore()).thenReturn(mock(AccountStore.class)); + WitnessScheduleStore schedule = mock(WitnessScheduleStore.class); + when(schedule.getActiveWitnesses()).thenReturn(Collections.emptyList()); + TronNetDelegate delegate = spy(new TronNetDelegate()); + ReflectUtils.setFieldValue(delegate, "dbManager", manager); + ReflectUtils.setFieldValue(delegate, "witnessScheduleStore", schedule); + doReturn(Collections.emptyList()).when(delegate).getActivePeer(); + doNothing().when(delegate).processBlock(any(), anyBoolean()); + Assert.assertTrue(block.validateSignature(properties, manager.getAccountStore())); + SyncService sync = new SyncService(); + PbftDataSyncHandler pbft = mock(PbftDataSyncHandler.class); + ReflectUtils.setFieldValue(sync, "tronNetDelegate", delegate); + ReflectUtils.setFieldValue(sync, "pbftDataSyncHandler", pbft); + PeerConnection sender = newPeer(10001); + try { + Method process = SyncService.class.getDeclaredMethod("processSyncBlock", + BlockCapsule.class, PeerConnection.class); + process.setAccessible(true); + process.invoke(sync, block, sender); + + verify(delegate).validBlock(block); + verify(delegate, never()).validAdvBlock(any(), any()); + verify(schedule, never()).getActiveWitnesses(); + if (accepted) { + verify(delegate).processBlock(block, true); + verify(pbft).processPBFTCommitData(block); + verify(sender, never()).disconnect(any()); + } else { + if (staleRoot) { + verify(block).validateSignature(properties, manager.getAccountStore()); + } + verify(delegate, never()).processBlock(any(), anyBoolean()); + verify(pbft, never()).processPBFTCommitData(any()); + verify(sender).disconnect(ReasonCode.BAD_BLOCK); + } + } finally { + sync.close(); + } + } + private static TronNetDelegate inactiveWitnessDelegate(long maintenanceInterval) throws Exception { TronNetDelegate delegate = spy(new TronNetDelegate());