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 9e5d2db7547..a6c450c3a5b 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -283,7 +283,12 @@ public class Manager { while (isRunRePushThread) { TransactionCapsule tx = null; try { - tx = getRePushTransactions().peek(); + synchronized (this) { + tx = getRePushTransactions().peek(); + if (tx != null) { + pushTransactionQueue.add(tx); + } + } if (tx != null) { this.rePush(tx); } else { @@ -302,9 +307,12 @@ public class Manager { throw e; }); } finally { - if (tx != null && getRePushTransactions().remove(tx)) { - Metrics.gaugeInc(MetricKeys.Gauge.MANAGER_QUEUE, -1, - MetricLabels.Gauge.QUEUE_REPUSH); + if (tx != null) { + if (getRePushTransactions().remove(tx)) { + Metrics.gaugeInc(MetricKeys.Gauge.MANAGER_QUEUE, -1, + MetricLabels.Gauge.QUEUE_REPUSH); + } + pushTransactionQueue.remove(tx); } } } @@ -1228,7 +1236,8 @@ public List getVerifyTxs(BlockCapsule block) { /** * Save a block. Untrusted callers must first use network prevalidation to check its timestamp, - * transaction structure, Merkle root and block signature. + * transaction structure and Merkle root. Check witness signatures before broadcast as well. + * Received block witness authorization is checked against committed state before admission. */ public void pushBlock(final BlockCapsule block) throws ValidateSignatureException, ContractValidateException, ContractExeException, @@ -1242,11 +1251,6 @@ public void pushBlock(final BlockCapsule block) synchronized (this) { Metrics.histogramObserve(blockedTimer.get()); blockedTimer.remove(); - if (Metrics.enabled()) { - Metrics.histogramObserve(MetricKeys.Histogram.BLOCK_TRANSACTION_COUNT, - block.getTransactions().size(), - StringUtil.encode58Check(block.getWitnessAddress().toByteArray())); - } long headerNumber = getDynamicPropertiesStore().getLatestBlockHeaderNumber(); if (block.getNum() <= headerNumber && khaosDb.containBlockInMiniStore(block.getBlockId())) { logger.info("Block {} is already exist.", block.getBlockId().getString()); @@ -1261,15 +1265,15 @@ public void pushBlock(final BlockCapsule block) block.getNum(), rePushTransactions.size(), pendingTransactions.size(), block.getTransactions().size(), txs.size()); - if (CommonParameter.getInstance().getShutdownBlockTime() != null - && CommonParameter.getInstance().getShutdownBlockTime() - .isSatisfiedBy(new Date(block.getTimeStamp()))) { - latestSolidityNumShutDown = block.getNum(); - } - try (PendingManager pm = new PendingManager(this)) { if (!block.generatedByMyself) { + // Pending changes have been reset; use committed witness permissions for admission. + // switchFork also verifies admitted fork blocks against their own parent state. + if (!block.validateSignature(getDynamicPropertiesStore(), getAccountStore())) { + throw new ValidateSignatureException( + "block " + block.getNum() + " signature invalid"); + } consensus.receiveBlock(block); } @@ -1324,6 +1328,7 @@ public void pushBlock(final BlockCapsule block) synchronized (forkLock) { switchFork(newBlock); } + onBlockApplied(block); logger.info(SAVE_BLOCK, newBlock); logger.warn( @@ -1353,6 +1358,7 @@ public void pushBlock(final BlockCapsule block) long newSolidNum = getDynamicPropertiesStore().getLatestSolidifiedBlockNum(); blockTrigger(newBlock, oldSolidNum, newSolidNum); } + onBlockApplied(block); logger.info(SAVE_BLOCK, newBlock); } //clear ownerAddressSet @@ -1381,6 +1387,20 @@ public void pushBlock(final BlockCapsule block) } } + private void onBlockApplied(BlockCapsule block) { + if (Metrics.enabled()) { + Metrics.histogramObserve(MetricKeys.Histogram.BLOCK_TRANSACTION_COUNT, + block.getTransactions().size(), + StringUtil.encode58Check(block.getWitnessAddress().toByteArray())); + } + + if (CommonParameter.getInstance().getShutdownBlockTime() != null + && CommonParameter.getInstance().getShutdownBlockTime() + .isSatisfiedBy(new Date(block.getTimeStamp()))) { + latestSolidityNumShutDown = block.getNum(); + } + } + void blockTrigger(final BlockCapsule block, long oldSolid, long newSolid) { // post block and logs for jsonrpc try { 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 2948862b181..6b5daf75dd0 100644 --- a/framework/src/main/java/org/tron/core/net/TronNetDelegate.java +++ b/framework/src/main/java/org/tron/core/net/TronNetDelegate.java @@ -353,6 +353,10 @@ public void validSignature(BlockCapsule block) throws P2pException { } } + /** + * Validate block structure and Merkle root. Advertisement also checks witness authorization; + * sync defers it to Manager, which checks committed state and revalidates forks after rewind. + */ public void validBlock(BlockCapsule block) throws P2pException { for (Transaction transaction : block.getInstance().getTransactionsList()) { int contractCount = transaction.getRawData().getContractCount(); @@ -373,7 +377,6 @@ public void validBlock(BlockCapsule block) throws P2pException { } catch (BadBlockException e) { throw new P2pException(TypeEnum.BLOCK_MERKLE_INVALID, e.getMessage()); } - validSignature(block); } public boolean validAdvBlock(PeerConnection peer, BlockCapsule block) throws P2pException { @@ -391,6 +394,7 @@ public boolean validAdvBlock(PeerConnection peer, BlockCapsule block) throws P2p } } validBlock(block); + validSignature(block); return activeWitness; } 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 5d2ea0c21b6..3aca66362e5 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 @@ -60,17 +60,25 @@ public void processMessage(PeerConnection peer, TronMessage msg) throws P2pExcep BlockId blockId = blockMessage.getBlockId(); BlockCapsule blockCapsule = blockMessage.getBlockCapsule(); + int witnessAddressSize = blockCapsule.getWitnessAddress().size(); + if (witnessAddressSize != Constant.TRON_ADDRESS_SIZE) { + throw new P2pException(TypeEnum.BAD_BLOCK, + "Invalid witness address size: " + witnessAddressSize); + } + if (blockCapsule.getInstance().getSerializedSize() > maxBlockSize) { logger.error("Receive bad block {} from peer {}, block size over limit", - blockMessage.getBlockId(), peer.getInetSocketAddress()); + blockId, peer.getInetSocketAddress()); throw new P2pException(TypeEnum.BAD_MESSAGE, "block size over limit"); } + long gap = blockCapsule.getTimeStamp() - System.currentTimeMillis(); if (gap >= BLOCK_PRODUCED_INTERVAL) { logger.error("Receive bad block {} from peer {}, block time error", - blockMessage.getBlockId(), peer.getInetSocketAddress()); + blockId, peer.getInetSocketAddress()); throw new P2pException(TypeEnum.BAD_MESSAGE, "block time error"); } + if (!fastForward && !peer.isRelayPeer()) { check(peer, blockMessage); } diff --git a/framework/src/test/java/org/tron/core/db/ManagerMockTest.java b/framework/src/test/java/org/tron/core/db/ManagerMockTest.java index fc2ea6da492..c713b8d7578 100644 --- a/framework/src/test/java/org/tron/core/db/ManagerMockTest.java +++ b/framework/src/test/java/org/tron/core/db/ManagerMockTest.java @@ -420,6 +420,46 @@ public void testRePush1() { dbManager.rePush(trx); } + @Test + public void rePushLoopCleansRegistrationForKnownTransaction() throws Exception { + assertRePushLoopCleansRegistration(true); + } + + @Test + public void rePushLoopCleansRegistrationForUnsupportedAuths() throws Exception { + assertRePushLoopCleansRegistration(false); + } + + private void assertRePushLoopCleansRegistration(boolean alreadyKnown) throws Exception { + Manager manager = new Manager(); + TransactionCapsule tx = new TransactionCapsule(Protocol.Transaction.newBuilder() + .setRawData(Protocol.Transaction.raw.newBuilder() + .addAuths(Protocol.authority.getDefaultInstance())).build()); + BlockingQueue repush = new LinkedBlockingQueue<>(); + BlockingQueue pushing = new LinkedBlockingQueue<>(); + repush.add(tx); + setField(manager, "rePushTransactions", repush); + setField(manager, "pushTransactionQueue", pushing); + ChainBaseManager chain = mock(ChainBaseManager.class); + TransactionStore transactions = mock(TransactionStore.class); + setField(manager, "chainBaseManager", chain); + when(chain.getTransactionStore()).thenReturn(transactions); + List registeredAtLookup = new ArrayList<>(); + when(transactions.has(any())).thenAnswer(invocation -> { + manager.stopRePushThread(); + registeredAtLookup.addAll(pushing); + return alreadyKnown; + }); + Field loop = Manager.class.getDeclaredField("rePushLoop"); + loop.setAccessible(true); + + ((Runnable) loop.get(manager)).run(); + + Assert.assertEquals(Collections.singletonList(tx), registeredAtLookup); + assertTrue(repush.isEmpty()); + assertTrue(pushing.isEmpty()); + } + @Test public void testPostSolidityFilter() throws Exception { Manager dbManager = spy(new Manager()); 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 64f352be546..45e8324f674 100755 --- a/framework/src/test/java/org/tron/core/db/ManagerTest.java +++ b/framework/src/test/java/org/tron/core/db/ManagerTest.java @@ -4,9 +4,11 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doAnswer; 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.times; import static org.mockito.Mockito.verify; @@ -27,12 +29,19 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -65,6 +74,7 @@ import org.tron.common.utils.Sha256Hash; import org.tron.common.utils.StringUtil; import org.tron.common.utils.Utils; +import org.tron.consensus.base.Param; import org.tron.consensus.dpos.DposSlot; import org.tron.core.ChainBaseManager; import org.tron.core.Constant; @@ -1661,32 +1671,269 @@ public void testClearSolidityContractTriggerCache() throws Exception { } @Test - public void testRePushResetsVerifiedOnOwnerAddressSetHit() throws Exception { - TransferContract transferContract = TransferContract.newBuilder() - .setAmount(1L) - .setOwnerAddress(ByteString.copyFrom( - ByteArray.fromHexString(Wallet.getAddressPreFixString() - + "548794500882809695A8A687866E76D4271A1ABC"))) - .setToAddress(ByteString.copyFrom( - ByteArray.fromHexString(Wallet.getAddressPreFixString() - + "A389132D6639FBDA4FBC8B659264E6B7C90DB086"))) - .build(); - TransactionCapsule tx = new TransactionCapsule(transferContract, ContractType.TransferContract); - tx.setVerified(true); // simulate mempool-cached state + public void freshMarkedOwnerTransactionVerifiesOnceOutsideLocks() throws Exception { + dbManager.stopRePushThread(); + byte[] witnessKey = ByteArray.fromHexString(privateKey); + byte[] witness = ECKey.fromPrivate(witnessKey).getAddress(); + chainManager.getWitnessScheduleStore().saveActiveWitnesses(new ArrayList<>()); + chainManager.addWitness(ByteString.copyFrom(witness)); + long time = 1533529947843L; + BlockCapsule parent = new BlockCapsule(blockGenerate.getSignedBlock( + ByteString.copyFrom(witness), time, witnessKey)); + dbManager.pushBlock(parent); + ECKey ownerKey = new ECKey(Utils.getRandom()); + byte[] owner = ownerKey.getAddress(); + chainManager.getAccountStore().put(owner, new AccountCapsule(Protocol.Account.newBuilder() + .setAddress(ByteString.copyFrom(owner)).setBalance(1_000_000_000L).build())); + Set owners = (Set) ReflectUtils.getFieldObject(dbManager, "ownerAddressSet"); + owners.add(ByteArray.toHexString(owner)); + TransactionCapsule tx = spy(transfer(owner, witness, 1L, parent, time + 60_000L)); + tx.sign(ownerKey.getPrivKeyBytes()); + Object transactionLock = ReflectUtils.getFieldObject(dbManager, "transactionLock"); + List verificationLocks = new ArrayList<>(); + doAnswer(invocation -> { + verificationLocks.add(Thread.holdsLock(dbManager) || Thread.holdsLock(transactionLock)); + return invocation.callRealMethod(); + }).when(tx).validatePubSignature(chainManager.getAccountStore(), + chainManager.getDynamicPropertiesStore()); - String ownerAddress = ByteArray.toHexString(tx.getOwnerAddress()); + Assert.assertTrue(dbManager.pushTransaction(tx)); - // Inject ownerAddress into ownerAddressSet via reflection - Set ownerAddressSet = - (Set) ReflectUtils.getFieldObject(dbManager, "ownerAddressSet"); - ownerAddressSet.add(ownerAddress); + Assert.assertTrue(dbManager.getPendingTransactions().contains(tx)); + Assert.assertEquals("A fresh transaction must verify once outside both locks", + Collections.singletonList(false), verificationLocks); + } + + @Test + public void rePushPreservesVerificationCacheWhenPermissionUnchanged() throws Exception { + dbManager.stopRePushThread(); + byte[] witnessKey = ByteArray.fromHexString(privateKey); + byte[] witness = ECKey.fromPrivate(witnessKey).getAddress(); + chainManager.getWitnessScheduleStore().saveActiveWitnesses(new ArrayList<>()); + chainManager.addWitness(ByteString.copyFrom(witness)); + long time = 1533529947843L; + BlockCapsule parent = new BlockCapsule(blockGenerate.getSignedBlock( + ByteString.copyFrom(witness), time, witnessKey)); + dbManager.pushBlock(parent); + ECKey ownerKey = new ECKey(Utils.getRandom()); + byte[] owner = ownerKey.getAddress(); + chainManager.getAccountStore().put(owner, new AccountCapsule(Protocol.Account.newBuilder() + .setAddress(ByteString.copyFrom(owner)).setBalance(1_000_000_000L).build())); + TransactionCapsule tx = spy(transfer(owner, witness, 1L, parent, time + 60_000L)); + tx.sign(ownerKey.getPrivKeyBytes()); + Assert.assertTrue(dbManager.pushTransaction(tx)); + tx.setTime(System.currentTimeMillis()); + try (PendingManager ignored = new PendingManager(dbManager)) { + // Reset the pending session and move the verified transaction to repush. + } + clearInvocations(tx); + doAnswer(invocation -> { + dbManager.stopRePushThread(); + return invocation.callRealMethod(); + }).when(tx).validateSignature(chainManager.getAccountStore(), + chainManager.getDynamicPropertiesStore()); + ReflectUtils.setFieldValue(dbManager, "isRunRePushThread", true); + + ((Runnable) ReflectUtils.getFieldObject(dbManager, "rePushLoop")).run(); + + Assert.assertTrue(dbManager.getPendingTransactions().contains(tx)); + Assert.assertTrue(dbManager.getRePushTransactions().isEmpty()); + BlockingQueue pushing = (BlockingQueue) + ReflectUtils.getFieldObject(dbManager, "pushTransactionQueue"); + Assert.assertTrue(pushing.isEmpty()); + verify(tx, never()).validatePubSignature(chainManager.getAccountStore(), + chainManager.getDynamicPropertiesStore()); + } + + @Test + public void rePushRejectsRevokedSignatureAfterRepushTimeout() throws Exception { + assertRePushRejectsRevokedSignature(false); + } + + @Test + public void rePushRejectsRevokedSignatureAfterGenerateBlockDiscard() throws Exception { + assertRePushRejectsRevokedSignature(true); + } - // rePush should reset isVerified to false before pushTransaction - dbManager.rePush(tx); + private void assertRePushRejectsRevokedSignature(boolean discardDuringGeneration) + throws Exception { + dbManager.stopRePushThread(); + byte[] witnessKey = ByteArray.fromHexString(privateKey); + byte[] witness = ECKey.fromPrivate(witnessKey).getAddress(); + chainManager.getWitnessScheduleStore().saveActiveWitnesses(new ArrayList<>()); + chainManager.addWitness(ByteString.copyFrom(witness)); + long time = 1533529947843L; + BlockCapsule parent = new BlockCapsule(blockGenerate.getSignedBlock( + ByteString.copyFrom(witness), time, witnessKey)); + dbManager.pushBlock(parent); + chainManager.getDynamicPropertiesStore().saveAllowMultiSign(1); + + ECKey oldKey = new ECKey(Utils.getRandom()); + ECKey newKey = new ECKey(Utils.getRandom()); + byte[] owner = oldKey.getAddress(); + AccountCapsule account = new AccountCapsule(Protocol.Account.newBuilder() + .setAddress(ByteString.copyFrom(owner)).setBalance(1_000_000_000L).build()); + chainManager.getAccountStore().put(owner, account); + TransactionCapsule tx = spy(transfer(owner, witness, 1L, parent, time + 60_000L)); + tx.sign(oldKey.getPrivKeyBytes()); + Assert.assertTrue(dbManager.pushTransaction(tx)); + Assert.assertTrue(tx.isVerified()); + tx.setTime(discardDuringGeneration ? System.currentTimeMillis() : 0); + dbManager.getPendingTransactions().remove(tx); + dbManager.getRePushTransactions().add(tx); + // Retain the reference before the permission update enters pending or a block. + TransactionCapsule held = dbManager.getRePushTransactions().peek(); + Assert.assertSame(tx, held); + BlockingQueue pushing = (BlockingQueue) + ReflectUtils.getFieldObject(dbManager, "pushTransactionQueue"); + List queuedDuringVerification = new ArrayList<>(); + doAnswer(invocation -> { + queuedDuringVerification.add(Collections.frequency(pushing, tx)); + return invocation.callRealMethod(); + }).when(tx).validatePubSignature(chainManager.getAccountStore(), + chainManager.getDynamicPropertiesStore()); + + ByteString newAddress = ByteString.copyFrom(newKey.getAddress()); + AccountContract.AccountPermissionUpdateContract contract = + AccountContract.AccountPermissionUpdateContract.newBuilder() + .setOwnerAddress(ByteString.copyFrom(owner)) + .setOwner(AccountCapsule.createDefaultOwnerPermission(newAddress)) + .addActives(AccountCapsule.createDefaultActivePermission(newAddress, + chainManager.getDynamicPropertiesStore())) + .build(); + TransactionCapsule update = new TransactionCapsule(contract, + ContractType.AccountPermissionUpdateContract); + update.setReference(parent.getNum(), parent.getBlockId().getBytes()); + update.setExpiration(time + 60_000L); + update.sign(oldKey.getPrivKeyBytes()); + + TransactionStore originalStore = chainManager.getTransactionStore(); + TransactionStore observedStore = spy(originalStore); + CountDownLatch peeked = new CountDownLatch(1); + CountDownLatch resume = new CountDownLatch(1); + AtomicBoolean firstLookup = new AtomicBoolean(true); + byte[] heldTransactionId = tx.getTransactionId().getBytes(); + doAnswer(invocation -> { + if (firstLookup.compareAndSet(true, false)) { + peeked.countDown(); + Assert.assertTrue("Timed out waiting for permission update", resume.await(10, + TimeUnit.SECONDS)); + } + return invocation.callRealMethod(); + }).when(observedStore).has(heldTransactionId); + ReflectUtils.setFieldValue(chainManager, "transactionStore", observedStore); + ExecutorService worker = Executors.newSingleThreadExecutor(); + ReflectUtils.setFieldValue(dbManager, "isRunRePushThread", true); + Future repush = worker.submit( + (Runnable) ReflectUtils.getFieldObject(dbManager, "rePushLoop")); + try { + Assert.assertTrue("Repush did not reach the handoff after peek", peeked.await(10, + TimeUnit.SECONDS)); + Assert.assertEquals(Collections.singletonList(tx), new ArrayList<>(pushing)); + BlockCapsule block; + if (discardDuringGeneration) { + Assert.assertTrue(dbManager.pushTransaction(update)); + boolean originalSort = Args.getInstance().isOpenTransactionSort(); + Args.getInstance().setOpenTransactionSort(false); + try { + synchronized (dbManager) { + Param.Miner miner = Param.getInstance().new Miner(witnessKey, + ByteString.copyFrom(witness), ByteString.copyFrom(witness)); + block = dbManager.generateBlock(miner, time + 3000, Long.MAX_VALUE); + Assert.assertNotNull(block); + Assert.assertEquals(1, block.getTransactions().size()); + Assert.assertEquals(update.getTransactionId(), + block.getTransactions().get(0).getTransactionId()); + Assert.assertTrue(dbManager.getRePushTransactions().isEmpty()); + Assert.assertTrue("generateBlock discarded the held transaction before clearing its " + + "verification cache", held.isVerified()); + dbManager.pushBlock(block); + } + } finally { + Args.getInstance().setOpenTransactionSort(originalSort); + } + } else { + Map keys = Maps.newHashMap(); + keys.put(ByteString.copyFrom(witness), privateKey); + block = blockWithTransfer(time + 3000, parent.getNum() + 1, + parent.getBlockId().getByteString(), keys, update); + dbManager.pushBlock(block); + } + + Assert.assertEquals(block.getBlockId(), chainManager.getHeadBlockId()); + Assert.assertEquals(newAddress, chainManager.getAccountStore().get(owner) + .getPermissionById(0).getKeys(0).getAddress()); + Assert.assertTrue(dbManager.getPendingTransactions().isEmpty()); + Assert.assertTrue(dbManager.getRePushTransactions().isEmpty()); + Set owners = (Set) ReflectUtils.getFieldObject(dbManager, "ownerAddressSet"); + Assert.assertTrue("The queued transaction must retain the owner marker", + owners.contains(ByteArray.toHexString(owner))); + TransactionCapsule uncached = new TransactionCapsule(held.getInstance()); + assertThrows(ValidateSignatureException.class, () -> uncached.validateSignature( + chainManager.getAccountStore(), chainManager.getDynamicPropertiesStore())); + + dbManager.stopRePushThread(); + resume.countDown(); + repush.get(10, TimeUnit.SECONDS); + + Assert.assertTrue("A removed transaction signed by the revoked key must not enter pending", + dbManager.getPendingTransactions().isEmpty()); + Assert.assertFalse(held.isVerified()); + Assert.assertTrue(pushing.isEmpty()); + Assert.assertEquals(Collections.singletonList(2), queuedDuringVerification); + } finally { + dbManager.stopRePushThread(); + resume.countDown(); + worker.shutdownNow(); + Assert.assertTrue(worker.awaitTermination(10, TimeUnit.SECONDS)); + ReflectUtils.setFieldValue(chainManager, "transactionStore", originalStore); + } + + TransactionCapsule replacement = new TransactionCapsule( + held.getInstance().toBuilder().clearSignature().build()); + replacement.sign(newKey.getPrivKeyBytes()); + Assert.assertTrue(replacement.validateSignature(chainManager.getAccountStore(), + chainManager.getDynamicPropertiesStore())); + TransactionCapsule cachedReplacement = spy(replacement); + doAnswer(invocation -> { + Assert.assertFalse("Repush must preserve the original verification outside the Manager lock", + Thread.holdsLock(dbManager)); + return invocation.callRealMethod(); + }).when(cachedReplacement).validatePubSignature(chainManager.getAccountStore(), + chainManager.getDynamicPropertiesStore()); + + dbManager.rePush(cachedReplacement); + + Assert.assertTrue(dbManager.getPendingTransactions().contains(cachedReplacement)); + Assert.assertTrue(pushing.isEmpty()); + verify(cachedReplacement, times(1)).validatePubSignature(chainManager.getAccountStore(), + chainManager.getDynamicPropertiesStore()); + } + + @Test + public void pendingManagerPreservesUnexpiredVerificationCache() throws Exception { + dbManager.stopRePushThread(); + TransactionCapsule pending = new TransactionCapsule(TransferContract.newBuilder().build(), + ContractType.TransferContract); + TransactionCapsule repush = new TransactionCapsule(TransferContract.newBuilder().setAmount(1) + .build(), ContractType.TransferContract); + pending.setVerified(true); + repush.setVerified(true); + pending.setTime(Long.MAX_VALUE); + repush.setTime(Long.MAX_VALUE); + dbManager.getPendingTransactions().add(pending); + dbManager.getRePushTransactions().add(repush); + + try (PendingManager ignored = new PendingManager(dbManager)) { + // Closing transfers the pending transaction and retains the unexpired repush entry. + } - // After rePush, isVerified must be false - Boolean verified = (Boolean) ReflectUtils.getFieldObject(tx, "isVerified"); - Assert.assertFalse(verified); + Assert.assertTrue(dbManager.getPendingTransactions().isEmpty()); + Assert.assertEquals(2, dbManager.getRePushTransactions().size()); + Assert.assertTrue(dbManager.getRePushTransactions().contains(pending)); + Assert.assertTrue(dbManager.getRePushTransactions().contains(repush)); + Assert.assertTrue(pending.isVerified()); + Assert.assertTrue(repush.isVerified()); } @Test diff --git a/framework/src/test/java/org/tron/core/db/ManagerWitnessSignatureTest.java b/framework/src/test/java/org/tron/core/db/ManagerWitnessSignatureTest.java new file mode 100644 index 00000000000..ee9daafd6d5 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db/ManagerWitnessSignatureTest.java @@ -0,0 +1,464 @@ +package org.tron.core.db; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.protobuf.ByteString; +import io.prometheus.client.CollectorRegistry; +import java.lang.reflect.Method; +import java.math.BigInteger; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.Test; +import org.tron.common.BaseMethodTest; +import org.tron.common.cron.CronExpression; +import org.tron.common.crypto.ECKey; +import org.tron.common.parameter.CommonParameter; +import org.tron.common.prometheus.MetricKeys; +import org.tron.common.prometheus.MetricLabels; +import org.tron.common.utils.ReflectUtils; +import org.tron.common.utils.Sha256Hash; +import org.tron.common.utils.StringUtil; +import org.tron.core.capsule.AccountCapsule; +import org.tron.core.capsule.BlockCapsule; +import org.tron.core.capsule.TransactionCapsule; +import org.tron.core.capsule.WitnessCapsule; +import org.tron.core.consensus.ConsensusService; +import org.tron.core.exception.UnLinkedBlockException; +import org.tron.core.exception.ValidateSignatureException; +import org.tron.core.net.TronNetDelegate; +import org.tron.core.net.message.adv.BlockMessage; +import org.tron.core.net.messagehandler.BlockMsgHandler; +import org.tron.core.net.messagehandler.PbftDataSyncHandler; +import org.tron.core.net.peer.PeerConnection; +import org.tron.core.net.service.adv.AdvService; +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.protos.Protocol.Account; +import org.tron.protos.Protocol.ReasonCode; +import org.tron.protos.Protocol.Transaction.Contract.ContractType; +import org.tron.protos.contract.AccountContract.AccountPermissionUpdateContract; + +public class ManagerWitnessSignatureTest extends BaseMethodTest { + + private final ECKey owner = ECKey.fromPrivate(BigInteger.valueOf(101)); + private final ECKey signer = ECKey.fromPrivate(BigInteger.valueOf(102)); + private final ECKey replacement = ECKey.fromPrivate(BigInteger.valueOf(103)); + private ByteString witness; + + @Override + protected void afterInit() { + context.getBean(ConsensusService.class).start(); + witness = ByteString.copyFrom(owner.getAddress()); + chainBaseManager.getDynamicPropertiesStore().saveAllowMultiSign(1); + chainBaseManager.getAccountStore().put(witness.toByteArray(), new AccountCapsule( + Account.newBuilder().setAddress(witness).setIsWitness(true).setBalance(1_000_000_000_000L) + .setOwnerPermission(AccountCapsule.createDefaultOwnerPermission(witness)) + .addActivePermission(AccountCapsule.createDefaultActivePermission(witness, + chainBaseManager.getDynamicPropertiesStore())) + .setWitnessPermission(AccountCapsule.createDefaultWitnessPermission( + ByteString.copyFrom(signer.getAddress()))).build())); + chainBaseManager.getWitnessStore().put(witness.toByteArray(), new WitnessCapsule(witness)); + chainBaseManager.getWitnessScheduleStore() + .saveActiveWitnesses(Collections.singletonList(witness)); + } + + @Test + public void acceptsCommittedWitnessPermission() throws Exception { + BlockCapsule block = nextBlock(signer); + context.getBean(TronNetDelegate.class).validSignature(block); + + dbManager.pushBlock(block); + + assertEquals(block.getBlockId(), chainBaseManager.getHeadBlockId()); + assertEquals(block.getBlockId(), chainBaseManager.getKhaosDb().getHeadId()); + assertTrue(chainBaseManager.getBlockStore().has(block.getBlockId().getBytes())); + } + + @Test + public void rejectsWitnessRevokedWhilePushWaitsForManagerLock() throws Exception { + BlockCapsule block = spy(nextBlock(signer)); + AtomicInteger validations = new AtomicInteger(); + doAnswer(invocation -> { + if (validations.incrementAndGet() > 1) { + assertTrue(Thread.holdsLock(dbManager)); + } + return invocation.callRealMethod(); + }).when(block).validateSignature(any(DynamicPropertiesStore.class), any(AccountStore.class)); + CountDownLatch ingressValidated = new CountDownLatch(1); + ExecutorService executor = Executors.newSingleThreadExecutor(); + Future pushed; + try { + synchronized (dbManager) { + pushed = executor.submit(() -> { + context.getBean(TronNetDelegate.class).validSignature(block); + ingressValidated.countDown(); + return assertThrows(ValidateSignatureException.class, () -> dbManager.pushBlock(block)); + }); + assertTrue(ingressValidated.await(10, TimeUnit.SECONDS)); + setWitnessPermission(replacement); + } + pushed.get(10, TimeUnit.SECONDS); + assertRejected(block); + } finally { + executor.shutdownNow(); + assertTrue(executor.awaitTermination(10, TimeUnit.SECONDS)); + } + } + + @Test + public void rejectsWitnessAuthorizedOnlyByPendingSession() throws Exception { + setWitnessPermission(replacement); + dbManager.getSession().setValue(dbManager.getRevokingStore().buildSession()); + setWitnessPermission(signer); + BlockCapsule block = nextBlock(signer); + context.getBean(TronNetDelegate.class).validSignature(block); + + assertThrows(ValidateSignatureException.class, () -> dbManager.pushBlock(block)); + + assertRejected(block); + assertFalse(dbManager.getSession().valid()); + assertEquals(ByteString.copyFrom(replacement.getAddress()), + ByteString.copyFrom(chainBaseManager.getAccountStore().get(witness.toByteArray()) + .getWitnessPermissionAddress())); + } + + @Test + public void acceptsCommittedPermissionAfterPendingRevocationReset() throws Exception { + BlockCapsule block = nextBlock(signer); + context.getBean(TronNetDelegate.class).validSignature(block); + dbManager.getSession().setValue(dbManager.getRevokingStore().buildSession()); + setWitnessPermission(replacement); + assertFalse(block.validateSignature(dbManager.getDynamicPropertiesStore(), + dbManager.getAccountStore())); + + dbManager.pushBlock(block); + + assertEquals(block.getBlockId(), chainBaseManager.getHeadBlockId()); + assertEquals(ByteString.copyFrom(signer.getAddress()), + ByteString.copyFrom(chainBaseManager.getAccountStore().get(witness.toByteArray()) + .getWitnessPermissionAddress())); + } + + @Test + public void rejectsWellFormedUnauthorizedWitness() throws Exception { + BlockCapsule block = nextBlock(replacement); + assertFalse(block.validateSignature(dbManager.getDynamicPropertiesStore(), + dbManager.getAccountStore())); + + assertThrows(ValidateSignatureException.class, () -> dbManager.pushBlock(block)); + + assertRejected(block); + } + + @Test + public void rejectsMalformedWitnessSignature() throws Exception { + chainBaseManager.getDynamicPropertiesStore().saveAllowStrictEcdsaValidation(1); + BlockCapsule signed = nextBlock(signer); + BlockCapsule block = new BlockCapsule(signed.getInstance().toBuilder() + .setBlockHeader(signed.getInstance().getBlockHeader().toBuilder() + .setWitnessSignature(ByteString.copyFrom(new byte[64]))).build()); + + assertThrows(ValidateSignatureException.class, () -> dbManager.pushBlock(block)); + + assertRejected(block); + } + + @Test + public void preservesTrustedGeneratedBlockBypassAndDuplicateHandling() throws Exception { + BlockCapsule block = nextBlock(replacement); + block.generatedByMyself = true; + setMatchingShutdownTime(block); + dbManager.pushBlock(block); + assertEquals(block.getNum(), dbManager.getLatestSolidityNumShutDown()); + setWitnessPermission(owner); + + dbManager.pushBlock(new BlockCapsule(block.getData())); + + assertEquals(block.getBlockId(), chainBaseManager.getHeadBlockId()); + assertEquals(block.getBlockId(), chainBaseManager.getKhaosDb().getHeadId()); + } + + @Test + public void rejectsForkWitnessNotAuthorizedAtCurrentHead() throws Exception { + BlockCapsule parent = nextBlock(signer); + dbManager.pushBlock(parent); + BlockCapsule oldHead = blockRevokingWitness(parent); + BlockCapsule fork = signedBlock(parent.getNum() + 1, parent.getBlockId(), + parent.getTimeStamp() + 3001, signer); + + assertThrows(ValidateSignatureException.class, () -> dbManager.pushBlock(fork)); + + assertEquals(oldHead.getBlockId(), chainBaseManager.getHeadBlockId()); + assertEquals(oldHead.getBlockId(), chainBaseManager.getKhaosDb().getHeadId()); + assertFalse(chainBaseManager.getKhaosDb().containBlockInMiniStore(fork.getBlockId())); + assertFalse(chainBaseManager.getBlockStore().has(fork.getBlockId().getBytes())); + } + + @Test + public void revalidatesAdmittedForkAgainstRewoundParentPermission() throws Exception { + BlockCapsule parent = nextBlock(signer); + dbManager.pushBlock(parent); + BlockCapsule oldHead = blockRevokingWitness(parent); + BlockCapsule fork = signedBlock(parent.getNum() + 1, parent.getBlockId(), + parent.getTimeStamp() + 3001, replacement); + dbManager.pushBlock(fork); + assertTrue(chainBaseManager.getKhaosDb().containBlockInMiniStore(fork.getBlockId())); + BlockCapsule forkHead = signedBlock(fork.getNum() + 1, fork.getBlockId(), + parent.getTimeStamp() + 6000, replacement); + + synchronized (dbManager) { + assertThrows(ValidateSignatureException.class, () -> dbManager.pushBlock(forkHead)); + + assertEquals(oldHead.getBlockId(), chainBaseManager.getHeadBlockId()); + assertEquals(oldHead.getBlockId(), chainBaseManager.getKhaosDb().getHeadId()); + assertFalse(chainBaseManager.getKhaosDb().containBlockInMiniStore(fork.getBlockId())); + assertFalse(chainBaseManager.getKhaosDb().containBlockInMiniStore(forkHead.getBlockId())); + assertEquals(ByteString.copyFrom(replacement.getAddress()), + ByteString.copyFrom(chainBaseManager.getAccountStore().get(witness.toByteArray()) + .getWitnessPermissionAddress())); + } + } + + @Test + public void syncValidatesWitnessOnceUnderManagerLock() throws Exception { + List validationLocks = new ArrayList<>(); + BlockCapsule block = recordValidationLocks(nextBlock(signer), validationLocks); + + processSyncBlock(block); + + assertEquals(Collections.singletonList(true), validationLocks); + assertEquals(block.getBlockId(), chainBaseManager.getHeadBlockId()); + } + + @Test + public void syncRejectsUnauthorizedWitnessBeforeChainOrKhaosAdmission() throws Exception { + List validationLocks = new ArrayList<>(); + BlockCapsule block = recordValidationLocks(nextBlock(replacement), validationLocks); + + PeerConnection peer = processSyncBlock(block); + + assertEquals(Collections.singletonList(true), validationLocks); + verify(peer).disconnect(ReasonCode.BAD_BLOCK); + assertRejected(block); + } + + @Test + public void invalidSyncWitnessCannotChangeMatchingShutdownTarget() throws Exception { + BlockCapsule block = nextBlock(replacement); + setMatchingShutdownTime(block); + long shutdownTarget = dbManager.getLatestSolidityNumShutDown(); + + PeerConnection peer = processSyncBlock(block); + + assertEquals(shutdownTarget, dbManager.getLatestSolidityNumShutDown()); + verify(peer).disconnect(ReasonCode.BAD_BLOCK); + assertRejected(block); + } + + @Test + public void validSyncWitnessUpdatesMatchingShutdownTarget() throws Exception { + BlockCapsule block = nextBlock(signer); + setMatchingShutdownTime(block); + boolean metricsEnabled = CommonParameter.getInstance().isMetricsPrometheusEnable(); + CommonParameter.getInstance().setMetricsPrometheusEnable(true); + try { + Double count = blockTransactionCount(); + + processSyncBlock(block); + + assertEquals((count == null ? 0 : count) + 1, blockTransactionCount(), 0); + assertEquals(block.getNum(), dbManager.getLatestSolidityNumShutDown()); + assertEquals(block.getBlockId(), chainBaseManager.getHeadBlockId()); + } finally { + CommonParameter.getInstance().setMetricsPrometheusEnable(metricsEnabled); + } + } + + @Test + public void unlinkedBlockCannotUpdateMetricsOrShutdownTarget() + throws Exception { + BlockCapsule block = signedBlock(1, Sha256Hash.of(new byte[]{1}), 1533529947000L, signer); + setMatchingShutdownTime(block); + long shutdownTarget = dbManager.getLatestSolidityNumShutDown(); + boolean metricsEnabled = CommonParameter.getInstance().isMetricsPrometheusEnable(); + CommonParameter.getInstance().setMetricsPrometheusEnable(true); + try { + Double count = blockTransactionCount(); + + assertThrows(UnLinkedBlockException.class, () -> dbManager.pushBlock(block)); + + assertEquals(count, blockTransactionCount()); + assertEquals(shutdownTarget, dbManager.getLatestSolidityNumShutDown()); + assertRejected(block); + } finally { + CommonParameter.getInstance().setMetricsPrometheusEnable(metricsEnabled); + } + } + + @Test + public void updatesMetricsAndShutdownTargetOnceAfterSuccessfulFork() throws Exception { + BlockCapsule parent = nextBlock(signer); + dbManager.pushBlock(parent); + dbManager.pushBlock(nextBlock(signer)); + BlockCapsule fork = signedBlock(parent.getNum() + 1, parent.getBlockId(), + parent.getTimeStamp() + 3001, signer); + BlockCapsule forkHead = signedBlock(fork.getNum() + 1, fork.getBlockId(), + parent.getTimeStamp() + 6000, signer); + CronExpression schedule = new CronExpression( + forkHead.getTimeStamp() / 1000 % 60 + " * * * * ?"); + assertFalse(schedule.isSatisfiedBy(new Date(fork.getTimeStamp()))); + assertTrue(schedule.isSatisfiedBy(new Date(forkHead.getTimeStamp()))); + CommonParameter.getInstance().setShutdownBlockTime(schedule); + long shutdownTarget = dbManager.getLatestSolidityNumShutDown(); + boolean metricsEnabled = CommonParameter.getInstance().isMetricsPrometheusEnable(); + CommonParameter.getInstance().setMetricsPrometheusEnable(true); + try { + Double count = blockTransactionCount(); + dbManager.pushBlock(fork); + + assertEquals(count, blockTransactionCount()); + assertEquals(shutdownTarget, dbManager.getLatestSolidityNumShutDown()); + dbManager.pushBlock(forkHead); + + assertEquals(forkHead.getNum(), dbManager.getLatestSolidityNumShutDown()); + assertEquals((count == null ? 0 : count) + 1, blockTransactionCount(), 0); + assertEquals(forkHead.getBlockId(), chainBaseManager.getHeadBlockId()); + dbManager.pushBlock(forkHead); + assertEquals((count == null ? 0 : count) + 1, blockTransactionCount(), 0); + } finally { + CommonParameter.getInstance().setMetricsPrometheusEnable(metricsEnabled); + } + } + + private Double blockTransactionCount() { + return CollectorRegistry.defaultRegistry.getSampleValue( + MetricKeys.Histogram.BLOCK_TRANSACTION_COUNT + "_count", + new String[]{MetricLabels.Histogram.MINER}, + new String[]{StringUtil.encode58Check(witness.toByteArray())}); + } + + @Test + public void advertisedBlockValidatesBeforeBroadcastAndAgainUnderManagerLock() throws Exception { + List validationLocks = new ArrayList<>(); + BlockCapsule block = recordValidationLocks(nextBlock(signer), validationLocks); + AdvService adv = mock(AdvService.class); + doAnswer(invocation -> { + assertEquals(Collections.singletonList(false), validationLocks); + assertEquals(0, chainBaseManager.getHeadBlockNum()); + return null; + }).when(adv).broadcast(any(BlockMessage.class)); + BlockMsgHandler handler = new BlockMsgHandler(); + ReflectUtils.setFieldValue(handler, "tronNetDelegate", context.getBean(TronNetDelegate.class)); + ReflectUtils.setFieldValue(handler, "advService", adv); + ReflectUtils.setFieldValue(handler, "witnessProductBlockService", + mock(WitnessProductBlockService.class)); + Method process = BlockMsgHandler.class.getDeclaredMethod("processBlock", + PeerConnection.class, BlockCapsule.class); + process.setAccessible(true); + + process.invoke(handler, new PeerConnection(), block); + + assertEquals(Arrays.asList(false, true), validationLocks); + verify(adv).broadcast(any(BlockMessage.class)); + assertEquals(block.getBlockId(), chainBaseManager.getHeadBlockId()); + } + + private void setMatchingShutdownTime(BlockCapsule block) throws Exception { + CronExpression schedule = new CronExpression("* * * * * ?"); + assertTrue(schedule.isSatisfiedBy(new Date(block.getTimeStamp()))); + CommonParameter.getInstance().setShutdownBlockTime(schedule); + } + + private PeerConnection processSyncBlock(BlockCapsule block) throws Exception { + SyncService service = context.getBean(SyncService.class); + ReflectUtils.setFieldValue(service, "pbftDataSyncHandler", mock(PbftDataSyncHandler.class)); + PeerConnection peer = mock(PeerConnection.class); + when(peer.getInetSocketAddress()).thenReturn(new InetSocketAddress("127.0.0.2", 10001)); + Method process = SyncService.class.getDeclaredMethod("processSyncBlock", + BlockCapsule.class, PeerConnection.class); + process.setAccessible(true); + process.invoke(service, block, peer); + return peer; + } + + private BlockCapsule recordValidationLocks(BlockCapsule original, List locks) + throws Exception { + BlockCapsule block = spy(original); + doAnswer(invocation -> { + locks.add(Thread.holdsLock(dbManager)); + return invocation.callRealMethod(); + }).when(block).validateSignature(any(DynamicPropertiesStore.class), any(AccountStore.class)); + return block; + } + + private BlockCapsule blockRevokingWitness(BlockCapsule parent) throws Exception { + AccountPermissionUpdateContract contract = AccountPermissionUpdateContract.newBuilder() + .setOwnerAddress(witness) + .setOwner(AccountCapsule.createDefaultOwnerPermission(witness)) + .setWitness(AccountCapsule.createDefaultWitnessPermission( + ByteString.copyFrom(replacement.getAddress()))) + .addActives(AccountCapsule.createDefaultActivePermission(witness, + chainBaseManager.getDynamicPropertiesStore())).build(); + TransactionCapsule update = new TransactionCapsule(contract, + ContractType.AccountPermissionUpdateContract); + update.setReference(parent.getNum(), parent.getBlockId().getBytes()); + update.setExpiration(parent.getTimeStamp() + 1_000_000L); + update.sign(owner.getPrivKeyBytes()); + BlockCapsule oldHead = nextBlock(signer); + oldHead.addTransaction(update); + oldHead.setMerkleRoot(); + oldHead.sign(signer.getPrivKeyBytes()); + dbManager.pushBlock(oldHead); + return oldHead; + } + + private void setWitnessPermission(ECKey key) { + Account account = chainBaseManager.getAccountStore().get(witness.toByteArray()).getInstance(); + chainBaseManager.getAccountStore().put(witness.toByteArray(), new AccountCapsule(account + .toBuilder().setWitnessPermission(AccountCapsule.createDefaultWitnessPermission( + ByteString.copyFrom(key.getAddress()))).build())); + } + + private BlockCapsule nextBlock(ECKey key) { + long time = chainBaseManager.getHeadBlockNum() == 0 ? 1533529947000L + : chainBaseManager.getHeadBlockTimeStamp() + 3000; + return signedBlock(chainBaseManager.getHeadBlockNum() + 1, + chainBaseManager.getHeadBlockId(), time, key); + } + + private BlockCapsule signedBlock(long number, Sha256Hash parent, long time, ECKey key) { + BlockCapsule block = new BlockCapsule(number, parent, time, witness); + block.setMerkleRoot(); + block.sign(key.getPrivKeyBytes()); + return block; + } + + private void assertRejected(BlockCapsule block) { + assertEquals(0, chainBaseManager.getHeadBlockNum()); + assertEquals(chainBaseManager.getGenesisBlockId(), chainBaseManager.getHeadBlockId()); + assertEquals(chainBaseManager.getGenesisBlockId(), chainBaseManager.getKhaosDb().getHeadId()); + assertFalse(chainBaseManager.getKhaosDb().containBlockInMiniStore(block.getBlockId())); + assertFalse(chainBaseManager.getBlockStore().has(block.getBlockId().getBytes())); + } +} 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 38c0b38f04b..45794f2465e 100644 --- a/framework/src/test/java/org/tron/core/net/TronNetDelegateTest.java +++ b/framework/src/test/java/org/tron/core/net/TronNetDelegateTest.java @@ -220,6 +220,7 @@ public void testValidBlockDoesNotCheckWitnessActivity() throws Exception { Mockito.doNothing().when(delegate).validSignature(block); delegate.validBlock(block); Mockito.verify(schedule, Mockito.never()).getActiveWitnesses(); + Mockito.verify(delegate, Mockito.never()).validSignature(block); } @Test @@ -320,7 +321,7 @@ public void testValidBlockAccepts80ByteBoundary() throws Exception { BlockCapsule block = blockWithTransaction(sizedTransaction(size)); signatureDelegate().validBlock(block); Mockito.verify(block).validateMerkleRoot(); - Mockito.verify(block).validateSignature(Mockito.any(), Mockito.any()); + Mockito.verify(block, Mockito.never()).validateSignature(Mockito.any(), Mockito.any()); } } @@ -411,7 +412,8 @@ public void testValidBlockAcceptsEmptyAndSingleContractTransactions() throws Exc delegate.validBlock(block); - Mockito.verify(block).validateSignature(Mockito.any(), Mockito.any()); + Mockito.verify(block).validateMerkleRoot(); + Mockito.verify(block, Mockito.never()).validateSignature(Mockito.any(), Mockito.any()); } } @@ -440,11 +442,11 @@ public void testCommonAndAdvertisedValidationDoNotRepeatIngressClockCheck() thro 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()); + Mockito.verify(block).validateSignature(Mockito.any(), Mockito.any()); } @Test - public void testValidBlockRejectsFalseSignature() throws Exception { + public void testValidSignatureRejectsFalseSignature() throws Exception { TronNetDelegate delegate = signatureDelegate(); BlockCapsule block = Mockito.spy(new BlockCapsule(1, Sha256Hash.ZERO_HASH, 0, ByteString.copyFromUtf8("witness"))); @@ -452,14 +454,14 @@ public void testValidBlockRejectsFalseSignature() throws Exception { Mockito.doReturn(false).when(block).validateSignature(Mockito.any(), Mockito.any()); P2pException error = Assert.assertThrows(P2pException.class, - () -> delegate.validBlock(block)); + () -> delegate.validSignature(block)); Assert.assertEquals(TypeEnum.BLOCK_SIGN_INVALID, error.getType()); Assert.assertEquals("valid signature failed.", error.getMessage()); } @Test - public void testValidBlockWrapsSignatureFailureWithCause() throws Exception { + public void testValidSignatureWrapsFailureWithCause() throws Exception { for (Exception failure : new Exception[]{new ValidateSignatureException("invalid signature"), new IllegalStateException("signature validation unavailable")}) { TronNetDelegate delegate = signatureDelegate(); @@ -469,7 +471,7 @@ public void testValidBlockWrapsSignatureFailureWithCause() throws Exception { Mockito.doThrow(failure).when(block).validateSignature(Mockito.any(), Mockito.any()); P2pException error = Assert.assertThrows(P2pException.class, - () -> delegate.validBlock(block)); + () -> delegate.validSignature(block)); Assert.assertEquals(TypeEnum.BLOCK_SIGN_INVALID, error.getType()); Assert.assertSame(failure, error.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 f5d60294b02..c153a73b2e8 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 @@ -10,12 +10,15 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import com.google.protobuf.ByteString; import io.prometheus.client.CollectorRegistry; +import java.math.BigInteger; import java.net.InetSocketAddress; import java.util.Collections; import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.tron.common.crypto.ECKey; import org.tron.common.parameter.CommonParameter; import org.tron.common.prometheus.MetricKeys; import org.tron.common.utils.ReflectUtils; @@ -161,7 +164,8 @@ public void testMatchedRequestHeadDuplicateIncrements() throws P2pException { private BlockMessage newBlockMessage(long number) { BlockCapsule capsule = new BlockCapsule(number, Sha256Hash.ZERO_HASH, - System.currentTimeMillis() - 60_000L, Sha256Hash.ZERO_HASH.getByteString()); + System.currentTimeMillis() - 60_000L, + ByteString.copyFrom(ECKey.fromPrivate(BigInteger.TEN).getAddress())); return new BlockMessage(capsule); } 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 78521764e9a..f8af0ea4c6e 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 @@ -83,9 +83,10 @@ public void before() throws Exception { public void testProcessMessage() { BlockCapsule blockCapsule; BlockMessage msg; + ByteString witness = ByteString.copyFrom(new byte[Constant.TRON_ADDRESS_SIZE]); try { blockCapsule = new BlockCapsule(1, Sha256Hash.ZERO_HASH, - System.currentTimeMillis(), Sha256Hash.ZERO_HASH.getByteString()); + System.currentTimeMillis(), witness); msg = new BlockMessage(blockCapsule); handler.processMessage(peer, msg); } catch (P2pException e) { @@ -100,8 +101,9 @@ public void testProcessMessage() { ByteString.copyFrom( new byte[Parameter.ChainConstant.BLOCK_SIZE + Constant.ONE_THOUSAND]))) .build()); - blockCapsule = new BlockCapsule(1, Sha256Hash.ZERO_HASH.getByteString(), - System.currentTimeMillis() + 10000, transactionList); + blockCapsule = new BlockCapsule(1, Sha256Hash.ZERO_HASH, + System.currentTimeMillis() + 10000, witness); + blockCapsule.addTransaction(new TransactionCapsule(transactionList.get(0))); msg = new BlockMessage(blockCapsule); System.out.println("len = " + blockCapsule.getInstance().getSerializedSize()); peer.getAdvInvRequest() @@ -114,7 +116,7 @@ public void testProcessMessage() { try { blockCapsule = new BlockCapsule(1, Sha256Hash.ZERO_HASH, - System.currentTimeMillis() + 10000, Sha256Hash.ZERO_HASH.getByteString()); + System.currentTimeMillis() + 10000, witness); msg = new BlockMessage(blockCapsule); peer.getAdvInvRequest() .put(new Item(msg.getBlockId(), InventoryType.BLOCK), System.currentTimeMillis()); @@ -126,7 +128,7 @@ public void testProcessMessage() { try { blockCapsule = new BlockCapsule(1, Sha256Hash.ZERO_HASH, - System.currentTimeMillis() + 1000, Sha256Hash.ZERO_HASH.getByteString()); + System.currentTimeMillis() + 1000, witness); msg = new BlockMessage(blockCapsule); peer.getSyncBlockRequested() .put(msg.getBlockId(), System.currentTimeMillis()); @@ -137,7 +139,7 @@ public void testProcessMessage() { try { blockCapsule = new BlockCapsule(1, Sha256Hash.ZERO_HASH, - System.currentTimeMillis() + 1000, Sha256Hash.ZERO_HASH.getByteString()); + System.currentTimeMillis() + 1000, witness); msg = new BlockMessage(blockCapsule); peer.getAdvInvRequest() .put(new Item(msg.getBlockId(), InventoryType.BLOCK), System.currentTimeMillis()); @@ -237,11 +239,64 @@ public void testInvalidWitnessIngress() throws Exception { } } + @Test + public void testUnauthorizedWitnessRejectedBeforeBroadcast() throws Exception { + for (boolean fastForward : new boolean[]{false, true}) { + BlockCapsule block = signedWitnessBlock(); + block.sign(ECKey.fromPrivate(BigInteger.valueOf(11)).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.BLOCK_SIGN_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 testWitnessValidatedBeforeBroadcastAndProcessing() throws Exception { + for (boolean fastForward : new boolean[]{false, true}) { + BlockCapsule block = signedWitnessBlock(); + 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); + + ingress.processMessage(sender, message); + + org.mockito.InOrder order = Mockito.inOrder(delegate, adv, relay); + order.verify(delegate).validSignature(any(BlockCapsule.class)); + if (fastForward) { + order.verify(relay).broadcast(any(BlockMessage.class)); + } else { + order.verify(adv).broadcast(any(BlockMessage.class)); + } + order.verify(delegate).processBlock(any(BlockCapsule.class), Mockito.eq(false)); + } + } + @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); + System.currentTimeMillis() + 60000, + ByteString.copyFrom(new byte[Constant.TRON_ADDRESS_SIZE])); BlockMessage message = new BlockMessage(block); PeerConnection sender = witnessPeer(); if (syncResponse) { @@ -380,6 +435,34 @@ public void testInvalidMerkleRootRejectedBeforeBroadcast() throws Exception { } } + @Test + public void testRejectsInvalidWitnessAddressBeforeSyncOrAdvertisement() throws Exception { + for (int size : new int[] {0, 20, 22, 2_000_000}) { + for (boolean sync : new boolean[] {true, false}) { + BlockCapsule block = new BlockCapsule(1, Sha256Hash.ZERO_HASH, 1234, + ByteString.copyFrom(new byte[size])); + BlockMessage message = new BlockMessage(block); + PeerConnection sender = witnessPeer(); + if (sync) { + 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 syncService = Mockito.mock(SyncService.class); + BlockMsgHandler ingress = witnessHandler(delegate, adv, syncService); + + P2pException error = Assert.assertThrows(P2pException.class, + () -> ingress.processMessage(sender, message)); + + Assert.assertEquals(P2pException.TypeEnum.BAD_BLOCK, error.getType()); + Assert.assertTrue(sender.getSyncBlockInProcess().isEmpty()); + Mockito.verifyNoInteractions(delegate, adv, syncService); + } + } + } + @Test public void testPaddedWitnessSyncHandoff() throws Exception { BlockCapsule canonical = signedWitnessBlock(); 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 85216d805c0..a1a9182b27f 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 @@ -587,7 +587,8 @@ private static TronNetDelegate inactiveWitnessDelegate(long maintenanceInterval) } private static BlockCapsule newBlock() { - BlockCapsule block = new BlockCapsule(1, Sha256Hash.ZERO_HASH, 0, ByteString.EMPTY); + BlockCapsule block = new BlockCapsule(1, Sha256Hash.ZERO_HASH, 0, + ByteString.copyFrom(ECKey.fromPrivate(BigInteger.TEN).getAddress())); block.setMerkleRoot(); return block; }