diff --git a/actuator/src/main/java/org/tron/core/utils/TransactionUtil.java b/actuator/src/main/java/org/tron/core/utils/TransactionUtil.java index aee866aac4..4863e0241b 100644 --- a/actuator/src/main/java/org/tron/core/utils/TransactionUtil.java +++ b/actuator/src/main/java/org/tron/core/utils/TransactionUtil.java @@ -258,10 +258,6 @@ public TransactionSignWeight getTransactionSignWeight(Transaction trx) { .isECKeyCryptoEngine(), trx.getRawData().toByteArray()), approveList); } if (trx.getPqAuthSigCount() > 0) { - if (!chainBaseManager.getDynamicPropertiesStore().isAnyPqSchemeAllowed()) { - throw new PermissionException( - "pq_auth_sig not allowed: no post-quantum scheme is activated"); - } try { long pqWeight = TransactionCapsule.validatePQSignatureGetWeight(trx, permission, chainBaseManager.getDynamicPropertiesStore(), approveList); 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 56233c7158..cc10a0bd62 100755 --- a/chainbase/src/main/java/org/tron/core/capsule/BlockCapsule.java +++ b/chainbase/src/main/java/org/tron/core/capsule/BlockCapsule.java @@ -229,10 +229,6 @@ public boolean validateSignature(DynamicPropertiesStore dynamicPropertiesStore, } if (hasPq) { - if (!dynamicPropertiesStore.isAnyPqSchemeAllowed()) { - throw new ValidateSignatureException( - "pq_auth_sig not allowed: no post-quantum scheme is activated"); - } return validatePQSignature(dynamicPropertiesStore, witnessPermissionAddress, header.getPqAuthSig()); } diff --git a/chainbase/src/main/java/org/tron/core/capsule/TransactionCapsule.java b/chainbase/src/main/java/org/tron/core/capsule/TransactionCapsule.java index bba761a26a..8d74af6d2a 100755 --- a/chainbase/src/main/java/org/tron/core/capsule/TransactionCapsule.java +++ b/chainbase/src/main/java/org/tron/core/capsule/TransactionCapsule.java @@ -496,7 +496,7 @@ public static boolean validateSignature(Transaction transaction, List approveList = new ArrayList<>(); long weight = checkWeight(permission, transaction.getSignatureList(), hash, approveList); - if (transaction.getPqAuthSigCount() > 0 && dynamicPropertiesStore.isAnyPqSchemeAllowed()) { + if (transaction.getPqAuthSigCount() > 0) { try { weight = StrictMathWrapper.addExact(weight, validatePQSignatureGetWeight(transaction, permission, dynamicPropertiesStore, @@ -767,18 +767,12 @@ public static long validatePQSignatureGetWeight(Transaction transaction, Permiss if (!signedAddresses.add(addrBs)) { throw new PermissionException(encode58Check(derivedAddr) + " has signed twice!"); } - Key matched = null; - for (Key k : permission.getKeysList()) { - if (k.getAddress().equals(addrBs)) { - matched = k; - break; - } - } - if (matched == null) { - throw new PermissionException( - "pq_auth_sig public key derives to " + encode58Check(derivedAddr) - + " but it is not contained of permission."); - } + Key matched = permission.getKeysList().stream() + .filter(k -> k.getAddress().equals(addrBs)) + .findFirst() + .orElseThrow(() -> new PermissionException( + "pq_auth_sig public key derives to " + encode58Check(derivedAddr) + + " but it is not contained of permission.")); if (!PQSchemeRegistry.verify(scheme, pk, digest, sig)) { throw new SignatureException("pq sig invalid"); } diff --git a/framework/src/main/java/org/tron/core/Wallet.java b/framework/src/main/java/org/tron/core/Wallet.java index 8bf365ca76..8d94c17c21 100755 --- a/framework/src/main/java/org/tron/core/Wallet.java +++ b/framework/src/main/java/org/tron/core/Wallet.java @@ -578,6 +578,12 @@ public GrpcAPI.Return broadcastTransaction(Transaction signedTransaction) { .setMessage(ByteString.copyFromUtf8("Server busy.")).build(); } + if (dbManager.isPqPendingFull(signedTransaction)) { + logger.warn("Broadcast transaction {} has failed, PQ pending pool full.", txID); + return builder.setResult(false).setCode(response_code.SERVER_BUSY) + .setMessage(ByteString.copyFromUtf8("PQ pending pool is full.")).build(); + } + if (trxCacheEnable) { if (dbManager.getTransactionIdCache().getIfPresent(txID) != null) { logger.warn("Broadcast transaction {} has failed, it already exists.", txID); 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 60c1424a11..6ff417a829 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -5,7 +5,6 @@ import static org.tron.common.math.Maths.min; import static org.tron.common.utils.Commons.adjustBalance; import static org.tron.core.Constant.TRANSACTION_MAX_BYTE_SIZE; -import static org.tron.core.exception.BadBlockException.TypeEnum.CALC_MERKLE_ROOT_FAILED; import static org.tron.protos.Protocol.Transaction.Contract.ContractType.TransferContract; import static org.tron.protos.Protocol.Transaction.Result.contractResult.SUCCESS; @@ -50,7 +49,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; -import org.tron.api.GrpcAPI; import org.tron.api.GrpcAPI.TransactionInfoList; import org.tron.common.args.GenesisBlock; import org.tron.common.bloom.Bloom; @@ -936,6 +934,8 @@ public boolean pushTransaction(final TransactionCapsule trx) } if (isPQTransaction(trx.getInstance()) && pqTransInPendingCounts.get() >= pqTransInPendingMaxCounts) { + logger.warn("pushTransaction {} rejected: PQ pending pool full ({}/{}).", + trx.getTransactionId(), pqTransInPendingCounts.get(), pqTransInPendingMaxCounts); return false; } if (!session.valid()) { @@ -2178,6 +2178,11 @@ public boolean isTooManyPending() { return getCachedTransactionSize() > maxTransactionPendingSize; } + public boolean isPqPendingFull(Protocol.Transaction transaction) { + return isPQTransaction(transaction) + && pqTransInPendingCounts.get() >= pqTransInPendingMaxCounts; + } + private void preValidateTransactionSign(List txs) throws InterruptedException, ValidateSignatureException { int transSize = txs.size(); diff --git a/framework/src/test/java/org/tron/core/WalletMockTest.java b/framework/src/test/java/org/tron/core/WalletMockTest.java index 0f5bb62275..063c72a5ce 100644 --- a/framework/src/test/java/org/tron/core/WalletMockTest.java +++ b/framework/src/test/java/org/tron/core/WalletMockTest.java @@ -1,6 +1,7 @@ package org.tron.core; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; @@ -568,6 +569,31 @@ private void mockEnv(Wallet wallet, TronException tronException) throws Exceptio field3.set(wallet, false); } + @Test + public void testBroadcastTransactionPqPendingFull() throws Exception { + Wallet wallet = new Wallet(); + injectTotalSignNum(wallet, 5); + + TronNetDelegate tronNetDelegateMock = mock(TronNetDelegate.class); + Manager managerMock = mock(Manager.class); + when(tronNetDelegateMock.isBlockUnsolidified()).thenReturn(false); + when(managerMock.isTooManyPending()).thenReturn(false); + when(managerMock.isPqPendingFull(any())).thenReturn(true); + + Field field = wallet.getClass().getDeclaredField("tronNetDelegate"); + field.setAccessible(true); + field.set(wallet, tronNetDelegateMock); + Field field2 = wallet.getClass().getDeclaredField("dbManager"); + field2.setAccessible(true); + field2.set(wallet, managerMock); + + GrpcAPI.Return ret = wallet.broadcastTransaction(Protocol.Transaction.newBuilder().build()); + assertEquals(GrpcAPI.Return.response_code.SERVER_BUSY, ret.getCode()); + assertFalse(ret.getResult()); + assertTrue(ret.getMessage().toStringUtf8().contains("PQ")); + Mockito.verify(managerMock, Mockito.never()).pushTransaction(any()); + } + @Test public void testBroadcastTransactionValidateSignatureException() throws Exception { try (MockedConstruction mocked = mockConstruction(TransactionMessage.class, diff --git a/framework/src/test/java/org/tron/core/capsule/TransactionCapsuleTest.java b/framework/src/test/java/org/tron/core/capsule/TransactionCapsuleTest.java index fc2115fbd3..cd6bba0260 100644 --- a/framework/src/test/java/org/tron/core/capsule/TransactionCapsuleTest.java +++ b/framework/src/test/java/org/tron/core/capsule/TransactionCapsuleTest.java @@ -10,6 +10,7 @@ import ch.qos.logback.core.read.ListAppender; import com.google.protobuf.Any; import com.google.protobuf.ByteString; +import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -30,6 +31,8 @@ import org.tron.common.utils.StringUtil; import org.tron.core.Wallet; import org.tron.core.config.args.Args; +import org.tron.core.exception.PermissionException; +import org.tron.core.exception.SignatureFormatException; import org.tron.core.exception.ValidateSignatureException; import org.tron.protos.Protocol.AccountType; import org.tron.protos.Protocol.Key; @@ -154,8 +157,6 @@ public void toStringRendersPqSignWithEcdsa() { s.contains("sign=") && s.contains("pq_sign(FN_DSA_512)=")); } - // --------------------- FN-DSA pq_auth_sig verification (V2) --------------------- - private static final String PQ_OWNER_HEX = "41abd4b9367799eaa3197fecb144eb71de1e049abc"; private static final String PQ_TO_HEX = "41548794500882809695a8a687866e76d4271a1abc"; @@ -754,4 +755,122 @@ public void toStringRendersEmptyContractList() { String rendered = new TransactionCapsule(empty).toString(); Assert.assertTrue(rendered.contains("contract list is empty")); } + + @Test + public void pqWrongKeyLengthThrows() throws Exception { + dbManager.getDynamicPropertiesStore().saveAllowFnDsa512(1L); + Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); + // Permission with one slot so count check passes; length check fires first. + Permission permission = Permission.newBuilder() + .setType(PermissionType.Owner).setThreshold(1) + .addKeys(Key.newBuilder() + .setAddress(ByteString.copyFrom(new byte[20])).setWeight(1)) + .build(); + PQAuthSig badLen = PQAuthSig.newBuilder() + .setScheme(PQScheme.FN_DSA_512) + .setPublicKey(ByteString.copyFrom(new byte[1])) // 1 != 896 + .setSignature(ByteString.copyFrom(new byte[1])) + .build(); + Transaction withBad = tx.toBuilder().addPqAuthSig(badLen).build(); + try { + TransactionCapsule.validatePQSignatureGetWeight(withBad, permission, + dbManager.getDynamicPropertiesStore(), new ArrayList<>()); + Assert.fail("wrong pk length should throw SignatureFormatException"); + } catch (SignatureFormatException e) { + Assert.assertTrue(e.getMessage().contains("length mismatch")); + } + } + + @Test + public void pqWeightOverflowThrows() throws Exception { + dbManager.getDynamicPropertiesStore().saveAllowFnDsa512(1L); + FNDSA512 kp1 = new FNDSA512(); + FNDSA512 kp2 = new FNDSA512(); + Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); + byte[] txid = txId(tx); + byte[] sig1 = FNDSA512.sign(kp1.getPrivateKey(), txid); + byte[] sig2 = FNDSA512.sign(kp2.getPrivateKey(), txid); + Permission permission = Permission.newBuilder() + .setType(PermissionType.Owner).setThreshold(1) + .addKeys(Key.newBuilder() + .setAddress(ByteString.copyFrom( + PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, kp1.getPublicKey()))) + .setWeight(Long.MAX_VALUE)) + .addKeys(Key.newBuilder() + .setAddress(ByteString.copyFrom( + PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, kp2.getPublicKey()))) + .setWeight(Long.MAX_VALUE)) + .build(); + Transaction signed = tx.toBuilder() + .addPqAuthSig(PQAuthSig.newBuilder() + .setScheme(PQScheme.FN_DSA_512) + .setPublicKey(ByteString.copyFrom(kp1.getPublicKey())) + .setSignature(ByteString.copyFrom(sig1))) + .addPqAuthSig(PQAuthSig.newBuilder() + .setScheme(PQScheme.FN_DSA_512) + .setPublicKey(ByteString.copyFrom(kp2.getPublicKey())) + .setSignature(ByteString.copyFrom(sig2))) + .build(); + try { + TransactionCapsule.validatePQSignatureGetWeight(signed, permission, + dbManager.getDynamicPropertiesStore(), new ArrayList<>()); + Assert.fail("Long.MAX_VALUE + Long.MAX_VALUE should throw PermissionException"); + } catch (PermissionException e) { + Assert.assertTrue(e.getMessage().contains("weight overflow")); + } + } + + @Test + public void pqKeyNotInPermissionThrows() throws Exception { + dbManager.getDynamicPropertiesStore().saveAllowFnDsa512(1L); + FNDSA512 knownKp = new FNDSA512(); + FNDSA512 strangerKp = new FNDSA512(); + Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); + byte[] sig = FNDSA512.sign(strangerKp.getPrivateKey(), txId(tx)); + Permission permission = Permission.newBuilder() + .setType(PermissionType.Owner).setThreshold(1) + .addKeys(Key.newBuilder() + .setAddress(ByteString.copyFrom( + PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, knownKp.getPublicKey()))) + .setWeight(1)) + .build(); + Transaction signed = tx.toBuilder() + .addPqAuthSig(PQAuthSig.newBuilder() + .setScheme(PQScheme.FN_DSA_512) + .setPublicKey(ByteString.copyFrom(strangerKp.getPublicKey())) + .setSignature(ByteString.copyFrom(sig))) + .build(); + try { + TransactionCapsule.validatePQSignatureGetWeight(signed, permission, + dbManager.getDynamicPropertiesStore(), new ArrayList<>()); + Assert.fail("key not in permission should throw PermissionException"); + } catch (PermissionException e) { + Assert.assertTrue(e.getMessage().contains("not contained of permission")); + } + } + + @Test + public void pqKeyFoundReturnsWeight() throws Exception { + dbManager.getDynamicPropertiesStore().saveAllowFnDsa512(1L); + FNDSA512 kp = new FNDSA512(); + Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); + byte[] sig = FNDSA512.sign(kp.getPrivateKey(), txId(tx)); + long expectedWeight = 7L; + Permission permission = Permission.newBuilder() + .setType(PermissionType.Owner).setThreshold(1) + .addKeys(Key.newBuilder() + .setAddress(ByteString.copyFrom( + PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, kp.getPublicKey()))) + .setWeight(expectedWeight)) + .build(); + Transaction signed = tx.toBuilder() + .addPqAuthSig(PQAuthSig.newBuilder() + .setScheme(PQScheme.FN_DSA_512) + .setPublicKey(ByteString.copyFrom(kp.getPublicKey())) + .setSignature(ByteString.copyFrom(sig))) + .build(); + long weight = TransactionCapsule.validatePQSignatureGetWeight(signed, permission, + dbManager.getDynamicPropertiesStore(), new ArrayList<>()); + Assert.assertEquals(expectedWeight, weight); + } } 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 946bef022d..c8769f222f 100644 --- a/framework/src/test/java/org/tron/core/db/ManagerMockTest.java +++ b/framework/src/test/java/org/tron/core/db/ManagerMockTest.java @@ -26,6 +26,7 @@ import java.util.Collections; import java.util.LinkedList; import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; @@ -702,6 +703,42 @@ public void testSwitchForkPassesValidSignatureBlockToApply() { verify(goodBlock, atLeastOnce()).setSwitch(true); } + @SneakyThrows + @Test + public void testPushTransactionPqPendingFull() { + Manager dbManager = spy(new Manager()); + + ChainBaseManager cbm = mock(ChainBaseManager.class); + DynamicPropertiesStore dps = mock(DynamicPropertiesStore.class); + AccountStore accountStore = mock(AccountStore.class); + when(cbm.getDynamicPropertiesStore()).thenReturn(dps); + when(cbm.getAccountStore()).thenReturn(accountStore); + setField(dbManager, "chainBaseManager", cbm); + + BalanceContract.TransferContract tc = BalanceContract.TransferContract.newBuilder() + .setOwnerAddress(ByteString.copyFromUtf8("aaa")) + .setToAddress(ByteString.copyFromUtf8("bbb")) + .setAmount(1) + .build(); + Protocol.Transaction txn = Protocol.Transaction.newBuilder() + .setRawData(Protocol.Transaction.raw.newBuilder() + .addContract(Protocol.Transaction.Contract.newBuilder() + .setParameter(com.google.protobuf.Any.pack(tc)) + .setType(Protocol.Transaction.Contract.ContractType.TransferContract))) + .addPqAuthSig(Protocol.PQAuthSig.getDefaultInstance()) + .build(); + TransactionCapsule trxMock = mock(TransactionCapsule.class); + when(trxMock.getInstance()).thenReturn(txn); + when(trxMock.validateSignature( + any(AccountStore.class), any(DynamicPropertiesStore.class))).thenReturn(true); + when(trxMock.getTransactionId()).thenReturn(Sha256Hash.ZERO_HASH); + + setField(dbManager, "pqTransInPendingCounts", new AtomicInteger(Integer.MAX_VALUE)); + + boolean result = dbManager.pushTransaction(trxMock); + Assert.assertFalse(result); + } + private static void setField(Object target, String name, Object value) throws Exception { Field f = target.getClass().getSuperclass() != null ? findField(target.getClass(), name)