From 7f36a3b4c064d279dab01070fc2b90cde67d6144 Mon Sep 17 00:00:00 2001 From: federico Date: Sun, 10 May 2026 12:03:51 +0800 Subject: [PATCH 1/9] feat(crypto): add Falcon-512 post-quantum signature support --- .../AccountPermissionUpdateActuator.java | 4 +- .../org/tron/core/utils/ProposalUtil.java | 15 +- .../tron/core/vm/PrecompiledContracts.java | 398 +++++++++++++++ .../org/tron/core/vm/config/ConfigLoader.java | 1 + .../org/tron/common/utils/LocalWitnesses.java | 86 ++++ .../org/tron/core/capsule/BlockCapsule.java | 115 ++++- .../tron/core/capsule/TransactionCapsule.java | 116 ++++- .../org/tron/core/db/BandwidthProcessor.java | 11 +- .../core/store/DynamicPropertiesStore.java | 40 ++ .../common/parameter/CommonParameter.java | 5 + .../src/main/java/org/tron/core/Constant.java | 8 + .../org/tron/core/vm/config/VMConfig.java | 10 + .../java/org/tron/consensus/base/Param.java | 25 + .../org/tron/common/crypto/pqc/FNDSA.java | 259 ++++++++++ .../common/crypto/pqc/PQSchemeRegistry.java | 223 ++++++++ .../tron/common/crypto/pqc/PQSignature.java | 72 +++ .../src/main/java/org/tron/core/Wallet.java | 5 + .../java/org/tron/core/config/args/Args.java | 63 +++ .../org/tron/core/config/args/ConfigKey.java | 13 + .../core/config/args/WitnessInitializer.java | 39 ++ .../tron/core/consensus/ConsensusService.java | 73 +++ .../tron/core/consensus/ProposalService.java | 4 + .../main/java/org/tron/core/db/Manager.java | 59 ++- framework/src/main/resources/config.conf | 19 + .../tron/common/crypto/pqc/FNDSAKatTest.java | 246 +++++++++ .../org/tron/common/crypto/pqc/FNDSATest.java | 443 ++++++++++++++++ .../crypto/pqc/PQSchemeRegistryTest.java | 130 +++++ .../crypto/pqc/PQSignatureDefaultsTest.java | 132 +++++ .../pqc/SignatureSchemeBenchmarkTest.java | 132 +++++ .../common/crypto/pqc/program/PQClient.java | 147 ++++++ .../common/crypto/pqc/program/PQFullNode.java | 118 +++++ .../crypto/pqc/program/PQWitnessNode.java | 204 ++++++++ .../runtime/vm/BatchValidateSignPQTest.java | 346 +++++++++++++ .../runtime/vm/FnDsaPrecompileTest.java | 186 +++++++ .../runtime/vm/ValidateMultiSignPQTest.java | 464 +++++++++++++++++ .../tron/common/utils/LocalWitnessesTest.java | 152 ++++++ .../org/tron/core/BandwidthProcessorTest.java | 116 +++++ .../AccountPermissionUpdateActuatorTest.java | 2 +- .../actuator/CreateAccountActuatorTest.java | 4 + .../ShieldedTransferActuatorTest.java | 2 +- .../core/actuator/utils/ProposalUtilTest.java | 26 + .../tron/core/capsule/BlockCapsulePQTest.java | 194 +++++++ .../core/capsule/TransactionCapsuleTest.java | 475 ++++++++++++++++++ .../tron/core/exception/TronErrorTest.java | 12 +- .../core/services/ProposalServiceTest.java | 17 + .../org/tron/core/services/http/UtilTest.java | 39 ++ framework/src/test/resources/config-test.conf | 7 +- protocol/src/main/protos/core/Tron.proto | 36 +- 48 files changed, 5264 insertions(+), 29 deletions(-) create mode 100644 crypto/src/main/java/org/tron/common/crypto/pqc/FNDSA.java create mode 100644 crypto/src/main/java/org/tron/common/crypto/pqc/PQSchemeRegistry.java create mode 100644 crypto/src/main/java/org/tron/common/crypto/pqc/PQSignature.java create mode 100644 framework/src/main/java/org/tron/core/config/args/ConfigKey.java create mode 100644 framework/src/test/java/org/tron/common/crypto/pqc/FNDSAKatTest.java create mode 100644 framework/src/test/java/org/tron/common/crypto/pqc/FNDSATest.java create mode 100644 framework/src/test/java/org/tron/common/crypto/pqc/PQSchemeRegistryTest.java create mode 100644 framework/src/test/java/org/tron/common/crypto/pqc/PQSignatureDefaultsTest.java create mode 100644 framework/src/test/java/org/tron/common/crypto/pqc/SignatureSchemeBenchmarkTest.java create mode 100644 framework/src/test/java/org/tron/common/crypto/pqc/program/PQClient.java create mode 100644 framework/src/test/java/org/tron/common/crypto/pqc/program/PQFullNode.java create mode 100644 framework/src/test/java/org/tron/common/crypto/pqc/program/PQWitnessNode.java create mode 100644 framework/src/test/java/org/tron/common/runtime/vm/BatchValidateSignPQTest.java create mode 100644 framework/src/test/java/org/tron/common/runtime/vm/FnDsaPrecompileTest.java create mode 100644 framework/src/test/java/org/tron/common/runtime/vm/ValidateMultiSignPQTest.java create mode 100644 framework/src/test/java/org/tron/common/utils/LocalWitnessesTest.java create mode 100644 framework/src/test/java/org/tron/core/capsule/BlockCapsulePQTest.java diff --git a/actuator/src/main/java/org/tron/core/actuator/AccountPermissionUpdateActuator.java b/actuator/src/main/java/org/tron/core/actuator/AccountPermissionUpdateActuator.java index f2eafb20a5e..2fd5f75f8dd 100644 --- a/actuator/src/main/java/org/tron/core/actuator/AccountPermissionUpdateActuator.java +++ b/actuator/src/main/java/org/tron/core/actuator/AccountPermissionUpdateActuator.java @@ -95,13 +95,14 @@ private boolean checkPermission(Permission permission) throws ContractValidateEx long weightSum = 0; List addressList = permission.getKeysList() .stream() - .map(x -> x.getAddress()) + .map(Key::getAddress) .distinct() .collect(toList()); if (addressList.size() != permission.getKeysList().size()) { throw new ContractValidateException( "address should be distinct in permission " + permission.getType()); } + for (Key key : permission.getKeysList()) { if (!DecodeUtil.addressValid(key.getAddress().toByteArray())) { throw new ContractValidateException("key is not a validate address"); @@ -237,4 +238,5 @@ public ByteString getOwnerAddress() throws InvalidProtocolBufferException { public long calcFee() { return chainBaseManager.getDynamicPropertiesStore().getUpdateAccountPermissionFee(); } + } diff --git a/actuator/src/main/java/org/tron/core/utils/ProposalUtil.java b/actuator/src/main/java/org/tron/core/utils/ProposalUtil.java index 74d332c5611..259a1a60bde 100644 --- a/actuator/src/main/java/org/tron/core/utils/ProposalUtil.java +++ b/actuator/src/main/java/org/tron/core/utils/ProposalUtil.java @@ -941,6 +941,17 @@ public static void validator(DynamicPropertiesStore dynamicPropertiesStore, } break; } + case ALLOW_FN_DSA_512: { + if (dynamicPropertiesStore.getAllowFnDsa512() == 1) { + throw new ContractValidateException( + "[ALLOW_FN_DSA_512] has been valid, no need to propose again"); + } + if (value != 1) { + throw new ContractValidateException( + "This value[ALLOW_FN_DSA_512] is only allowed to be 1"); + } + break; + } default: break; } @@ -1029,7 +1040,9 @@ public enum ProposalType { // current value, value range ALLOW_TVM_PRAGUE(95), // 0, 1 ALLOW_TVM_OSAKA(96), // 0, 1 ALLOW_HARDEN_RESOURCE_CALCULATION(97), // 0, 1 - ALLOW_HARDEN_EXCHANGE_CALCULATION(98); // 0, 1 + ALLOW_HARDEN_EXCHANGE_CALCULATION(98), // 0, 1 + ALLOW_FN_DSA_512(100); // 0, 1 + private long code; ProposalType(long code) { diff --git a/actuator/src/main/java/org/tron/core/vm/PrecompiledContracts.java b/actuator/src/main/java/org/tron/core/vm/PrecompiledContracts.java index 1ac96b9d59d..27308c4412b 100644 --- a/actuator/src/main/java/org/tron/core/vm/PrecompiledContracts.java +++ b/actuator/src/main/java/org/tron/core/vm/PrecompiledContracts.java @@ -53,6 +53,8 @@ import org.tron.common.crypto.Rsv; import org.tron.common.crypto.SignUtils; import org.tron.common.crypto.SignatureInterface; +import org.tron.common.crypto.pqc.FNDSA; +import org.tron.common.crypto.pqc.PQSchemeRegistry; import org.tron.common.crypto.zksnark.BN128; import org.tron.common.crypto.zksnark.BN128Fp; import org.tron.common.crypto.zksnark.BN128G1; @@ -83,6 +85,7 @@ import org.tron.core.vm.utils.MUtil; import org.tron.core.vm.utils.VoteRewardUtil; import org.tron.protos.Protocol; +import org.tron.protos.Protocol.PQScheme; import org.tron.protos.Protocol.Permission; @Slf4j(topic = "VM") @@ -117,6 +120,11 @@ public class PrecompiledContracts { private static final Blake2F blake2F = new Blake2F(); private static final P256Verify p256Verify = new P256Verify(); + private static final VerifyFnDsa verifyFnDsa = new VerifyFnDsa(); + private static final ValidateMultiSignPQ validateMultiSignPQ = + new ValidateMultiSignPQ(); + private static final BatchValidateSignPQ batchValidateSignPQ = new BatchValidateSignPQ(); + // FreezeV2 PrecompileContracts private static final GetChainParameter getChainParameter = new GetChainParameter(); private static final AvailableUnfreezeV2Size availableUnfreezeV2Size = new AvailableUnfreezeV2Size(); @@ -212,6 +220,24 @@ public class PrecompiledContracts { private static final DataWord p256VerifyAddr = new DataWord( "0000000000000000000000000000000000000000000000000000000000000100"); + // EIP-8052 0x16: FN-DSA / Falcon-512 verify (FIPS-206 draft). Input layout: + // [msg 32B | sig_len 2B (big-endian) | sig sig_len B (1..752) | pk 896B]. + // Variable-length signature is prefixed with a 2-byte length field. + private static final DataWord verifyFnDsaAddr = new DataWord( + "0000000000000000000000000000000000000000000000000000000000000016"); + + // 0x17: algorithm-agnostic Permission multi-sign — accepts both ECDSA and + // Falcon-512 signatures against the same Permission.keys[] in one call, + // matching transaction-side §2.3.5 mixed-weight semantics. + private static final DataWord validateMultiSignPQAddr = new DataWord( + "0000000000000000000000000000000000000000000000000000000000000017"); + + // 0x18: batch independent Falcon-512 verify — bitmap of (sig, pk, addr) + // matches; mixed-algorithm contracts call 0x0A and 0x18 separately and OR + // the bitmaps client-side. + private static final DataWord batchValidateSignPqAddr = new DataWord( + "0000000000000000000000000000000000000000000000000000000000000018"); + public static PrecompiledContract getOptimizedContractForConstant(PrecompiledContract contract) { try { Constructor constructor = contract.getClass().getDeclaredConstructor(); @@ -297,6 +323,18 @@ public static PrecompiledContract getContractForAddress(DataWord address) { return p256Verify; } + if (VMConfig.allowFnDsa512() && address.equals(verifyFnDsaAddr)) { + return verifyFnDsa; + } + + if (VMConfig.allowFnDsa512() && address.equals(validateMultiSignPQAddr)) { + return validateMultiSignPQ; + } + + if (VMConfig.allowFnDsa512() && address.equals(batchValidateSignPqAddr)) { + return batchValidateSignPQ; + } + if (VMConfig.allowTvmFreezeV2()) { if (address.equals(getChainParameterAddr)) { return getChainParameter; @@ -2380,4 +2418,364 @@ public Pair execute(byte[] data) { } } + /** + * Verifies a FN-DSA / Falcon-512 signature (FIPS-206 draft). EIP-8052 / TRON extension. + * + *

Input layout (variable-length, EIP-8052-inspired): + *

+   *   [msg 32B | sig_len 2B (big-endian, 1..752) | sig sig_len B | pk 896B]
+   * 
+ * Minimum input: 32 + 2 + 1 + 896 = 931 bytes. + * + *

Returns a 32-byte word: 1 on valid signature, 0 otherwise. + * Malformed input (wrong lengths, out-of-range sig_len) returns 0 without error. + */ + public static class VerifyFnDsa extends PrecompiledContract { + + private static final int MSG_LEN = 32; + private static final int SIG_LEN_FIELD = 2; + private static final int PK_LEN = FNDSA.PUBLIC_KEY_LENGTH; + private static final int MAX_SIG_LEN = FNDSA.SIGNATURE_LENGTH; + private static final int MIN_INPUT_LEN = MSG_LEN + SIG_LEN_FIELD + 1 + PK_LEN; + + @Override + public long getEnergyForData(byte[] data) { + return 2500; + } + + @Override + public Pair execute(byte[] data) { + if (data == null || data.length < MIN_INPUT_LEN) { + return Pair.of(true, DataWord.ZERO().getData()); + } + try { + byte[] msg = copyOfRange(data, 0, MSG_LEN); + int sigLen = ((data[MSG_LEN] & 0xFF) << 8) | (data[MSG_LEN + 1] & 0xFF); + if (sigLen < 1 || sigLen > MAX_SIG_LEN) { + return Pair.of(true, DataWord.ZERO().getData()); + } + int pkOffset = MSG_LEN + SIG_LEN_FIELD + sigLen; + if (data.length < pkOffset + PK_LEN) { + return Pair.of(true, DataWord.ZERO().getData()); + } + byte[] sig = copyOfRange(data, MSG_LEN + SIG_LEN_FIELD, pkOffset); + byte[] pk = copyOfRange(data, pkOffset, pkOffset + PK_LEN); + boolean ok = FNDSA.verify(pk, msg, sig); + return Pair.of(true, ok ? DataWord.ONE().getData() : DataWord.ZERO().getData()); + } catch (Throwable t) { + return Pair.of(true, DataWord.ZERO().getData()); + } + } + } + + + /** + * 0x17 ValidateMultiSign — algorithm-agnostic Permission multi-sign. + *

Mirrors 0x09 hash construction ({@code SHA-256(address ‖ permissionId(int4B) ‖ data)}) + * and threshold/dedup semantics, while accepting Falcon-512 entries alongside ECDSA + * against the same {@code Permission.keys[]}. The {@code data} field stays {@code bytes32} + * so the hash is bit-identical to 0x09. + * + *

ABI: + *

+   *   validateMultiSign(
+   *       address account,           // word[0]
+   *       uint256 permissionId,      // word[1]
+   *       bytes32 data,              // word[2]
+   *       bytes[] ecdsaSignatures,   // word[3] = offset; each entry 65 B
+   *       bytes[] pqSignatures,      // word[4] = offset; each entry 1..752 B
+   *       bytes[] pqPublicKeys       // word[5] = offset; each entry 896 B
+   *   ) returns (bool)
+   * 
+ * + *

{@code MAX_SIZE = 5} applies to the total signature count + * ({@code ecdsaCnt + pqCnt}). Energy is split: {@code ecdsaCnt × 1500 + pqCnt × 15000}. + */ + public static class ValidateMultiSignPQ extends PrecompiledContract { + + private static final int ECDSA_ENERGY_PER_SIGN = 1500; + private static final int PQ_ENERGY_PER_SIGN = 15000; + private static final int MAX_SIZE = 5; + private static final int PK_LEN = FNDSA.PUBLIC_KEY_LENGTH; + private static final int MAX_SIG_LEN = FNDSA.SIGNATURE_LENGTH; + + @Override + public long getEnergyForData(byte[] data) { + try { + DataWord[] words = DataWord.parseArray(data); + int ecdsaCnt = words[words[3].intValueSafe() / WORD_SIZE].intValueSafe(); + int pqCnt = words[words[4].intValueSafe() / WORD_SIZE].intValueSafe(); + return (long) ecdsaCnt * ECDSA_ENERGY_PER_SIGN + + (long) pqCnt * PQ_ENERGY_PER_SIGN; + } catch (Throwable t) { + return (long) MAX_SIZE * PQ_ENERGY_PER_SIGN; + } + } + + @Override + public Pair execute(byte[] rawData) { + try { + DataWord[] words = DataWord.parseArray(rawData); + byte[] address = words[0].toTronAddress(); + int permissionId = words[1].intValueSafe(); + byte[] data = words[2].getData(); + + byte[] combine = ByteUtil.merge(address, ByteArray.fromInt(permissionId), data); + byte[] hash = Sha256Hash.hash(CommonParameter + .getInstance().isECKeyCryptoEngine(), combine); + + int ecdsaArrayWord = words[3].intValueSafe() / WORD_SIZE; + int pqSigArrayWord = words[4].intValueSafe() / WORD_SIZE; + int pqPkArrayWord = words[5].intValueSafe() / WORD_SIZE; + + int ecdsaCnt = words[ecdsaArrayWord].intValueSafe(); + int pqSigCnt = words[pqSigArrayWord].intValueSafe(); + int pqPkCnt = words[pqPkArrayWord].intValueSafe(); + + if (pqSigCnt != pqPkCnt + || ecdsaCnt + pqSigCnt == 0 + || ecdsaCnt + pqSigCnt > MAX_SIZE) { + return Pair.of(true, DATA_FALSE); + } + + byte[][] ecdsaSigs = extractSigArray(words, ecdsaArrayWord, rawData); + byte[][] pqSigs = extractBytesArray(words, pqSigArrayWord, rawData); + byte[][] pqPks = extractBytesArray(words, pqPkArrayWord, rawData); + + AccountCapsule account = this.getDeposit().getAccount(address); + if (account == null) { + return Pair.of(true, DATA_FALSE); + } + Permission permission = account.getPermissionById(permissionId); + if (permission == null) { + return Pair.of(true, DATA_FALSE); + } + + long totalWeight = 0L; + List executedSignList = new ArrayList<>(); + + for (byte[] sign : ecdsaSigs) { + byte[] recoveredAddr = recoverAddrBySign(sign, hash); + byte[] dedupKey = merge(recoveredAddr, sign); + if (ByteArray.matrixContains(executedSignList, recoveredAddr)) { + if (ByteArray.matrixContains(executedSignList, dedupKey)) { + continue; + } + MUtil.checkCPUTime(); + } + long weight = TransactionCapsule.getWeight(permission, recoveredAddr); + if (weight == 0) { + return Pair.of(true, DATA_FALSE); + } + totalWeight += weight; + executedSignList.add(dedupKey); + executedSignList.add(recoveredAddr); + } + + for (int i = 0; i < pqSigs.length; i++) { + byte[] sig = pqSigs[i]; + byte[] pk = pqPks[i]; + if (pk == null || pk.length != PK_LEN + || sig == null || sig.length < 1 || sig.length > MAX_SIG_LEN) { + return Pair.of(true, DATA_FALSE); + } + byte[] derivedAddr; + try { + derivedAddr = PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, pk); + } catch (Throwable t) { + return Pair.of(true, DATA_FALSE); + } + // Falcon-512 signing is randomized: the same key can produce many distinct + // valid signatures for the same hash. Dedup must therefore key on the + // derived address alone, otherwise an attacker could replay one key into + // the threshold N times via N different signatures. + if (ByteArray.matrixContains(executedSignList, derivedAddr)) { + continue; + } + long weight = TransactionCapsule.getWeight(permission, derivedAddr); + if (weight == 0) { + return Pair.of(true, DATA_FALSE); + } + if (!FNDSA.verify(pk, hash, sig)) { + return Pair.of(true, DATA_FALSE); + } + totalWeight += weight; + executedSignList.add(derivedAddr); + } + + if (totalWeight >= permission.getThreshold()) { + return Pair.of(true, dataOne()); + } + } catch (Throwable t) { + if (t instanceof OutOfTimeException) { + throw t; + } + logger.info("ValidateMultiSign(0x17) error:{}", t.getMessage()); + } + return Pair.of(true, DATA_FALSE); + } + } + + /** + * 0x18 BatchValidateSignPQ — independent per-element Falcon-512 verify. + *

Returns a 256-bit bitmap (matching 0x0A) where bit {@code i} is set iff + * {@code derive(pk_i) == expectedAddr_i} AND {@code FNDSA.verify(pk_i, hash, sig_i)}. + * + *

ABI: + *

+   *   batchValidateSignPQ(
+   *       bytes32   hash,                  // word[0]
+   *       bytes[]   signatures,            // word[1] = offset; each 1..752 B
+   *       bytes[]   publicKeys,            // word[2] = offset; each 896 B
+   *       bytes32[] expectedAddresses      // word[3] = offset; 21-byte addr in low 21 bytes
+   *   ) returns (bytes32)
+   * 
+ * + *

Reuses the {@code BatchValidateSign.workers} pool when not in a constant + * call and enforces {@code getCPUTimeLeftInNanoSecond()} timeout. {@code MAX_SIZE = 16}. + * Energy is {@code cnt × 15000}. + */ + public static class BatchValidateSignPQ extends PrecompiledContract { + + private static final int ENERGY_PER_SIGN = 15000; + private static final int MAX_SIZE = 16; + private static final int PK_LEN = FNDSA.PUBLIC_KEY_LENGTH; + private static final int MAX_SIG_LEN = FNDSA.SIGNATURE_LENGTH; + + @Override + public long getEnergyForData(byte[] data) { + try { + DataWord[] words = DataWord.parseArray(data); + int cnt = words[words[1].intValueSafe() / WORD_SIZE].intValueSafe(); + return (long) cnt * ENERGY_PER_SIGN; + } catch (Throwable t) { + return (long) MAX_SIZE * ENERGY_PER_SIGN; + } + } + + @Override + public Pair execute(byte[] data) { + try { + return doExecute(data); + } catch (Throwable t) { + if (t instanceof OutOfTimeException) { + throw (OutOfTimeException) t; + } + if (t instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + return Pair.of(true, new byte[WORD_SIZE]); + } + } + + private Pair doExecute(byte[] data) + throws InterruptedException, ExecutionException { + DataWord[] words = DataWord.parseArray(data); + byte[] hash = words[0].getData(); + + int sigArrayWord = words[1].intValueSafe() / WORD_SIZE; + int pkArrayWord = words[2].intValueSafe() / WORD_SIZE; + int addrArrayWord = words[3].intValueSafe() / WORD_SIZE; + + int sigArraySize = words[sigArrayWord].intValueSafe(); + int pkArraySize = words[pkArrayWord].intValueSafe(); + int addrArraySize = words[addrArrayWord].intValueSafe(); + + if (sigArraySize > MAX_SIZE || pkArraySize > MAX_SIZE + || addrArraySize > MAX_SIZE + || sigArraySize != pkArraySize || sigArraySize != addrArraySize) { + return Pair.of(true, DATA_FALSE); + } + + byte[][] signatures = extractBytesArray(words, sigArrayWord, data); + byte[][] publicKeys = extractBytesArray(words, pkArrayWord, data); + byte[][] addresses = extractBytes32Array(words, addrArrayWord); + + int cnt = signatures.length; + if (cnt == 0) { + return Pair.of(true, DATA_FALSE); + } + + byte[] res = new byte[WORD_SIZE]; + if (isConstantCall()) { + for (int i = 0; i < cnt; i++) { + if (verifyOne(signatures[i], publicKeys[i], hash, addresses[i])) { + res[i] = 1; + } + } + } else { + CountDownLatch countDownLatch = new CountDownLatch(cnt); + List> futures = new ArrayList<>(cnt); + + for (int i = 0; i < cnt; i++) { + Future future = BatchValidateSign.workers.submit( + new PqVerifyTask(countDownLatch, hash, signatures[i], + publicKeys[i], addresses[i], i)); + futures.add(future); + } + + boolean withNoTimeout = countDownLatch + .await(getCPUTimeLeftInNanoSecond(), TimeUnit.NANOSECONDS); + + if (!withNoTimeout) { + logger.info("BatchValidateSignPQ timeout"); + throw Program.Exception.notEnoughTime("call BatchValidateSignPQ precompile method"); + } + + for (Future future : futures) { + PqVerifyResult r = future.get(); + if (r.success) { + res[r.nonce] = 1; + } + } + } + return Pair.of(true, res); + } + + private static boolean verifyOne(byte[] sig, byte[] pk, byte[] hash, + byte[] expectedAddr) { + if (pk == null || pk.length != PK_LEN + || sig == null || sig.length < 1 || sig.length > MAX_SIG_LEN) { + return false; + } + try { + byte[] derived = PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, pk); + if (!DataWord.equalAddressByteArray(derived, expectedAddr)) { + return false; + } + return FNDSA.verify(pk, hash, sig); + } catch (Throwable t) { + return false; + } + } + + @AllArgsConstructor + private static class PqVerifyTask implements Callable { + + private CountDownLatch countDownLatch; + private byte[] hash; + private byte[] signature; + private byte[] publicKey; + private byte[] expectedAddr; + private int nonce; + + @Override + public PqVerifyResult call() { + try { + return new PqVerifyResult( + verifyOne(signature, publicKey, hash, expectedAddr), nonce); + } finally { + countDownLatch.countDown(); + } + } + } + + @AllArgsConstructor + private static class PqVerifyResult { + + private boolean success; + private int nonce; + } + } + } diff --git a/actuator/src/main/java/org/tron/core/vm/config/ConfigLoader.java b/actuator/src/main/java/org/tron/core/vm/config/ConfigLoader.java index 881eb861bea..6a992ae5f0d 100644 --- a/actuator/src/main/java/org/tron/core/vm/config/ConfigLoader.java +++ b/actuator/src/main/java/org/tron/core/vm/config/ConfigLoader.java @@ -47,6 +47,7 @@ public static void load(StoreFactory storeFactory) { VMConfig.initAllowTvmSelfdestructRestriction(ds.getAllowTvmSelfdestructRestriction()); VMConfig.initAllowTvmOsaka(ds.getAllowTvmOsaka()); VMConfig.initAllowHardenResourceCalculation(ds.getAllowHardenResourceCalculation()); + VMConfig.initAllowFnDsa512(ds.getAllowFnDsa512()); } } } diff --git a/chainbase/src/main/java/org/tron/common/utils/LocalWitnesses.java b/chainbase/src/main/java/org/tron/common/utils/LocalWitnesses.java index 7179045ea7e..dc8f4f0c4be 100644 --- a/chainbase/src/main/java/org/tron/common/utils/LocalWitnesses.java +++ b/chainbase/src/main/java/org/tron/common/utils/LocalWitnesses.java @@ -18,14 +18,17 @@ import com.google.common.collect.Lists; import java.util.List; import lombok.Getter; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.tron.common.crypto.ECKey; import org.tron.common.crypto.SignInterface; import org.tron.common.crypto.SignUtils; +import org.tron.common.crypto.pqc.PQSchemeRegistry; import org.tron.core.config.Parameter.ChainConstant; import org.tron.core.exception.TronError; +import org.tron.protos.Protocol.PQScheme; @Slf4j(topic = "app") public class LocalWitnesses { @@ -33,6 +36,40 @@ public class LocalWitnesses { @Getter private List privateKeys = Lists.newArrayList(); + /** + * Pre-derived PQ private keys in hex format, one per witness. The expected + * byte length depends on {@link #pqScheme}: 1280 bytes (2560 hex chars) for + * FN-DSA-512. Index-aligned with {@link #pqPublicKeys}. + * + *

Configured directly (rather than derived from a seed on the node) so + * the runtime path is not exposed to potential cross-platform floating-point + * non-determinism in BC's Falcon keygen — operators generate the keypair + * off-line and ship both halves to the node. + */ + @Getter + private List pqPrivateKeys = Lists.newArrayList(); + + /** + * PQ public keys in hex format, one per witness. The expected byte length + * depends on {@link #pqScheme}: 896 bytes (1792 hex chars) for FN-DSA-512. + * Index-aligned with {@link #pqPrivateKeys}. + */ + @Getter + private List pqPublicKeys = Lists.newArrayList(); + + /** PQ signature scheme used by the configured {@link #pqPrivateKeys}. */ + @Getter + private PQScheme pqScheme = PQScheme.FN_DSA_512; + + public void setPqScheme(PQScheme pqScheme) { + if (pqScheme == null || !PQSchemeRegistry.contains(pqScheme)) { + throw new TronError("unsupported PQ signature scheme: " + pqScheme, + TronError.ErrCode.WITNESS_INIT); + } + this.pqScheme = pqScheme; + } + + @Setter @Getter private byte[] witnessAccountAddress; @@ -95,6 +132,55 @@ public void addPrivateKeys(String privateKey) { this.privateKeys.add(privateKey); } + /** + * Pre-derived PQ keypairs (priv + pub) used as signing keys under + * {@link #pqScheme}. The two lists must be the same length and index-aligned; + * each entry must be a hex string whose byte length matches the scheme's + * required private/public key size. Callers must therefore set the scheme + * via {@link #setPqScheme(PQScheme)} before calling this method when + * targeting a non-default scheme. + */ + public void setPqKeypairs(final List pqPrivateKeys, + final List pqPublicKeys) { + if (CollectionUtils.isEmpty(pqPrivateKeys) + && CollectionUtils.isEmpty(pqPublicKeys)) { + return; + } + int privCount = pqPrivateKeys == null ? 0 : pqPrivateKeys.size(); + int pubCount = pqPublicKeys == null ? 0 : pqPublicKeys.size(); + if (privCount != pubCount) { + throw new TronError(String.format( + "PQ keypair list size mismatch: priv=%d, pub=%d", privCount, pubCount), + TronError.ErrCode.WITNESS_INIT); + } + int expectedPrivLen = PQSchemeRegistry.getPrivateKeyLength(pqScheme); + int expectedPubLen = PQSchemeRegistry.getPublicKeyLength(pqScheme); + for (int i = 0; i < privCount; i++) { + validatePqKey(pqPrivateKeys.get(i), expectedPrivLen, "PQ private key"); + validatePqKey(pqPublicKeys.get(i), expectedPubLen, "PQ public key"); + } + this.pqPrivateKeys = pqPrivateKeys; + this.pqPublicKeys = pqPublicKeys; + } + + private static void validatePqKey(String key, int expectedLen, String label) { + String hex = key; + // Match downstream ByteArray.fromHexString, which only strips lowercase "0x". + if (StringUtils.startsWith(hex, "0x")) { + hex = hex.substring(2); + } + int expectedHexLen = expectedLen * 2; + if (StringUtils.isBlank(hex) || hex.length() != expectedHexLen) { + throw new TronError(String.format("%s must be %d hex chars, actual: %d", + label, expectedHexLen, StringUtils.isBlank(hex) ? 0 : hex.length()), + TronError.ErrCode.WITNESS_INIT); + } + if (!StringUtil.isHexadecimal(hex)) { + throw new TronError(label + " must be hex string", + TronError.ErrCode.WITNESS_INIT); + } + } + //get the first one recently public String getPrivateKey() { if (CollectionUtils.isEmpty(privateKeys)) { 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 34b7853d4d1..dd0e2126bac 100755 --- a/chainbase/src/main/java/org/tron/core/capsule/BlockCapsule.java +++ b/chainbase/src/main/java/org/tron/core/capsule/BlockCapsule.java @@ -33,6 +33,7 @@ import org.tron.common.bloom.Bloom; import org.tron.common.crypto.SignInterface; import org.tron.common.crypto.SignUtils; +import org.tron.common.crypto.pqc.PQSchemeRegistry; import org.tron.common.parameter.CommonParameter; import org.tron.common.utils.ByteArray; import org.tron.common.utils.Sha256Hash; @@ -46,6 +47,10 @@ import org.tron.core.store.DynamicPropertiesStore; import org.tron.protos.Protocol.Block; import org.tron.protos.Protocol.BlockHeader; +import org.tron.protos.Protocol.Key; +import org.tron.protos.Protocol.PQScheme; +import org.tron.protos.Protocol.PQAuthSig; +import org.tron.protos.Protocol.Permission; import org.tron.protos.Protocol.Transaction; @Slf4j(topic = "capsule") @@ -177,6 +182,16 @@ public void sign(byte[] privateKey) { } + public void setPqAuthSig(PQAuthSig pqAuthSig) { + BlockHeader blockHeader = this.block.getBlockHeader().toBuilder() + .setPqAuthSig(pqAuthSig).build(); + this.block = this.block.toBuilder().setBlockHeader(blockHeader).build(); + } + + public byte[] getRawHashBytes() { + return getRawHash().getBytes(); + } + private Sha256Hash getRawHash() { return Sha256Hash.of(CommonParameter.getInstance().isECKeyCryptoEngine(), this.block.getBlockHeader().getRawData().toByteArray()); @@ -184,27 +199,104 @@ private Sha256Hash getRawHash() { public boolean validateSignature(DynamicPropertiesStore dynamicPropertiesStore, AccountStore accountStore) throws ValidateSignatureException { + BlockHeader header = block.getBlockHeader(); + boolean hasLegacy = !header.getWitnessSignature().isEmpty(); + PQAuthSig pqAuthSig = header.getPqAuthSig(); + boolean hasPq = pqAuthSig != null + && pqAuthSig.getSignature() != null + && !pqAuthSig.getSignature().isEmpty(); + + if (hasLegacy && hasPq) { + throw new ValidateSignatureException( + "witness_signature and pq_auth_sig are mutually exclusive"); + } + if (!hasLegacy && !hasPq) { + throw new ValidateSignatureException("missing witness signature"); + } + + byte[] witnessAccountAddress = header.getRawData().getWitnessAddress().toByteArray(); + if (hasPq) { + return validatePQSignature(dynamicPropertiesStore, accountStore, + witnessAccountAddress, pqAuthSig); + } + return validateLegacySignature(dynamicPropertiesStore, accountStore, witnessAccountAddress); + } + + private boolean validateLegacySignature(DynamicPropertiesStore dynamicPropertiesStore, + AccountStore accountStore, byte[] witnessAccountAddress) + throws ValidateSignatureException { try { byte[] sigAddress = SignUtils.signatureToAddress(getRawHash().getBytes(), TransactionCapsule.getBase64FromByteString( block.getBlockHeader().getWitnessSignature()), CommonParameter.getInstance().isECKeyCryptoEngine()); - byte[] witnessAccountAddress = block.getBlockHeader().getRawData().getWitnessAddress() - .toByteArray(); - if (dynamicPropertiesStore.getAllowMultiSign() != 1) { return Arrays.equals(sigAddress, witnessAccountAddress); - } else { - byte[] witnessPermissionAddress = accountStore.get(witnessAccountAddress) - .getWitnessPermissionAddress(); - return Arrays.equals(sigAddress, witnessPermissionAddress); } - + byte[] witnessPermissionAddress = accountStore.get(witnessAccountAddress) + .getWitnessPermissionAddress(); + return Arrays.equals(sigAddress, witnessPermissionAddress); } catch (SignatureException e) { throw new ValidateSignatureException(e.getMessage()); } } + /** + * Verify a PQ-signed block header. V2 binds the signing key by deriving its + * 21-byte address from the in-band {@code public_key} and matching against + * the witness account's Witness Permission keys[]. + */ + private boolean validatePQSignature(DynamicPropertiesStore dynamicPropertiesStore, + AccountStore accountStore, byte[] witnessAccountAddress, PQAuthSig pqAuthSig) + throws ValidateSignatureException { + PQScheme scheme = pqAuthSig.getScheme(); + if (!PQSchemeRegistry.contains(scheme)) { + throw new ValidateSignatureException( + "pq_auth_sig scheme " + scheme + " is not registered"); + } + if (!dynamicPropertiesStore.isPqSchemeAllowed(scheme)) { + throw new ValidateSignatureException( + "pq_auth_sig scheme " + scheme + " is not activated"); + } + + AccountCapsule accountCapsule = accountStore.get(witnessAccountAddress); + Permission witnessPermission = null; + if (accountCapsule != null && accountCapsule.getInstance().hasWitnessPermission()) { + witnessPermission = accountCapsule.getInstance().getWitnessPermission(); + } + if (witnessPermission == null || witnessPermission.getKeysCount() == 0) { + throw new ValidateSignatureException( + "pq_auth_sig present but witness permission is not configured"); + } + + byte[] publicKey = pqAuthSig.getPublicKey().toByteArray(); + if (publicKey.length != PQSchemeRegistry.getPublicKeyLength(scheme)) { + throw new ValidateSignatureException( + "pq_auth_sig public key length mismatch for scheme " + scheme); + } + byte[] signature = pqAuthSig.getSignature().toByteArray(); + if (!PQSchemeRegistry.isValidSignatureLength(scheme, signature.length)) { + throw new ValidateSignatureException( + "pq_auth_sig signature length mismatch for scheme " + scheme); + } + + byte[] derivedAddr = PQSchemeRegistry.computeAddress(scheme, publicKey); + Key matched = null; + for (Key k : witnessPermission.getKeysList()) { + if (Arrays.equals(k.getAddress().toByteArray(), derivedAddr)) { + matched = k; + break; + } + } + if (matched == null) { + throw new ValidateSignatureException( + "pq_auth_sig public key does not match any witness permission key"); + } + + byte[] digest = getRawHash().getBytes(); + return PQSchemeRegistry.verify(scheme, publicKey, digest, signature); + } + public BlockId getBlockId() { if (blockId.equals(Sha256Hash.ZERO_HASH)) { blockId = @@ -325,7 +417,12 @@ public long getTimeStamp() { } public boolean hasWitnessSignature() { - return !getInstance().getBlockHeader().getWitnessSignature().isEmpty(); + BlockHeader header = getInstance().getBlockHeader(); + if (!header.getWitnessSignature().isEmpty()) { + return true; + } + PQAuthSig auth = header.getPqAuthSig(); + return auth != null && !auth.getSignature().isEmpty(); } @Override 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 bb4b70cde1b..24fe9fd9964 100755 --- a/chainbase/src/main/java/org/tron/core/capsule/TransactionCapsule.java +++ b/chainbase/src/main/java/org/tron/core/capsule/TransactionCapsule.java @@ -45,7 +45,9 @@ import org.tron.common.crypto.Rsv; import org.tron.common.crypto.SignInterface; import org.tron.common.crypto.SignUtils; +import org.tron.common.crypto.pqc.PQSchemeRegistry; import org.tron.common.es.ExecutorServiceManager; +import org.tron.common.math.StrictMathWrapper; import org.tron.common.overlay.message.Message; import org.tron.common.parameter.CommonParameter; import org.tron.common.utils.ByteArray; @@ -66,6 +68,8 @@ import org.tron.core.store.AccountStore; import org.tron.core.store.DynamicPropertiesStore; import org.tron.protos.Protocol.Key; +import org.tron.protos.Protocol.PQScheme; +import org.tron.protos.Protocol.PQAuthSig; import org.tron.protos.Protocol.Permission; import org.tron.protos.Protocol.Permission.PermissionType; import org.tron.protos.Protocol.Transaction; @@ -487,11 +491,25 @@ public static boolean validateSignature(Transaction transaction, throw new PermissionException("permission isn't exit"); } checkPermission(permissionId, permission, contract); - long weight = checkWeight(permission, transaction.getSignatureList(), hash, null); - if (weight >= permission.getThreshold()) { - return true; + + // Hybrid weight: ECDSA signatures and PQ witnesses share one threshold + // check. The two domains derive distinct addresses (Keccak vs SHA-256 + // tagged with 0x41), so a key entry contributes to at most one path. + java.util.Set signedAddresses = new java.util.HashSet<>(); + List approveList = new ArrayList<>(); + long weight = checkWeight(permission, transaction.getSignatureList(), hash, approveList); + signedAddresses.addAll(approveList); + + if (transaction.getPqAuthSigCount() > 0) { + try { + weight = StrictMathWrapper.addExact(weight, + validatePQSignature(transaction, permission, signedAddresses, + dynamicPropertiesStore)); + } catch (ArithmeticException e) { + throw new PermissionException("weight overflow"); + } } - return false; + return weight >= permission.getThreshold(); } public void resetResult() { @@ -640,12 +658,20 @@ public boolean validatePubSignature(AccountStore accountStore, DynamicPropertiesStore dynamicPropertiesStore) throws ValidateSignatureException { if (!isVerified) { - if (this.transaction.getSignatureCount() <= 0 - || this.transaction.getRawData().getContractCount() <= 0) { - throw new ValidateSignatureException("miss sig or contract"); + int legacyCount = this.transaction.getSignatureCount(); + int pqCount = this.transaction.getPqAuthSigCount(); + + if (pqCount > 0 && !dynamicPropertiesStore.isAnyPqSchemeAllowed()) { + throw new ValidateSignatureException( + "pq_auth_sig not allowed: no post-quantum scheme is activated"); + } + if (legacyCount == 0 && pqCount == 0) { + throw new ValidateSignatureException("miss sig"); } - if (this.transaction.getSignatureCount() > dynamicPropertiesStore - .getTotalSignNum()) { + if (this.transaction.getRawData().getContractCount() <= 0) { + throw new ValidateSignatureException("miss contract"); + } + if (legacyCount + pqCount > dynamicPropertiesStore.getTotalSignNum()) { throw new ValidateSignatureException("too many signatures"); } @@ -681,6 +707,78 @@ void logSlowSigVerify(long startNs) { } } + /** + * Verify {@code transaction.pq_auth_sig[]} entries against {@code permission} + * and return the combined weight contributed by valid PQ witnesses. + * + *

V2 four-step verification per witness: + *

    + *
  1. Resolve the permission context (caller passes {@code permission}).
  2. + *
  3. Derive the 21-byte address from {@code witness.public_key} via the + * scheme's fingerprint hash.
  4. + *
  5. Match against {@code permission.keys[].address}; reject duplicates + * and addresses already counted by the legacy ECDSA path.
  6. + *
  7. Verify the signature over {@code txid} directly; the + * {@code permission_id} is already bound by {@code txid} since it is + * part of {@code raw_data}.
  8. + *
+ */ + static long validatePQSignature(Transaction transaction, Permission permission, + java.util.Set signedAddresses, + DynamicPropertiesStore dynamicPropertiesStore) + throws PermissionException { + byte[] digest = computeRawHash(transaction).getBytes(); + + long weight = 0L; + for (PQAuthSig witness : transaction.getPqAuthSigList()) { + PQScheme scheme = witness.getScheme(); + if (!PQSchemeRegistry.contains(scheme)) { + throw new PermissionException("unsupported pq scheme: " + scheme); + } + if (!dynamicPropertiesStore.isPqSchemeAllowed(scheme)) { + throw new PermissionException(scheme + " is not activated"); + } + byte[] pk = witness.getPublicKey().toByteArray(); + byte[] sig = witness.getSignature().toByteArray(); + if (pk.length != PQSchemeRegistry.getPublicKeyLength(scheme) + || !PQSchemeRegistry.isValidSignatureLength(scheme, sig.length)) { + throw new PermissionException("public key or signature length mismatch"); + } + byte[] derivedAddr = PQSchemeRegistry.computeAddress(scheme, pk); + ByteString addrBs = ByteString.copyFrom(derivedAddr); + 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."); + } + if (!PQSchemeRegistry.verify(scheme, pk, digest, sig)) { + throw new PermissionException("pq sig invalid"); + } + try { + weight = StrictMathWrapper.addExact(weight, matched.getWeight()); + } catch (ArithmeticException e) { + throw new PermissionException("weight overflow"); + } + } + return weight; + } + + private static Sha256Hash computeRawHash(Transaction transaction) { + return Sha256Hash.of(CommonParameter.getInstance().isECKeyCryptoEngine(), + transaction.getRawData().toByteArray()); + } + /** * validate signature */ diff --git a/chainbase/src/main/java/org/tron/core/db/BandwidthProcessor.java b/chainbase/src/main/java/org/tron/core/db/BandwidthProcessor.java index ece16b25819..1d2987d4d44 100644 --- a/chainbase/src/main/java/org/tron/core/db/BandwidthProcessor.java +++ b/chainbase/src/main/java/org/tron/core/db/BandwidthProcessor.java @@ -140,8 +140,17 @@ public void consume(TransactionCapsule trx, TransactionTrace trace) if (optimizeTxs) { long maxCreateAccountTxSize = dynamicPropertiesStore.getMaxCreateAccountTxSize(); int signatureCount = trx.getInstance().getSignatureCount(); + long sigOverhead = signatureCount * PER_SIGN_LENGTH; + if (trx.getInstance().getPqAuthSigCount() > 0) { + long pqAuthSigBytes = 0L; + for (org.tron.protos.Protocol.PQAuthSig aw + : trx.getInstance().getPqAuthSigList()) { + pqAuthSigBytes += aw.getSerializedSize(); + } + sigOverhead += pqAuthSigBytes; + } long createAccountBytesSize = trx.getInstance().toBuilder().clearRet() - .build().getSerializedSize() - (signatureCount * PER_SIGN_LENGTH); + .build().getSerializedSize() - sigOverhead; if (createAccountBytesSize > maxCreateAccountTxSize) { throw new TooBigTransactionException(String.format( "Too big new account transaction, TxId %s, the size is %d bytes, maxTxSize %d", diff --git a/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java b/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java index 0f74f20d379..ea88128c54a 100644 --- a/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java +++ b/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java @@ -21,6 +21,7 @@ import org.tron.common.utils.Sha256Hash; import org.tron.core.capsule.BytesCapsule; import org.tron.core.config.Parameter.ChainConstant; +import org.tron.protos.Protocol.PQScheme; import org.tron.core.db.TronStoreWithRevoking; import org.tron.core.exception.BadItemException; import org.tron.core.exception.ItemNotFoundException; @@ -258,6 +259,8 @@ public class DynamicPropertiesStore extends TronStoreWithRevoking private static final byte[] TURKISH_KEY_MIGRATION_DONE = "TURKISH_KEY_MIGRATION_DONE".getBytes(); + private static final byte[] ALLOW_FN_DSA_512 = "ALLOW_FN_DSA_512".getBytes(); + @Autowired private DynamicPropertiesStore(@Value("properties") String dbName) { super(dbName); @@ -3083,6 +3086,43 @@ public long getTurkishKeyMigrationDone() { .orElse(0L); } + public long getAllowFnDsa512() { + return Optional.ofNullable(getUnchecked(ALLOW_FN_DSA_512)) + .map(BytesCapsule::getData) + .map(ByteArray::toLong) + .orElse(CommonParameter.getInstance().getAllowFnDsa512()); + } + + public void saveAllowFnDsa512(long value) { + this.put(ALLOW_FN_DSA_512, new BytesCapsule(ByteArray.fromLong(value))); + } + + public boolean allowFnDsa512() { + return getAllowFnDsa512() == 1L; + } + + /** Returns true iff at least one post-quantum signature scheme is currently activated. */ + public boolean isAnyPqSchemeAllowed() { + return allowFnDsa512(); + } + + /** + * Per-scheme governance check. V2 launches with FN-DSA-512 only. Future schemes will + * each get their own flag. + */ + public boolean isPqSchemeAllowed(PQScheme scheme) { + if (scheme == null) { + return false; + } + switch (scheme) { + case UNKNOWN_PQ_SCHEME: // proto3 default → Falcon-512 (see PQSchemeRegistry#resolve) + case FN_DSA_512: + return allowFnDsa512(); + default: + return false; + } + } + private static class DynamicResourceProperties { private static final byte[] ONE_DAY_NET_LIMIT = "ONE_DAY_NET_LIMIT".getBytes(); diff --git a/common/src/main/java/org/tron/common/parameter/CommonParameter.java b/common/src/main/java/org/tron/common/parameter/CommonParameter.java index f2831b4168f..d18f09a9ea7 100644 --- a/common/src/main/java/org/tron/common/parameter/CommonParameter.java +++ b/common/src/main/java/org/tron/common/parameter/CommonParameter.java @@ -671,6 +671,11 @@ public class CommonParameter { @Setter public long allowTvmBlob; + @Getter + @Setter + public long allowFnDsa512; + + private static double calcMaxTimeRatio() { return 5.0; } diff --git a/common/src/main/java/org/tron/core/Constant.java b/common/src/main/java/org/tron/core/Constant.java index 1437d319346..370a442fc06 100644 --- a/common/src/main/java/org/tron/core/Constant.java +++ b/common/src/main/java/org/tron/core/Constant.java @@ -60,6 +60,14 @@ public class Constant { // Crypto engine public static final String ECKey_ENGINE = "ECKey"; + // Post-quantum (FIPS 206 draft) FN-DSA / Falcon-512 signature constants. + // Falcon signatures are variable-length; SIGNATURE_MAX_LENGTH is the protocol-level + // upper bound, not an exact length. + public static final int FN_DSA_PUBLIC_KEY_LENGTH = 896; + public static final int FN_DSA_SIGNATURE_MAX_LENGTH = 752; + public static final String PQ_TX_AUTH_DOMAIN = "TRON_TX_AUTH_V1"; + public static final String PQ_BLOCK_AUTH_DOMAIN = "TRON_BLOCK_AUTH_V1"; + // Network public static final String LOCAL_HOST = "127.0.0.1"; diff --git a/common/src/main/java/org/tron/core/vm/config/VMConfig.java b/common/src/main/java/org/tron/core/vm/config/VMConfig.java index 94c1e50284e..2df06e2dd22 100644 --- a/common/src/main/java/org/tron/core/vm/config/VMConfig.java +++ b/common/src/main/java/org/tron/core/vm/config/VMConfig.java @@ -65,6 +65,8 @@ public class VMConfig { private static boolean ALLOW_HARDEN_RESOURCE_CALCULATION = false; + private static boolean ALLOW_FN_DSA_512 = false; + private VMConfig() { } @@ -184,6 +186,10 @@ public static void initAllowHardenResourceCalculation(long allow) { ALLOW_HARDEN_RESOURCE_CALCULATION = allow == 1; } + public static void initAllowFnDsa512(long allow) { + ALLOW_FN_DSA_512 = allow == 1; + } + public static boolean getEnergyLimitHardFork() { return CommonParameter.ENERGY_LIMIT_HARD_FORK; } @@ -291,4 +297,8 @@ public static boolean allowTvmOsaka() { public static boolean allowHardenResourceCalculation() { return ALLOW_HARDEN_RESOURCE_CALCULATION; } + + public static boolean allowFnDsa512() { + return ALLOW_FN_DSA_512; + } } diff --git a/consensus/src/main/java/org/tron/consensus/base/Param.java b/consensus/src/main/java/org/tron/consensus/base/Param.java index f7b7de3d084..a2692cf4c55 100644 --- a/consensus/src/main/java/org/tron/consensus/base/Param.java +++ b/consensus/src/main/java/org/tron/consensus/base/Param.java @@ -5,6 +5,7 @@ import lombok.Getter; import lombok.Setter; import org.tron.common.args.GenesisBlock; +import org.tron.protos.Protocol.PQScheme; public class Param { @@ -67,11 +68,35 @@ public class Miner { @Setter private ByteString witnessAddress; + private byte[] pqPrivateKey; + + private byte[] pqPublicKey; + + @Getter + @Setter + private PQScheme pqScheme; + public Miner(byte[] privateKey, ByteString privateKeyAddress, ByteString witnessAddress) { this.privateKey = privateKey; this.privateKeyAddress = privateKeyAddress; this.witnessAddress = witnessAddress; } + + public byte[] getPQPrivateKey() { + return pqPrivateKey == null ? null : pqPrivateKey.clone(); + } + + public void setPQPrivateKey(byte[] pqPrivateKey) { + this.pqPrivateKey = pqPrivateKey == null ? null : pqPrivateKey.clone(); + } + + public byte[] getPQPublicKey() { + return pqPublicKey == null ? null : pqPublicKey.clone(); + } + + public void setPQPublicKey(byte[] pqPublicKey) { + this.pqPublicKey = pqPublicKey == null ? null : pqPublicKey.clone(); + } } public Miner getMiner() { diff --git a/crypto/src/main/java/org/tron/common/crypto/pqc/FNDSA.java b/crypto/src/main/java/org/tron/common/crypto/pqc/FNDSA.java new file mode 100644 index 00000000000..3a9e22316c5 --- /dev/null +++ b/crypto/src/main/java/org/tron/common/crypto/pqc/FNDSA.java @@ -0,0 +1,259 @@ +package org.tron.common.crypto.pqc; + +import java.security.SecureRandom; +import org.bouncycastle.crypto.AsymmetricCipherKeyPair; +import org.bouncycastle.crypto.params.ParametersWithRandom; +import org.bouncycastle.crypto.prng.FixedSecureRandom; +import org.bouncycastle.pqc.crypto.falcon.FalconKeyGenerationParameters; +import org.bouncycastle.pqc.crypto.falcon.FalconKeyPairGenerator; +import org.bouncycastle.pqc.crypto.falcon.FalconParameters; +import org.bouncycastle.pqc.crypto.falcon.FalconPrivateKeyParameters; +import org.bouncycastle.pqc.crypto.falcon.FalconPublicKeyParameters; +import org.bouncycastle.pqc.crypto.falcon.FalconSigner; +import org.tron.protos.Protocol.PQScheme; + +/** + * FIPS 206 (draft) FN-DSA / Falcon-512 keypair-bound signer/verifier. Instance + * methods sign/verify with the bound keypair, static {@link #sign(byte[], byte[])} + * / {@link #verify} provide stateless entry points used by + * {@link PQSchemeRegistry}. + * + *

Falcon signatures are variable-length: {@link #SIGNATURE_LENGTH} + * is the protocol-level upper bound, not an exact length. The + * {@link PQSignature#validateSignature} default treats this as + * {@code <= SIGNATURE_LENGTH}. BouncyCastle 1.79's {@code FalconNIST.CRYPTO_BYTES} + * for Falcon-512 is 690 bytes, well below the 752-byte protocol cap. + */ +public final class FNDSA implements PQSignature { + + /** + * Falcon-512 encoded private key from BC: f || g || F, where f and g are each + * {@link #F_G_ENCODED_LENGTH} bytes (6 bits per coefficient × N=512 / 8) and F is + * {@link #BIG_F_ENCODED_LENGTH} bytes (8 bits per coefficient × N=512 / 8). + */ + public static final int F_G_ENCODED_LENGTH = 384; + public static final int BIG_F_ENCODED_LENGTH = 512; + public static final int PRIVATE_KEY_LENGTH = + F_G_ENCODED_LENGTH + F_G_ENCODED_LENGTH + BIG_F_ENCODED_LENGTH; + /** + * Falcon-512 public key from BC: 14 * N / 8 = 896 bytes (the modq-encoded h polynomial). + * The 1-byte serialization header is stripped from {@code getH()}. + */ + public static final int PUBLIC_KEY_LENGTH = 896; + /** + * Extended private key encoding {@code f ‖ g ‖ F ‖ h}: the standard BC private key + * (1280 B) with the 896-byte public key {@code h} appended. Lets the holder recover + * the address without re-running keygen, since BC currently has no public API for + * deriving {@code h} from {@code (f, g)} alone (see bcgit/bc-java#2297). + */ + public static final int PRIVATE_KEY_WITH_PUBLIC_KEY_LENGTH = + PRIVATE_KEY_LENGTH + PUBLIC_KEY_LENGTH; + /** Protocol-level upper bound on Falcon-512 signature length (variable). */ + public static final int SIGNATURE_LENGTH = 752; + /** Falcon keygen seeds an internal SHAKE256 from 48 bytes of randomness. */ + public static final int SEED_LENGTH = 48; + + private static final FalconParameters PARAMS = FalconParameters.falcon_512; + + private final byte[] privateKey; + private final byte[] publicKey; + + public FNDSA() { + AsymmetricCipherKeyPair kp = generateKeyPair(new SecureRandom()); + this.privateKey = ((FalconPrivateKeyParameters) kp.getPrivate()).getEncoded(); + this.publicKey = ((FalconPublicKeyParameters) kp.getPublic()).getH(); + } + + public FNDSA(byte[] seed) { + if (seed == null || seed.length != SEED_LENGTH) { + throw new IllegalArgumentException("FN-DSA seed length must be " + SEED_LENGTH); + } + AsymmetricCipherKeyPair kp = generateKeyPair(new FixedSecureRandom(seed)); + this.privateKey = ((FalconPrivateKeyParameters) kp.getPrivate()).getEncoded(); + this.publicKey = ((FalconPublicKeyParameters) kp.getPublic()).getH(); + } + + public FNDSA(byte[] privateKey, byte[] publicKey) { + if (privateKey == null || privateKey.length != PRIVATE_KEY_LENGTH) { + throw new IllegalArgumentException( + "FN-DSA private key length must be " + PRIVATE_KEY_LENGTH); + } + if (publicKey == null || publicKey.length != PUBLIC_KEY_LENGTH) { + throw new IllegalArgumentException( + "FN-DSA public key length must be " + PUBLIC_KEY_LENGTH); + } + this.privateKey = privateKey.clone(); + this.publicKey = publicKey.clone(); + } + + /** + * Builds an instance from the extended private key encoding {@code f ‖ g ‖ F ‖ h} + * ({@link #PRIVATE_KEY_WITH_PUBLIC_KEY_LENGTH} bytes), as produced by + * {@link #getPrivateKeyWithPublicKey()}. Provided as a static factory rather + * than an additional {@code FNDSA(byte[])} constructor because Java cannot + * overload {@link #FNDSA(byte[]) the seed constructor} on length alone. + */ + public static FNDSA fromPrivateKeyWithPublicKey(byte[] extendedPrivateKey) { + if (extendedPrivateKey == null + || extendedPrivateKey.length != PRIVATE_KEY_WITH_PUBLIC_KEY_LENGTH) { + throw new IllegalArgumentException( + "FN-DSA extended private key length must be " + + PRIVATE_KEY_WITH_PUBLIC_KEY_LENGTH); + } + byte[] sk = new byte[PRIVATE_KEY_LENGTH]; + byte[] pk = new byte[PUBLIC_KEY_LENGTH]; + System.arraycopy(extendedPrivateKey, 0, sk, 0, PRIVATE_KEY_LENGTH); + System.arraycopy(extendedPrivateKey, PRIVATE_KEY_LENGTH, pk, 0, PUBLIC_KEY_LENGTH); + return new FNDSA(sk, pk); + } + + @Override + public PQScheme getScheme() { + return PQScheme.FN_DSA_512; + } + + @Override + public int getPrivateKeyLength() { + return PRIVATE_KEY_LENGTH; + } + + @Override + public int getPublicKeyLength() { + return PUBLIC_KEY_LENGTH; + } + + /** Returns the protocol-level signature length upper bound (signatures are variable-length). */ + @Override + public int getSignatureLength() { + return SIGNATURE_LENGTH; + } + + @Override + public byte[] getPrivateKey() { + return privateKey.clone(); + } + + /** + * Returns the private key with the 896-byte public key {@code h} appended: + * {@code f ‖ g ‖ F ‖ h} (total {@link #PRIVATE_KEY_WITH_PUBLIC_KEY_LENGTH} bytes). + * Use this format on disk / in config when the consumer needs to recover the + * address from the private key alone — neither BC's encoded private key nor + * the 48-byte keygen seed (without re-running keygen) suffice today. + */ + public byte[] getPrivateKeyWithPublicKey() { + byte[] out = new byte[PRIVATE_KEY_WITH_PUBLIC_KEY_LENGTH]; + System.arraycopy(privateKey, 0, out, 0, PRIVATE_KEY_LENGTH); + System.arraycopy(publicKey, 0, out, PRIVATE_KEY_LENGTH, PUBLIC_KEY_LENGTH); + return out; + } + + @Override + public byte[] getPublicKey() { + return publicKey.clone(); + } + + @Override + public byte[] getAddress() { + return PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, publicKey); + } + + @Override + public byte[] sign(byte[] message) { + return sign(privateKey, message); + } + + @Override + public boolean verify(byte[] message, byte[] signature) { + return verify(publicKey, message, signature); + } + + public static boolean verify(byte[] publicKey, byte[] message, byte[] signature) { + if (publicKey == null || publicKey.length != PUBLIC_KEY_LENGTH) { + throw new IllegalArgumentException( + "FN-DSA public key length must be " + PUBLIC_KEY_LENGTH); + } + if (signature == null || signature.length == 0 || signature.length > SIGNATURE_LENGTH) { + throw new IllegalArgumentException( + "FN-DSA signature length must be 1.." + SIGNATURE_LENGTH); + } + if (message == null) { + throw new IllegalArgumentException("message must not be null"); + } + FalconPublicKeyParameters pk = new FalconPublicKeyParameters(PARAMS, publicKey); + FalconSigner verifier = new FalconSigner(); + verifier.init(false, pk); + try { + return verifier.verifySignature(message, signature); + } catch (RuntimeException e) { + return false; + } + } + + /** + * Signs {@code message} using either the bare private key + * ({@link #PRIVATE_KEY_LENGTH} bytes, {@code f ‖ g ‖ F}) or the extended form + * ({@link #PRIVATE_KEY_WITH_PUBLIC_KEY_LENGTH} bytes, {@code f ‖ g ‖ F ‖ h}). + * The trailing {@code h} segment is ignored — only {@code (f, g, F)} feed BC's signer. + */ + public static byte[] sign(byte[] privateKey, byte[] message) { + validatePrivateKeyBytes(privateKey); + if (message == null) { + throw new IllegalArgumentException("message must not be null"); + } + byte[] f = new byte[F_G_ENCODED_LENGTH]; + byte[] g = new byte[F_G_ENCODED_LENGTH]; + byte[] bigF = new byte[BIG_F_ENCODED_LENGTH]; + System.arraycopy(privateKey, 0, f, 0, f.length); + System.arraycopy(privateKey, f.length, g, 0, g.length); + System.arraycopy(privateKey, f.length + g.length, bigF, 0, bigF.length); + FalconPrivateKeyParameters sk = new FalconPrivateKeyParameters(PARAMS, f, g, bigF, new byte[0]); + FalconSigner signer = new FalconSigner(); + signer.init(true, new ParametersWithRandom(sk, new SecureRandom())); + try { + return signer.generateSignature(message); + } catch (Exception e) { + throw new IllegalStateException("FN-DSA signing failed", e); + } + } + + /** + * Recovers the public key when the input is in the extended form + * {@code f ‖ g ‖ F ‖ h} ({@link #PRIVATE_KEY_WITH_PUBLIC_KEY_LENGTH} bytes). + * Throws {@link UnsupportedOperationException} for the bare {@code f ‖ g ‖ F} + * form: BouncyCastle currently has no public API to compute {@code h = g · f⁻¹} + * mod q, so callers must persist {@code h} alongside the private key (use + * {@link #getPrivateKeyWithPublicKey()}) or re-run keygen from a stored seed. + * See bcgit/bc-java#2297. + */ + public static byte[] derivePublicKey(byte[] privateKey) { + if (privateKey != null && privateKey.length == PRIVATE_KEY_WITH_PUBLIC_KEY_LENGTH) { + byte[] pk = new byte[PUBLIC_KEY_LENGTH]; + System.arraycopy(privateKey, PRIVATE_KEY_LENGTH, pk, 0, PUBLIC_KEY_LENGTH); + return pk; + } + throw new UnsupportedOperationException( + "FN-DSA public key cannot be derived from the bare encoded private key; " + + "supply the extended form (f ‖ g ‖ F ‖ h) or both halves to the " + + "(privateKey, publicKey) constructor"); + } + + public static byte[] computeAddress(byte[] publicKey) { + return PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, publicKey); + } + + private static AsymmetricCipherKeyPair generateKeyPair(SecureRandom random) { + FalconKeyPairGenerator generator = new FalconKeyPairGenerator(); + generator.init(new FalconKeyGenerationParameters(random, PARAMS)); + return generator.generateKeyPair(); + } + + private static void validatePrivateKeyBytes(byte[] privateKey) { + if (privateKey == null + || (privateKey.length != PRIVATE_KEY_LENGTH + && privateKey.length != PRIVATE_KEY_WITH_PUBLIC_KEY_LENGTH)) { + throw new IllegalArgumentException( + "FN-DSA private key length must be " + PRIVATE_KEY_LENGTH + + " or " + PRIVATE_KEY_WITH_PUBLIC_KEY_LENGTH); + } + } +} diff --git a/crypto/src/main/java/org/tron/common/crypto/pqc/PQSchemeRegistry.java b/crypto/src/main/java/org/tron/common/crypto/pqc/PQSchemeRegistry.java new file mode 100644 index 00000000000..b3965ac4cb8 --- /dev/null +++ b/crypto/src/main/java/org/tron/common/crypto/pqc/PQSchemeRegistry.java @@ -0,0 +1,223 @@ +package org.tron.common.crypto.pqc; + +import java.util.Collections; +import java.util.EnumMap; +import java.util.Map; +import org.tron.common.crypto.Hash; +import org.tron.protos.Protocol.PQScheme; + +/** + * Static dispatch table for post-quantum signature schemes keyed by + * {@link PQScheme}. Each entry binds a scheme to its public-key length, + * signature length, seed length, fingerprint hash function, and stateless + * sign/verify/keygen operations. Legacy ECDSA secp256k1 / SM2 schemes are NOT + * registered — they flow through the existing {@code SignInterface} path. + * + *

Address binding (V2). A PQ-derived TRON address is + * {@code 0x41 ‖ deriveHash(scheme, public_key)[12..32]}, matching the ECDSA + * flow's {@code 0x41 ‖ Keccak-256(public_key)[12..32]} so PQ and ECDSA + * addresses share the same derivation shape. The hash function is scheme- + * specific (see {@link #deriveHash}); {@code FN_DSA_512} uses Keccak-256. + * + *

Wire-format default. {@code UNKNOWN_PQ_SCHEME = 0} is the proto3 + * default (reserved for the {@code UNKNOWN_} API-evolution slot); on the wire + * it is interpreted as {@code FN_DSA_512} so V2-launch witnesses pay zero + * bytes for the scheme tag. All public methods normalize via + * {@link #resolve(PQScheme)} before dispatch. + */ +public final class PQSchemeRegistry { + + /** Stateless sign/verify/keygen dispatch bound to a single PQ scheme. */ + public interface SignatureOps { + byte[] sign(byte[] privateKey, byte[] message); + + boolean verify(byte[] publicKey, byte[] message, byte[] signature); + + PQSignature fromSeed(byte[] seed); + + PQSignature fromKeypair(byte[] privateKey, byte[] publicKey); + } + + /** + * Fingerprint hash used to derive a 21-byte TRON address from a PQ public key. + * V2 first launch uses Keccak-256 for FN_DSA_512 to match the ECDSA address + * derivation; later schemes may bind to a different hash if the PQ scheme has + * its own canonical fingerprint. + */ + public interface FingerprintHash { + /** Returns the full digest of {@code data} (no truncation). */ + byte[] digest(byte[] data); + } + + private static final FingerprintHash KECCAK_256 = Hash::sha3; + + private static final class SchemeInfo { + final int privateKeyLength; + final int publicKeyLength; + final int signatureLength; + final int seedLength; + final FingerprintHash hash; + final SignatureOps ops; + + SchemeInfo(int privateKeyLength, int publicKeyLength, int signatureLength, + int seedLength, FingerprintHash hash, SignatureOps ops) { + this.privateKeyLength = privateKeyLength; + this.publicKeyLength = publicKeyLength; + this.signatureLength = signatureLength; + this.seedLength = seedLength; + this.hash = hash; + this.ops = ops; + } + } + + private static final Map SCHEMES; + + static { + EnumMap m = new EnumMap<>(PQScheme.class); + m.put(PQScheme.FN_DSA_512, new SchemeInfo( + FNDSA.PRIVATE_KEY_LENGTH, FNDSA.PUBLIC_KEY_LENGTH, + FNDSA.SIGNATURE_LENGTH, FNDSA.SEED_LENGTH, + KECCAK_256, + new SignatureOps() { + @Override + public byte[] sign(byte[] privateKey, byte[] message) { + return FNDSA.sign(privateKey, message); + } + + @Override + public boolean verify(byte[] publicKey, byte[] message, byte[] signature) { + return FNDSA.verify(publicKey, message, signature); + } + + @Override + public PQSignature fromSeed(byte[] seed) { + return new FNDSA(seed); + } + + @Override + public PQSignature fromKeypair(byte[] privateKey, byte[] publicKey) { + return new FNDSA(privateKey, publicKey); + } + })); + SCHEMES = Collections.unmodifiableMap(m); + } + + private PQSchemeRegistry() { + } + + /** + * Map a wire-format {@link PQScheme} to its registered scheme. The proto3 + * default {@code UNKNOWN_PQ_SCHEME} is normalized to {@code FN_DSA_512} so + * V2-launch witnesses that omit the scheme tag are decoded as Falcon-512. + * {@code null} and {@code UNRECOGNIZED} pass through unchanged so the + * caller-side {@code contains}/{@code require} checks reject them. + */ + public static PQScheme resolve(PQScheme scheme) { + if (scheme == PQScheme.UNKNOWN_PQ_SCHEME) { + return PQScheme.FN_DSA_512; + } + return scheme; + } + + public static boolean contains(PQScheme scheme) { + PQScheme resolved = resolve(scheme); + return resolved != null && SCHEMES.containsKey(resolved); + } + + public static int getPrivateKeyLength(PQScheme scheme) { + return require(scheme).privateKeyLength; + } + + public static int getPublicKeyLength(PQScheme scheme) { + return require(scheme).publicKeyLength; + } + + public static int getSignatureLength(PQScheme scheme) { + return require(scheme).signatureLength; + } + + public static int getSeedLength(PQScheme scheme) { + return require(scheme).seedLength; + } + + /** + * Per-scheme signature-length predicate. Fixed-length schemes require exact + * equality with {@link #getSignatureLength(PQScheme)}; variable-length + * schemes ({@code FN_DSA_512}) treat that value as an upper bound and accept + * any {@code 1..max}. + */ + public static boolean isValidSignatureLength(PQScheme scheme, int length) { + PQScheme resolved = resolve(scheme); + SchemeInfo info = require(resolved); + if (resolved == PQScheme.FN_DSA_512) { + return length > 0 && length <= info.signatureLength; + } + return length == info.signatureLength; + } + + public static byte[] sign(PQScheme scheme, byte[] privateKey, byte[] message) { + return require(scheme).ops.sign(privateKey, message); + } + + public static boolean verify( + PQScheme scheme, byte[] publicKey, byte[] message, byte[] signature) { + return require(scheme).ops.verify(publicKey, message, signature); + } + + public static PQSignature fromSeed(PQScheme scheme, byte[] seed) { + return require(scheme).ops.fromSeed(seed); + } + + /** + * Build a keypair-bound {@link PQSignature} from already-derived private and + * public key bytes. Used by the witness-config path when the operator has + * pre-computed the keypair off-line and wants to bypass on-node keygen. + * Validates {@code privateKey} and {@code publicKey} lengths against the + * scheme; cryptographic consistency between the two halves is the caller's + * responsibility. + */ + public static PQSignature fromKeypair( + PQScheme scheme, byte[] privateKey, byte[] publicKey) { + return require(scheme).ops.fromKeypair(privateKey, publicKey); + } + + /** + * Scheme-dispatched fingerprint hash of a PQ public key. Returns the full + * digest; callers truncate to 20 bytes when deriving the address suffix. + */ + public static byte[] deriveHash(PQScheme scheme, byte[] publicKey) { + SchemeInfo info = require(scheme); + if (publicKey == null || publicKey.length != info.publicKeyLength) { + throw new IllegalArgumentException( + "invalid public key length for " + scheme + ": " + + (publicKey == null ? -1 : publicKey.length)); + } + return info.hash.digest(publicKey); + } + + /** + * Derive the 21-byte TRON address from a PQ public key as + * {@code 0x41 ‖ deriveHash(scheme, public_key)[12..32]} — the rightmost 20 + * bytes of the digest, matching the ECDSA address derivation slice. + */ + public static byte[] computeAddress(PQScheme scheme, byte[] publicKey) { + byte[] h = deriveHash(scheme, publicKey); + byte[] addr = new byte[21]; + addr[0] = 0x41; + System.arraycopy(h, h.length - 20, addr, 1, 20); + return addr; + } + + private static SchemeInfo require(PQScheme scheme) { + if (scheme == null) { + throw new IllegalArgumentException("scheme must not be null"); + } + PQScheme resolved = resolve(scheme); + SchemeInfo info = SCHEMES.get(resolved); + if (info == null) { + throw new IllegalArgumentException( + "no PQSignature registered for scheme: " + scheme); + } + return info; + } +} diff --git a/crypto/src/main/java/org/tron/common/crypto/pqc/PQSignature.java b/crypto/src/main/java/org/tron/common/crypto/pqc/PQSignature.java new file mode 100644 index 00000000000..dbba2683ef0 --- /dev/null +++ b/crypto/src/main/java/org/tron/common/crypto/pqc/PQSignature.java @@ -0,0 +1,72 @@ +package org.tron.common.crypto.pqc; + +import org.tron.protos.Protocol.PQScheme; + +/** + * Post-quantum signature scheme facade bound to a keypair. Instance methods + * (sign/verify/getAddress/getPublicKey/getPrivateKey) operate on the held + * keypair. Stateless dispatch by {@link PQScheme} is provided by + * {@link PQSchemeRegistry}. + */ +public interface PQSignature { + + PQScheme getScheme(); + + int getPrivateKeyLength(); + + int getPublicKeyLength(); + + int getSignatureLength(); + + byte[] getPrivateKey(); + + byte[] getPublicKey(); + + /** + * 21-byte TRON address derived from the held public key as + * {@code 0x41 ‖ deriveHash(scheme, public_key)[12..32]} (see + * {@link PQSchemeRegistry#computeAddress}). + */ + byte[] getAddress(); + + /** Sign {@code message} with the held private key; returns the raw signature. */ + byte[] sign(byte[] message); + + /** + * Verify {@code signature} over {@code message} against the held public key. + * + * @return true iff the signature is cryptographically valid for the bound keypair + */ + boolean verify(byte[] message, byte[] signature); + + default void validatePrivateKey(byte[] privateKey) { + if (privateKey == null || privateKey.length != getPrivateKeyLength()) { + throw new IllegalArgumentException( + "invalid " + getScheme() + " private key length: " + + (privateKey == null ? "null" : privateKey.length) + + ", expected " + getPrivateKeyLength()); + } + } + + default void validatePublicKey(byte[] publicKey) { + if (publicKey == null || publicKey.length != getPublicKeyLength()) { + throw new IllegalArgumentException( + "invalid " + getScheme() + " public key length: " + + (publicKey == null ? "null" : publicKey.length) + + ", expected " + getPublicKeyLength()); + } + } + + /** + * Default upper-bound check, sufficient for variable-length schemes (FN_DSA_512). + * Fixed-length schemes override this with strict equality. + */ + default void validateSignature(byte[] signature) { + if (signature == null || signature.length == 0 || signature.length > getSignatureLength()) { + throw new IllegalArgumentException( + "invalid " + getScheme() + " signature length: " + + (signature == null ? "null" : signature.length) + + ", expected 1.." + getSignatureLength()); + } + } +} diff --git a/framework/src/main/java/org/tron/core/Wallet.java b/framework/src/main/java/org/tron/core/Wallet.java index 0482643d8d0..3f8766f8ac4 100755 --- a/framework/src/main/java/org/tron/core/Wallet.java +++ b/framework/src/main/java/org/tron/core/Wallet.java @@ -1495,6 +1495,11 @@ public Protocol.ChainParameters getChainParameters() { .setValue(dbManager.getDynamicPropertiesStore().getAllowHardenExchangeCalculation()) .build()); + builder.addChainParameter(Protocol.ChainParameters.ChainParameter.newBuilder() + .setKey("getAllowFnDsa512") + .setValue(dbManager.getDynamicPropertiesStore().getAllowFnDsa512()) + .build()); + return builder.build(); } diff --git a/framework/src/main/java/org/tron/core/config/args/Args.java b/framework/src/main/java/org/tron/core/config/args/Args.java index de8b7dba1ad..d6ef74c6ef7 100644 --- a/framework/src/main/java/org/tron/core/config/args/Args.java +++ b/framework/src/main/java/org/tron/core/config/args/Args.java @@ -21,6 +21,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.EnumSet; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; @@ -43,6 +44,7 @@ import org.tron.common.args.GenesisBlock; import org.tron.common.args.Witness; import org.tron.common.cron.CronExpression; +import org.tron.common.crypto.pqc.PQSchemeRegistry; import org.tron.common.logsfilter.EventPluginConfig; import org.tron.common.logsfilter.FilterQuery; import org.tron.common.logsfilter.TriggerConfig; @@ -63,6 +65,7 @@ import org.tron.p2p.dns.update.PublishConfig; import org.tron.p2p.utils.NetUtil; import org.tron.program.Version; +import org.tron.protos.Protocol.PQScheme; @Slf4j(topic = "app") @NoArgsConstructor @@ -778,6 +781,10 @@ public static void applyConfigParams( eventConfig = EventConfig.fromConfig(config); applyEventConfig(eventConfig); + PARAMETER.allowFnDsa512 = + config.hasPath(ConfigKey.COMMITTEE_ALLOW_FN_DSA_512) ? config + .getInt(ConfigKey.COMMITTEE_ALLOW_FN_DSA_512) : 0; + logConfig(); } @@ -918,6 +925,9 @@ private static void applyCLIParams(CLIParameter cmd, JCommander jc) { } } + private static final EnumSet WITNESS_PQ_SCHEMES = EnumSet.of( + PQScheme.FN_DSA_512); + private static void initLocalWitnesses(Config config, CLIParameter cmd) { // not a witness node, skip if (!PARAMETER.isWitness()) { @@ -948,6 +958,59 @@ private static void initLocalWitnesses(Config config, CLIParameter cmd) { return; } + // path 4: PQ pre-derived keypair configuration + if (config.hasPath(ConfigKey.LOCAL_WITNESS_PQ_KEYS)) { + List pqEntries = config.getStringList(ConfigKey.LOCAL_WITNESS_PQ_KEYS); + if (!pqEntries.isEmpty()) { + localWitnesses = new LocalWitnesses(); + // Scheme must be applied before keypairs — key-length validation depends on it. + if (config.hasPath(ConfigKey.LOCAL_WITNESS_PQ_SCHEME)) { + String schemeName = config.getString(ConfigKey.LOCAL_WITNESS_PQ_SCHEME); + try { + PQScheme scheme = PQScheme.valueOf(schemeName); + if (!WITNESS_PQ_SCHEMES.contains(scheme)) { + throw new TronError("invalid " + ConfigKey.LOCAL_WITNESS_PQ_SCHEME + + ": " + schemeName + "; valid values: " + WITNESS_PQ_SCHEMES, + TronError.ErrCode.WITNESS_INIT); + } + localWitnesses.setPqScheme(scheme); + } catch (IllegalArgumentException e) { + throw new TronError("invalid " + ConfigKey.LOCAL_WITNESS_PQ_SCHEME + + ": " + schemeName, TronError.ErrCode.WITNESS_INIT); + } + } + // Each entry is the extended private key f‖g‖F‖h (priv ‖ pub) hex, + // sized (privLen + pubLen) bytes for the active scheme. We split here + // so downstream consumers (ConsensusService, LocalWitnesses) keep the + // same priv/pub split they already use — derivePublicKey(priv) replaces + // the previous explicit `pub` config field. + PQScheme scheme = localWitnesses.getPqScheme(); + int privHexLen = PQSchemeRegistry.getPrivateKeyLength(scheme) * 2; + int extHexLen = privHexLen + PQSchemeRegistry.getPublicKeyLength(scheme) * 2; + List pqPrivateKeys = new ArrayList<>(pqEntries.size()); + List pqPublicKeys = new ArrayList<>(pqEntries.size()); + for (int i = 0; i < pqEntries.size(); i++) { + String hex = pqEntries.get(i); + String stripped = hex != null && hex.startsWith("0x") ? hex.substring(2) : hex; + if (stripped == null || stripped.length() != extHexLen) { + throw new TronError(String.format( + "%s[%d] must be %d hex chars (extended priv‖pub for %s), actual: %d", + ConfigKey.LOCAL_WITNESS_PQ_KEYS, i, extHexLen, scheme, + stripped == null ? 0 : stripped.length()), + TronError.ErrCode.WITNESS_INIT); + } + pqPrivateKeys.add(stripped.substring(0, privHexLen)); + pqPublicKeys.add(stripped.substring(privHexLen)); + } + localWitnesses.setPqKeypairs(pqPrivateKeys, pqPublicKeys); + byte[] address = WitnessInitializer.resolvePqAuthSigAddress(lwConfig.getAccountAddress()); + if (address != null) { + localWitnesses.setWitnessAccountAddress(address); + } + return; + } + } + // no private key source configured throw new TronError("This is a witness node, but localWitnesses is null", TronError.ErrCode.WITNESS_INIT); diff --git a/framework/src/main/java/org/tron/core/config/args/ConfigKey.java b/framework/src/main/java/org/tron/core/config/args/ConfigKey.java new file mode 100644 index 00000000000..71fb1c907a6 --- /dev/null +++ b/framework/src/main/java/org/tron/core/config/args/ConfigKey.java @@ -0,0 +1,13 @@ +package org.tron.core.config.args; + +public final class ConfigKey { + + public static final String COMMITTEE_ALLOW_FN_DSA_512 = "committee.allowFnDsa512"; + + public static final String LOCAL_WITNESS_PQ_KEYS = "localwitness_pq_keys"; + + public static final String LOCAL_WITNESS_PQ_SCHEME = "localwitness_pq_scheme"; + + private ConfigKey() { + } +} diff --git a/framework/src/main/java/org/tron/core/config/args/WitnessInitializer.java b/framework/src/main/java/org/tron/core/config/args/WitnessInitializer.java index c2ce2ba0046..f8068511c0d 100644 --- a/framework/src/main/java/org/tron/core/config/args/WitnessInitializer.java +++ b/framework/src/main/java/org/tron/core/config/args/WitnessInitializer.java @@ -112,6 +112,45 @@ public static LocalWitnesses initFromKeystore( return witnesses; } + /** + * Init for PQ-only witness nodes (no legacy ECDSA key). The witness account + * address must be supplied explicitly because there is no ECDSA key to derive it from. + */ + public static LocalWitnesses initFromPQOnly(String witnessAccountAddress) { + if (StringUtils.isBlank(witnessAccountAddress)) { + throw new TronError( + "localWitnessAccountAddress must be set for PQ-only witness nodes", + TronError.ErrCode.WITNESS_INIT); + } + byte[] address = Commons.decodeFromBase58Check(witnessAccountAddress); + if (address == null) { + throw new TronError( + "LocalWitnessAccountAddress format is incorrect", + TronError.ErrCode.WITNESS_INIT); + } + LocalWitnesses witnesses = new LocalWitnesses(); + witnesses.initWitnessAccountAddress(address, false); + logger.debug("Initialised PQ-only witness with address {}", witnessAccountAddress); + return witnesses; + } + + /** + * Resolve witness address for PQ seed configuration. + */ + public static byte[] resolvePqAuthSigAddress(String witnessAccountAddress) { + if (StringUtils.isEmpty(witnessAccountAddress)) { + return null; + } + byte[] address = Commons.decodeFromBase58Check(witnessAccountAddress); + if (address != null) { + logger.debug("Got localWitnessAccountAddress from config.conf"); + } else { + throw new TronError("LocalWitnessAccountAddress format from config is incorrect", + TronError.ErrCode.WITNESS_INIT); + } + return address; + } + static byte[] resolveWitnessAddress( LocalWitnesses witnesses, String witnessAccountAddress) { if (StringUtils.isEmpty(witnessAccountAddress)) { diff --git a/framework/src/main/java/org/tron/core/consensus/ConsensusService.java b/framework/src/main/java/org/tron/core/consensus/ConsensusService.java index ef8f30ef498..5f54b62b955 100644 --- a/framework/src/main/java/org/tron/core/consensus/ConsensusService.java +++ b/framework/src/main/java/org/tron/core/consensus/ConsensusService.java @@ -10,13 +10,17 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.tron.common.crypto.SignUtils; +import org.tron.common.crypto.pqc.PQSchemeRegistry; +import org.tron.common.crypto.pqc.PQSignature; import org.tron.common.parameter.CommonParameter; import org.tron.consensus.Consensus; import org.tron.consensus.base.Param; import org.tron.consensus.base.Param.Miner; import org.tron.core.capsule.WitnessCapsule; import org.tron.core.config.args.Args; +import org.tron.core.exception.TronError; import org.tron.core.store.WitnessStore; +import org.tron.protos.Protocol.PQScheme; @Slf4j(topic = "consensus") @Component @@ -46,6 +50,14 @@ public void start() { param.setAgreeNodeCount(parameter.getAgreeNodeCount()); List miners = new ArrayList<>(); List privateKeys = Args.getLocalWitnesses().getPrivateKeys(); + List pqPrivateKeys = Args.getLocalWitnesses().getPqPrivateKeys(); + List pqPublicKeys = Args.getLocalWitnesses().getPqPublicKeys(); + if (pqPublicKeys.size() != pqPrivateKeys.size()) { + throw new TronError( + "localwitness_pq_keys size mismatch: " + pqPrivateKeys.size() + + " private vs " + pqPublicKeys.size() + " public", + TronError.ErrCode.WITNESS_INIT); + } if (privateKeys.size() > 1) { for (String key : privateKeys) { byte[] privateKey = fromHexString(key); @@ -76,6 +88,31 @@ public void start() { Miner miner = param.new Miner(privateKey, ByteString.copyFrom(privateKeyAddress), ByteString.copyFrom(witnessAddress)); miners.add(miner); + } else if (pqPrivateKeys.size() > 1) { + PQScheme scheme = Args.getLocalWitnesses().getPqScheme(); + requireSupportedPqScheme(scheme); + for (int i = 0; i < pqPrivateKeys.size(); i++) { + byte[] privBytes = fromHexString(pqPrivateKeys.get(i)); + byte[] pubBytes = fromHexString(pqPublicKeys.get(i)); + PQSignature keypair = PQSchemeRegistry.fromKeypair(scheme, privBytes, pubBytes); + byte[] sk = keypair.getPrivateKey(); + byte[] pk = keypair.getPublicKey(); + byte[] pqAddress = keypair.getAddress(); + WitnessCapsule witnessCapsule = witnessStore.get(pqAddress); + if (null == witnessCapsule) { + logger.warn("Witness {} is not in witnessStore.", Hex.toHexString(pqAddress)); + } + ByteString pqAddressBs = ByteString.copyFrom(pqAddress); + Miner miner = param.new Miner(null, pqAddressBs, pqAddressBs); + miner.setPQPrivateKey(sk); + miner.setPQPublicKey(pk); + miner.setPqScheme(scheme); + miners.add(miner); + logger.info("Add {} witness (from configured keypair): {}, size: {}", + scheme, Hex.toHexString(pqAddress), miners.size()); + } + } else if (pqPrivateKeys.size() == 1) { + miners.add(buildPQOnlyMinerFromKeypair(param, pqPrivateKeys.get(0), pqPublicKeys.get(0))); } param.setMiners(miners); @@ -85,6 +122,42 @@ public void start() { logger.info("consensus service start success"); } + private Miner buildPQOnlyMinerFromKeypair(Param param, String pqPrivateKey, + String pqPublicKey) { + PQScheme scheme = Args.getLocalWitnesses().getPqScheme(); + requireSupportedPqScheme(scheme); + byte[] privBytes = fromHexString(pqPrivateKey); + byte[] pubBytes = fromHexString(pqPublicKey); + PQSignature keypair = PQSchemeRegistry.fromKeypair(scheme, privBytes, pubBytes); + byte[] sk = keypair.getPrivateKey(); + byte[] pk = keypair.getPublicKey(); + byte[] pqAddress = keypair.getAddress(); + byte[] witnessAddress = Args.getLocalWitnesses().getWitnessAccountAddress(); + if (witnessAddress == null || witnessAddress.length == 0) { + witnessAddress = pqAddress; + } + WitnessCapsule witnessCapsule = witnessStore.get(witnessAddress); + if (null == witnessCapsule) { + logger.warn("Witness {} is not in witnessStore.", Hex.toHexString(witnessAddress)); + } + // In multi-signature mode, the address derived from the PQ key may differ from witnessAddress. + Miner miner = param.new Miner(null, ByteString.copyFrom(pqAddress), + ByteString.copyFrom(witnessAddress)); + miner.setPQPrivateKey(sk); + miner.setPQPublicKey(pk); + miner.setPqScheme(scheme); + logger.info("Add {} witness (from configured keypair): {}", + scheme, Hex.toHexString(witnessAddress)); + return miner; + } + + private static void requireSupportedPqScheme(PQScheme scheme) { + if (!PQSchemeRegistry.contains(scheme)) { + throw new TronError("unsupported PQ witness scheme: " + scheme, + TronError.ErrCode.WITNESS_INIT); + } + } + public void stop() { logger.info("consensus service closed start."); consensus.stop(); diff --git a/framework/src/main/java/org/tron/core/consensus/ProposalService.java b/framework/src/main/java/org/tron/core/consensus/ProposalService.java index 543deab2fc6..42e24a767be 100644 --- a/framework/src/main/java/org/tron/core/consensus/ProposalService.java +++ b/framework/src/main/java/org/tron/core/consensus/ProposalService.java @@ -412,6 +412,10 @@ public static boolean process(Manager manager, ProposalCapsule proposalCapsule) .saveAllowHardenExchangeCalculation(entry.getValue()); break; } + case ALLOW_FN_DSA_512: { + manager.getDynamicPropertiesStore().saveAllowFnDsa512(entry.getValue()); + break; + } default: find = false; break; 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 a534b9d1c5d..d6bdffb62ec 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -55,6 +55,7 @@ import org.tron.common.args.GenesisBlock; import org.tron.common.bloom.Bloom; import org.tron.common.cron.CronExpression; +import org.tron.common.crypto.pqc.PQSchemeRegistry; import org.tron.common.es.ExecutorServiceManager; import org.tron.common.exit.ExitManager; import org.tron.common.logsfilter.EventPluginLoader; @@ -171,6 +172,8 @@ import org.tron.core.utils.TransactionRegister; import org.tron.protos.Protocol; import org.tron.protos.Protocol.AccountType; +import org.tron.protos.Protocol.PQAuthSig; +import org.tron.protos.Protocol.PQScheme; import org.tron.protos.Protocol.Permission; import org.tron.protos.Protocol.Transaction; import org.tron.protos.Protocol.Transaction.Contract; @@ -1740,7 +1743,7 @@ public BlockCapsule generateBlock(Miner miner, long blockTime, long timeout) { session.reset(); blockCapsule.setMerkleRoot(); - blockCapsule.sign(miner.getPrivateKey()); + signBlockCapsule(blockCapsule, miner); BlockCapsule capsule = new BlockCapsule(blockCapsule.getInstance()); capsule.generatedByMyself = true; @@ -1756,6 +1759,60 @@ public BlockCapsule generateBlock(Miner miner, long blockTime, long timeout) { return capsule; } + private void signBlockCapsule(BlockCapsule blockCapsule, Miner miner) { + PQScheme scheme = resolveWitnessScheme(miner); + if (scheme != null && PQSchemeRegistry.contains(scheme)) { + signWitnessAuth(blockCapsule, miner, scheme); + } else { + blockCapsule.sign(miner.getPrivateKey()); + } + } + + private PQScheme resolveWitnessScheme(Miner miner) { + if (!chainBaseManager.getDynamicPropertiesStore().isAnyPqSchemeAllowed()) { + return null; + } + PQScheme scheme = miner.getPqScheme(); + if (scheme == null || !PQSchemeRegistry.contains(scheme)) { + return null; + } + if (!chainBaseManager.getDynamicPropertiesStore().isPqSchemeAllowed(scheme)) { + return null; + } + byte[] witnessAddress = miner.getWitnessAddress().toByteArray(); + AccountCapsule accountCapsule = chainBaseManager.getAccountStore().get(witnessAddress); + if (accountCapsule == null || !accountCapsule.getInstance().hasWitnessPermission()) { + return null; + } + Permission witnessPermission = accountCapsule.getInstance().getWitnessPermission(); + if (witnessPermission.getKeysCount() == 0) { + return null; + } + return scheme; + } + + private void signWitnessAuth(BlockCapsule blockCapsule, Miner miner, PQScheme scheme) { + byte[] pqPrivateKey = miner.getPQPrivateKey(); + byte[] pqPublicKey = miner.getPQPublicKey(); + if (pqPrivateKey == null || pqPublicKey == null) { + throw new IllegalStateException( + "witness permission requires " + scheme + + " but local PQ key material is not configured"); + } + byte[] digest = blockCapsule.getRawHashBytes(); + byte[] signature = PQSchemeRegistry.sign(scheme, pqPrivateKey, digest); + PQAuthSig.Builder builder = PQAuthSig.newBuilder() + .setPublicKey(ByteString.copyFrom(pqPublicKey)) + .setSignature(ByteString.copyFrom(signature)); + // FN_DSA_512 is the launch scheme: leave scheme at the proto3 default + // (UNKNOWN_PQ_SCHEME) and rely on PQSchemeRegistry.resolve() on the read + // path so the tag costs zero wire bytes per block. + if (scheme != PQScheme.FN_DSA_512) { + builder.setScheme(scheme); + } + blockCapsule.setPqAuthSig(builder.build()); + } + private void filterOwnerAddress(TransactionCapsule transactionCapsule, Set result) { byte[] owner = transactionCapsule.getOwnerAddress(); String ownerAddress = ByteArray.toHexString(owner); diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf index b180ecd6d10..91eb905dbf4 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -702,6 +702,23 @@ localwitness = [ # "localwitnesskeystore.json" # ] +# Scheme used by localwitness_pq_keys. Defaults to FN_DSA_512. +# V2 first launch only allows FN_DSA_512 (Falcon-512, FIPS 206 draft). +# localwitness_pq_scheme = "FN_DSA_512" + +# Post-quantum witness signing keypairs, hex-encoded. Each entry is the +# extended private key f‖g‖F‖h (priv ‖ pub) as one hex string. For FN_DSA_512 +# the total is 2176 bytes (4352 hex chars): 1280 B Falcon-512 private key +# (f‖g‖F) followed by the 896 B public key h. Operators MUST generate the +# keypair off-line on a single platform and distribute the extended key; +# on-node keygen is intentionally bypassed because BouncyCastle's Falcon +# FFT/FPR code paths are not declared strictfp and could in theory diverge +# across JVMs/architectures. Used only after the ALLOW_FN_DSA_512 proposal +# is active and the witness Permission has been upgraded to FN_DSA_512. +# localwitness_pq_keys = [ +# "<4352 hex chars>" +# ] + block = { needSyncCheck = true maintenanceTimeInterval = 21600000 // 6 hours: 21600000(ms) @@ -800,6 +817,8 @@ committee = { # allowTvmBlob = 0 # consensusLogicOptimization = 0 # allowOptimizedReturnValueOfChainId = 0 + # allowTvmOsaka = 0 + # allowMlDsa = 0 } event.subscribe = { diff --git a/framework/src/test/java/org/tron/common/crypto/pqc/FNDSAKatTest.java b/framework/src/test/java/org/tron/common/crypto/pqc/FNDSAKatTest.java new file mode 100644 index 00000000000..1c6656d8f38 --- /dev/null +++ b/framework/src/test/java/org/tron/common/crypto/pqc/FNDSAKatTest.java @@ -0,0 +1,246 @@ +package org.tron.common.crypto.pqc; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + +import java.security.MessageDigest; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import org.junit.Test; +import org.tron.common.crypto.Hash; +import org.tron.protos.Protocol.PQScheme; + +/** + * Known-Answer Tests (KAT) for FN-DSA / Falcon-512. + * + *

Five seed vectors covering boundary patterns (incrementing, all-zero, + * all-ones, all-{@code 0xAA}, descending) lock in the deterministic + * seed → keypair derivation pinned by BouncyCastle 1.79's + * {@code FalconKeyPairGenerator}. Reference {@code pk}/{@code sk} digests and + * the V2 fingerprint address are captured from this same codebase / BC 1.79; + * the role of the test is regression detection — any change in seeding, + * encoding, or fingerprint derivation lights up. + * + *

Falcon signing is randomized so signature bytes cannot be pinned. Sign / + * verify is exercised per-vector and cross-vector to confirm signatures only + * verify under their own key. + */ +public class FNDSAKatTest { + + private static final class KatVector { + final String label; + final byte[] seed; + final String pkSha256; + final String skSha256; + + KatVector(String label, byte[] seed, String pkSha256, String skSha256) { + this.label = label; + this.seed = seed; + this.pkSha256 = pkSha256; + this.skSha256 = skSha256; + } + } + + private static byte[] seedIncrementing() { + byte[] s = new byte[FNDSA.SEED_LENGTH]; + for (int i = 0; i < s.length; i++) { + s[i] = (byte) i; + } + return s; + } + + private static byte[] seedDescending() { + byte[] s = new byte[FNDSA.SEED_LENGTH]; + for (int i = 0; i < s.length; i++) { + s[i] = (byte) (FNDSA.SEED_LENGTH - 1 - i); + } + return s; + } + + private static byte[] seedFilled(int b) { + byte[] s = new byte[FNDSA.SEED_LENGTH]; + Arrays.fill(s, (byte) b); + return s; + } + + private static final KatVector[] VECTORS = { + new KatVector("incrementing", seedIncrementing(), + "1cc09837c6931f9c5988e59ad0acd4e8bc5f13e274573d0edb444822cd4afc90", + "960a83b03e1a8a075002be97f7a92959a2b60c91184cabac06172d8821c32d6a"), + new KatVector("all_zero", seedFilled(0x00), + "708a446d675ee40027562aa2f853b9de0d9c876a08187133bb227c6d372aa1f2", + "fb05b4c139c8fd08b9ae3ecf3da9cc375623aeef38b20ecdb5bbd8c7c02e7324"), + new KatVector("all_ff", seedFilled(0xff), + "4744e8d541a208ae10f62f5175c6eda7b695f3fd32b2145a38f8b16665a350b0", + "e9adaa331dd9dc8d5881578e25bee75050105d7885bc7eac4e5e7f7fbba5612d"), + new KatVector("all_aa", seedFilled(0xaa), + "0894fd3551559bf8dbfd2ca828081c4f6998a16d65e63c595cf24178a2f952d3", + "b2c4678087cba90219fb590bf618a88eb663db96c1ad9c572ff86d38e8d78e1f"), + new KatVector("descending", seedDescending(), + "d2191201811bf061040a012d1799dcdacb055e844d99164e0ddc45c71007d829", + "dce0af30c51875158f3ea7c24b4ced289f49ce6123148994dc2a79548e678c2f"), + }; + + private static byte[] sha256(byte[] in) { + try { + return MessageDigest.getInstance("SHA-256").digest(in); + } catch (Exception e) { + throw new AssertionError("SHA-256 unavailable", e); + } + } + + private static String hex(byte[] b) { + StringBuilder sb = new StringBuilder(b.length * 2); + for (byte x : b) { + sb.append(String.format("%02x", x)); + } + return sb.toString(); + } + + @Test + public void allVectorsDeriveExpectedPublicAndPrivateKey() { + for (KatVector v : VECTORS) { + FNDSA k = new FNDSA(v.seed); + assertEquals(v.label + ": pk length", + FNDSA.PUBLIC_KEY_LENGTH, k.getPublicKey().length); + assertEquals(v.label + ": sk length", + FNDSA.PRIVATE_KEY_LENGTH, k.getPrivateKey().length); + assertEquals(v.label + ": pk SHA-256 must match KAT vector", + v.pkSha256, hex(sha256(k.getPublicKey()))); + assertEquals(v.label + ": sk SHA-256 must match KAT vector", + v.skSha256, hex(sha256(k.getPrivateKey()))); + } + } + + @Test + public void allVectorsDeriveExpectedAddress() { + for (KatVector v : VECTORS) { + FNDSA k = new FNDSA(v.seed); + byte[] addr = k.getAddress(); + assertEquals(v.label + ": address length", + 21, addr.length); + + byte[] viaRegistry = + PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, k.getPublicKey()); + assertArrayEquals(v.label + ": registry dispatch must match instance", + addr, viaRegistry); + } + } + + @Test + public void addressIsExactly0x41PlusKeccak256RightmostBytesOfPublicKey() { + for (KatVector v : VECTORS) { + FNDSA k = new FNDSA(v.seed); + byte[] pk = k.getPublicKey(); + byte[] hash = Hash.sha3(pk); + byte[] expected = new byte[21]; + expected[0] = 0x41; + System.arraycopy(hash, hash.length - 20, expected, 1, 20); + assertArrayEquals(v.label + ": address must be 0x41 ‖ Keccak-256(pk)[12..32]", + expected, k.getAddress()); + } + } + + @Test + public void allVectorsAreReproducibleAcrossInstances() { + for (KatVector v : VECTORS) { + FNDSA a = new FNDSA(v.seed); + FNDSA b = new FNDSA(v.seed); + assertArrayEquals(v.label + ": pk reproducible", a.getPublicKey(), b.getPublicKey()); + assertArrayEquals(v.label + ": sk reproducible", a.getPrivateKey(), b.getPrivateKey()); + assertArrayEquals(v.label + ": addr reproducible", a.getAddress(), b.getAddress()); + } + } + + @Test + public void distinctSeedsProduceDistinctKeysAndAddresses() { + Set pkDigests = new HashSet<>(); + Set skDigests = new HashSet<>(); + Set addresses = new HashSet<>(); + for (KatVector v : VECTORS) { + pkDigests.add(v.pkSha256); + skDigests.add(v.skSha256); + addresses.add(hex(new FNDSA(v.seed).getAddress())); + } + assertEquals("KAT pk digests must be pairwise distinct", + VECTORS.length, pkDigests.size()); + assertEquals("KAT sk digests must be pairwise distinct", + VECTORS.length, skDigests.size()); + assertEquals("KAT addresses must be pairwise distinct", + VECTORS.length, addresses.size()); + } + + @Test + public void signaturesFromKatKeysVerifyUnderTheirOwnPublicKey() { + byte[][] messages = { + new byte[0], + "x".getBytes(), + "tron-fn-dsa-kat-message".getBytes(), + new byte[1024], + }; + for (KatVector v : VECTORS) { + FNDSA k = new FNDSA(v.seed); + for (byte[] msg : messages) { + byte[] sig = k.sign(msg); + assertTrue(v.label + ": signature must be non-empty", + sig.length > 0); + assertTrue(v.label + ": signature must respect 752-byte upper bound", + sig.length <= FNDSA.SIGNATURE_LENGTH); + assertTrue(v.label + ": signature must verify under its own pk", + FNDSA.verify(k.getPublicKey(), msg, sig)); + assertTrue(v.label + ": registry verify must accept own signature", + PQSchemeRegistry.verify( + PQScheme.FN_DSA_512, k.getPublicKey(), msg, sig)); + } + } + } + + @Test + public void signatureFromVectorAFailsUnderVectorBPublicKey() { + byte[] msg = "tron-fn-dsa-kat-cross".getBytes(); + FNDSA[] keys = new FNDSA[VECTORS.length]; + byte[][] sigs = new byte[VECTORS.length][]; + for (int i = 0; i < VECTORS.length; i++) { + keys[i] = new FNDSA(VECTORS[i].seed); + sigs[i] = keys[i].sign(msg); + } + for (int i = 0; i < VECTORS.length; i++) { + for (int j = 0; j < VECTORS.length; j++) { + if (i == j) { + assertTrue(VECTORS[i].label + ": self-verify must succeed", + FNDSA.verify(keys[i].getPublicKey(), msg, sigs[i])); + } else { + assertFalse("signature from " + VECTORS[i].label + + " must NOT verify under " + VECTORS[j].label, + FNDSA.verify(keys[j].getPublicKey(), msg, sigs[i])); + } + } + } + } + + @Test + public void distinctSeedsAtRuntimeAlsoProduceDistinctRuntimePublicKeys() { + // Belt-and-braces: the sanity check above only compared hard-coded digests. + // Re-derive at runtime and confirm they're still pairwise distinct. + byte[][] pks = new byte[VECTORS.length][]; + for (int i = 0; i < VECTORS.length; i++) { + pks[i] = new FNDSA(VECTORS[i].seed).getPublicKey(); + } + for (int i = 0; i < VECTORS.length; i++) { + for (int j = i + 1; j < VECTORS.length; j++) { + assertFalse( + VECTORS[i].label + " and " + VECTORS[j].label + + " produced identical pk bytes", + Arrays.equals(pks[i], pks[j])); + assertNotEquals( + VECTORS[i].label + " and " + VECTORS[j].label + + " produced identical pk digests", + hex(sha256(pks[i])), hex(sha256(pks[j]))); + } + } + } +} diff --git a/framework/src/test/java/org/tron/common/crypto/pqc/FNDSATest.java b/framework/src/test/java/org/tron/common/crypto/pqc/FNDSATest.java new file mode 100644 index 00000000000..6298b1d251b --- /dev/null +++ b/framework/src/test/java/org/tron/common/crypto/pqc/FNDSATest.java @@ -0,0 +1,443 @@ +package org.tron.common.crypto.pqc; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.security.SecureRandom; +import org.bouncycastle.crypto.AsymmetricCipherKeyPair; +import org.bouncycastle.crypto.params.ParametersWithRandom; +import org.bouncycastle.pqc.crypto.falcon.FalconKeyGenerationParameters; +import org.bouncycastle.pqc.crypto.falcon.FalconKeyPairGenerator; +import org.bouncycastle.pqc.crypto.falcon.FalconParameters; +import org.bouncycastle.pqc.crypto.falcon.FalconPrivateKeyParameters; +import org.bouncycastle.pqc.crypto.falcon.FalconPublicKeyParameters; +import org.bouncycastle.pqc.crypto.falcon.FalconSigner; +import org.junit.Before; +import org.junit.Test; +import org.tron.protos.Protocol.PQScheme; + +public class FNDSATest { + + private static final FalconParameters PARAMS = FalconParameters.falcon_512; + + private FNDSA keypair; + private FalconPublicKeyParameters pk; + private FalconPrivateKeyParameters sk; + + @Before + public void setUp() { + AsymmetricCipherKeyPair kp = freshKeyPair(); + pk = (FalconPublicKeyParameters) kp.getPublic(); + sk = (FalconPrivateKeyParameters) kp.getPrivate(); + keypair = new FNDSA(sk.getEncoded(), pk.getH()); + } + + private static AsymmetricCipherKeyPair freshKeyPair() { + FalconKeyPairGenerator gen = new FalconKeyPairGenerator(); + gen.init(new FalconKeyGenerationParameters(new SecureRandom(), PARAMS)); + return gen.generateKeyPair(); + } + + private byte[] rawSign(byte[] message) { + FalconSigner signer = new FalconSigner(); + signer.init(true, new ParametersWithRandom(sk, new SecureRandom())); + try { + return signer.generateSignature(message); + } catch (Exception e) { + throw new AssertionError("failed to sign in test setup", e); + } + } + + @Test + public void schemeAndLengthsMatchFips206Draft() { + assertEquals(PQScheme.FN_DSA_512, keypair.getScheme()); + assertEquals(FNDSA.PUBLIC_KEY_LENGTH, keypair.getPublicKeyLength()); + assertEquals(FNDSA.SIGNATURE_LENGTH, keypair.getSignatureLength()); + assertEquals(FNDSA.PRIVATE_KEY_LENGTH, keypair.getPrivateKeyLength()); + assertEquals(FNDSA.PUBLIC_KEY_LENGTH, pk.getH().length); + } + + @Test + public void publicKeyHasFixedLength() { + for (int i = 0; i < 4; i++) { + AsymmetricCipherKeyPair kp = freshKeyPair(); + byte[] pkBytes = ((FalconPublicKeyParameters) kp.getPublic()).getH(); + assertEquals(FNDSA.PUBLIC_KEY_LENGTH, pkBytes.length); + } + } + + @Test + public void privateKeyEncodingHasFixedLength() { + for (int i = 0; i < 4; i++) { + AsymmetricCipherKeyPair kp = freshKeyPair(); + byte[] skBytes = ((FalconPrivateKeyParameters) kp.getPrivate()).getEncoded(); + assertEquals(FNDSA.PRIVATE_KEY_LENGTH, skBytes.length); + } + } + + @Test + public void signProducesVerifiableSignatureWithinBound() { + byte[] msg = "hello, fn-dsa".getBytes(); + byte[] sig = FNDSA.sign(sk.getEncoded(), msg); + assertTrue("signature must be non-empty", sig.length > 0); + assertTrue( + "signature must respect protocol-level upper bound", + sig.length <= FNDSA.SIGNATURE_LENGTH); + assertTrue(FNDSA.verify(pk.getH(), msg, sig)); + } + + @Test + public void signatureBoundaryAtMaxAcceptedByLengthCheck() { + byte[] sig = new byte[FNDSA.SIGNATURE_LENGTH]; + keypair.validateSignature(sig); + } + + @Test + public void signatureBoundaryAboveMaxRejected() { + byte[] sig = new byte[FNDSA.SIGNATURE_LENGTH + 1]; + try { + keypair.validateSignature(sig); + fail("signature longer than upper bound should be rejected"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("signature length")); + } + } + + @Test + public void minimalValidLengthAcceptedByLengthCheck() { + byte[] sig = new byte[1]; + keypair.validateSignature(sig); + } + + @Test + public void emptySignatureRejectedByLengthCheck() { + byte[] sig = new byte[0]; + try { + keypair.validateSignature(sig); + fail("empty signature should be rejected"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("signature length")); + } + } + + @Test + public void verifyRejectsSignatureLongerThanUpperBound() { + byte[] msg = new byte[] {1, 2, 3}; + byte[] tooLong = new byte[FNDSA.SIGNATURE_LENGTH + 1]; + try { + FNDSA.verify(pk.getH(), msg, tooLong); + fail("signature exceeding upper bound should be rejected at static verify"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("signature length")); + } + } + + @Test + public void verifyRejectsEmptySignature() { + byte[] msg = new byte[] {1, 2, 3}; + byte[] empty = new byte[0]; + try { + FNDSA.verify(pk.getH(), msg, empty); + fail("empty signature should be rejected at static verify"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("signature length")); + } + } + + @Test + public void invalidPublicKeyLengthRejected() { + byte[] badPk = new byte[FNDSA.PUBLIC_KEY_LENGTH - 1]; + byte[] msg = new byte[] {1}; + byte[] sig = new byte[16]; + try { + FNDSA.verify(badPk, msg, sig); + fail("short public key should be rejected"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("public key length")); + } + } + + @Test + public void nullMessageRejected() { + byte[] sig = new byte[16]; + try { + FNDSA.verify(pk.getH(), null, sig); + fail("null message should be rejected"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("message")); + } + } + + @Test + public void signatureBoundToMessage() { + byte[] msg = "hello".getBytes(); + byte[] sig = rawSign(msg); + byte[] tamperedMsg = "hellp".getBytes(); + assertFalse(keypair.verify(tamperedMsg, sig)); + } + + @Test + public void tamperedSignatureFailsVerification() { + byte[] msg = "payload".getBytes(); + byte[] sig = rawSign(msg); + sig[0] ^= 0x01; + assertFalse(keypair.verify(msg, sig)); + } + + @Test + public void wrongPublicKeyFailsVerification() { + byte[] msg = "payload".getBytes(); + byte[] sig = rawSign(msg); + AsymmetricCipherKeyPair other = freshKeyPair(); + byte[] otherPk = ((FalconPublicKeyParameters) other.getPublic()).getH(); + assertFalse(FNDSA.verify(otherPk, msg, sig)); + } + + @Test + public void crossAlgoSignatureRejected() { + // FN-DSA upper bound is 752 bytes; ML-DSA-44 (2420), ML-DSA-65 (3309), + // SLH-DSA (7856) all exceed it and must be rejected at the length check. + byte[] msg = "cross-algo".getBytes(); + int[] foreignLengths = {2420, 3309, 7856}; + for (int len : foreignLengths) { + byte[] foreign = new byte[len]; + try { + FNDSA.verify(pk.getH(), msg, foreign); + fail("foreign-scheme signature length " + len + " should be rejected for FN-DSA"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("signature length")); + } + } + } + + @Test + public void emptyMessageVerifiesConsistently() { + byte[] msg = new byte[0]; + byte[] sig = rawSign(msg); + assertTrue(keypair.verify(msg, sig)); + } + + @Test + public void keypairBoundInstanceSignsAndVerifies() { + FNDSA signer = new FNDSA(); + byte[] msg = "keypair-bound".getBytes(); + byte[] sig = signer.sign(msg); + assertTrue(sig.length > 0 && sig.length <= FNDSA.SIGNATURE_LENGTH); + assertTrue(signer.verify(msg, sig)); + } + + @Test + public void fromSeedIsDeterministic() { + byte[] seed = new byte[FNDSA.SEED_LENGTH]; + for (int i = 0; i < seed.length; i++) { + seed[i] = (byte) i; + } + FNDSA a = new FNDSA(seed); + FNDSA b = new FNDSA(seed); + assertArrayEquals(a.getPublicKey(), b.getPublicKey()); + assertArrayEquals(a.getPrivateKey(), b.getPrivateKey()); + } + + @Test(expected = IllegalArgumentException.class) + public void invalidSeedLengthRejected() { + new FNDSA(new byte[FNDSA.SEED_LENGTH - 1]); + } + + @Test(expected = UnsupportedOperationException.class) + public void derivePublicKeyFromEncodedPrivateKeyUnsupported() { + FNDSA.derivePublicKey(sk.getEncoded()); + } + + @Test + public void computeAddressIs21Bytes() { + assertEquals(21, FNDSA.computeAddress(pk.getH()).length); + } + + @Test + public void registryDispatchMatchesDirectCalls() { + byte[] msg = "registry-dispatch".getBytes(); + byte[] sigDirect = FNDSA.sign(sk.getEncoded(), msg); + assertTrue(PQSchemeRegistry.verify( + PQScheme.FN_DSA_512, pk.getH(), msg, sigDirect)); + byte[] sigViaRegistry = PQSchemeRegistry.sign( + PQScheme.FN_DSA_512, sk.getEncoded(), msg); + assertTrue(FNDSA.verify(pk.getH(), msg, sigViaRegistry)); + assertEquals(FNDSA.PUBLIC_KEY_LENGTH, + PQSchemeRegistry.getPublicKeyLength(PQScheme.FN_DSA_512)); + assertEquals(FNDSA.SIGNATURE_LENGTH, + PQSchemeRegistry.getSignatureLength(PQScheme.FN_DSA_512)); + } + + @Test + public void registryIsValidSignatureLengthRespectsUpperBound() { + assertTrue(PQSchemeRegistry.isValidSignatureLength(PQScheme.FN_DSA_512, 1)); + assertTrue(PQSchemeRegistry.isValidSignatureLength( + PQScheme.FN_DSA_512, FNDSA.SIGNATURE_LENGTH)); + assertFalse(PQSchemeRegistry.isValidSignatureLength(PQScheme.FN_DSA_512, 0)); + assertFalse(PQSchemeRegistry.isValidSignatureLength( + PQScheme.FN_DSA_512, FNDSA.SIGNATURE_LENGTH + 1)); + } + + @Test + public void registryComputeAddressMatchesDirect() { + assertArrayEquals( + FNDSA.computeAddress(pk.getH()), + PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, pk.getH())); + } + + @Test(expected = IllegalArgumentException.class) + public void seedConstructorRejectsNull() { + new FNDSA((byte[]) null); + } + + @Test + public void keypairConstructorRejectsNullPrivateKey() { + try { + new FNDSA(null, pk.getH()); + fail("null private key must be rejected"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("private key length")); + } + } + + @Test + public void keypairConstructorRejectsWrongPrivateKeyLength() { + try { + new FNDSA(new byte[FNDSA.PRIVATE_KEY_LENGTH - 1], pk.getH()); + fail("short private key must be rejected"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("private key length")); + } + } + + @Test + public void keypairConstructorRejectsNullPublicKey() { + try { + new FNDSA(sk.getEncoded(), null); + fail("null public key must be rejected"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("public key length")); + } + } + + @Test + public void keypairConstructorRejectsWrongPublicKeyLength() { + try { + new FNDSA(sk.getEncoded(), new byte[FNDSA.PUBLIC_KEY_LENGTH + 1]); + fail("over-long public key must be rejected"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("public key length")); + } + } + + @Test + public void extendedPrivateKeyRoundTripsThroughFromAndGetters() { + byte[] extended = keypair.getPrivateKeyWithPublicKey(); + assertEquals(FNDSA.PRIVATE_KEY_WITH_PUBLIC_KEY_LENGTH, extended.length); + FNDSA restored = FNDSA.fromPrivateKeyWithPublicKey(extended); + assertArrayEquals(keypair.getPrivateKey(), restored.getPrivateKey()); + assertArrayEquals(keypair.getPublicKey(), restored.getPublicKey()); + // The recovered keypair must produce verifiable signatures and recover its address. + byte[] msg = "extended-key-roundtrip".getBytes(); + byte[] sig = restored.sign(msg); + assertTrue(restored.verify(msg, sig)); + assertArrayEquals(keypair.getAddress(), restored.getAddress()); + } + + @Test(expected = IllegalArgumentException.class) + public void fromExtendedPrivateKeyRejectsNull() { + FNDSA.fromPrivateKeyWithPublicKey(null); + } + + @Test(expected = IllegalArgumentException.class) + public void fromExtendedPrivateKeyRejectsWrongLength() { + FNDSA.fromPrivateKeyWithPublicKey(new byte[FNDSA.PRIVATE_KEY_LENGTH]); + } + + @Test + public void derivePublicKeyFromExtendedFormReturnsAppendedPublicKey() { + byte[] extended = keypair.getPrivateKeyWithPublicKey(); + byte[] derived = FNDSA.derivePublicKey(extended); + assertArrayEquals(keypair.getPublicKey(), derived); + } + + @Test(expected = UnsupportedOperationException.class) + public void derivePublicKeyRejectsNull() { + FNDSA.derivePublicKey(null); + } + + @Test + public void staticSignAcceptsExtendedPrivateKey() { + byte[] extended = keypair.getPrivateKeyWithPublicKey(); + byte[] msg = "static-sign-extended".getBytes(); + byte[] sig = FNDSA.sign(extended, msg); + assertTrue(sig.length > 0 && sig.length <= FNDSA.SIGNATURE_LENGTH); + assertTrue(FNDSA.verify(pk.getH(), msg, sig)); + } + + @Test + public void staticSignRejectsNullPrivateKey() { + try { + FNDSA.sign(null, new byte[] {1}); + fail("null private key must be rejected"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("private key length")); + } + } + + @Test + public void staticSignRejectsWrongPrivateKeyLength() { + try { + FNDSA.sign(new byte[FNDSA.PRIVATE_KEY_LENGTH - 1], new byte[] {1}); + fail("short private key must be rejected"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("private key length")); + } + } + + @Test + public void staticSignRejectsNullMessage() { + try { + FNDSA.sign(sk.getEncoded(), null); + fail("null message must be rejected"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("message")); + } + } + + @Test + public void staticVerifyRejectsNullPublicKey() { + try { + FNDSA.verify(null, new byte[] {1}, new byte[16]); + fail("null public key must be rejected"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("public key length")); + } + } + + @Test + public void unknownPqSchemeResolvesToFnDsa512() { + assertEquals(PQScheme.FN_DSA_512, + PQSchemeRegistry.resolve(PQScheme.UNKNOWN_PQ_SCHEME)); + assertTrue(PQSchemeRegistry.contains(PQScheme.UNKNOWN_PQ_SCHEME)); + assertEquals(FNDSA.PUBLIC_KEY_LENGTH, + PQSchemeRegistry.getPublicKeyLength(PQScheme.UNKNOWN_PQ_SCHEME)); + assertEquals(FNDSA.SIGNATURE_LENGTH, + PQSchemeRegistry.getSignatureLength(PQScheme.UNKNOWN_PQ_SCHEME)); + assertTrue(PQSchemeRegistry.isValidSignatureLength( + PQScheme.UNKNOWN_PQ_SCHEME, FNDSA.SIGNATURE_LENGTH)); + assertArrayEquals( + FNDSA.computeAddress(pk.getH()), + PQSchemeRegistry.computeAddress(PQScheme.UNKNOWN_PQ_SCHEME, pk.getH())); + + byte[] msg = "unknown-resolves-to-falcon".getBytes(); + byte[] sig = PQSchemeRegistry.sign( + PQScheme.UNKNOWN_PQ_SCHEME, sk.getEncoded(), msg); + assertTrue(PQSchemeRegistry.verify( + PQScheme.UNKNOWN_PQ_SCHEME, pk.getH(), msg, sig)); + assertTrue(FNDSA.verify(pk.getH(), msg, sig)); + } +} diff --git a/framework/src/test/java/org/tron/common/crypto/pqc/PQSchemeRegistryTest.java b/framework/src/test/java/org/tron/common/crypto/pqc/PQSchemeRegistryTest.java new file mode 100644 index 00000000000..203d4625ca8 --- /dev/null +++ b/framework/src/test/java/org/tron/common/crypto/pqc/PQSchemeRegistryTest.java @@ -0,0 +1,130 @@ +package org.tron.common.crypto.pqc; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.Arrays; +import org.junit.Test; +import org.tron.protos.Protocol.PQScheme; + +/** + * Covers the static dispatch helpers of {@link PQSchemeRegistry} and the + * defensive paths exercised by callers passing {@code null}, {@code UNRECOGNIZED} + * or wrong-shaped public keys. + */ +public class PQSchemeRegistryTest { + + @Test + public void containsRejectsNullScheme() { + assertFalse(PQSchemeRegistry.contains(null)); + } + + @Test + public void containsRejectsUnrecognized() { + assertFalse(PQSchemeRegistry.contains(PQScheme.UNRECOGNIZED)); + } + + @Test + public void containsAcceptsRegisteredScheme() { + assertTrue(PQSchemeRegistry.contains(PQScheme.FN_DSA_512)); + } + + @Test + public void getSeedLengthReturnsRegisteredValue() { + assertEquals(FNDSA.SEED_LENGTH, + PQSchemeRegistry.getSeedLength(PQScheme.FN_DSA_512)); + // UNKNOWN_PQ_SCHEME normalizes to FN_DSA_512. + assertEquals(FNDSA.SEED_LENGTH, + PQSchemeRegistry.getSeedLength(PQScheme.UNKNOWN_PQ_SCHEME)); + } + + @Test + public void getPrivateKeyLengthReturnsRegisteredValue() { + assertEquals(FNDSA.PRIVATE_KEY_LENGTH, + PQSchemeRegistry.getPrivateKeyLength(PQScheme.FN_DSA_512)); + } + + @Test + public void fromSeedDispatchesToFalcon() { + byte[] seed = new byte[FNDSA.SEED_LENGTH]; + Arrays.fill(seed, (byte) 0x07); + PQSignature sig = PQSchemeRegistry.fromSeed(PQScheme.FN_DSA_512, seed); + assertNotNull(sig); + assertEquals(PQScheme.FN_DSA_512, sig.getScheme()); + // Same seed must yield deterministic keypair across direct and dispatched paths. + FNDSA direct = new FNDSA(seed); + assertArrayEquals(direct.getPublicKey(), sig.getPublicKey()); + assertArrayEquals(direct.getPrivateKey(), sig.getPrivateKey()); + } + + @Test + public void fromKeypairDispatchesAndPreservesAddress() { + byte[] seed = new byte[FNDSA.SEED_LENGTH]; + Arrays.fill(seed, (byte) 0x09); + FNDSA src = new FNDSA(seed); + PQSignature sig = PQSchemeRegistry.fromKeypair( + PQScheme.FN_DSA_512, src.getPrivateKey(), src.getPublicKey()); + assertArrayEquals(src.getAddress(), sig.getAddress()); + byte[] msg = "from-keypair".getBytes(); + byte[] s = sig.sign(msg); + assertTrue(sig.verify(msg, s)); + } + + @Test + public void deriveHashRejectsNullPublicKey() { + try { + PQSchemeRegistry.deriveHash(PQScheme.FN_DSA_512, null); + fail("null public key must be rejected"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("public key length")); + } + } + + @Test + public void deriveHashRejectsWrongLengthPublicKey() { + try { + PQSchemeRegistry.deriveHash( + PQScheme.FN_DSA_512, new byte[FNDSA.PUBLIC_KEY_LENGTH - 1]); + fail("short public key must be rejected"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("public key length")); + } + } + + @Test + public void requireRejectsNullScheme() { + try { + PQSchemeRegistry.getPublicKeyLength(null); + fail("null scheme must be rejected"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("scheme")); + } + } + + @Test + public void requireRejectsUnrecognizedScheme() { + try { + PQSchemeRegistry.getPublicKeyLength(PQScheme.UNRECOGNIZED); + fail("UNRECOGNIZED scheme must be rejected"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("PQSignature registered")); + } + } + + @Test + public void resolvePassesThroughNonDefaultSchemes() { + assertEquals(PQScheme.FN_DSA_512, + PQSchemeRegistry.resolve(PQScheme.FN_DSA_512)); + // null should pass through so contains/require can decide. + assertTrue(PQSchemeRegistry.resolve(null) == null); + } + + @Test + public void isValidSignatureLengthRejectsZero() { + assertFalse(PQSchemeRegistry.isValidSignatureLength(PQScheme.FN_DSA_512, 0)); + } +} diff --git a/framework/src/test/java/org/tron/common/crypto/pqc/PQSignatureDefaultsTest.java b/framework/src/test/java/org/tron/common/crypto/pqc/PQSignatureDefaultsTest.java new file mode 100644 index 00000000000..748885e998f --- /dev/null +++ b/framework/src/test/java/org/tron/common/crypto/pqc/PQSignatureDefaultsTest.java @@ -0,0 +1,132 @@ +package org.tron.common.crypto.pqc; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.junit.Before; +import org.junit.Test; +import org.tron.protos.Protocol.PQScheme; + +/** + * Drives the {@link PQSignature} default validator branches (null and + * length-mismatch) via a minimal in-test implementation. {@link FNDSA} + * exposes these defaults but the cryptographic instances exercise mostly the + * happy paths; the explicit fixture here forces the error legs. + */ +public class PQSignatureDefaultsTest { + + private PQSignature stub; + + @Before + public void setUp() { + stub = new PQSignature() { + @Override + public PQScheme getScheme() { + return PQScheme.FN_DSA_512; + } + + @Override + public int getPrivateKeyLength() { + return 16; + } + + @Override + public int getPublicKeyLength() { + return 8; + } + + @Override + public int getSignatureLength() { + return 32; + } + + @Override + public byte[] getPrivateKey() { + return new byte[getPrivateKeyLength()]; + } + + @Override + public byte[] getPublicKey() { + return new byte[getPublicKeyLength()]; + } + + @Override + public byte[] getAddress() { + return new byte[21]; + } + + @Override + public byte[] sign(byte[] message) { + return new byte[1]; + } + + @Override + public boolean verify(byte[] message, byte[] signature) { + return false; + } + }; + } + + @Test + public void validatePrivateKeyRejectsNull() { + try { + stub.validatePrivateKey(null); + fail("null private key must be rejected"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("private key length")); + assertTrue(expected.getMessage().contains("null")); + } + } + + @Test + public void validatePrivateKeyRejectsWrongLength() { + try { + stub.validatePrivateKey(new byte[stub.getPrivateKeyLength() - 1]); + fail("short private key must be rejected"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("private key length")); + } + } + + @Test + public void validatePrivateKeyAcceptsExactLength() { + stub.validatePrivateKey(new byte[stub.getPrivateKeyLength()]); + } + + @Test + public void validatePublicKeyRejectsNull() { + try { + stub.validatePublicKey(null); + fail("null public key must be rejected"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("public key length")); + assertTrue(expected.getMessage().contains("null")); + } + } + + @Test + public void validatePublicKeyRejectsWrongLength() { + try { + stub.validatePublicKey(new byte[stub.getPublicKeyLength() + 1]); + fail("over-long public key must be rejected"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("public key length")); + } + } + + @Test + public void validatePublicKeyAcceptsExactLength() { + stub.validatePublicKey(new byte[stub.getPublicKeyLength()]); + } + + @Test + public void validateSignatureRejectsNull() { + try { + stub.validateSignature(null); + fail("null signature must be rejected"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("signature length")); + assertTrue(expected.getMessage().contains("null")); + } + } +} diff --git a/framework/src/test/java/org/tron/common/crypto/pqc/SignatureSchemeBenchmarkTest.java b/framework/src/test/java/org/tron/common/crypto/pqc/SignatureSchemeBenchmarkTest.java new file mode 100644 index 00000000000..f7fdb8d7876 --- /dev/null +++ b/framework/src/test/java/org/tron/common/crypto/pqc/SignatureSchemeBenchmarkTest.java @@ -0,0 +1,132 @@ +package org.tron.common.crypto.pqc; + +import java.security.SignatureException; +import java.util.Locale; +import org.junit.Test; +import org.tron.common.crypto.ECKey; +import org.tron.common.crypto.ECKey.ECDSASignature; +import org.tron.common.crypto.Hash; + +/** + * Micro-benchmark comparing key generation, signing and verification latency for + * secp256k1 ECDSA (ECKey) and FN-DSA / Falcon-512. Numbers are reported + * in microseconds (avg of {@link #ITERATIONS} iterations after {@link #WARMUP} warm-up rounds). + */ +public class SignatureSchemeBenchmarkTest { + + private static final int WARMUP = 20; + private static final int ITERATIONS = 500; + private static final byte[] MESSAGE = "tron-pq-benchmark-message".getBytes(); + private static final byte[] MESSAGE_HASH = Hash.sha3(MESSAGE); + + @Test + public void benchmarkAllSchemes() { + Result eckey = benchEcKey(); + Result fndsa = benchFnDsa(); + + System.out.println(String.format(Locale.ROOT, + "=== Signature scheme benchmark (avg over %d iterations, warmup %d) ===", + ITERATIONS, WARMUP)); + System.out.println(String.format(Locale.ROOT, + "%-12s | %12s | %12s | %12s", + "scheme", "keygen (us)", "sign (us)", "verify (us)")); + System.out.println("-------------+--------------+--------------+--------------"); + printResult(eckey); + printResult(fndsa); + } + + private Result benchEcKey() { + for (int i = 0; i < WARMUP; i++) { + ECKey k = new ECKey(); + ECDSASignature s = k.sign(MESSAGE_HASH); + try { + ECKey.signatureToAddress(MESSAGE_HASH, s); + } catch (SignatureException e) { + throw new AssertionError(e); + } + } + + long keygenNs = 0; + ECKey[] keys = new ECKey[ITERATIONS]; + for (int i = 0; i < ITERATIONS; i++) { + long t0 = System.nanoTime(); + keys[i] = new ECKey(); + keygenNs += System.nanoTime() - t0; + } + + long signNs = 0; + ECDSASignature[] sigs = new ECDSASignature[ITERATIONS]; + for (int i = 0; i < ITERATIONS; i++) { + long t0 = System.nanoTime(); + sigs[i] = keys[i].sign(MESSAGE_HASH); + signNs += System.nanoTime() - t0; + } + + long verifyNs = 0; + for (int i = 0; i < ITERATIONS; i++) { + long t0 = System.nanoTime(); + try { + ECKey.signatureToAddress(MESSAGE_HASH, sigs[i]); + } catch (SignatureException e) { + throw new AssertionError(e); + } + verifyNs += System.nanoTime() - t0; + } + return new Result("ECKey(secp)", keygenNs, signNs, verifyNs); + } + + private Result benchFnDsa() { + for (int i = 0; i < WARMUP; i++) { + FNDSA k = new FNDSA(); + byte[] sig = k.sign(MESSAGE); + k.verify(MESSAGE, sig); + } + + long keygenNs = 0; + FNDSA[] keys = new FNDSA[ITERATIONS]; + for (int i = 0; i < ITERATIONS; i++) { + long t0 = System.nanoTime(); + keys[i] = new FNDSA(); + keygenNs += System.nanoTime() - t0; + } + + long signNs = 0; + byte[][] sigs = new byte[ITERATIONS][]; + for (int i = 0; i < ITERATIONS; i++) { + long t0 = System.nanoTime(); + sigs[i] = keys[i].sign(MESSAGE); + signNs += System.nanoTime() - t0; + } + + long verifyNs = 0; + for (int i = 0; i < ITERATIONS; i++) { + long t0 = System.nanoTime(); + keys[i].verify(MESSAGE, sigs[i]); + verifyNs += System.nanoTime() - t0; + } + return new Result("FN-DSA-512", keygenNs, signNs, verifyNs); + } + + private static void printResult(Result r) { + System.out.println(String.format(Locale.ROOT, + "%-12s | %12.2f | %12.2f | %12.2f", + r.name, + r.keygenNs / 1_000.0 / ITERATIONS, + r.signNs / 1_000.0 / ITERATIONS, + r.verifyNs / 1_000.0 / ITERATIONS)); + } + + private static final class Result { + final String name; + final long keygenNs; + final long signNs; + final long verifyNs; + + Result(String name, long keygenNs, long signNs, long verifyNs) { + this.name = name; + this.keygenNs = keygenNs; + this.signNs = signNs; + this.verifyNs = verifyNs; + } + } +} diff --git a/framework/src/test/java/org/tron/common/crypto/pqc/program/PQClient.java b/framework/src/test/java/org/tron/common/crypto/pqc/program/PQClient.java new file mode 100644 index 00000000000..6522611946c --- /dev/null +++ b/framework/src/test/java/org/tron/common/crypto/pqc/program/PQClient.java @@ -0,0 +1,147 @@ +package org.tron.common.crypto.pqc.program; + +import com.google.protobuf.Any; +import com.google.protobuf.ByteString; +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import java.nio.ByteBuffer; +import java.security.MessageDigest; +import java.util.Arrays; +import java.util.concurrent.TimeUnit; +import org.tron.api.GrpcAPI.EmptyMessage; +import org.tron.api.GrpcAPI.Return; +import org.tron.api.WalletGrpc; +import org.tron.api.WalletGrpc.WalletBlockingStub; +import org.tron.common.crypto.pqc.FNDSA; +import org.tron.common.utils.ByteArray; +import org.tron.protos.Protocol.Block; +import org.tron.protos.Protocol.PQAuthSig; +import org.tron.protos.Protocol.Transaction; +import org.tron.protos.Protocol.Transaction.Contract.ContractType; +import org.tron.protos.contract.BalanceContract.TransferContract; + +/** + * Demo client that connects to {@link PQWitnessNode} and broadcasts an FN-DSA-512 + * signed transfer transaction. + * + * The keypair is derived from the same fixed seed used by PQWitnessNode, so no + * out-of-band key exchange is needed. + * + * Usage: + * Terminal 1 — start the witness node first: + * ./gradlew :framework:run -PmainClass=org.tron.common.crypto.pqc.program.PQWitnessNode + * Terminal 2 — broadcast a PQC transaction: + * ./gradlew :framework:run -PmainClass=org.tron.common.crypto.pqc.program.PQClient + * + * Optional JVM args: + * -Dpqc.host=localhost (default: localhost) + * -Dpqc.port=50051 (default: 50051) + */ +public class PQClient { + + private static final String HOST = + System.getProperty("pqc.host", "localhost"); + private static final int PORT = + Integer.parseInt(System.getProperty("pqc.port", "50051")); + + /** Recipient of the demo transfer. */ + private static final byte[] TO_ADDR = + ByteArray.fromHexString("41f522cc20ca18b636bdd93b4fb15ea84cc2b4e001"); + + public static void main(String[] args) throws Exception { + // Force INFO level: logback-test.xml (on the test classpath) sets root=DEBUG + // which is far too noisy for a demo run. + ((ch.qos.logback.classic.Logger) org.slf4j.LoggerFactory + .getLogger(ch.qos.logback.classic.Logger.ROOT_LOGGER_NAME)) + .setLevel(ch.qos.logback.classic.Level.INFO); + + // ── 1. Derive user keypair from same fixed seed as PQWitnessNode ───── + byte[] userSeed = new byte[FNDSA.SEED_LENGTH]; + Arrays.fill(userSeed, (byte) 0x02); + FNDSA userKp = new FNDSA(userSeed); + + byte[] userPub = userKp.getPublicKey(); + byte[] userPriv = userKp.getPrivateKey(); + byte[] signerAddr = FNDSA.computeAddress(userPub); + byte[] ownerAddr = PQWitnessNode.USER_ADDR; + + System.out.println("=== PQC Client ==="); + System.out.println("Connecting to " + HOST + ":" + PORT); + System.out.println("Owner address: " + ByteArray.toHexString(ownerAddr)); + System.out.println("Signer address: " + ByteArray.toHexString(signerAddr)); + + // ── 2. Connect via gRPC ─────────────────────────────────────────────── + ManagedChannel channel = ManagedChannelBuilder + .forAddress(HOST, PORT) + .usePlaintext() + .build(); + WalletBlockingStub stub = WalletGrpc.newBlockingStub(channel) + .withDeadlineAfter(10, TimeUnit.SECONDS); + + try { + // ── 3. Fetch reference block for TaPoS ─────────────────────────── + Block head = stub.getNowBlock(EmptyMessage.getDefaultInstance()); + byte[] headerRaw = head.getBlockHeader().getRawData().toByteArray(); + long refNum = head.getBlockHeader().getRawData().getNumber(); + byte[] blockHash = sha256(headerRaw); + + System.out.println("Reference block: #" + refNum + + " hash=" + ByteArray.toHexString(Arrays.copyOfRange(blockHash, 0, 8)) + "..."); + + // ── 4. Build the transfer transaction ───────────────────────────── + Transaction.raw rawData = Transaction.raw.newBuilder() + .addContract(Transaction.Contract.newBuilder() + .setType(ContractType.TransferContract) + .setParameter(Any.pack(TransferContract.newBuilder() + .setOwnerAddress(ByteString.copyFrom(ownerAddr)) + .setToAddress(ByteString.copyFrom(TO_ADDR)) + .setAmount(1_000_000L) // 1 TRX + .build())) + .setPermissionId(0)) + // TaPoS fields + .setRefBlockHash(ByteString.copyFrom(Arrays.copyOfRange(blockHash, 8, 16))) + .setRefBlockBytes(ByteString.copyFrom(longToBytes(refNum), 6, 2)) + .setExpiration(System.currentTimeMillis() + 60_000L) + .build(); + + Transaction tx = Transaction.newBuilder().setRawData(rawData).build(); + + // ── 5. Sign with FN-DSA-512 pq_auth_sig ───────────────────────────── + byte[] txId = sha256(rawData.toByteArray()); + byte[] sig = FNDSA.sign(userPriv, txId); + + // FN_DSA_512 is the launch scheme → leave scheme at proto3 default and + // let PQSchemeRegistry.resolve() normalize it on the verifier side. + Transaction signedTx = tx.toBuilder() + .addPqAuthSig(PQAuthSig.newBuilder() + .setPublicKey(ByteString.copyFrom(userPub)) + .setSignature(ByteString.copyFrom(sig))) + .build(); + + System.out.println("TX id: " + ByteArray.toHexString(txId)); + + // ── 6. Broadcast ────────────────────────────────────────────────── + Return result = stub.broadcastTransaction(signedTx); + System.out.println("Broadcast result: " + result.getCode() + + " — " + result.getMessage().toStringUtf8()); + + if (result.getResult()) { + System.out.println("SUCCESS: PQC-signed transaction accepted by the node."); + } else { + System.out.println("REJECTED: " + result.getCode()); + } + + } finally { + channel.shutdown(); + channel.awaitTermination(5, TimeUnit.SECONDS); + } + } + + private static byte[] sha256(byte[] data) throws Exception { + return MessageDigest.getInstance("SHA-256").digest(data); + } + + private static byte[] longToBytes(long value) { + return ByteBuffer.allocate(8).putLong(value).array(); + } +} diff --git a/framework/src/test/java/org/tron/common/crypto/pqc/program/PQFullNode.java b/framework/src/test/java/org/tron/common/crypto/pqc/program/PQFullNode.java new file mode 100644 index 00000000000..7f628f84b70 --- /dev/null +++ b/framework/src/test/java/org/tron/common/crypto/pqc/program/PQFullNode.java @@ -0,0 +1,118 @@ +package org.tron.common.crypto.pqc.program; + +import java.io.File; +import java.net.InetSocketAddress; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Collections; +import org.tron.common.application.Application; +import org.tron.common.application.ApplicationFactory; +import org.tron.common.application.TronApplicationContext; +import org.tron.common.crypto.pqc.FNDSA; +import org.tron.common.utils.ByteArray; +import org.tron.core.ChainBaseManager; +import org.tron.core.config.DefaultConfig; +import org.tron.core.config.args.Args; +import org.tron.core.db.Manager; + +/** + * Demo fullnode that dials {@link PQWitnessNode} via P2P and syncs PQ-signed blocks. + * + * Both nodes share the same deterministic PQ genesis pre-state (witness account with an + * FN-DSA-512 witness permission + demo user account with an FN-DSA-512 owner permission), + * installed via {@link PQWitnessNode#installPQGenesisState}. Once the witness produces + * a block it is broadcast over P2P; this node validates {@code BlockHeader.pq_auth_sig} + * against the same on-chain public key and applies the block. + * + * Usage: + * Terminal 1 — start the witness node first: + * ./gradlew :framework:run -PmainClass=org.tron.common.crypto.pqc.program.PQWitnessNode + * Terminal 2 — start a fullnode that syncs from it: + * ./gradlew :framework:run -PmainClass=org.tron.common.crypto.pqc.program.PQFullNode + * + * Optional JVM args: + * -Dpqc.witness.host=127.0.0.1 (default: 127.0.0.1) + * -Dpqc.witness.p2p.port=18888 (default: PQWitnessNode.P2P_PORT) + */ +public class PQFullNode { + + /** gRPC port (different from PQWitnessNode so both can run on one host). */ + static final int GRPC_PORT = 50052; + /** Full-node HTTP port (different from PQWitnessNode). */ + static final int HTTP_PORT = 8091; + /** P2P listen port (different from PQWitnessNode). */ + static final int P2P_PORT = 18889; + + private static final String WITNESS_HOST = + System.getProperty("pqc.witness.host", "127.0.0.1"); + private static final int WITNESS_P2P_PORT = Integer.parseInt( + System.getProperty("pqc.witness.p2p.port", String.valueOf(PQWitnessNode.P2P_PORT))); + + public static void main(String[] args) throws Exception { + // Force INFO level: logback-test.xml (on the test classpath) sets root=DEBUG + // which is far too noisy for a demo run. + ((ch.qos.logback.classic.Logger) org.slf4j.LoggerFactory + .getLogger(ch.qos.logback.classic.Logger.ROOT_LOGGER_NAME)) + .setLevel(ch.qos.logback.classic.Level.INFO); + + // ── 1. Derive the same deterministic keys used by PQWitnessNode ────── + FNDSA witnessKp = new FNDSA(PQWitnessNode.WITNESS_SEED); + FNDSA userKp = new FNDSA(PQWitnessNode.USER_SEED); + + byte[] witnessPub = witnessKp.getPublicKey(); + byte[] userPub = userKp.getPublicKey(); + + System.out.println("=== PQC Full Node ==="); + System.out.println("Peer (witness): " + WITNESS_HOST + ":" + WITNESS_P2P_PORT); + System.out.println("gRPC port: " + GRPC_PORT); + System.out.println("HTTP port: " + HTTP_PORT); + System.out.println("P2P port: " + P2P_PORT); + System.out.println("Witness address (expected): " + + ByteArray.toHexString(FNDSA.computeAddress(witnessPub))); + + // ── 2. Configure node (no -w: this is a pure fullnode) ──────────────── + File dbDir = Files.createTempDirectory("pqc-fullnode-").toFile(); + dbDir.deleteOnExit(); + + Args.setParam(new String[]{"--output-directory", dbDir.getAbsolutePath()}, + "config-test.conf"); + Args.getInstance().setRpcEnable(true); + Args.getInstance().setFullNodeHttpEnable(true); + Args.getInstance().setFullNodeHttpPort(HTTP_PORT); + Args.getInstance().setSolidityNodeHttpEnable(false); + Args.getInstance().setRpcPort(GRPC_PORT); + Args.getInstance().setNodeListenPort(P2P_PORT); + Args.getInstance().setNeedSyncCheck(false); + Args.getInstance().setMinEffectiveConnection(0); + Args.getInstance().genesisBlock.setWitnesses(new ArrayList<>()); + + // Point to the witness node as the only seed peer. + // Mutable list — startup appends persisted peers to it. + Args.getInstance().getSeedNode().setAddressList(new ArrayList<>( + Collections.singletonList(new InetSocketAddress(WITNESS_HOST, WITNESS_P2P_PORT)))); + + // ── 3. Start Spring context ─────────────────────────────────────────── + TronApplicationContext context = new TronApplicationContext(DefaultConfig.class); + Application app = ApplicationFactory.create(context); + Manager db = context.getBean(Manager.class); + ChainBaseManager chain = context.getBean(ChainBaseManager.class); + + // ── 4. Install matching PQ genesis pre-state ────────────────────────── + // Without this the incoming pq_auth_sig would fail to validate because + // this node wouldn't know the witness's FN-DSA-512 public key. + PQWitnessNode.installPQGenesisState(db, chain, witnessPub, userPub); + + // ── 5. Start P2P + gRPC (no ConsensusService.start — we don't produce) ─ + app.startup(); + + System.out.println("\nFull node running, syncing from witness. Send Ctrl-C to stop.\n"); + + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + System.out.println("Shutting down..."); + context.close(); + Args.clearParam(); + })); + + Thread.currentThread().join(); + } +} diff --git a/framework/src/test/java/org/tron/common/crypto/pqc/program/PQWitnessNode.java b/framework/src/test/java/org/tron/common/crypto/pqc/program/PQWitnessNode.java new file mode 100644 index 00000000000..f677a44cf01 --- /dev/null +++ b/framework/src/test/java/org/tron/common/crypto/pqc/program/PQWitnessNode.java @@ -0,0 +1,204 @@ +package org.tron.common.crypto.pqc.program; + +import com.google.protobuf.ByteString; +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import org.bouncycastle.util.encoders.Hex; +import org.tron.common.application.Application; +import org.tron.common.application.ApplicationFactory; +import org.tron.common.application.TronApplicationContext; +import org.tron.common.crypto.pqc.FNDSA; +import org.tron.common.crypto.pqc.PQSchemeRegistry; +import org.tron.common.utils.ByteArray; +import org.tron.core.ChainBaseManager; +import org.tron.core.capsule.AccountCapsule; +import org.tron.core.capsule.WitnessCapsule; +import org.tron.core.config.DefaultConfig; +import org.tron.core.config.args.Args; +import org.tron.core.consensus.ConsensusService; +import org.tron.core.db.Manager; +import org.tron.protos.Protocol.Account; +import org.tron.protos.Protocol.AccountType; +import org.tron.protos.Protocol.Key; +import org.tron.protos.Protocol.PQScheme; +import org.tron.protos.Protocol.Permission; +import org.tron.protos.Protocol.Permission.PermissionType; + +/** + * Demo witness node with FN-DSA-512 block production. + * + * Starts an in-process TRON node configured with a PQC witness keypair and + * a user account that holds an FN-DSA-512 owner permission — ready to receive + * transactions from {@link PQClient}. + * + * Keypairs are derived from fixed seeds so PQClient can derive matching keys + * without any out-of-band coordination. + * + * Usage: + * Terminal 1 — start this node: + * ./gradlew :framework:run -PmainClass=org.tron.common.crypto.pqc.program.PQWitnessNode + * Terminal 2 — broadcast a PQC transaction: + * ./gradlew :framework:run -PmainClass=org.tron.common.crypto.pqc.program.PQClient + */ +public class PQWitnessNode { + + /** Fixed seed for the FN-DSA-512 witness keypair (shared with PQClient for derivation). */ + static final byte[] WITNESS_SEED = filledSeed(0x01); + /** Fixed seed for the FN-DSA-512 user keypair (shared with PQClient for derivation). */ + static final byte[] USER_SEED = filledSeed(0x02); + + /** gRPC port the node listens on. */ + static final int GRPC_PORT = 50051; + + /** Full-node HTTP port. */ + static final int HTTP_PORT = 8090; + + /** P2P listen port (shared with PQFullNode so it can dial in as a seed peer). */ + static final int P2P_PORT = 18888; + + /** Fixed on-chain address for the demo user account. */ + static final byte[] USER_ADDR = + ByteArray.fromHexString("41abd4b9367799eaa3197fecb144eb71de1e049abc"); + + public static void main(String[] args) throws Exception { + // Force INFO level: logback-test.xml (on the test classpath) sets root=DEBUG + // which is far too noisy for a demo run. + ((ch.qos.logback.classic.Logger) org.slf4j.LoggerFactory + .getLogger(ch.qos.logback.classic.Logger.ROOT_LOGGER_NAME)) + .setLevel(ch.qos.logback.classic.Level.INFO); + + // ── 1. Derive deterministic keypairs ────────────────────────────────── + FNDSA witnessKp = new FNDSA(WITNESS_SEED); + FNDSA userKp = new FNDSA(USER_SEED); + + byte[] witnessPub = witnessKp.getPublicKey(); + byte[] witnessAddr = FNDSA.computeAddress(witnessPub); + byte[] userPub = userKp.getPublicKey(); + byte[] signerAddr = FNDSA.computeAddress(userPub); + + System.out.println("=== PQC Witness Node ==="); + System.out.println("Witness address (FN-DSA-512): " + ByteArray.toHexString(witnessAddr)); + System.out.println("User address: " + ByteArray.toHexString(USER_ADDR)); + System.out.println("User signer address: " + ByteArray.toHexString(signerAddr)); + System.out.println("gRPC port: " + GRPC_PORT); + System.out.println("HTTP port: " + HTTP_PORT); + System.out.println("P2P port: " + P2P_PORT); + + // ── 2. Configure node ───────────────────────────────────────────────── + File dbDir = Files.createTempDirectory("pqc-node-").toFile(); + dbDir.deleteOnExit(); + + // Inject the witness keypair via a temp HOCON config that includes + // config-test.conf and overrides localwitness_pq_keys with the extended + // priv‖pub hex derived from WITNESS_SEED (matches what PQClient derives). + Path conf = writeWitnessConfig(witnessKp); + + Args.setParam(new String[]{"--output-directory", dbDir.getAbsolutePath(), "-w"}, + conf.toString()); + Args.getInstance().setRpcEnable(true); + Args.getInstance().setFullNodeHttpEnable(true); + Args.getInstance().setFullNodeHttpPort(HTTP_PORT); + Args.getInstance().setRpcPort(GRPC_PORT); + Args.getInstance().setNodeListenPort(P2P_PORT); + Args.getInstance().setNeedSyncCheck(false); + Args.getInstance().setMinEffectiveConnection(0); + Args.getInstance().genesisBlock.setWitnesses(new ArrayList<>()); + + // ── 3. Start Spring context ─────────────────────────────────────────── + TronApplicationContext context = new TronApplicationContext(DefaultConfig.class); + Application app = ApplicationFactory.create(context); + Manager db = context.getBean(Manager.class); + ChainBaseManager chain = context.getBean(ChainBaseManager.class); + + // ── 4. Install PQ genesis pre-state (shared with PQFullNode) ───────── + installPQGenesisState(db, chain, witnessPub, userPub); + + // ── 5. Start consensus (DposTask auto-produces blocks) ─────────────── + context.getBean(ConsensusService.class).start(); + + // ── 6. Start gRPC / P2P server ─────────────────────────────────────── + app.startup(); + + System.out.println("\nNode is running. Send Ctrl-C to stop."); + System.out.println("Run PQClient or PQFullNode in another terminal.\n"); + + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + System.out.println("Shutting down..."); + context.close(); + Args.clearParam(); + })); + + Thread.currentThread().join(); // block until Ctrl-C + } + + /** + * Apply the PQ-specific pre-state that must exist on every node participating + * in the demo network. Both PQWitnessNode and PQFullNode call this so their + * genesis state matches before the first PQ block is produced / received. + */ + static void installPQGenesisState(Manager db, ChainBaseManager chain, + byte[] witnessPub, byte[] userPub) { + byte[] witnessAddr = PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, witnessPub); + ByteString witnessAddrBs = ByteString.copyFrom(witnessAddr); + byte[] signerAddr = PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, userPub); + ByteString signerAddrBs = ByteString.copyFrom(signerAddr); + + // Activate FN-DSA on the local chain params. + db.getDynamicPropertiesStore().saveAllowFnDsa512(1L); + db.getDynamicPropertiesStore().saveAllowMultiSign(1L); + + // Witness account with FN-DSA-512 witness permission. Address-as-fingerprint + // binds the public key in-band; no separate pq_key field is stored. + Permission witnessPerm = Permission.newBuilder() + .setType(PermissionType.Witness) + .setId(1).setPermissionName("witness").setThreshold(1) + .addKeys(Key.newBuilder() + .setAddress(witnessAddrBs).setWeight(1)) + .build(); + db.getAccountStore().put(witnessAddr, new AccountCapsule(Account.newBuilder() + .setAddress(witnessAddrBs).setType(AccountType.Normal) + .setBalance(1_000_000_000L).setIsWitness(true) + .setWitnessPermission(witnessPerm).build())); + + // The witness must be in the witness store BEFORE consensus starts so that + // DposService.start() includes it in the active-witness schedule. + chain.getWitnessStore().put(witnessAddr, new WitnessCapsule(witnessAddrBs)); + chain.getWitnessScheduleStore().saveActiveWitnesses(new ArrayList<>()); + chain.addWitness(witnessAddrBs); + + // User account with FN-DSA-512 owner permission. + Permission userOwnerPerm = Permission.newBuilder() + .setType(PermissionType.Owner).setPermissionName("owner").setThreshold(1) + .addKeys(Key.newBuilder() + .setAddress(signerAddrBs).setWeight(1)) + .build(); + AccountCapsule userCapsule = new AccountCapsule( + ByteString.copyFrom(USER_ADDR), ByteString.copyFromUtf8("pquser"), AccountType.Normal); + userCapsule.setBalance(100_000_000L); // 100 TRX + userCapsule.updatePermissions(userOwnerPerm, null, Collections.emptyList()); + db.getAccountStore().put(USER_ADDR, userCapsule); + } + + private static byte[] filledSeed(int value) { + byte[] seed = new byte[FNDSA.SEED_LENGTH]; + Arrays.fill(seed, (byte) value); + return seed; + } + + private static Path writeWitnessConfig(FNDSA witnessKp) throws java.io.IOException { + Path conf = Files.createTempFile("pqc-witness-", ".conf"); + conf.toFile().deleteOnExit(); + String body = "include classpath(\"config-test.conf\")\n" + + "localwitness_pq_scheme = \"FN_DSA_512\"\n" + + "localwitness_pq_keys = [\n" + + " \"" + Hex.toHexString(witnessKp.getPrivateKeyWithPublicKey()) + "\"\n" + + "]\n"; + Files.write(conf, body.getBytes(StandardCharsets.UTF_8)); + return conf; + } +} diff --git a/framework/src/test/java/org/tron/common/runtime/vm/BatchValidateSignPQTest.java b/framework/src/test/java/org/tron/common/runtime/vm/BatchValidateSignPQTest.java new file mode 100644 index 00000000000..aae387ad2b7 --- /dev/null +++ b/framework/src/test/java/org/tron/common/runtime/vm/BatchValidateSignPQTest.java @@ -0,0 +1,346 @@ +package org.tron.common.runtime.vm; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.tuple.Pair; +import org.bouncycastle.util.encoders.Hex; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.tron.common.crypto.pqc.FNDSA; +import org.tron.common.crypto.pqc.PQSchemeRegistry; +import org.tron.common.utils.client.utils.AbiUtil; +import org.tron.core.vm.PrecompiledContracts; +import org.tron.core.vm.PrecompiledContracts.BatchValidateSignPQ; +import org.tron.core.vm.PrecompiledContracts.PrecompiledContract; +import org.tron.core.vm.config.VMConfig; +import org.tron.protos.Protocol.PQScheme; + +/** + * Unit tests for the 0x18 batch independent Falcon-512 verify precompile. + * Returns a 256-bit bitmap where bit i is set iff + * {@code derive(pk_i) == expectedAddr_i && FNDSA.verify(pk_i, hash, sig_i)}. + * Stateless — no chain DB. + */ +@Slf4j +public class BatchValidateSignPQTest { + + private static final DataWord ADDR_0X18 = new DataWord( + "0000000000000000000000000000000000000000000000000000000000000018"); + + private static final String METHOD_SIGN = + "batchvalidatesignpq(bytes32,bytes[],bytes[],bytes32[])"; + + private static final byte[] HASH; + + static { + HASH = new byte[32]; + for (int i = 0; i < 32; i++) { + HASH[i] = (byte) (i + 1); + } + } + + private final BatchValidateSignPQ contract = new BatchValidateSignPQ(); + + @Before + public void enableProposal() { + VMConfig.initAllowFnDsa512(1L); + } + + @After + public void disableProposal() { + VMConfig.initAllowFnDsa512(0L); + } + + @Test + public void switchOff_returnsNull() { + VMConfig.initAllowFnDsa512(0L); + Assert.assertNull(PrecompiledContracts.getContractForAddress(ADDR_0X18)); + } + + @Test + public void switchOn_returnsContract() { + PrecompiledContract pc = PrecompiledContracts.getContractForAddress(ADDR_0X18); + Assert.assertNotNull(pc); + Assert.assertTrue(pc instanceof BatchValidateSignPQ); + } + + @Test + public void constantCall_allValid_setsAllBits() { + contract.setConstantCall(true); + int n = 4; + List sigs = new ArrayList<>(n); + List pks = new ArrayList<>(n); + List addrs = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + FNDSA k = new FNDSA(); + sigs.add(Hex.toHexString(k.sign(HASH))); + pks.add(Hex.toHexString(k.getPublicKey())); + addrs.add(addrAsBytes32Hex(k.getPublicKey())); + } + byte[] res = run(HASH, sigs, pks, addrs).getRight(); + for (int i = 0; i < n; i++) { + Assert.assertEquals("bit " + i, 1, res[i]); + } + for (int i = n; i < 32; i++) { + Assert.assertEquals("padding bit " + i, 0, res[i]); + } + } + + @Test + public void constantCall_mismatchedAddress_clearsBit() { + contract.setConstantCall(true); + FNDSA k1 = new FNDSA(); + FNDSA k2 = new FNDSA(); + List sigs = Arrays.asList( + Hex.toHexString(k1.sign(HASH)), + Hex.toHexString(k2.sign(HASH))); + List pks = Arrays.asList( + Hex.toHexString(k1.getPublicKey()), + Hex.toHexString(k2.getPublicKey())); + // entry 1's address is wrong + List addrs = Arrays.asList( + addrAsBytes32Hex(k1.getPublicKey()), + addrAsBytes32Hex(k1.getPublicKey())); + + byte[] res = run(HASH, sigs, pks, addrs).getRight(); + Assert.assertEquals(1, res[0]); + Assert.assertEquals(0, res[1]); + } + + @Test + public void constantCall_tamperedSignature_clearsBit() { + contract.setConstantCall(true); + FNDSA k = new FNDSA(); + byte[] sig = k.sign(HASH); + sig[0] ^= 0x01; + List sigs = Collections1(Hex.toHexString(sig)); + List pks = Collections1(Hex.toHexString(k.getPublicKey())); + List addrs = Collections1(addrAsBytes32Hex(k.getPublicKey())); + + byte[] res = run(HASH, sigs, pks, addrs).getRight(); + Assert.assertEquals(0, res[0]); + } + + @Test + public void constantCall_wrongPkLength_clearsBit() { + contract.setConstantCall(true); + FNDSA k = new FNDSA(); + byte[] truncatedPk = Arrays.copyOf(k.getPublicKey(), k.getPublicKey().length - 1); + List sigs = Collections1(Hex.toHexString(k.sign(HASH))); + List pks = Collections1(Hex.toHexString(truncatedPk)); + List addrs = Collections1(addrAsBytes32Hex(k.getPublicKey())); + + byte[] res = run(HASH, sigs, pks, addrs).getRight(); + Assert.assertEquals(0, res[0]); + } + + @Test + public void asyncPath_allValid_setsAllBits() { + contract.setConstantCall(false); + contract.setVmShouldEndInUs(System.nanoTime() / 1000 + 5_000_000L); + int n = 4; + List sigs = new ArrayList<>(n); + List pks = new ArrayList<>(n); + List addrs = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + FNDSA k = new FNDSA(); + sigs.add(Hex.toHexString(k.sign(HASH))); + pks.add(Hex.toHexString(k.getPublicKey())); + addrs.add(addrAsBytes32Hex(k.getPublicKey())); + } + byte[] res = run(HASH, sigs, pks, addrs).getRight(); + for (int i = 0; i < n; i++) { + Assert.assertEquals("bit " + i, 1, res[i]); + } + } + + @Test + public void mismatchedArrayLengths_returnsZero() { + contract.setConstantCall(true); + FNDSA k = new FNDSA(); + List sigs = Collections1(Hex.toHexString(k.sign(HASH))); + List pks = Arrays.asList( + Hex.toHexString(k.getPublicKey()), Hex.toHexString(k.getPublicKey())); + List addrs = Collections1(addrAsBytes32Hex(k.getPublicKey())); + + byte[] res = run(HASH, sigs, pks, addrs).getRight(); + Assert.assertArrayEquals(new byte[32], res); + } + + @Test + public void overMaxSize_returnsZero() { + contract.setConstantCall(true); + contract.setVmShouldEndInUs(System.nanoTime() / 1000 + 30_000_000L); + int n = 17; + List sigs = new ArrayList<>(n); + List pks = new ArrayList<>(n); + List addrs = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + FNDSA k = new FNDSA(); + sigs.add(Hex.toHexString(k.sign(HASH))); + pks.add(Hex.toHexString(k.getPublicKey())); + addrs.add(addrAsBytes32Hex(k.getPublicKey())); + } + byte[] res = run(HASH, sigs, pks, addrs).getRight(); + Assert.assertArrayEquals(new byte[32], res); + } + + @Test + public void energyScalesWithCount() { + contract.setConstantCall(true); + int n = 3; + List sigs = new ArrayList<>(n); + List pks = new ArrayList<>(n); + List addrs = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + FNDSA k = new FNDSA(); + sigs.add(Hex.toHexString(k.sign(HASH))); + pks.add(Hex.toHexString(k.getPublicKey())); + addrs.add(addrAsBytes32Hex(k.getPublicKey())); + } + byte[] input = encode(HASH, sigs, pks, addrs); + Assert.assertEquals(3L * 15000L, contract.getEnergyForData(input)); + } + + @Test + public void emptyArrays_returnsAllZero() { + contract.setConstantCall(true); + byte[] res = run(HASH, new ArrayList<>(), new ArrayList<>(), new ArrayList<>()).getRight(); + Assert.assertArrayEquals(new byte[32], res); + } + + @Test + public void differentHash_clearsAllBits() { + contract.setConstantCall(true); + int n = 3; + List sigs = new ArrayList<>(n); + List pks = new ArrayList<>(n); + List addrs = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + FNDSA k = new FNDSA(); + // Sign HASH... + sigs.add(Hex.toHexString(k.sign(HASH))); + pks.add(Hex.toHexString(k.getPublicKey())); + addrs.add(addrAsBytes32Hex(k.getPublicKey())); + } + // ...but verify against a different hash. + byte[] otherHash = new byte[32]; + Arrays.fill(otherHash, (byte) 0xAA); + + byte[] res = run(otherHash, sigs, pks, addrs).getRight(); + Assert.assertArrayEquals(new byte[32], res); + } + + @Test + public void atMaxSize16_setsAllBits() { + contract.setConstantCall(true); + int n = 16; + List sigs = new ArrayList<>(n); + List pks = new ArrayList<>(n); + List addrs = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + FNDSA k = new FNDSA(); + sigs.add(Hex.toHexString(k.sign(HASH))); + pks.add(Hex.toHexString(k.getPublicKey())); + addrs.add(addrAsBytes32Hex(k.getPublicKey())); + } + contract.setVmShouldEndInUs(System.nanoTime() / 1000 + 30_000_000L); + byte[] res = run(HASH, sigs, pks, addrs).getRight(); + for (int i = 0; i < n; i++) { + Assert.assertEquals("bit " + i, 1, res[i]); + } + for (int i = n; i < 32; i++) { + Assert.assertEquals("padding bit " + i, 0, res[i]); + } + } + + @Test + public void asyncPath_mixedValidInvalid() { + contract.setConstantCall(false); + contract.setVmShouldEndInUs(System.nanoTime() / 1000 + 10_000_000L); + int n = 4; + List sigs = new ArrayList<>(n); + List pks = new ArrayList<>(n); + List addrs = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + FNDSA k = new FNDSA(); + byte[] sig = k.sign(HASH); + // Tamper entries 1 and 3. + if (i == 1 || i == 3) { + sig[0] ^= 0x01; + } + sigs.add(Hex.toHexString(sig)); + pks.add(Hex.toHexString(k.getPublicKey())); + addrs.add(addrAsBytes32Hex(k.getPublicKey())); + } + byte[] res = run(HASH, sigs, pks, addrs).getRight(); + Assert.assertEquals(1, res[0]); + Assert.assertEquals(0, res[1]); + Assert.assertEquals(1, res[2]); + Assert.assertEquals(0, res[3]); + } + + @Test + public void sigTooLong_clearsBit() { + contract.setConstantCall(true); + FNDSA k = new FNDSA(); + byte[] oversized = new byte[800]; + Arrays.fill(oversized, (byte) 0x99); + List sigs = Collections1(Hex.toHexString(oversized)); + List pks = Collections1(Hex.toHexString(k.getPublicKey())); + List addrs = Collections1(addrAsBytes32Hex(k.getPublicKey())); + + byte[] res = run(HASH, sigs, pks, addrs).getRight(); + Assert.assertEquals(0, res[0]); + } + + // -------- helpers -------- + + private Pair run(byte[] hash, List sigs, + List pks, List addrs) { + byte[] input = encode(hash, sigs, pks, addrs); + contract.setVmShouldEndInUs(System.nanoTime() / 1000 + 5_000_000L); + Pair ret = contract.execute(input); + logger.info("0x18 bitmap: {}", Hex.toHexString(ret.getRight())); + return ret; + } + + private byte[] encode(byte[] hash, List sigs, List pks, List addrs) { + List parameters = Arrays.asList( + "0x" + Hex.toHexString(hash), + toHexList(sigs), + toHexList(pks), + toHexList(addrs)); + return Hex.decode(AbiUtil.parseParameters(METHOD_SIGN, parameters)); + } + + private static List toHexList(List hexes) { + List out = new ArrayList<>(hexes.size()); + for (String h : hexes) { + out.add(h.startsWith("0x") ? h : ("0x" + h)); + } + return out; + } + + private static List Collections1(String s) { + List l = new ArrayList<>(1); + l.add(s); + return l; + } + + /** + * Build a bytes32 hex string whose low 21 bytes hold the derived TRON address + * (high 11 bytes left zero). Matches {@code DataWord.equalAddressByteArray}'s + * "compare last 20 bytes" semantics. + */ + private static String addrAsBytes32Hex(byte[] pk) { + byte[] addr21 = PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, pk); + byte[] padded = new byte[32]; + System.arraycopy(addr21, 0, padded, 32 - addr21.length, addr21.length); + return "0x" + Hex.toHexString(padded); + } +} diff --git a/framework/src/test/java/org/tron/common/runtime/vm/FnDsaPrecompileTest.java b/framework/src/test/java/org/tron/common/runtime/vm/FnDsaPrecompileTest.java new file mode 100644 index 00000000000..4e21409e2ce --- /dev/null +++ b/framework/src/test/java/org/tron/common/runtime/vm/FnDsaPrecompileTest.java @@ -0,0 +1,186 @@ +package org.tron.common.runtime.vm; + +import org.apache.commons.lang3.tuple.Pair; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.tron.common.crypto.pqc.FNDSA; +import org.tron.core.vm.PrecompiledContracts; +import org.tron.core.vm.PrecompiledContracts.PrecompiledContract; +import org.tron.core.vm.config.VMConfig; + +/** + * Unit tests for the FN-DSA / Falcon-512 (0x16) verify precompile (EIP-8052 / TRON extension). + * Input layout: [msg 32B | sig_len 2B | sig sig_len B | pk 896B]. Stateless — no chain DB. + */ +public class FnDsaPrecompileTest { + + private static final DataWord FNDSA_ADDR = new DataWord( + "0000000000000000000000000000000000000000000000000000000000000016"); + + private static final byte[] MESSAGE_HASH = new byte[32]; + + static { + for (int i = 0; i < 32; i++) { + MESSAGE_HASH[i] = (byte) i; + } + } + + @Before + public void enableProposal() { + VMConfig.initAllowFnDsa512(1L); + } + + @After + public void disableProposal() { + VMConfig.initAllowFnDsa512(0L); + } + + @Test + public void switchOff_returnsNull() { + VMConfig.initAllowFnDsa512(0L); + Assert.assertNull(PrecompiledContracts.getContractForAddress(FNDSA_ADDR)); + } + + @Test + public void switchOn_returnsContract() { + Assert.assertNotNull(PrecompiledContracts.getContractForAddress(FNDSA_ADDR)); + } + + @Test + public void validSignature_returnsOne() { + FNDSA key = new FNDSA(); + byte[] sig = key.sign(MESSAGE_HASH); + byte[] input = buildInput(MESSAGE_HASH, sig, key.getPublicKey()); + + PrecompiledContract pc = PrecompiledContracts.getContractForAddress(FNDSA_ADDR); + Pair result = pc.execute(input); + + Assert.assertTrue(result.getLeft()); + Assert.assertArrayEquals(DataWord.ONE().getData(), result.getRight()); + Assert.assertEquals(2500, pc.getEnergyForData(input)); + } + + @Test + public void tamperedMessage_returnsZero() { + FNDSA key = new FNDSA(); + byte[] sig = key.sign(MESSAGE_HASH); + byte[] tampered = MESSAGE_HASH.clone(); + tampered[0] ^= 0x01; + byte[] input = buildInput(tampered, sig, key.getPublicKey()); + + Pair result = + PrecompiledContracts.getContractForAddress(FNDSA_ADDR).execute(input); + + Assert.assertTrue(result.getLeft()); + Assert.assertArrayEquals(DataWord.ZERO().getData(), result.getRight()); + } + + @Test + public void tamperedSignature_returnsZero() { + FNDSA key = new FNDSA(); + byte[] sig = key.sign(MESSAGE_HASH); + sig[0] ^= 0x01; + byte[] input = buildInput(MESSAGE_HASH, sig, key.getPublicKey()); + + Pair result = + PrecompiledContracts.getContractForAddress(FNDSA_ADDR).execute(input); + + Assert.assertTrue(result.getLeft()); + Assert.assertArrayEquals(DataWord.ZERO().getData(), result.getRight()); + } + + @Test + public void wrongPublicKey_returnsZero() { + FNDSA signer = new FNDSA(); + FNDSA other = new FNDSA(); + byte[] sig = signer.sign(MESSAGE_HASH); + byte[] input = buildInput(MESSAGE_HASH, sig, other.getPublicKey()); + + Pair result = + PrecompiledContracts.getContractForAddress(FNDSA_ADDR).execute(input); + + Assert.assertTrue(result.getLeft()); + Assert.assertArrayEquals(DataWord.ZERO().getData(), result.getRight()); + } + + @Test + public void nullInput_returnsZero() { + Pair result = + PrecompiledContracts.getContractForAddress(FNDSA_ADDR).execute(null); + + Assert.assertTrue(result.getLeft()); + Assert.assertArrayEquals(DataWord.ZERO().getData(), result.getRight()); + } + + @Test + public void shortInput_returnsZero() { + Pair result = + PrecompiledContracts.getContractForAddress(FNDSA_ADDR).execute(new byte[100]); + + Assert.assertTrue(result.getLeft()); + Assert.assertArrayEquals(DataWord.ZERO().getData(), result.getRight()); + } + + @Test + public void zeroSigLen_returnsZero() { + FNDSA key = new FNDSA(); + byte[] pk = key.getPublicKey(); + // sig_len = 0 is invalid (must be >= 1) + // input must be >= MIN_INPUT_LEN (931 = 32 + 2 + 1 + 896) to reach the sigLen check + byte[] input = new byte[32 + 2 + pk.length + 1]; + System.arraycopy(MESSAGE_HASH, 0, input, 0, 32); + // sig_len bytes = 0x00 0x00 → sigLen = 0 + System.arraycopy(pk, 0, input, 34, pk.length); + + Pair result = + PrecompiledContracts.getContractForAddress(FNDSA_ADDR).execute(input); + + Assert.assertTrue(result.getLeft()); + Assert.assertArrayEquals(DataWord.ZERO().getData(), result.getRight()); + } + + @Test + public void oversizedSigLen_returnsZero() { + // sig_len = 753, which exceeds FNDSA.SIGNATURE_LENGTH (752) + byte[] input = new byte[32 + 2 + 753 + FNDSA.PUBLIC_KEY_LENGTH]; + input[32] = 0x02; // high byte + input[33] = (byte) 0xF1; // low byte → 0x02F1 = 753 + Pair result = + PrecompiledContracts.getContractForAddress(FNDSA_ADDR).execute(input); + + Assert.assertTrue(result.getLeft()); + Assert.assertArrayEquals(DataWord.ZERO().getData(), result.getRight()); + } + + @Test + public void sigLenLargerThanActualData_returnsZero() { + FNDSA key = new FNDSA(); + byte[] sig = key.sign(MESSAGE_HASH); + // claim sig is 100 bytes longer than it is + byte[] input = buildInput(MESSAGE_HASH, sig, key.getPublicKey()); + // corrupt sig_len field to claim a larger sig + int claimedLen = sig.length + 100; + input[32] = (byte) ((claimedLen >> 8) & 0xFF); + input[33] = (byte) (claimedLen & 0xFF); + + Pair result = + PrecompiledContracts.getContractForAddress(FNDSA_ADDR).execute(input); + + Assert.assertTrue(result.getLeft()); + Assert.assertArrayEquals(DataWord.ZERO().getData(), result.getRight()); + } + + /** Encodes input as [msg 32B | sig_len 2B | sig | pk]. */ + private static byte[] buildInput(byte[] msg, byte[] sig, byte[] pk) { + int sigLen = sig.length; + byte[] out = new byte[32 + 2 + sigLen + pk.length]; + System.arraycopy(msg, 0, out, 0, 32); + out[32] = (byte) ((sigLen >> 8) & 0xFF); + out[33] = (byte) (sigLen & 0xFF); + System.arraycopy(sig, 0, out, 34, sigLen); + System.arraycopy(pk, 0, out, 34 + sigLen, pk.length); + return out; + } +} diff --git a/framework/src/test/java/org/tron/common/runtime/vm/ValidateMultiSignPQTest.java b/framework/src/test/java/org/tron/common/runtime/vm/ValidateMultiSignPQTest.java new file mode 100644 index 00000000000..37a7cd7aa02 --- /dev/null +++ b/framework/src/test/java/org/tron/common/runtime/vm/ValidateMultiSignPQTest.java @@ -0,0 +1,464 @@ +package org.tron.common.runtime.vm; + +import com.google.protobuf.ByteString; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.tuple.Pair; +import org.bouncycastle.util.encoders.Hex; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.tron.common.BaseTest; +import org.tron.common.TestConstants; +import org.tron.common.crypto.ECKey; +import org.tron.common.crypto.pqc.FNDSA; +import org.tron.common.crypto.pqc.PQSchemeRegistry; +import org.tron.common.parameter.CommonParameter; +import org.tron.common.utils.ByteArray; +import org.tron.common.utils.ByteUtil; +import org.tron.common.utils.Sha256Hash; +import org.tron.common.utils.StringUtil; +import org.tron.common.utils.client.utils.AbiUtil; +import org.tron.core.capsule.AccountCapsule; +import org.tron.core.config.args.Args; +import org.tron.core.store.StoreFactory; +import org.tron.core.vm.PrecompiledContracts; +import org.tron.core.vm.PrecompiledContracts.PrecompiledContract; +import org.tron.core.vm.PrecompiledContracts.ValidateMultiSignPQ; +import org.tron.core.vm.config.VMConfig; +import org.tron.core.vm.repository.Repository; +import org.tron.core.vm.repository.RepositoryImpl; +import org.tron.protos.Protocol; +import org.tron.protos.Protocol.PQScheme; + +/** + * Unit tests for the 0x17 algorithm-agnostic Permission multi-sign precompile. + * Mirrors 0x09 hash construction and threshold semantics, while supporting + * Falcon-512 entries alongside ECDSA against the same Permission.keys[]. + */ +@Slf4j +public class ValidateMultiSignPQTest extends BaseTest { + + private static final DataWord ADDR_0X17 = new DataWord( + "0000000000000000000000000000000000000000000000000000000000000017"); + + private static final String METHOD_SIGN = + "validatemultisign(address,uint256,bytes32,bytes[],bytes[],bytes[])"; + + private static final byte[] longData; + + static { + Args.setParam(new String[]{"--output-directory", dbPath(), "--debug"}, TestConstants.TEST_CONF); + longData = new byte[1000]; + Arrays.fill(longData, (byte) 7); + } + + private final ValidateMultiSignPQ contract = new ValidateMultiSignPQ(); + + @Before + public void before() { + dbManager.getDynamicPropertiesStore().saveAllowMultiSign(1); + dbManager.getDynamicPropertiesStore().saveTotalSignNum(5); + VMConfig.initAllowFnDsa512(1L); + } + + @Test + public void switchOff_returnsNull() { + VMConfig.initAllowFnDsa512(0L); + Assert.assertNull(PrecompiledContracts.getContractForAddress(ADDR_0X17)); + } + + @Test + public void switchOn_returnsContract() { + PrecompiledContract pc = PrecompiledContracts.getContractForAddress(ADDR_0X17); + Assert.assertNotNull(pc); + Assert.assertTrue(pc instanceof ValidateMultiSignPQ); + } + + @Test + public void unknownAccount_returnsZero() { + ECKey owner = new ECKey(); + byte[] data = Sha256Hash.hash(CommonParameter.getInstance().isECKeyCryptoEngine(), longData); + byte[] toSign = computeHash(owner.getAddress(), 2, data); + List ecdsaSigs = Collections.singletonList( + Hex.toHexString(new ECKey().sign(toSign).toByteArray())); + Assert.assertArrayEquals(DataWord.ZERO().getData(), + runContract(owner.getAddress(), 2, data, ecdsaSigs, + Collections.emptyList(), Collections.emptyList()).getRight()); + } + + @Test + public void pureEcdsaThresholdReached_returnsOne() { + ECKey k1 = new ECKey(); + ECKey k2 = new ECKey(); + ECKey owner = new ECKey(); + setupPermission(owner, Arrays.asList(k1.getAddress(), k2.getAddress()), + Arrays.asList(1, 1), 2, Collections.emptyList(), Collections.emptyList()); + + byte[] data = Sha256Hash.hash(CommonParameter.getInstance().isECKeyCryptoEngine(), longData); + byte[] toSign = computeHash(owner.getAddress(), 2, data); + List ecdsaSigs = Arrays.asList( + Hex.toHexString(k1.sign(toSign).toByteArray()), + Hex.toHexString(k2.sign(toSign).toByteArray())); + + Assert.assertArrayEquals(DataWord.ONE().getData(), + runContract(owner.getAddress(), 2, data, ecdsaSigs, + Collections.emptyList(), Collections.emptyList()).getRight()); + } + + @Test + public void purePqThresholdReached_returnsOne() { + FNDSA pq1 = new FNDSA(); + FNDSA pq2 = new FNDSA(); + ECKey owner = new ECKey(); + byte[] addr1 = PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, pq1.getPublicKey()); + byte[] addr2 = PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, pq2.getPublicKey()); + setupPermission(owner, Collections.emptyList(), Collections.emptyList(), + 2, Arrays.asList(addr1, addr2), Arrays.asList(1, 1)); + + byte[] data = Sha256Hash.hash(CommonParameter.getInstance().isECKeyCryptoEngine(), longData); + byte[] toSign = computeHash(owner.getAddress(), 2, data); + + List pqSigs = Arrays.asList( + Hex.toHexString(pq1.sign(toSign)), + Hex.toHexString(pq2.sign(toSign))); + List pqPks = Arrays.asList( + Hex.toHexString(pq1.getPublicKey()), + Hex.toHexString(pq2.getPublicKey())); + + Assert.assertArrayEquals(DataWord.ONE().getData(), + runContract(owner.getAddress(), 2, data, + Collections.emptyList(), pqSigs, pqPks).getRight()); + } + + @Test + public void mixedEcdsaAndPq_returnsOne() { + ECKey k1 = new ECKey(); + FNDSA pq1 = new FNDSA(); + ECKey owner = new ECKey(); + byte[] pqAddr = PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, pq1.getPublicKey()); + setupPermission(owner, Collections.singletonList(k1.getAddress()), Collections.singletonList(1), + 2, Collections.singletonList(pqAddr), Collections.singletonList(1)); + + byte[] data = Sha256Hash.hash(CommonParameter.getInstance().isECKeyCryptoEngine(), longData); + byte[] toSign = computeHash(owner.getAddress(), 2, data); + + List ecdsaSigs = Collections.singletonList( + Hex.toHexString(k1.sign(toSign).toByteArray())); + List pqSigs = Collections.singletonList(Hex.toHexString(pq1.sign(toSign))); + List pqPks = Collections.singletonList(Hex.toHexString(pq1.getPublicKey())); + + Assert.assertArrayEquals(DataWord.ONE().getData(), + runContract(owner.getAddress(), 2, data, ecdsaSigs, pqSigs, pqPks).getRight()); + } + + @Test + public void pqSignatureForgery_returnsZero() { + FNDSA pq1 = new FNDSA(); + ECKey owner = new ECKey(); + byte[] pqAddr = PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, pq1.getPublicKey()); + setupPermission(owner, Collections.emptyList(), Collections.emptyList(), + 1, Collections.singletonList(pqAddr), Collections.singletonList(1)); + + byte[] data = Sha256Hash.hash(CommonParameter.getInstance().isECKeyCryptoEngine(), longData); + byte[] toSign = computeHash(owner.getAddress(), 2, data); + byte[] forgedSig = pq1.sign(toSign); + forgedSig[10] ^= 0x01; + + List pqSigs = Collections.singletonList(Hex.toHexString(forgedSig)); + List pqPks = Collections.singletonList(Hex.toHexString(pq1.getPublicKey())); + + Assert.assertArrayEquals(DataWord.ZERO().getData(), + runContract(owner.getAddress(), 2, data, + Collections.emptyList(), pqSigs, pqPks).getRight()); + } + + @Test + public void wrongPqPublicKeyLength_returnsZero() { + FNDSA pq1 = new FNDSA(); + ECKey owner = new ECKey(); + byte[] pqAddr = PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, pq1.getPublicKey()); + setupPermission(owner, Collections.emptyList(), Collections.emptyList(), + 1, Collections.singletonList(pqAddr), Collections.singletonList(1)); + + byte[] data = Sha256Hash.hash(CommonParameter.getInstance().isECKeyCryptoEngine(), longData); + byte[] toSign = computeHash(owner.getAddress(), 2, data); + byte[] truncatedPk = Arrays.copyOf(pq1.getPublicKey(), pq1.getPublicKey().length - 1); + + List pqSigs = Collections.singletonList(Hex.toHexString(pq1.sign(toSign))); + List pqPks = Collections.singletonList(Hex.toHexString(truncatedPk)); + + Assert.assertArrayEquals(DataWord.ZERO().getData(), + runContract(owner.getAddress(), 2, data, + Collections.emptyList(), pqSigs, pqPks).getRight()); + } + + @Test + public void mismatchedPqArrayLengths_returnsZero() { + FNDSA pq1 = new FNDSA(); + FNDSA pq2 = new FNDSA(); + ECKey owner = new ECKey(); + byte[] addr1 = PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, pq1.getPublicKey()); + setupPermission(owner, Collections.emptyList(), Collections.emptyList(), + 1, Collections.singletonList(addr1), Collections.singletonList(1)); + + byte[] data = Sha256Hash.hash(CommonParameter.getInstance().isECKeyCryptoEngine(), longData); + byte[] toSign = computeHash(owner.getAddress(), 2, data); + + List pqSigs = Arrays.asList( + Hex.toHexString(pq1.sign(toSign)), + Hex.toHexString(pq2.sign(toSign))); + List pqPks = Collections.singletonList(Hex.toHexString(pq1.getPublicKey())); + + Assert.assertArrayEquals(DataWord.ZERO().getData(), + runContract(owner.getAddress(), 2, data, + Collections.emptyList(), pqSigs, pqPks).getRight()); + } + + @Test + public void totalCountOverMaxSize_returnsZero() { + ECKey owner = new ECKey(); + List ecdsaAddrs = new ArrayList<>(); + List ecdsaWeights = new ArrayList<>(); + List ecdsaKeys = new ArrayList<>(); + for (int i = 0; i < 6; i++) { + ECKey k = new ECKey(); + ecdsaKeys.add(k); + ecdsaAddrs.add(k.getAddress()); + ecdsaWeights.add(1); + } + setupPermission(owner, ecdsaAddrs, ecdsaWeights, 6, + Collections.emptyList(), Collections.emptyList()); + + byte[] data = Sha256Hash.hash(CommonParameter.getInstance().isECKeyCryptoEngine(), longData); + byte[] toSign = computeHash(owner.getAddress(), 2, data); + List ecdsaSigs = new ArrayList<>(); + for (ECKey k : ecdsaKeys) { + ecdsaSigs.add(Hex.toHexString(k.sign(toSign).toByteArray())); + } + + Assert.assertArrayEquals(DataWord.ZERO().getData(), + runContract(owner.getAddress(), 2, data, ecdsaSigs, + Collections.emptyList(), Collections.emptyList()).getRight()); + } + + @Test + public void duplicatePqSig_doesNotDoubleCount() { + FNDSA pq1 = new FNDSA(); + ECKey owner = new ECKey(); + byte[] pqAddr = PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, pq1.getPublicKey()); + setupPermission(owner, Collections.emptyList(), Collections.emptyList(), + 2, Collections.singletonList(pqAddr), Collections.singletonList(1)); + + byte[] data = Sha256Hash.hash(CommonParameter.getInstance().isECKeyCryptoEngine(), longData); + byte[] toSign = computeHash(owner.getAddress(), 2, data); + byte[] sig = pq1.sign(toSign); + + List pqSigs = Arrays.asList(Hex.toHexString(sig), Hex.toHexString(sig)); + List pqPks = Arrays.asList( + Hex.toHexString(pq1.getPublicKey()), Hex.toHexString(pq1.getPublicKey())); + + Assert.assertArrayEquals(DataWord.ZERO().getData(), + runContract(owner.getAddress(), 2, data, + Collections.emptyList(), pqSigs, pqPks).getRight()); + } + + @Test + public void energyChargesEcdsaAndPqSeparately() { + FNDSA pq1 = new FNDSA(); + ECKey k1 = new ECKey(); + ECKey owner = new ECKey(); + byte[] pqAddr = PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, pq1.getPublicKey()); + setupPermission(owner, Collections.singletonList(k1.getAddress()), Collections.singletonList(1), + 2, Collections.singletonList(pqAddr), Collections.singletonList(1)); + + byte[] data = Sha256Hash.hash(CommonParameter.getInstance().isECKeyCryptoEngine(), longData); + byte[] toSign = computeHash(owner.getAddress(), 2, data); + List ecdsaSigs = Collections.singletonList( + Hex.toHexString(k1.sign(toSign).toByteArray())); + List pqSigs = Collections.singletonList(Hex.toHexString(pq1.sign(toSign))); + List pqPks = Collections.singletonList(Hex.toHexString(pq1.getPublicKey())); + + byte[] input = encodeInput(owner.getAddress(), 2, data, ecdsaSigs, pqSigs, pqPks); + // 1 ECDSA × 1500 + 1 PQ × 15000 = 16500 + Assert.assertEquals(16500L, contract.getEnergyForData(input)); + } + + @Test + public void thresholdNotReached_returnsZero() { + ECKey k1 = new ECKey(); + ECKey k2 = new ECKey(); + ECKey owner = new ECKey(); + setupPermission(owner, Arrays.asList(k1.getAddress(), k2.getAddress()), + Arrays.asList(1, 1), 2, Collections.emptyList(), Collections.emptyList()); + + byte[] data = Sha256Hash.hash(CommonParameter.getInstance().isECKeyCryptoEngine(), longData); + byte[] toSign = computeHash(owner.getAddress(), 2, data); + // Only one valid signature; threshold is 2. + List ecdsaSigs = Collections.singletonList( + Hex.toHexString(k1.sign(toSign).toByteArray())); + + Assert.assertArrayEquals(DataWord.ZERO().getData(), + runContract(owner.getAddress(), 2, data, ecdsaSigs, + Collections.emptyList(), Collections.emptyList()).getRight()); + } + + @Test + public void pqKeyNotInPermission_returnsZero() { + FNDSA inPerm = new FNDSA(); + FNDSA outsider = new FNDSA(); + ECKey owner = new ECKey(); + byte[] inAddr = PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, inPerm.getPublicKey()); + setupPermission(owner, Collections.emptyList(), Collections.emptyList(), + 1, Collections.singletonList(inAddr), Collections.singletonList(1)); + + byte[] data = Sha256Hash.hash(CommonParameter.getInstance().isECKeyCryptoEngine(), longData); + byte[] toSign = computeHash(owner.getAddress(), 2, data); + + // Outsider produces a perfectly valid Falcon signature, but its derived + // address is not in Permission.keys[] → weight 0 → not counted. + List pqSigs = Collections.singletonList(Hex.toHexString(outsider.sign(toSign))); + List pqPks = Collections.singletonList(Hex.toHexString(outsider.getPublicKey())); + + Assert.assertArrayEquals(DataWord.ZERO().getData(), + runContract(owner.getAddress(), 2, data, + Collections.emptyList(), pqSigs, pqPks).getRight()); + } + + @Test + public void pqSigTooLong_returnsZero() { + FNDSA pq1 = new FNDSA(); + ECKey owner = new ECKey(); + byte[] pqAddr = PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, pq1.getPublicKey()); + setupPermission(owner, Collections.emptyList(), Collections.emptyList(), + 1, Collections.singletonList(pqAddr), Collections.singletonList(1)); + + byte[] data = Sha256Hash.hash(CommonParameter.getInstance().isECKeyCryptoEngine(), longData); + + // Pad sig past the 752-byte cap. + byte[] oversized = new byte[800]; + Arrays.fill(oversized, (byte) 0x42); + List pqSigs = Collections.singletonList(Hex.toHexString(oversized)); + List pqPks = Collections.singletonList(Hex.toHexString(pq1.getPublicKey())); + + Assert.assertArrayEquals(DataWord.ZERO().getData(), + runContract(owner.getAddress(), 2, data, + Collections.emptyList(), pqSigs, pqPks).getRight()); + } + + @Test + public void bothArraysEmpty_returnsZero() { + ECKey k1 = new ECKey(); + ECKey owner = new ECKey(); + setupPermission(owner, Collections.singletonList(k1.getAddress()), + Collections.singletonList(1), 1, + Collections.emptyList(), Collections.emptyList()); + + byte[] data = Sha256Hash.hash(CommonParameter.getInstance().isECKeyCryptoEngine(), longData); + + Assert.assertArrayEquals(DataWord.ZERO().getData(), + runContract(owner.getAddress(), 2, data, + Collections.emptyList(), Collections.emptyList(), + Collections.emptyList()).getRight()); + } + + @Test + public void mixedFailingPqAborts_returnsZero() { + // Mirrors 0x09 semantics: a verify failure on any submitted entry aborts + // the whole call with DATA_FALSE — even if other entries would alone meet + // threshold. Verifies 0x17 does not silently skip a forged PQ signature. + ECKey k1 = new ECKey(); + ECKey k2 = new ECKey(); + FNDSA pq1 = new FNDSA(); + ECKey owner = new ECKey(); + byte[] pqAddr = PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, pq1.getPublicKey()); + setupPermission(owner, + Arrays.asList(k1.getAddress(), k2.getAddress()), Arrays.asList(1, 1), + 2, Collections.singletonList(pqAddr), Collections.singletonList(1)); + + byte[] data = Sha256Hash.hash(CommonParameter.getInstance().isECKeyCryptoEngine(), longData); + byte[] toSign = computeHash(owner.getAddress(), 2, data); + + List ecdsaSigs = Arrays.asList( + Hex.toHexString(k1.sign(toSign).toByteArray()), + Hex.toHexString(k2.sign(toSign).toByteArray())); + byte[] forged = pq1.sign(toSign); + forged[0] ^= 0x55; + List pqSigs = Collections.singletonList(Hex.toHexString(forged)); + List pqPks = Collections.singletonList(Hex.toHexString(pq1.getPublicKey())); + + Assert.assertArrayEquals(DataWord.ZERO().getData(), + runContract(owner.getAddress(), 2, data, ecdsaSigs, pqSigs, pqPks).getRight()); + } + + // -------- helpers -------- + + private void setupPermission(ECKey owner, + List ecdsaKeyAddrs, List ecdsaWeights, + int threshold, + List pqKeyAddrs, List pqWeights) { + AccountCapsule account = new AccountCapsule(ByteString.copyFrom(owner.getAddress()), + Protocol.AccountType.Normal, System.currentTimeMillis(), true, + dbManager.getDynamicPropertiesStore()); + + Protocol.Permission.Builder perm = Protocol.Permission.newBuilder() + .setType(Protocol.Permission.PermissionType.Active) + .setId(2) + .setPermissionName("active") + .setThreshold(threshold) + .setOperations(ByteString.copyFrom(ByteArray.fromHexString( + "0000000000000000000000000000000000000000000000000000000000000000"))); + for (int i = 0; i < ecdsaKeyAddrs.size(); i++) { + perm.addKeys(Protocol.Key.newBuilder() + .setAddress(ByteString.copyFrom(ecdsaKeyAddrs.get(i))) + .setWeight(ecdsaWeights.get(i)).build()); + } + for (int i = 0; i < pqKeyAddrs.size(); i++) { + perm.addKeys(Protocol.Key.newBuilder() + .setAddress(ByteString.copyFrom(pqKeyAddrs.get(i))) + .setWeight(pqWeights.get(i)).build()); + } + account.updatePermissions(account.getPermissionById(0), null, + Collections.singletonList(perm.build())); + dbManager.getAccountStore().put(owner.getAddress(), account); + } + + private byte[] computeHash(byte[] address, int permissionId, byte[] data) { + byte[] combined = ByteUtil.merge(address, ByteArray.fromInt(permissionId), data); + return Sha256Hash.hash(CommonParameter.getInstance().isECKeyCryptoEngine(), combined); + } + + private byte[] encodeInput(byte[] ownerAddr, int permissionId, byte[] data, + List ecdsaSigs, List pqSigs, List pqPks) { + List parameters = Arrays.asList( + StringUtil.encode58Check(ownerAddr), + permissionId, + "0x" + Hex.toHexString(data), + toHexList(ecdsaSigs), + toHexList(pqSigs), + toHexList(pqPks)); + return Hex.decode(AbiUtil.parseParameters(METHOD_SIGN, parameters)); + } + + private Pair runContract(byte[] ownerAddr, int permissionId, byte[] data, + List ecdsaSigs, List pqSigs, + List pqPks) { + byte[] input = encodeInput(ownerAddr, permissionId, data, ecdsaSigs, pqSigs, pqPks); + Repository deposit = RepositoryImpl.createRoot(StoreFactory.getInstance()); + contract.setRepository(deposit); + Pair ret = contract.execute(input); + logger.info("0x17 result: {}", Hex.toHexString(ret.getRight())); + return ret; + } + + private static List toHexList(List hexes) { + List out = new ArrayList<>(hexes.size()); + for (String h : hexes) { + out.add(h.startsWith("0x") ? h : ("0x" + h)); + } + return out; + } +} diff --git a/framework/src/test/java/org/tron/common/utils/LocalWitnessesTest.java b/framework/src/test/java/org/tron/common/utils/LocalWitnessesTest.java new file mode 100644 index 00000000000..22e99ef4571 --- /dev/null +++ b/framework/src/test/java/org/tron/common/utils/LocalWitnessesTest.java @@ -0,0 +1,152 @@ +package org.tron.common.utils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.Collections; +import org.bouncycastle.util.encoders.Hex; +import org.junit.BeforeClass; +import org.junit.Test; +import org.tron.common.crypto.pqc.FNDSA; +import org.tron.core.exception.TronError; +import org.tron.protos.Protocol.PQScheme; + +public class LocalWitnessesTest { + + // Real Falcon-512 keypair generated once per test class. We exercise the + // (priv, pub) keypair config path with bytes that satisfy the BC ops, so the + // tests below never hit cross-platform FFT determinism concerns. + private static String priv; + private static String pub; + private static String priv2; + private static String pub2; + + @BeforeClass + public static void generateKeypairs() { + FNDSA k1 = new FNDSA(); + FNDSA k2 = new FNDSA(); + priv = Hex.toHexString(k1.getPrivateKey()); + pub = Hex.toHexString(k1.getPublicKey()); + priv2 = Hex.toHexString(k2.getPrivateKey()); + pub2 = Hex.toHexString(k2.getPublicKey()); + } + + @Test + public void fnDsa512AcceptsValidKeypair() { + LocalWitnesses lw = new LocalWitnesses(); + lw.setPqKeypairs(Collections.singletonList(priv), Collections.singletonList(pub)); + assertEquals(PQScheme.FN_DSA_512, lw.getPqScheme()); + assertEquals(1, lw.getPqPrivateKeys().size()); + assertEquals(1, lw.getPqPublicKeys().size()); + } + + @Test + public void fnDsa512AcceptsMultipleKeypairs() { + LocalWitnesses lw = new LocalWitnesses(); + lw.setPqKeypairs(Arrays.asList(priv, priv2), Arrays.asList(pub, pub2)); + assertEquals(2, lw.getPqPrivateKeys().size()); + assertEquals(2, lw.getPqPublicKeys().size()); + } + + @Test + public void mismatchedListSizesRejected() { + LocalWitnesses lw = new LocalWitnesses(); + TronError err = assertThrows(TronError.class, + () -> lw.setPqKeypairs(Arrays.asList(priv, priv2), Collections.singletonList(pub))); + assertEquals(TronError.ErrCode.WITNESS_INIT, err.getErrCode()); + assertTrue(err.getMessage().contains("size mismatch")); + } + + @Test + public void wrongLengthPrivateKeyRejected() { + LocalWitnesses lw = new LocalWitnesses(); + String shortPriv = priv.substring(2); + TronError err = assertThrows(TronError.class, + () -> lw.setPqKeypairs(Collections.singletonList(shortPriv), + Collections.singletonList(pub))); + assertEquals(TronError.ErrCode.WITNESS_INIT, err.getErrCode()); + assertTrue(err.getMessage().contains("PQ private key")); + // FN-DSA-512 private key is 1280 bytes = 2560 hex chars. + assertTrue(err.getMessage().contains("2560")); + } + + @Test + public void wrongLengthPublicKeyRejected() { + LocalWitnesses lw = new LocalWitnesses(); + String shortPub = pub.substring(2); + TronError err = assertThrows(TronError.class, + () -> lw.setPqKeypairs(Collections.singletonList(priv), + Collections.singletonList(shortPub))); + assertEquals(TronError.ErrCode.WITNESS_INIT, err.getErrCode()); + assertTrue(err.getMessage().contains("PQ public key")); + // FN-DSA-512 public key is 896 bytes = 1792 hex chars. + assertTrue(err.getMessage().contains("1792")); + } + + @Test + public void nonHexPrivateKeyRejected() { + LocalWitnesses lw = new LocalWitnesses(); + String badPriv = "zz" + priv.substring(2); + TronError err = assertThrows(TronError.class, + () -> lw.setPqKeypairs(Collections.singletonList(badPriv), + Collections.singletonList(pub))); + assertEquals(TronError.ErrCode.WITNESS_INIT, err.getErrCode()); + assertTrue(err.getMessage().contains("hex")); + } + + @Test + public void unsupportedSchemeRejected() { + LocalWitnesses lw = new LocalWitnesses(); + TronError err = assertThrows(TronError.class, + () -> lw.setPqScheme(PQScheme.UNRECOGNIZED)); + assertEquals(TronError.ErrCode.WITNESS_INIT, err.getErrCode()); + assertTrue(err.getMessage().contains("unsupported PQ signature scheme")); + } + + @Test + public void nullSchemeRejected() { + LocalWitnesses lw = new LocalWitnesses(); + TronError err = assertThrows(TronError.class, () -> lw.setPqScheme(null)); + assertEquals(TronError.ErrCode.WITNESS_INIT, err.getErrCode()); + assertTrue(err.getMessage().contains("unsupported PQ signature scheme")); + } + + @Test + public void supportedSchemeAccepted() { + LocalWitnesses lw = new LocalWitnesses(); + lw.setPqScheme(PQScheme.FN_DSA_512); + assertEquals(PQScheme.FN_DSA_512, lw.getPqScheme()); + } + + @Test + public void emptyKeypairsAreNoop() { + LocalWitnesses lw = new LocalWitnesses(); + lw.setPqKeypairs(Collections.emptyList(), Collections.emptyList()); + lw.setPqKeypairs(null, null); + assertEquals(0, lw.getPqPrivateKeys().size()); + assertEquals(0, lw.getPqPublicKeys().size()); + } + + @Test + public void zeroXPrefixedHexAccepted() { + // validatePqKey strips a leading "0x" before measuring the length, so + // hex strings with the prefix must be accepted. + LocalWitnesses lw = new LocalWitnesses(); + lw.setPqKeypairs( + Collections.singletonList("0x" + priv), + Collections.singletonList("0x" + pub)); + assertEquals(1, lw.getPqPrivateKeys().size()); + } + + @Test + public void blankKeyRejected() { + LocalWitnesses lw = new LocalWitnesses(); + TronError err = assertThrows(TronError.class, + () -> lw.setPqKeypairs(Collections.singletonList(""), + Collections.singletonList(pub))); + assertEquals(TronError.ErrCode.WITNESS_INIT, err.getErrCode()); + assertTrue(err.getMessage().contains("PQ private key")); + } +} diff --git a/framework/src/test/java/org/tron/core/BandwidthProcessorTest.java b/framework/src/test/java/org/tron/core/BandwidthProcessorTest.java index cf652af3650..f6fede77523 100755 --- a/framework/src/test/java/org/tron/core/BandwidthProcessorTest.java +++ b/framework/src/test/java/org/tron/core/BandwidthProcessorTest.java @@ -881,4 +881,120 @@ public void testCalculateGlobalNetLimit() { .calculateGlobalNetLimitV2(accountCapsule.getAllFrozenBalanceForBandwidth()); Assert.assertTrue(netLimitV2 > 0); } + + @Test + public void pqPQAuthWitnessBytesSubtractedInCreateAccountCap() throws Exception { + chainBaseManager.getDynamicPropertiesStore().saveLatestBlockHeaderTimestamp(1526647838000L); + chainBaseManager.getDynamicPropertiesStore().saveTotalNetWeight(10_000_000L); + + AccountCapsule ownerCapsule = new AccountCapsule( + ByteString.copyFromUtf8("owner"), + ByteString.copyFrom(ByteArray.fromHexString(OWNER_ADDRESS)), + AccountType.Normal, + chainBaseManager.getDynamicPropertiesStore().getAssetIssueFee()); + ownerCapsule.setBalance(10_000_000L); + long expireTime = DateTime.now().getMillis() + 6 * 86_400_000; + ownerCapsule.setFrozenForBandwidth(2_000_000L, expireTime); + chainBaseManager.getAccountStore().put(ownerCapsule.getAddress().toByteArray(), ownerCapsule); + + chainBaseManager.getAccountStore().delete(ByteArray.fromHexString(TO_ADDRESS)); + + TransferContract contract = TransferContract.newBuilder() + .setOwnerAddress(ByteString.copyFrom(ByteArray.fromHexString(OWNER_ADDRESS))) + .setToAddress(ByteString.copyFrom(ByteArray.fromHexString(TO_ADDRESS))) + .setAmount(100L) + .build(); + + byte[] fakeSig = new byte[752]; + byte[] fakePub = new byte[897]; + Protocol.PQAuthSig pqAuthSig = Protocol.PQAuthSig.newBuilder() + .setScheme(Protocol.PQScheme.FN_DSA_512) + .setPublicKey(ByteString.copyFrom(fakePub)) + .setSignature(ByteString.copyFrom(fakeSig)) + .build(); + + TransactionCapsule baseTrx = new TransactionCapsule(contract, + chainBaseManager.getAccountStore()); + Transaction withAuth = baseTrx.getInstance().toBuilder() + .addPqAuthSig(pqAuthSig) + .build(); + TransactionCapsule trx = new TransactionCapsule(withAuth); + TransactionTrace trace = new TransactionTrace(trx, StoreFactory.getInstance(), + new RuntimeImpl()); + + long cap = chainBaseManager.getDynamicPropertiesStore().getMaxCreateAccountTxSize(); + long rawSize = trx.getInstance().toBuilder().clearRet().build().getSerializedSize(); + Assert.assertTrue("test precondition: raw tx must exceed cap with pq_auth_sig", + rawSize > cap); + + BandwidthProcessor processor = new BandwidthProcessor(chainBaseManager); + try { + processor.consume(trx, trace); + } catch (TooBigTransactionException e) { + Assert.fail("PQ pq_auth_sig bytes should be deducted from create-account cap check"); + } finally { + chainBaseManager.getAccountStore().delete(ByteArray.fromHexString(OWNER_ADDRESS)); + chainBaseManager.getAccountStore().delete(ByteArray.fromHexString(TO_ADDRESS)); + } + } + + @Test + public void pqPQAuthWitnessCountedInBandwidthUsage() throws Exception { + chainBaseManager.getDynamicPropertiesStore().saveLatestBlockHeaderTimestamp(1526647838000L); + chainBaseManager.getDynamicPropertiesStore().saveTotalNetWeight(10_000_000L); + + AccountCapsule ownerCapsule = new AccountCapsule( + ByteString.copyFromUtf8("owner"), + ByteString.copyFrom(ByteArray.fromHexString(OWNER_ADDRESS)), + AccountType.Normal, + chainBaseManager.getDynamicPropertiesStore().getAssetIssueFee()); + ownerCapsule.setBalance(10_000_000L); + long expireTime = DateTime.now().getMillis() + 6 * 86_400_000; + ownerCapsule.setFrozenForBandwidth(2_000_000L, expireTime); + chainBaseManager.getAccountStore().put(ownerCapsule.getAddress().toByteArray(), ownerCapsule); + + AccountCapsule toAddressCapsule = new AccountCapsule( + ByteString.copyFromUtf8("to"), + ByteString.copyFrom(ByteArray.fromHexString(TO_ADDRESS)), + AccountType.Normal, + 0L); + chainBaseManager.getAccountStore().put(toAddressCapsule.getAddress().toByteArray(), + toAddressCapsule); + + TransferContract contract = TransferContract.newBuilder() + .setOwnerAddress(ByteString.copyFrom(ByteArray.fromHexString(OWNER_ADDRESS))) + .setToAddress(ByteString.copyFrom(ByteArray.fromHexString(TO_ADDRESS))) + .setAmount(100L) + .build(); + + byte[] fakeSig = new byte[752]; + byte[] fakePub = new byte[897]; + Protocol.PQAuthSig pqAuthSig = Protocol.PQAuthSig.newBuilder() + .setScheme(Protocol.PQScheme.FN_DSA_512) + .setPublicKey(ByteString.copyFrom(fakePub)) + .setSignature(ByteString.copyFrom(fakeSig)) + .build(); + + TransactionCapsule baseTrx = new TransactionCapsule(contract, + chainBaseManager.getAccountStore()); + Transaction withAuth = baseTrx.getInstance().toBuilder() + .addPqAuthSig(pqAuthSig) + .build(); + TransactionCapsule trx = new TransactionCapsule(withAuth); + TransactionTrace trace = new TransactionTrace(trx, StoreFactory.getInstance(), + new RuntimeImpl()); + + long expectedBytes = trx.getInstance().toBuilder().clearRet().build().getSerializedSize() + + (chainBaseManager.getDynamicPropertiesStore().supportVM() + ? Constant.MAX_RESULT_SIZE_IN_TX : 0); + + BandwidthProcessor processor = new BandwidthProcessor(chainBaseManager); + try { + processor.consume(trx, trace); + Assert.assertEquals(expectedBytes, trace.getReceipt().getNetUsage()); + } finally { + chainBaseManager.getAccountStore().delete(ByteArray.fromHexString(OWNER_ADDRESS)); + chainBaseManager.getAccountStore().delete(ByteArray.fromHexString(TO_ADDRESS)); + } + } } diff --git a/framework/src/test/java/org/tron/core/actuator/AccountPermissionUpdateActuatorTest.java b/framework/src/test/java/org/tron/core/actuator/AccountPermissionUpdateActuatorTest.java index 250f7b9dc01..1fa2ef9d16f 100644 --- a/framework/src/test/java/org/tron/core/actuator/AccountPermissionUpdateActuatorTest.java +++ b/framework/src/test/java/org/tron/core/actuator/AccountPermissionUpdateActuatorTest.java @@ -1019,4 +1019,4 @@ public void checkActiveDefaultOperations() { } -} \ No newline at end of file +} diff --git a/framework/src/test/java/org/tron/core/actuator/CreateAccountActuatorTest.java b/framework/src/test/java/org/tron/core/actuator/CreateAccountActuatorTest.java index 4cb8e639089..6c1923a9ce4 100755 --- a/framework/src/test/java/org/tron/core/actuator/CreateAccountActuatorTest.java +++ b/framework/src/test/java/org/tron/core/actuator/CreateAccountActuatorTest.java @@ -220,6 +220,10 @@ public void commonErrorCheck() { } + // PQ-native account creation is deferred per V2 scope: AccountCreateContract.pq_key + // has been removed (reserved 4) and CreateAccountActuator no longer carries any PQ + // validation logic. Tests for that path were dropped along with the field. + private void processAndCheckInvalid(CreateAccountActuator actuator, TransactionResultCapsule ret, String failMsg, String expectedMsg) { diff --git a/framework/src/test/java/org/tron/core/actuator/ShieldedTransferActuatorTest.java b/framework/src/test/java/org/tron/core/actuator/ShieldedTransferActuatorTest.java index 578f9f5ebed..1d08ab53b77 100755 --- a/framework/src/test/java/org/tron/core/actuator/ShieldedTransferActuatorTest.java +++ b/framework/src/test/java/org/tron/core/actuator/ShieldedTransferActuatorTest.java @@ -275,7 +275,7 @@ public void publicAddressToPublicAddressNoPublicSign() { Assert.assertTrue(dbManager.pushTransaction(transactionCap)); } catch (ValidateSignatureException e) { Assert.assertTrue(e instanceof ValidateSignatureException); - Assert.assertEquals("miss sig or contract", e.getMessage()); + Assert.assertEquals("miss sig", e.getMessage()); } catch (Exception e) { Assert.assertTrue(false); } diff --git a/framework/src/test/java/org/tron/core/actuator/utils/ProposalUtilTest.java b/framework/src/test/java/org/tron/core/actuator/utils/ProposalUtilTest.java index 16a3cb3a5bb..92f35d7b79f 100644 --- a/framework/src/test/java/org/tron/core/actuator/utils/ProposalUtilTest.java +++ b/framework/src/test/java/org/tron/core/actuator/utils/ProposalUtilTest.java @@ -797,4 +797,30 @@ public void blockVersionCheck() { } } } + + @Test + public void validateAllowFnDsa512() { + long code = ProposalType.ALLOW_FN_DSA_512.getCode(); + + ContractValidateException thrown = assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, code, 0)); + assertEquals("This value[ALLOW_FN_DSA_512] is only allowed to be 1", thrown.getMessage()); + + thrown = assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, code, 2)); + assertEquals("This value[ALLOW_FN_DSA_512] is only allowed to be 1", thrown.getMessage()); + + try { + ProposalUtil.validator(dynamicPropertiesStore, forkUtils, code, 1); + } catch (ContractValidateException e) { + Assert.fail("value=1 should be accepted: " + e.getMessage()); + } + + dynamicPropertiesStore.saveAllowFnDsa512(1L); + thrown = assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, code, 1)); + assertEquals("[ALLOW_FN_DSA_512] has been valid, no need to propose again", + thrown.getMessage()); + dynamicPropertiesStore.saveAllowFnDsa512(0L); + } } diff --git a/framework/src/test/java/org/tron/core/capsule/BlockCapsulePQTest.java b/framework/src/test/java/org/tron/core/capsule/BlockCapsulePQTest.java new file mode 100644 index 00000000000..f224652046e --- /dev/null +++ b/framework/src/test/java/org/tron/core/capsule/BlockCapsulePQTest.java @@ -0,0 +1,194 @@ +package org.tron.core.capsule; + +import com.google.protobuf.ByteString; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.tron.common.BaseTest; +import org.tron.common.TestConstants; +import org.tron.common.crypto.ECKey; +import org.tron.common.crypto.pqc.FNDSA; +import org.tron.common.crypto.pqc.PQSchemeRegistry; +import org.tron.common.utils.Sha256Hash; +import org.tron.core.config.args.Args; +import org.tron.core.exception.ValidateSignatureException; +import org.tron.protos.Protocol.Account; +import org.tron.protos.Protocol.AccountType; +import org.tron.protos.Protocol.Key; +import org.tron.protos.Protocol.PQAuthSig; +import org.tron.protos.Protocol.PQScheme; +import org.tron.protos.Protocol.Permission; +import org.tron.protos.Protocol.Permission.PermissionType; + +public class BlockCapsulePQTest extends BaseTest { + + private ECKey witnessKey; + private byte[] witnessAddress; + private FNDSA pqKeypair; + private byte[] pqAddress; + + @BeforeClass + public static void init() { + Args.setParam(new String[] {"-d", dbPath()}, TestConstants.TEST_CONF); + } + + @Before + public void setUp() { + witnessKey = new ECKey(); + witnessAddress = witnessKey.getAddress(); + pqKeypair = new FNDSA(); + pqAddress = PQSchemeRegistry.computeAddress( + PQScheme.FN_DSA_512, pqKeypair.getPublicKey()); + } + + /** + * Build a witness account whose witness permission key is bound to the + * given address. For PQ scenarios, pass {@link #pqAddress}; for legacy ECDSA + * scenarios, pass {@link #witnessAddress}. + */ + private AccountCapsule buildWitnessAccount(byte[] keyAddress) { + Key kb = Key.newBuilder() + .setAddress(ByteString.copyFrom(keyAddress)) + .setWeight(1) + .build(); + Permission witnessPerm = Permission.newBuilder() + .setType(PermissionType.Witness) + .setId(1) + .setPermissionName("witness") + .setThreshold(1) + .addKeys(kb) + .build(); + Account account = Account.newBuilder() + .setAccountName(ByteString.copyFromUtf8("w")) + .setAddress(ByteString.copyFrom(witnessAddress)) + .setType(AccountType.Normal) + .setBalance(1_000_000_000L) + .setIsWitness(true) + .setWitnessPermission(witnessPerm) + .build(); + return new AccountCapsule(account); + } + + private BlockCapsule buildSignedBlock(byte[] parentHash) { + BlockCapsule block = new BlockCapsule( + 1L, + Sha256Hash.wrap(ByteString.copyFrom(parentHash)), + System.currentTimeMillis(), + ByteString.copyFrom(witnessAddress)); + block.sign(witnessKey.getPrivKeyBytes()); + return block; + } + + private BlockCapsule buildUnsignedBlock(byte[] parentHash) { + return new BlockCapsule( + 1L, + Sha256Hash.wrap(ByteString.copyFrom(parentHash)), + System.currentTimeMillis(), + ByteString.copyFrom(witnessAddress)); + } + + private byte[] signPQ(byte[] message) { + return FNDSA.sign(pqKeypair.getPrivateKey(), message); + } + + private PQAuthSig buildPQAuthSig(byte[] signature) { + return PQAuthSig.newBuilder() + .setScheme(PQScheme.FN_DSA_512) + .setPublicKey(ByteString.copyFrom(pqKeypair.getPublicKey())) + .setSignature(ByteString.copyFrom(signature)) + .build(); + } + + @Test + public void legacyValidateWithoutPQAuthSigAcceptedBeforeActivation() throws Exception { + dbManager.getDynamicPropertiesStore().saveAllowMultiSign(1L); + dbManager.getDynamicPropertiesStore().saveAllowFnDsa512(0L); + AccountCapsule witness = buildWitnessAccount(witnessAddress); + dbManager.getAccountStore().put(witnessAddress, witness); + + byte[] parentHash = new byte[32]; + BlockCapsule block = buildSignedBlock(parentHash); + Assert.assertTrue(block.validateSignature( + dbManager.getDynamicPropertiesStore(), dbManager.getAccountStore())); + } + + @Test(expected = ValidateSignatureException.class) + public void pqAuthSigBeforeActivationRejected() throws Exception { + dbManager.getDynamicPropertiesStore().saveAllowMultiSign(1L); + dbManager.getDynamicPropertiesStore().saveAllowFnDsa512(0L); + AccountCapsule witness = buildWitnessAccount(pqAddress); + dbManager.getAccountStore().put(witnessAddress, witness); + + byte[] parentHash = new byte[32]; + BlockCapsule block = buildUnsignedBlock(parentHash); + byte[] digest = block.getRawHashBytes(); + block.setPqAuthSig(buildPQAuthSig(signPQ(digest))); + block.validateSignature( + dbManager.getDynamicPropertiesStore(), dbManager.getAccountStore()); + } + + @Test(expected = ValidateSignatureException.class) + public void bothLegacyAndPQAuthSigRejected() throws Exception { + dbManager.getDynamicPropertiesStore().saveAllowMultiSign(1L); + dbManager.getDynamicPropertiesStore().saveAllowFnDsa512(1L); + AccountCapsule witness = buildWitnessAccount(pqAddress); + dbManager.getAccountStore().put(witnessAddress, witness); + + byte[] parentHash = new byte[32]; + BlockCapsule block = buildSignedBlock(parentHash); + byte[] digest = block.getRawHashBytes(); + block.setPqAuthSig(buildPQAuthSig(signPQ(digest))); + block.validateSignature( + dbManager.getDynamicPropertiesStore(), dbManager.getAccountStore()); + } + + @Test + public void pqOnlyAccepted() throws Exception { + dbManager.getDynamicPropertiesStore().saveAllowMultiSign(1L); + dbManager.getDynamicPropertiesStore().saveAllowFnDsa512(1L); + AccountCapsule witness = buildWitnessAccount(pqAddress); + dbManager.getAccountStore().put(witnessAddress, witness); + + byte[] parentHash = new byte[32]; + BlockCapsule block = buildUnsignedBlock(parentHash); + byte[] digest = block.getRawHashBytes(); + block.setPqAuthSig(buildPQAuthSig(signPQ(digest))); + Assert.assertTrue(block.validateSignature( + dbManager.getDynamicPropertiesStore(), dbManager.getAccountStore())); + } + + @Test + public void tamperedPQAuthSigFails() throws Exception { + dbManager.getDynamicPropertiesStore().saveAllowMultiSign(1L); + dbManager.getDynamicPropertiesStore().saveAllowFnDsa512(1L); + AccountCapsule witness = buildWitnessAccount(pqAddress); + dbManager.getAccountStore().put(witnessAddress, witness); + + byte[] parentHash = new byte[32]; + BlockCapsule block = buildUnsignedBlock(parentHash); + byte[] digest = block.getRawHashBytes(); + byte[] pqSig = signPQ(digest); + pqSig[pqSig.length - 1] ^= 0x01; + block.setPqAuthSig(buildPQAuthSig(pqSig)); + Assert.assertFalse(block.validateSignature( + dbManager.getDynamicPropertiesStore(), dbManager.getAccountStore())); + } + + @Test(expected = ValidateSignatureException.class) + public void signerNotInWitnessPermissionRejected() throws Exception { + dbManager.getDynamicPropertiesStore().saveAllowMultiSign(1L); + dbManager.getDynamicPropertiesStore().saveAllowFnDsa512(1L); + // Witness permission key bound to a different address (the legacy ECDSA + // address), so the PQ signer's derived address won't match. + AccountCapsule witness = buildWitnessAccount(witnessAddress); + dbManager.getAccountStore().put(witnessAddress, witness); + + byte[] parentHash = new byte[32]; + BlockCapsule block = buildUnsignedBlock(parentHash); + byte[] digest = block.getRawHashBytes(); + block.setPqAuthSig(buildPQAuthSig(signPQ(digest))); + block.validateSignature( + dbManager.getDynamicPropertiesStore(), dbManager.getAccountStore()); + } +} 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 9c2e004931e..078e6153f39 100644 --- a/framework/src/test/java/org/tron/core/capsule/TransactionCapsuleTest.java +++ b/framework/src/test/java/org/tron/core/capsule/TransactionCapsuleTest.java @@ -8,6 +8,7 @@ import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.read.ListAppender; +import com.google.protobuf.Any; import com.google.protobuf.ByteString; import java.util.List; import java.util.concurrent.TimeUnit; @@ -20,15 +21,28 @@ import org.slf4j.LoggerFactory; import org.tron.common.BaseTest; import org.tron.common.TestConstants; +import org.tron.common.crypto.ECKey; +import org.tron.common.crypto.pqc.FNDSA; +import org.tron.common.crypto.pqc.PQSchemeRegistry; +import org.tron.common.utils.ByteArray; +import org.tron.common.utils.Sha256Hash; import org.tron.common.utils.StringUtil; import org.tron.core.Wallet; import org.tron.core.config.args.Args; +import org.tron.core.exception.ValidateSignatureException; import org.tron.protos.Protocol.AccountType; +import org.tron.protos.Protocol.Key; +import org.tron.protos.Protocol.PQAuthSig; +import org.tron.protos.Protocol.PQScheme; +import org.tron.protos.Protocol.Permission; +import org.tron.protos.Protocol.Permission.PermissionType; import org.tron.protos.Protocol.Transaction; import org.tron.protos.Protocol.Transaction.Contract.ContractType; import org.tron.protos.Protocol.Transaction.Result; import org.tron.protos.Protocol.Transaction.Result.contractResult; import org.tron.protos.Protocol.Transaction.raw; +import org.tron.protos.contract.BalanceContract.TransferContract; +import org.tron.protos.contract.SmartContractOuterClass.TriggerSmartContract; @Slf4j public class TransactionCapsuleTest extends BaseTest { @@ -113,6 +127,73 @@ public void slowVerify() { } } + // --------------------- FN-DSA pq_auth_sig verification (V2) --------------------- + + private static final String PQ_OWNER_HEX = + "41abd4b9367799eaa3197fecb144eb71de1e049abc"; + private static final String PQ_TO_HEX = + "41548794500882809695a8a687866e76d4271a1abc"; + + private Transaction buildTransferTx(String ownerHex, int permissionId) { + TransferContract transfer = TransferContract.newBuilder() + .setOwnerAddress(ByteString.copyFrom(ByteArray.fromHexString(ownerHex))) + .setToAddress(ByteString.copyFrom(ByteArray.fromHexString(PQ_TO_HEX))) + .setAmount(1L) + .build(); + Transaction.Contract c = Transaction.Contract.newBuilder() + .setType(ContractType.TransferContract) + .setParameter(Any.pack(transfer)) + .setPermissionId(permissionId) + .build(); + raw rawData = raw.newBuilder().addContract(c).build(); + return Transaction.newBuilder().setRawData(rawData).build(); + } + + /** + * V2: bind the PQ public key to the permission via address-as-fingerprint. + * The signer address is derived from the public key by the scheme's + * fingerprint hash (see {@link PQSchemeRegistry#computeAddress}). + */ + private void putAccountWithPQPermission( + String ownerHex, byte[] pqPublicKey, PQScheme scheme) { + byte[] addr = ByteArray.fromHexString(ownerHex); + byte[] signerAddr = PQSchemeRegistry.computeAddress(scheme, pqPublicKey); + Key pqKey = Key.newBuilder() + .setAddress(ByteString.copyFrom(signerAddr)) + .setWeight(1L) + .build(); + Permission owner = Permission.newBuilder() + .setType(PermissionType.Owner) + .setPermissionName("owner") + .setThreshold(1) + .addKeys(pqKey) + .build(); + AccountCapsule acc = new AccountCapsule(ByteString.copyFrom(addr), + ByteString.copyFromUtf8("pqowner"), AccountType.Normal); + acc.updatePermissions(owner, null, java.util.Collections.emptyList()); + dbManager.getAccountStore().put(addr, acc); + } + + @Test + public void pqAuthSigBeforeActivationRejected() { + dbManager.getDynamicPropertiesStore().saveAllowFnDsa512(0L); + Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0).toBuilder() + .addPqAuthSig(PQAuthSig.newBuilder() + .setScheme(PQScheme.FN_DSA_512) + .setPublicKey(ByteString.copyFrom(new byte[FNDSA.PUBLIC_KEY_LENGTH])) + .setSignature(ByteString.copyFrom(new byte[FNDSA.SIGNATURE_LENGTH])) + .build()) + .build(); + TransactionCapsule cap = new TransactionCapsule(tx); + try { + cap.validatePubSignature(dbManager.getAccountStore(), + dbManager.getDynamicPropertiesStore()); + Assert.fail("should reject pq_auth_sig before activation"); + } catch (ValidateSignatureException e) { + Assert.assertTrue(e.getMessage().contains("no post-quantum scheme is activated")); + } + } + @Test public void fastVerify() { Logger capsuleLogger = (Logger) LoggerFactory.getLogger("capsule"); @@ -134,4 +215,398 @@ public void fastVerify() { capsuleLogger.setLevel(originalLevel); } } + + @Test + public void validPQAuthSigAccepted() throws Exception { + dbManager.getDynamicPropertiesStore().saveAllowFnDsa512(1L); + FNDSA kp = new FNDSA(); + putAccountWithPQPermission(PQ_OWNER_HEX, kp.getPublicKey(), PQScheme.FN_DSA_512); + + Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); + byte[] txid = Sha256Hash.of(true, tx.getRawData().toByteArray()).getBytes(); + + byte[] sig = FNDSA.sign(kp.getPrivateKey(), txid); + + Transaction signed = tx.toBuilder() + .addPqAuthSig(PQAuthSig.newBuilder() + .setScheme(PQScheme.FN_DSA_512) + .setPublicKey(ByteString.copyFrom(kp.getPublicKey())) + .setSignature(ByteString.copyFrom(sig)) + .build()) + .build(); + TransactionCapsule cap = new TransactionCapsule(signed); + Assert.assertTrue(cap.validatePubSignature(dbManager.getAccountStore(), + dbManager.getDynamicPropertiesStore())); + } + + @Test + public void duplicateSignerRejected() throws Exception { + dbManager.getDynamicPropertiesStore().saveAllowFnDsa512(1L); + FNDSA kp = new FNDSA(); + putAccountWithPQPermission(PQ_OWNER_HEX, kp.getPublicKey(), PQScheme.FN_DSA_512); + + Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); + byte[] txid = Sha256Hash.of(true, tx.getRawData().toByteArray()).getBytes(); + + byte[] sig = FNDSA.sign(kp.getPrivateKey(), txid); + PQAuthSig w = PQAuthSig.newBuilder() + .setScheme(PQScheme.FN_DSA_512) + .setPublicKey(ByteString.copyFrom(kp.getPublicKey())) + .setSignature(ByteString.copyFrom(sig)) + .build(); + Transaction signed = tx.toBuilder().addPqAuthSig(w).addPqAuthSig(w).build(); + + TransactionCapsule cap = new TransactionCapsule(signed); + try { + cap.validatePubSignature(dbManager.getAccountStore(), + dbManager.getDynamicPropertiesStore()); + Assert.fail("duplicate signer should be rejected"); + } catch (ValidateSignatureException e) { + Assert.assertTrue(e.getMessage().contains("has signed twice")); + } + } + + @Test + public void tamperedPQAuthSigRejected() throws Exception { + dbManager.getDynamicPropertiesStore().saveAllowFnDsa512(1L); + FNDSA kp = new FNDSA(); + putAccountWithPQPermission(PQ_OWNER_HEX, kp.getPublicKey(), PQScheme.FN_DSA_512); + + Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); + byte[] txid = Sha256Hash.of(true, tx.getRawData().toByteArray()).getBytes(); + + byte[] sig = FNDSA.sign(kp.getPrivateKey(), txid); + sig[0] ^= 0x01; + + Transaction signed = tx.toBuilder() + .addPqAuthSig(PQAuthSig.newBuilder() + .setScheme(PQScheme.FN_DSA_512) + .setPublicKey(ByteString.copyFrom(kp.getPublicKey())) + .setSignature(ByteString.copyFrom(sig)) + .build()) + .build(); + TransactionCapsule cap = new TransactionCapsule(signed); + try { + cap.validatePubSignature(dbManager.getAccountStore(), + dbManager.getDynamicPropertiesStore()); + Assert.fail("tampered signature should be rejected"); + } catch (ValidateSignatureException e) { + Assert.assertTrue(e.getMessage().contains("pq sig invalid")); + } + } + + @Test + public void signerNotInPermissionRejected() throws Exception { + dbManager.getDynamicPropertiesStore().saveAllowFnDsa512(1L); + FNDSA known = new FNDSA(); + putAccountWithPQPermission(PQ_OWNER_HEX, known.getPublicKey(), PQScheme.FN_DSA_512); + + // Sign with a *different* keypair → derived address is not in the permission. + FNDSA stranger = new FNDSA(); + Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); + byte[] txid = Sha256Hash.of(true, tx.getRawData().toByteArray()).getBytes(); + + byte[] sig = FNDSA.sign(stranger.getPrivateKey(), txid); + + Transaction signed = tx.toBuilder() + .addPqAuthSig(PQAuthSig.newBuilder() + .setScheme(PQScheme.FN_DSA_512) + .setPublicKey(ByteString.copyFrom(stranger.getPublicKey())) + .setSignature(ByteString.copyFrom(sig)) + .build()) + .build(); + TransactionCapsule cap = new TransactionCapsule(signed); + try { + cap.validatePubSignature(dbManager.getAccountStore(), + dbManager.getDynamicPropertiesStore()); + Assert.fail("signer outside permission should be rejected"); + } catch (ValidateSignatureException e) { + Assert.assertTrue(e.getMessage().contains("not contained of permission")); + } + } + + /** + * TRC20 transfer(address,uint256) call data: 4-byte selector + 32-byte address + 32-byte amount. + */ + private Transaction buildTrc20TransferTx(String ownerHex, int permissionId) { + byte[] selector = ByteArray.fromHexString("a9059cbb"); + byte[] toAddrPadded = new byte[32]; + byte[] toRaw = ByteArray.fromHexString(PQ_TO_HEX.substring(2)); // strip "41" + System.arraycopy(toRaw, 0, toAddrPadded, 12, 20); + byte[] amountPadded = new byte[32]; + amountPadded[31] = (byte) 100; // 100 tokens + byte[] callData = new byte[selector.length + toAddrPadded.length + amountPadded.length]; + System.arraycopy(selector, 0, callData, 0, 4); + System.arraycopy(toAddrPadded, 0, callData, 4, 32); + System.arraycopy(amountPadded, 0, callData, 36, 32); + + byte[] contractAddr = ByteArray.fromHexString("41a614f803b6fd780986a42c78ec9c7f77e6ded13c"); + TriggerSmartContract trigger = TriggerSmartContract.newBuilder() + .setOwnerAddress(ByteString.copyFrom(ByteArray.fromHexString(ownerHex))) + .setContractAddress(ByteString.copyFrom(contractAddr)) + .setData(ByteString.copyFrom(callData)) + .build(); + Transaction.Contract c = Transaction.Contract.newBuilder() + .setType(ContractType.TriggerSmartContract) + .setParameter(Any.pack(trigger)) + .setPermissionId(permissionId) + .build(); + raw rawData = raw.newBuilder() + .addContract(c) + .setFeeLimit(150_000_000L) + .build(); + return Transaction.newBuilder().setRawData(rawData).build(); + } + + /** + * Returns [serializedSize, packSize, maxTxPerBlock] rows ordered by signature size: + * ECKey, FN-DSA-512. + */ + private long[][] measureSizes(Transaction baseTx) { + final long blockLimit = 2_000_000L; + + // ECKey (ECDSA): 65-byte signature in `signature` field + ECKey ecKey = new ECKey(); + TransactionCapsule ecCap = new TransactionCapsule(baseTx); + ecCap.sign(ecKey.getPrivKeyBytes()); + long ecSerial = ecCap.getInstance().toByteArray().length; + long ecPack = ecCap.computeTrxSizeForBlockMessage(); + + // FN-DSA-512: variable-length signature (<= 752 bytes) + 897-byte public key + FNDSA kpFn = new FNDSA(); + byte[] txidFn = Sha256Hash.of(true, baseTx.getRawData().toByteArray()).getBytes(); + byte[] sigFn = FNDSA.sign(kpFn.getPrivateKey(), txidFn); + Transaction txFn = baseTx.toBuilder() + .addPqAuthSig(PQAuthSig.newBuilder() + .setScheme(PQScheme.FN_DSA_512) + .setPublicKey(ByteString.copyFrom(kpFn.getPublicKey())) + .setSignature(ByteString.copyFrom(sigFn)) + .build()) + .build(); + TransactionCapsule capFn = new TransactionCapsule(txFn); + long dFnSerial = txFn.toByteArray().length; + long dFnPack = capFn.computeTrxSizeForBlockMessage(); + + return new long[][]{ + {ecSerial, ecPack, blockLimit / ecPack}, + {dFnSerial, dFnPack, blockLimit / dFnPack}, + }; + } + + @Test + public void transactionSizeComparisonByScheme() { + long[][] trx = measureSizes(buildTransferTx(PQ_OWNER_HEX, 0)); + long[][] trc20 = measureSizes(buildTrc20TransferTx(PQ_OWNER_HEX, 0)); + + String[] labels = {"ECKey (ECDSA)", "FN-DSA-512"}; + System.out.println("=== TRX transfer ==="); + for (int i = 0; i < labels.length; i++) { + System.out.printf(" %s: serial=%d B pack=%d B maxTx/block=%d%n", + labels[i], trx[i][0], trx[i][1], trx[i][2]); + } + System.out.println("=== TRC20 transfer ==="); + for (int i = 0; i < labels.length; i++) { + System.out.printf(" %s: serial=%d B pack=%d B maxTx/block=%d%n", + labels[i], trc20[i][0], trc20[i][1], trc20[i][2]); + } + + // FN-DSA-512 envelope is larger than ECKey, so it fits fewer txs per block. + Assert.assertTrue(trx[1][0] > trx[0][0]); + Assert.assertTrue(trc20[1][0] > trc20[0][0]); + Assert.assertTrue(trx[1][2] < trx[0][2]); + Assert.assertTrue(trc20[1][2] < trc20[0][2]); + } + + @Test + public void pqAuthSigWrongPublicKeyLengthRejected() throws Exception { + dbManager.getDynamicPropertiesStore().saveAllowFnDsa512(1L); + FNDSA kp = new FNDSA(); + putAccountWithPQPermission(PQ_OWNER_HEX, kp.getPublicKey(), PQScheme.FN_DSA_512); + + Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); + byte[] txid = Sha256Hash.of(true, tx.getRawData().toByteArray()).getBytes(); + byte[] sig = FNDSA.sign(kp.getPrivateKey(), txid); + + // Truncate public key by one byte to force the length-mismatch branch. + byte[] shortPub = new byte[FNDSA.PUBLIC_KEY_LENGTH - 1]; + System.arraycopy(kp.getPublicKey(), 0, shortPub, 0, shortPub.length); + + Transaction signed = tx.toBuilder() + .addPqAuthSig(PQAuthSig.newBuilder() + .setScheme(PQScheme.FN_DSA_512) + .setPublicKey(ByteString.copyFrom(shortPub)) + .setSignature(ByteString.copyFrom(sig)) + .build()) + .build(); + TransactionCapsule cap = new TransactionCapsule(signed); + try { + cap.validatePubSignature(dbManager.getAccountStore(), + dbManager.getDynamicPropertiesStore()); + Assert.fail("wrong public key length must be rejected"); + } catch (ValidateSignatureException e) { + Assert.assertTrue(e.getMessage().contains("public key or signature length mismatch")); + } + } + + @Test + public void pqAuthSigWrongSignatureLengthRejected() throws Exception { + dbManager.getDynamicPropertiesStore().saveAllowFnDsa512(1L); + FNDSA kp = new FNDSA(); + putAccountWithPQPermission(PQ_OWNER_HEX, kp.getPublicKey(), PQScheme.FN_DSA_512); + + Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); + + // Empty signature is not a valid FN-DSA-512 length, hits the same branch. + Transaction signed = tx.toBuilder() + .addPqAuthSig(PQAuthSig.newBuilder() + .setScheme(PQScheme.FN_DSA_512) + .setPublicKey(ByteString.copyFrom(kp.getPublicKey())) + .setSignature(ByteString.EMPTY) + .build()) + .build(); + TransactionCapsule cap = new TransactionCapsule(signed); + try { + cap.validatePubSignature(dbManager.getAccountStore(), + dbManager.getDynamicPropertiesStore()); + Assert.fail("wrong signature length must be rejected"); + } catch (ValidateSignatureException e) { + Assert.assertTrue(e.getMessage().contains("public key or signature length mismatch")); + } + } + + @Test + public void pqAuthSigUnsupportedSchemeRejected() throws Exception { + dbManager.getDynamicPropertiesStore().saveAllowFnDsa512(1L); + FNDSA kp = new FNDSA(); + putAccountWithPQPermission(PQ_OWNER_HEX, kp.getPublicKey(), PQScheme.FN_DSA_512); + + Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); + byte[] txid = Sha256Hash.of(true, tx.getRawData().toByteArray()).getBytes(); + byte[] sig = FNDSA.sign(kp.getPrivateKey(), txid); + + // setSchemeValue(99) sets an unknown numeric tag; reading back yields + // PQScheme.UNRECOGNIZED, which PQSchemeRegistry.contains() rejects. + Transaction signed = tx.toBuilder() + .addPqAuthSig(PQAuthSig.newBuilder() + .setSchemeValue(99) + .setPublicKey(ByteString.copyFrom(kp.getPublicKey())) + .setSignature(ByteString.copyFrom(sig)) + .build()) + .build(); + TransactionCapsule cap = new TransactionCapsule(signed); + try { + cap.validatePubSignature(dbManager.getAccountStore(), + dbManager.getDynamicPropertiesStore()); + Assert.fail("unsupported scheme must be rejected"); + } catch (ValidateSignatureException e) { + Assert.assertTrue(e.getMessage().contains("unsupported pq scheme")); + } + } + + @Test + public void validatePubSignatureRejectsMissingSig() { + Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); + TransactionCapsule cap = new TransactionCapsule(tx); + try { + cap.validatePubSignature(dbManager.getAccountStore(), + dbManager.getDynamicPropertiesStore()); + Assert.fail("transaction with no signatures must be rejected"); + } catch (ValidateSignatureException e) { + Assert.assertTrue(e.getMessage().contains("miss sig")); + } + } + + @Test + public void validatePubSignatureRejectsMissingContract() throws Exception { + dbManager.getDynamicPropertiesStore().saveAllowFnDsa512(1L); + FNDSA kp = new FNDSA(); + byte[] sig = FNDSA.sign(kp.getPrivateKey(), new byte[32]); + + // No contracts in raw_data, but a pq_auth_sig is attached so we get past + // the "miss sig" guard and into the "miss contract" branch. + Transaction tx = Transaction.newBuilder() + .setRawData(raw.newBuilder().build()) + .addPqAuthSig(PQAuthSig.newBuilder() + .setScheme(PQScheme.FN_DSA_512) + .setPublicKey(ByteString.copyFrom(kp.getPublicKey())) + .setSignature(ByteString.copyFrom(sig)) + .build()) + .build(); + TransactionCapsule cap = new TransactionCapsule(tx); + try { + cap.validatePubSignature(dbManager.getAccountStore(), + dbManager.getDynamicPropertiesStore()); + Assert.fail("transaction with no contracts must be rejected"); + } catch (ValidateSignatureException e) { + Assert.assertTrue(e.getMessage().contains("miss contract")); + } + } + + @Test + public void validatePubSignatureRejectsTooManySignatures() throws Exception { + dbManager.getDynamicPropertiesStore().saveAllowFnDsa512(1L); + int original = dbManager.getDynamicPropertiesStore().getTotalSignNum(); + try { + dbManager.getDynamicPropertiesStore().saveTotalSignNum(1); + FNDSA a = new FNDSA(); + FNDSA b = new FNDSA(); + putAccountWithPQPermission(PQ_OWNER_HEX, a.getPublicKey(), PQScheme.FN_DSA_512); + + Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); + byte[] txid = Sha256Hash.of(true, tx.getRawData().toByteArray()).getBytes(); + byte[] sigA = FNDSA.sign(a.getPrivateKey(), txid); + byte[] sigB = FNDSA.sign(b.getPrivateKey(), txid); + + Transaction signed = tx.toBuilder() + .addPqAuthSig(PQAuthSig.newBuilder() + .setScheme(PQScheme.FN_DSA_512) + .setPublicKey(ByteString.copyFrom(a.getPublicKey())) + .setSignature(ByteString.copyFrom(sigA)) + .build()) + .addPqAuthSig(PQAuthSig.newBuilder() + .setScheme(PQScheme.FN_DSA_512) + .setPublicKey(ByteString.copyFrom(b.getPublicKey())) + .setSignature(ByteString.copyFrom(sigB)) + .build()) + .build(); + TransactionCapsule cap = new TransactionCapsule(signed); + try { + cap.validatePubSignature(dbManager.getAccountStore(), + dbManager.getDynamicPropertiesStore()); + Assert.fail("more sigs than totalSignNum must be rejected"); + } catch (ValidateSignatureException e) { + Assert.assertTrue(e.getMessage().contains("too many signatures")); + } + } finally { + dbManager.getDynamicPropertiesStore().saveTotalSignNum(original); + } + } + + @Test + public void fnDsaPQAuthSigRejectedWhenNotActivated() throws Exception { + dbManager.getDynamicPropertiesStore().saveAllowFnDsa512(0L); + FNDSA kp = new FNDSA(); + putAccountWithPQPermission(PQ_OWNER_HEX, kp.getPublicKey(), PQScheme.FN_DSA_512); + + Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); + byte[] txid = Sha256Hash.of(true, tx.getRawData().toByteArray()).getBytes(); + + byte[] sig = FNDSA.sign(kp.getPrivateKey(), txid); + + Transaction signed = tx.toBuilder() + .addPqAuthSig(PQAuthSig.newBuilder() + .setScheme(PQScheme.FN_DSA_512) + .setPublicKey(ByteString.copyFrom(kp.getPublicKey())) + .setSignature(ByteString.copyFrom(sig)) + .build()) + .build(); + TransactionCapsule cap = new TransactionCapsule(signed); + try { + cap.validatePubSignature(dbManager.getAccountStore(), + dbManager.getDynamicPropertiesStore()); + Assert.fail("FN-DSA must be rejected when ALLOW_FN_DSA_512 is 0"); + } catch (ValidateSignatureException expected) { + Assert.assertTrue(expected.getMessage().contains("no post-quantum scheme is activated")); + } + } } diff --git a/framework/src/test/java/org/tron/core/exception/TronErrorTest.java b/framework/src/test/java/org/tron/core/exception/TronErrorTest.java index 91559d86362..11538bd967e 100644 --- a/framework/src/test/java/org/tron/core/exception/TronErrorTest.java +++ b/framework/src/test/java/org/tron/core/exception/TronErrorTest.java @@ -16,6 +16,7 @@ import com.typesafe.config.ConfigObject; import java.io.IOException; import java.lang.reflect.Field; +import java.nio.file.Files; import java.nio.file.Path; import java.util.Collections; import java.util.HashMap; @@ -116,9 +117,16 @@ public void LogLoadTest() throws IOException { } @Test - public void witnessInitTest() { + public void witnessInitTest() throws IOException { + // Inherit config-test.conf and override every witness-key source so that + // --witness has nothing to initialize from. + Path conf = temporaryFolder.newFile("no-witness.conf").toPath(); + String content = "include classpath(\"" + TestConstants.TEST_CONF + "\")\n" + + "localwitness = []\n" + + "localwitness_pq_keys = []\n"; + Files.write(conf, content.getBytes()); TronError thrown = assertThrows(TronError.class, () -> { - Args.setParam(new String[]{"--witness"}, TestConstants.TEST_CONF); + Args.setParam(new String[]{"--witness"}, conf.toString()); }); assertEquals(TronError.ErrCode.WITNESS_INIT, thrown.getErrCode()); } diff --git a/framework/src/test/java/org/tron/core/services/ProposalServiceTest.java b/framework/src/test/java/org/tron/core/services/ProposalServiceTest.java index 5732e6f1cde..070a49bdd85 100644 --- a/framework/src/test/java/org/tron/core/services/ProposalServiceTest.java +++ b/framework/src/test/java/org/tron/core/services/ProposalServiceTest.java @@ -1,6 +1,7 @@ package org.tron.core.services; import static org.tron.core.Constant.MAX_PROPOSAL_EXPIRE_TIME; +import static org.tron.core.utils.ProposalUtil.ProposalType.ALLOW_FN_DSA_512; import static org.tron.core.utils.ProposalUtil.ProposalType.CONSENSUS_LOGIC_OPTIMIZATION; import static org.tron.core.utils.ProposalUtil.ProposalType.ENERGY_FEE; import static org.tron.core.utils.ProposalUtil.ProposalType.PROPOSAL_EXPIRE_TIME; @@ -151,4 +152,20 @@ public void testProposalExpireTime() { Assert.assertEquals(MAX_PROPOSAL_EXPIRE_TIME - 3000, window); } + @Test + public void testProcessAllowFnDsa512() { + dbManager.getDynamicPropertiesStore().saveAllowFnDsa512(0L); + Assert.assertFalse(dbManager.getDynamicPropertiesStore().allowFnDsa512()); + + Proposal proposal = Proposal.newBuilder() + .putParameters(ALLOW_FN_DSA_512.getCode(), 1L).build(); + ProposalCapsule proposalCapsule = new ProposalCapsule(proposal); + boolean result = ProposalService.process(dbManager, proposalCapsule); + Assert.assertTrue(result); + + Assert.assertEquals(1L, dbManager.getDynamicPropertiesStore().getAllowFnDsa512()); + Assert.assertTrue(dbManager.getDynamicPropertiesStore().allowFnDsa512()); + + dbManager.getDynamicPropertiesStore().saveAllowFnDsa512(0L); + } } \ No newline at end of file diff --git a/framework/src/test/java/org/tron/core/services/http/UtilTest.java b/framework/src/test/java/org/tron/core/services/http/UtilTest.java index ebcb530bca3..95110f2b267 100644 --- a/framework/src/test/java/org/tron/core/services/http/UtilTest.java +++ b/framework/src/test/java/org/tron/core/services/http/UtilTest.java @@ -15,6 +15,8 @@ import org.tron.core.config.args.Args; import org.tron.core.utils.TransactionUtil; import org.tron.protos.Protocol; +import org.tron.protos.Protocol.PQAuthSig; +import org.tron.protos.Protocol.PQScheme; import org.tron.protos.Protocol.Transaction; public class UtilTest extends BaseTest { @@ -189,4 +191,41 @@ public void testPackTransaction() { TransactionSignWeight txSignWeight = transactionUtil.getTransactionSignWeight(transaction); Assert.assertNotNull(txSignWeight); } + + @Test + public void roundtripPQAuthSigJson() throws Exception { + byte[] sig = new byte[752]; + byte[] pubKey = new byte[897]; + for (int i = 0; i < sig.length; i++) { + sig[i] = (byte) (i & 0xff); + } + for (int i = 0; i < pubKey.length; i++) { + pubKey[i] = (byte) ((i * 7) & 0xff); + } + PQAuthSig pqAuthSig = PQAuthSig.newBuilder() + .setScheme(PQScheme.FN_DSA_512) + .setPublicKey(ByteString.copyFrom(pubKey)) + .setSignature(ByteString.copyFrom(sig)) + .build(); + Transaction original = Transaction.newBuilder() + .setRawData(Transaction.raw.newBuilder().setTimestamp(1L).build()) + .addPqAuthSig(pqAuthSig) + .build(); + + String json = Util.printTransactionToJSON(original, false).toJSONString(); + Assert.assertTrue("JSON output should contain pq_auth_sig field", + json.contains("pq_auth_sig")); + + Transaction.Builder rebuilt = Transaction.newBuilder(); + JsonFormat.merge(json, rebuilt, false); + Transaction decoded = rebuilt.build(); + + Assert.assertEquals(1, decoded.getPqAuthSigCount()); + Assert.assertEquals(pqAuthSig.getScheme(), + decoded.getPqAuthSig(0).getScheme()); + Assert.assertEquals(pqAuthSig.getPublicKey(), + decoded.getPqAuthSig(0).getPublicKey()); + Assert.assertEquals(pqAuthSig.getSignature(), + decoded.getPqAuthSig(0).getSignature()); + } } diff --git a/framework/src/test/resources/config-test.conf b/framework/src/test/resources/config-test.conf index 21cebbfeef4..99afce48263 100644 --- a/framework/src/test/resources/config-test.conf +++ b/framework/src/test/resources/config-test.conf @@ -349,10 +349,13 @@ genesis.block = { // and the localwitness is configured with the private key of the witnessPermissionAddress in the witness account. // When it is empty,the localwitness is configured with the private key of the witness account. -//localWitnessAccountAddress = +// localWitnessAccountAddress = localwitness = [ +] +localwitness_pq_scheme = "FN_DSA_512" +localwitness_pq_keys = [ ] block = { @@ -387,4 +390,4 @@ node.dynamicConfig.enable = true event.subscribe = { enable = false } -node.dynamicConfig.checkInterval = 0 \ No newline at end of file +node.dynamicConfig.checkInterval = 0 diff --git a/protocol/src/main/protos/core/Tron.proto b/protocol/src/main/protos/core/Tron.proto index 6a294c32b0c..d9ca33f7f53 100644 --- a/protocol/src/main/protos/core/Tron.proto +++ b/protocol/src/main/protos/core/Tron.proto @@ -16,6 +16,18 @@ enum AccountType { Contract = 2; } +// Post-quantum signature scheme identifier used by PQAuthSig. +// UNKNOWN_PQ_SCHEME = 0 is the proto3 default and is reserved per the +// java-tron API evolution standard (issue #6515) so unset / unrecognized +// values are detectable by JSON consumers; it MUST never be registered +// in PQSchemeRegistry. FN_DSA_512 = 1 is the V2 launch scheme. +// New schemes are reserved for future activation (see PQSchemeRegistry). +enum PQScheme { + UNKNOWN_PQ_SCHEME = 0; + FN_DSA_512 = 1; + reserved 2 to 15; +} + // AccountId, (name, address) use name, (null, address) use address, (name, null) use name, message AccountId { bytes name = 1; @@ -241,7 +253,17 @@ message Account { message Key { bytes address = 1; - int64 weight = 2; + int64 weight = 2; +} + +// Per-signer post-quantum authentication witness for a transaction or block. +// The signing public key is carried in-band; node verifies binding via +// derived_addr = 0x41 ‖ deriveHash(scheme, public_key)[0:20] +// and matches against Permission.keys[].address. +message PQAuthSig { + PQScheme scheme = 1; + bytes public_key = 2; + bytes signature = 3; } message DelegatedResource { @@ -448,6 +470,12 @@ message Transaction { // only support size = 1, repeated list here for muti-sig extension repeated bytes signature = 2; repeated Result ret = 5; + // Post-quantum authentication signatures. Each entry binds a signing + // public key to its derived address and the corresponding signature. + // ECDSA signatures (`signature` above) and PQAuthSig entries may co-exist + // on multi-sig transactions, contributing weight independently to the + // permission's threshold. + repeated PQAuthSig pq_auth_sig = 6; } message TransactionInfo { @@ -514,6 +542,12 @@ message BlockHeader { } raw raw_data = 1; bytes witness_signature = 2; + // Post-quantum block signature. Exactly one of {witness_signature, + // pq_auth_sig} SHALL be present per block: SRs with an ECDSA-only Witness + // Permission set witness_signature; SRs whose Witness Permission carries a + // PQ-derived Key set pq_auth_sig instead. The verifier dispatches by which + // field is populated. + PQAuthSig pq_auth_sig = 3; } // block From 4b45b4db1b6a486fdeed6223ecb84df50abb1164 Mon Sep 17 00:00:00 2001 From: federico Date: Mon, 11 May 2026 02:16:37 +0800 Subject: [PATCH 2/9] refactor(pqc): harden ALLOW_FN_DSA_512 proposal validation and PQ-only witness init --- .../org/tron/core/utils/ProposalUtil.java | 15 +++-- .../java/org/tron/core/config/args/Args.java | 24 +++----- .../core/config/args/WitnessInitializer.java | 55 +++++++++-------- .../core/actuator/utils/ProposalUtilTest.java | 60 +++++++++++++++---- 4 files changed, 94 insertions(+), 60 deletions(-) diff --git a/actuator/src/main/java/org/tron/core/utils/ProposalUtil.java b/actuator/src/main/java/org/tron/core/utils/ProposalUtil.java index 259a1a60bde..b7954165e4c 100644 --- a/actuator/src/main/java/org/tron/core/utils/ProposalUtil.java +++ b/actuator/src/main/java/org/tron/core/utils/ProposalUtil.java @@ -942,13 +942,18 @@ public static void validator(DynamicPropertiesStore dynamicPropertiesStore, break; } case ALLOW_FN_DSA_512: { - if (dynamicPropertiesStore.getAllowFnDsa512() == 1) { + if (!forkController.pass(ForkBlockVersionEnum.VERSION_4_8_2)) { throw new ContractValidateException( - "[ALLOW_FN_DSA_512] has been valid, no need to propose again"); + "Bad chain parameter id [ALLOW_FN_DSA_512]"); } - if (value != 1) { + if (value != 0 && value != 1) { throw new ContractValidateException( - "This value[ALLOW_FN_DSA_512] is only allowed to be 1"); + "This value[ALLOW_FN_DSA_512] is only allowed to be 0 or 1"); + } + if (dynamicPropertiesStore.getAllowFnDsa512() == value) { + throw new ContractValidateException( + "[ALLOW_FN_DSA_512] has been set to " + value + + ", no need to propose again"); } break; } @@ -1041,7 +1046,7 @@ public enum ProposalType { // current value, value range ALLOW_TVM_OSAKA(96), // 0, 1 ALLOW_HARDEN_RESOURCE_CALCULATION(97), // 0, 1 ALLOW_HARDEN_EXCHANGE_CALCULATION(98), // 0, 1 - ALLOW_FN_DSA_512(100); // 0, 1 + ALLOW_FN_DSA_512(99); // 0, 1 private long code; diff --git a/framework/src/main/java/org/tron/core/config/args/Args.java b/framework/src/main/java/org/tron/core/config/args/Args.java index d6ef74c6ef7..de874f90dde 100644 --- a/framework/src/main/java/org/tron/core/config/args/Args.java +++ b/framework/src/main/java/org/tron/core/config/args/Args.java @@ -962,29 +962,26 @@ private static void initLocalWitnesses(Config config, CLIParameter cmd) { if (config.hasPath(ConfigKey.LOCAL_WITNESS_PQ_KEYS)) { List pqEntries = config.getStringList(ConfigKey.LOCAL_WITNESS_PQ_KEYS); if (!pqEntries.isEmpty()) { - localWitnesses = new LocalWitnesses(); - // Scheme must be applied before keypairs — key-length validation depends on it. + PQScheme scheme = PQScheme.FN_DSA_512; if (config.hasPath(ConfigKey.LOCAL_WITNESS_PQ_SCHEME)) { String schemeName = config.getString(ConfigKey.LOCAL_WITNESS_PQ_SCHEME); try { - PQScheme scheme = PQScheme.valueOf(schemeName); - if (!WITNESS_PQ_SCHEMES.contains(scheme)) { - throw new TronError("invalid " + ConfigKey.LOCAL_WITNESS_PQ_SCHEME - + ": " + schemeName + "; valid values: " + WITNESS_PQ_SCHEMES, - TronError.ErrCode.WITNESS_INIT); - } - localWitnesses.setPqScheme(scheme); + scheme = PQScheme.valueOf(schemeName); } catch (IllegalArgumentException e) { throw new TronError("invalid " + ConfigKey.LOCAL_WITNESS_PQ_SCHEME + ": " + schemeName, TronError.ErrCode.WITNESS_INIT); } + if (!WITNESS_PQ_SCHEMES.contains(scheme)) { + throw new TronError("invalid " + ConfigKey.LOCAL_WITNESS_PQ_SCHEME + + ": " + schemeName + "; valid values: " + WITNESS_PQ_SCHEMES, + TronError.ErrCode.WITNESS_INIT); + } } // Each entry is the extended private key f‖g‖F‖h (priv ‖ pub) hex, // sized (privLen + pubLen) bytes for the active scheme. We split here // so downstream consumers (ConsensusService, LocalWitnesses) keep the // same priv/pub split they already use — derivePublicKey(priv) replaces // the previous explicit `pub` config field. - PQScheme scheme = localWitnesses.getPqScheme(); int privHexLen = PQSchemeRegistry.getPrivateKeyLength(scheme) * 2; int extHexLen = privHexLen + PQSchemeRegistry.getPublicKeyLength(scheme) * 2; List pqPrivateKeys = new ArrayList<>(pqEntries.size()); @@ -1002,11 +999,8 @@ private static void initLocalWitnesses(Config config, CLIParameter cmd) { pqPrivateKeys.add(stripped.substring(0, privHexLen)); pqPublicKeys.add(stripped.substring(privHexLen)); } - localWitnesses.setPqKeypairs(pqPrivateKeys, pqPublicKeys); - byte[] address = WitnessInitializer.resolvePqAuthSigAddress(lwConfig.getAccountAddress()); - if (address != null) { - localWitnesses.setWitnessAccountAddress(address); - } + localWitnesses = WitnessInitializer.initFromPQOnly( + scheme, pqPrivateKeys, pqPublicKeys, lwConfig.getAccountAddress()); return; } } diff --git a/framework/src/main/java/org/tron/core/config/args/WitnessInitializer.java b/framework/src/main/java/org/tron/core/config/args/WitnessInitializer.java index f8068511c0d..9251f2151c7 100644 --- a/framework/src/main/java/org/tron/core/config/args/WitnessInitializer.java +++ b/framework/src/main/java/org/tron/core/config/args/WitnessInitializer.java @@ -7,6 +7,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.tron.common.crypto.SignInterface; +import org.tron.common.crypto.pqc.PQSchemeRegistry; import org.tron.common.utils.ByteArray; import org.tron.common.utils.Commons; import org.tron.common.utils.LocalWitnesses; @@ -14,6 +15,7 @@ import org.tron.core.exception.TronError; import org.tron.keystore.Credentials; import org.tron.keystore.WalletUtils; +import org.tron.protos.Protocol.PQScheme; @Slf4j public class WitnessInitializer { @@ -113,42 +115,39 @@ public static LocalWitnesses initFromKeystore( } /** - * Init for PQ-only witness nodes (no legacy ECDSA key). The witness account - * address must be supplied explicitly because there is no ECDSA key to derive it from. + * Init for PQ-only witness nodes (no legacy ECDSA key). When + * {@code witnessAccountAddress} is blank, the address is derived from the + * first PQ public key via {@link PQSchemeRegistry#computeAddress(PQScheme, + * byte[])}. */ - public static LocalWitnesses initFromPQOnly(String witnessAccountAddress) { - if (StringUtils.isBlank(witnessAccountAddress)) { - throw new TronError( - "localWitnessAccountAddress must be set for PQ-only witness nodes", - TronError.ErrCode.WITNESS_INIT); - } - byte[] address = Commons.decodeFromBase58Check(witnessAccountAddress); - if (address == null) { + public static LocalWitnesses initFromPQOnly(PQScheme scheme, + List pqPrivateKeys, List pqPublicKeys, + String witnessAccountAddress) { + if (pqPublicKeys == null || pqPublicKeys.isEmpty()) { throw new TronError( - "LocalWitnessAccountAddress format is incorrect", + "PQ public keys must be set for PQ-only witness nodes", TronError.ErrCode.WITNESS_INIT); } LocalWitnesses witnesses = new LocalWitnesses(); - witnesses.initWitnessAccountAddress(address, false); - logger.debug("Initialised PQ-only witness with address {}", witnessAccountAddress); - return witnesses; - } + witnesses.setPqScheme(scheme); + witnesses.setPqKeypairs(pqPrivateKeys, pqPublicKeys); - /** - * Resolve witness address for PQ seed configuration. - */ - public static byte[] resolvePqAuthSigAddress(String witnessAccountAddress) { - if (StringUtils.isEmpty(witnessAccountAddress)) { - return null; - } - byte[] address = Commons.decodeFromBase58Check(witnessAccountAddress); - if (address != null) { - logger.debug("Got localWitnessAccountAddress from config.conf"); + byte[] address; + if (StringUtils.isBlank(witnessAccountAddress)) { + byte[] firstPubKey = ByteArray.fromHexString(pqPublicKeys.get(0)); + address = PQSchemeRegistry.computeAddress(scheme, firstPubKey); + logger.debug("Derived PQ-only witness address from public key"); } else { - throw new TronError("LocalWitnessAccountAddress format from config is incorrect", - TronError.ErrCode.WITNESS_INIT); + address = Commons.decodeFromBase58Check(witnessAccountAddress); + if (address == null) { + throw new TronError( + "LocalWitnessAccountAddress format is incorrect", + TronError.ErrCode.WITNESS_INIT); + } + logger.debug("Got localWitnessAccountAddress from config.conf"); } - return address; + witnesses.initWitnessAccountAddress(address, false); + return witnesses; } static byte[] resolveWitnessAddress( diff --git a/framework/src/test/java/org/tron/core/actuator/utils/ProposalUtilTest.java b/framework/src/test/java/org/tron/core/actuator/utils/ProposalUtilTest.java index 92f35d7b79f..1e7be605104 100644 --- a/framework/src/test/java/org/tron/core/actuator/utils/ProposalUtilTest.java +++ b/framework/src/test/java/org/tron/core/actuator/utils/ProposalUtilTest.java @@ -801,26 +801,62 @@ public void blockVersionCheck() { @Test public void validateAllowFnDsa512() { long code = ProposalType.ALLOW_FN_DSA_512.getCode(); + ThrowingRunnable proposeZero = () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + code, 0); + ThrowingRunnable proposeOne = () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + code, 1); + ThrowingRunnable proposeTwo = () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + code, 2); - ContractValidateException thrown = assertThrows(ContractValidateException.class, - () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, code, 0)); - assertEquals("This value[ALLOW_FN_DSA_512] is only allowed to be 1", thrown.getMessage()); + byte[] stats = new byte[27]; + forkUtils.getManager().getDynamicPropertiesStore() + .statsByVersion(ForkBlockVersionEnum.VERSION_4_8_1.getValue(), stats); + long maintenanceTimeInterval = forkUtils.getManager().getDynamicPropertiesStore() + .getMaintenanceTimeInterval(); + long hardForkTime = + ((ForkBlockVersionEnum.VERSION_4_8_2.getHardForkTime() - 1) / maintenanceTimeInterval + 1) + * maintenanceTimeInterval; + forkUtils.getManager().getDynamicPropertiesStore() + .saveLatestBlockHeaderTimestamp(hardForkTime - 1); - thrown = assertThrows(ContractValidateException.class, - () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, code, 2)); - assertEquals("This value[ALLOW_FN_DSA_512] is only allowed to be 1", thrown.getMessage()); + // 1) before fork 4.8.2 -> rejected + ContractValidateException thrown = assertThrows(ContractValidateException.class, proposeOne); + assertEquals("Bad chain parameter id [ALLOW_FN_DSA_512]", thrown.getMessage()); + forkUtils.getManager().getDynamicPropertiesStore() + .saveLatestBlockHeaderTimestamp(hardForkTime + 1); + Arrays.fill(stats, (byte) 1); + forkUtils.getManager().getDynamicPropertiesStore() + .statsByVersion(ForkBlockVersionEnum.VERSION_4_8_2.getValue(), stats); + + // 2) value not in {0, 1} -> rejected + thrown = assertThrows(ContractValidateException.class, proposeTwo); + assertEquals("This value[ALLOW_FN_DSA_512] is only allowed to be 0 or 1", thrown.getMessage()); + + // 3) current value is 0 (default), proposing 0 again -> rejected + thrown = assertThrows(ContractValidateException.class, proposeZero); + assertEquals("[ALLOW_FN_DSA_512] has been set to 0, no need to propose again", + thrown.getMessage()); + + // 4) value=1 to enable -> ok try { - ProposalUtil.validator(dynamicPropertiesStore, forkUtils, code, 1); - } catch (ContractValidateException e) { - Assert.fail("value=1 should be accepted: " + e.getMessage()); + proposeOne.run(); + } catch (Throwable e) { + Assert.fail("Should pass when toggling 0 -> 1: " + e.getMessage()); } + // 5) after activation, proposing 1 again -> rejected dynamicPropertiesStore.saveAllowFnDsa512(1L); - thrown = assertThrows(ContractValidateException.class, - () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, code, 1)); - assertEquals("[ALLOW_FN_DSA_512] has been valid, no need to propose again", + thrown = assertThrows(ContractValidateException.class, proposeOne); + assertEquals("[ALLOW_FN_DSA_512] has been set to 1, no need to propose again", thrown.getMessage()); + + // 6) value=0 to disable -> ok (toggle back off) + try { + proposeZero.run(); + } catch (Throwable e) { + Assert.fail("Should pass when toggling 1 -> 0: " + e.getMessage()); + } dynamicPropertiesStore.saveAllowFnDsa512(0L); } } From c32015b50bd2a568f57e1e6fb78e2feef4a5c956 Mon Sep 17 00:00:00 2001 From: federico Date: Wed, 13 May 2026 18:38:53 +0800 Subject: [PATCH 3/9] fix(crypto): address PQ review feedback --- .../tron/core/vm/PrecompiledContracts.java | 139 +++++--- .../org/tron/core/capsule/BlockCapsule.java | 19 +- .../tron/core/capsule/TransactionCapsule.java | 3 +- .../java/org/tron/consensus/base/Param.java | 17 + .../crypto/pqc/{FNDSA.java => FNDSA512.java} | 55 +++- .../common/crypto/pqc/PQSchemeRegistry.java | 12 +- .../java/org/tron/core/config/args/Args.java | 26 +- .../core/config/args/WitnessInitializer.java | 17 +- .../tron/core/consensus/ConsensusService.java | 7 + .../main/java/org/tron/core/db/Manager.java | 33 +- framework/src/main/resources/config.conf | 2 +- ...FNDSAKatTest.java => FNDSA512KatTest.java} | 42 +-- .../pqc/{FNDSATest.java => FNDSA512Test.java} | 132 ++++---- .../crypto/pqc/PQSchemeRegistryTest.java | 16 +- .../crypto/pqc/PQSignatureDefaultsTest.java | 2 +- .../pqc/SignatureSchemeBenchmarkTest.java | 6 +- .../common/crypto/pqc/program/PQClient.java | 24 +- .../common/crypto/pqc/program/PQFullNode.java | 8 +- .../common/crypto/pqc/program/PQTxSender.java | 299 ++++++++++++++++++ .../crypto/pqc/program/PQWitnessNode.java | 14 +- ...st.java => BatchValidateFnDsa512Test.java} | 48 +-- .../runtime/vm/FnDsaPrecompileTest.java | 39 ++- ...st.java => ValidateMultiFnDsa512Test.java} | 40 +-- .../tron/common/utils/LocalWitnessesTest.java | 6 +- .../org/tron/core/BandwidthProcessorTest.java | 9 +- .../core/actuator/utils/ProposalUtilTest.java | 1 + .../tron/core/capsule/BlockCapsulePQTest.java | 30 +- .../core/capsule/TransactionCapsuleTest.java | 81 ++--- protocol/src/main/protos/core/Tron.proto | 2 +- 29 files changed, 830 insertions(+), 299 deletions(-) rename crypto/src/main/java/org/tron/common/crypto/pqc/{FNDSA.java => FNDSA512.java} (83%) rename framework/src/test/java/org/tron/common/crypto/pqc/{FNDSAKatTest.java => FNDSA512KatTest.java} (88%) rename framework/src/test/java/org/tron/common/crypto/pqc/{FNDSATest.java => FNDSA512Test.java} (76%) create mode 100644 framework/src/test/java/org/tron/common/crypto/pqc/program/PQTxSender.java rename framework/src/test/java/org/tron/common/runtime/vm/{BatchValidateSignPQTest.java => BatchValidateFnDsa512Test.java} (89%) rename framework/src/test/java/org/tron/common/runtime/vm/{ValidateMultiSignPQTest.java => ValidateMultiFnDsa512Test.java} (95%) diff --git a/actuator/src/main/java/org/tron/core/vm/PrecompiledContracts.java b/actuator/src/main/java/org/tron/core/vm/PrecompiledContracts.java index 27308c4412b..f5e0f63dc59 100644 --- a/actuator/src/main/java/org/tron/core/vm/PrecompiledContracts.java +++ b/actuator/src/main/java/org/tron/core/vm/PrecompiledContracts.java @@ -53,7 +53,7 @@ import org.tron.common.crypto.Rsv; import org.tron.common.crypto.SignUtils; import org.tron.common.crypto.SignatureInterface; -import org.tron.common.crypto.pqc.FNDSA; +import org.tron.common.crypto.pqc.FNDSA512; import org.tron.common.crypto.pqc.PQSchemeRegistry; import org.tron.common.crypto.zksnark.BN128; import org.tron.common.crypto.zksnark.BN128Fp; @@ -120,10 +120,10 @@ public class PrecompiledContracts { private static final Blake2F blake2F = new Blake2F(); private static final P256Verify p256Verify = new P256Verify(); - private static final VerifyFnDsa verifyFnDsa = new VerifyFnDsa(); - private static final ValidateMultiSignPQ validateMultiSignPQ = - new ValidateMultiSignPQ(); - private static final BatchValidateSignPQ batchValidateSignPQ = new BatchValidateSignPQ(); + private static final VerifyFnDsa512 verifyFnDsa512 = new VerifyFnDsa512(); + private static final ValidateMultiFnDsa512 validateMultiFnDsa512 = + new ValidateMultiFnDsa512(); + private static final BatchValidateFnDsa512 batchValidateFnDsa512 = new BatchValidateFnDsa512(); // FreezeV2 PrecompileContracts private static final GetChainParameter getChainParameter = new GetChainParameter(); @@ -223,19 +223,19 @@ public class PrecompiledContracts { // EIP-8052 0x16: FN-DSA / Falcon-512 verify (FIPS-206 draft). Input layout: // [msg 32B | sig_len 2B (big-endian) | sig sig_len B (1..752) | pk 896B]. // Variable-length signature is prefixed with a 2-byte length field. - private static final DataWord verifyFnDsaAddr = new DataWord( + private static final DataWord verifyFnDsa512Addr = new DataWord( "0000000000000000000000000000000000000000000000000000000000000016"); // 0x17: algorithm-agnostic Permission multi-sign — accepts both ECDSA and // Falcon-512 signatures against the same Permission.keys[] in one call, // matching transaction-side §2.3.5 mixed-weight semantics. - private static final DataWord validateMultiSignPQAddr = new DataWord( + private static final DataWord validateMultiFnDsa512Addr = new DataWord( "0000000000000000000000000000000000000000000000000000000000000017"); // 0x18: batch independent Falcon-512 verify — bitmap of (sig, pk, addr) // matches; mixed-algorithm contracts call 0x0A and 0x18 separately and OR // the bitmaps client-side. - private static final DataWord batchValidateSignPqAddr = new DataWord( + private static final DataWord batchValidateFnDsa512Addr = new DataWord( "0000000000000000000000000000000000000000000000000000000000000018"); public static PrecompiledContract getOptimizedContractForConstant(PrecompiledContract contract) { @@ -323,16 +323,18 @@ public static PrecompiledContract getContractForAddress(DataWord address) { return p256Verify; } - if (VMConfig.allowFnDsa512() && address.equals(verifyFnDsaAddr)) { - return verifyFnDsa; - } - - if (VMConfig.allowFnDsa512() && address.equals(validateMultiSignPQAddr)) { - return validateMultiSignPQ; - } - - if (VMConfig.allowFnDsa512() && address.equals(batchValidateSignPqAddr)) { - return batchValidateSignPQ; + // FN-DSA-512 is the first PQ signature scheme supported by TRON, so its proposal flag + // gates every PQ-related precompile (single verify, multisig verify, batch verify). + if (VMConfig.allowFnDsa512()) { + if (address.equals(verifyFnDsa512Addr)) { + return verifyFnDsa512; + } + if (address.equals(validateMultiFnDsa512Addr)) { + return validateMultiFnDsa512; + } + if (address.equals(batchValidateFnDsa512Addr)) { + return batchValidateFnDsa512; + } } if (VMConfig.allowTvmFreezeV2()) { @@ -475,6 +477,35 @@ private static boolean isValidAbiEncoding(byte[] data, int headerWords, int item return tail > 0 && tail % multiplyExact(itemWords, WORD_SIZE) == 0; } + /** + * Structural pre-check for ABI head: word-aligned length and room for the + * fixed head. The PQ precompiles cannot reuse {@link #isValidAbiEncoding} + * because their {@code bytes[]} entries (PQ signatures, 1..752 bytes) are + * variable-length, so the trailing divisibility check does not apply. + */ + private static boolean isValidAbiHead(byte[] data, int headWords) { + return data != null + && data.length % WORD_SIZE == 0 + && data.length >= multiplyExact(headWords, WORD_SIZE); + } + + /** + * Verifies that the array offset stored at {@code words[offsetWordIndex]} is + * word-aligned, falls inside the dynamic data region (≥ head), and points to + * a length word that still fits inside {@code words}. Sister check to + * {@link #isValidAbiEncoding} for ABIs whose items are not uniform width. + */ + private static boolean isValidArrayOffset(DataWord[] words, int offsetWordIndex, + int headWords) { + long offsetBytes = words[offsetWordIndex].longValueSafe(); + if (offsetBytes < (long) headWords * WORD_SIZE + || offsetBytes % WORD_SIZE != 0) { + return false; + } + long lengthWordIdx = offsetBytes / WORD_SIZE; + return lengthWordIdx < words.length; + } + public abstract static class PrecompiledContract { protected static final byte[] DATA_FALSE = new byte[WORD_SIZE]; @@ -2425,22 +2456,23 @@ public Pair execute(byte[] data) { *
    *   [msg 32B | sig_len 2B (big-endian, 1..752) | sig sig_len B | pk 896B]
    * 
- * Minimum input: 32 + 2 + 1 + 896 = 931 bytes. + * Total length must equal exactly {@code 32 + 2 + sig_len + 896} (no trailing + * bytes; matches 0x100 P256Verify / EIP-7951 strictness). * *

Returns a 32-byte word: 1 on valid signature, 0 otherwise. * Malformed input (wrong lengths, out-of-range sig_len) returns 0 without error. */ - public static class VerifyFnDsa extends PrecompiledContract { + public static class VerifyFnDsa512 extends PrecompiledContract { private static final int MSG_LEN = 32; private static final int SIG_LEN_FIELD = 2; - private static final int PK_LEN = FNDSA.PUBLIC_KEY_LENGTH; - private static final int MAX_SIG_LEN = FNDSA.SIGNATURE_LENGTH; + private static final int PK_LEN = FNDSA512.PUBLIC_KEY_LENGTH; + private static final int MAX_SIG_LEN = FNDSA512.SIGNATURE_LENGTH; private static final int MIN_INPUT_LEN = MSG_LEN + SIG_LEN_FIELD + 1 + PK_LEN; @Override public long getEnergyForData(byte[] data) { - return 2500; + return 4000; } @Override @@ -2455,14 +2487,17 @@ public Pair execute(byte[] data) { return Pair.of(true, DataWord.ZERO().getData()); } int pkOffset = MSG_LEN + SIG_LEN_FIELD + sigLen; - if (data.length < pkOffset + PK_LEN) { + // Strict equality (cf. 0x100 P256Verify): one logical input ↔ one encoding, + // leaves room for future EIP-8052 trailing fields. + if (data.length != pkOffset + PK_LEN) { return Pair.of(true, DataWord.ZERO().getData()); } byte[] sig = copyOfRange(data, MSG_LEN + SIG_LEN_FIELD, pkOffset); byte[] pk = copyOfRange(data, pkOffset, pkOffset + PK_LEN); - boolean ok = FNDSA.verify(pk, msg, sig); + boolean ok = FNDSA512.verify(pk, msg, sig); return Pair.of(true, ok ? DataWord.ONE().getData() : DataWord.ZERO().getData()); } catch (Throwable t) { + logger.info("VerifyFnDsa512 error:{}", t.getMessage()); return Pair.of(true, DataWord.ZERO().getData()); } } @@ -2489,15 +2524,17 @@ public Pair execute(byte[] data) { * * *

{@code MAX_SIZE = 5} applies to the total signature count - * ({@code ecdsaCnt + pqCnt}). Energy is split: {@code ecdsaCnt × 1500 + pqCnt × 15000}. + * ({@code ecdsaCnt + pqCnt}). Energy is split: {@code ecdsaCnt × 1500 + pqCnt × 2000}. */ - public static class ValidateMultiSignPQ extends PrecompiledContract { + public static class ValidateMultiFnDsa512 extends PrecompiledContract { private static final int ECDSA_ENERGY_PER_SIGN = 1500; - private static final int PQ_ENERGY_PER_SIGN = 15000; + private static final int PQ_ENERGY_PER_SIGN = 2000; private static final int MAX_SIZE = 5; - private static final int PK_LEN = FNDSA.PUBLIC_KEY_LENGTH; - private static final int MAX_SIG_LEN = FNDSA.SIGNATURE_LENGTH; + private static final int PK_LEN = FNDSA512.PUBLIC_KEY_LENGTH; + private static final int MAX_SIG_LEN = FNDSA512.SIGNATURE_LENGTH; + // address, permissionId, data, ecdsaOffset, pqSigOffset, pqPkOffset. + private static final int ABI_HEAD_WORDS = 6; @Override public long getEnergyForData(byte[] data) { @@ -2514,8 +2551,16 @@ public long getEnergyForData(byte[] data) { @Override public Pair execute(byte[] rawData) { + if (!isValidAbiHead(rawData, ABI_HEAD_WORDS)) { + return Pair.of(false, EMPTY_BYTE_ARRAY); + } try { DataWord[] words = DataWord.parseArray(rawData); + if (!isValidArrayOffset(words, 3, ABI_HEAD_WORDS) + || !isValidArrayOffset(words, 4, ABI_HEAD_WORDS) + || !isValidArrayOffset(words, 5, ABI_HEAD_WORDS)) { + return Pair.of(false, EMPTY_BYTE_ARRAY); + } byte[] address = words[0].toTronAddress(); int permissionId = words[1].intValueSafe(); byte[] data = words[2].getData(); @@ -2596,7 +2641,7 @@ public Pair execute(byte[] rawData) { if (weight == 0) { return Pair.of(true, DATA_FALSE); } - if (!FNDSA.verify(pk, hash, sig)) { + if (!FNDSA512.verify(pk, hash, sig)) { return Pair.of(true, DATA_FALSE); } totalWeight += weight; @@ -2617,13 +2662,13 @@ public Pair execute(byte[] rawData) { } /** - * 0x18 BatchValidateSignPQ — independent per-element Falcon-512 verify. + * 0x18 BatchValidateFnDsa512 — independent per-element Falcon-512 verify. *

Returns a 256-bit bitmap (matching 0x0A) where bit {@code i} is set iff - * {@code derive(pk_i) == expectedAddr_i} AND {@code FNDSA.verify(pk_i, hash, sig_i)}. + * {@code derive(pk_i) == expectedAddr_i} AND {@code FNDSA512.verify(pk_i, hash, sig_i)}. * *

ABI: *

-   *   batchValidateSignPQ(
+   *   batchValidateFnDsa512(
    *       bytes32   hash,                  // word[0]
    *       bytes[]   signatures,            // word[1] = offset; each 1..752 B
    *       bytes[]   publicKeys,            // word[2] = offset; each 896 B
@@ -2633,14 +2678,16 @@ public Pair execute(byte[] rawData) {
    *
    * 

Reuses the {@code BatchValidateSign.workers} pool when not in a constant * call and enforces {@code getCPUTimeLeftInNanoSecond()} timeout. {@code MAX_SIZE = 16}. - * Energy is {@code cnt × 15000}. + * Energy is {@code cnt × 2000}. */ - public static class BatchValidateSignPQ extends PrecompiledContract { + public static class BatchValidateFnDsa512 extends PrecompiledContract { - private static final int ENERGY_PER_SIGN = 15000; + private static final int ENERGY_PER_SIGN = 2000; private static final int MAX_SIZE = 16; - private static final int PK_LEN = FNDSA.PUBLIC_KEY_LENGTH; - private static final int MAX_SIG_LEN = FNDSA.SIGNATURE_LENGTH; + private static final int PK_LEN = FNDSA512.PUBLIC_KEY_LENGTH; + private static final int MAX_SIG_LEN = FNDSA512.SIGNATURE_LENGTH; + // hash, sigArrayOffset, pkArrayOffset, addrArrayOffset. + private static final int ABI_HEAD_WORDS = 4; @Override public long getEnergyForData(byte[] data) { @@ -2670,7 +2717,15 @@ public Pair execute(byte[] data) { private Pair doExecute(byte[] data) throws InterruptedException, ExecutionException { + if (!isValidAbiHead(data, ABI_HEAD_WORDS)) { + return Pair.of(false, EMPTY_BYTE_ARRAY); + } DataWord[] words = DataWord.parseArray(data); + if (!isValidArrayOffset(words, 1, ABI_HEAD_WORDS) + || !isValidArrayOffset(words, 2, ABI_HEAD_WORDS) + || !isValidArrayOffset(words, 3, ABI_HEAD_WORDS)) { + return Pair.of(false, EMPTY_BYTE_ARRAY); + } byte[] hash = words[0].getData(); int sigArrayWord = words[1].intValueSafe() / WORD_SIZE; @@ -2718,8 +2773,8 @@ private Pair doExecute(byte[] data) .await(getCPUTimeLeftInNanoSecond(), TimeUnit.NANOSECONDS); if (!withNoTimeout) { - logger.info("BatchValidateSignPQ timeout"); - throw Program.Exception.notEnoughTime("call BatchValidateSignPQ precompile method"); + logger.info("BatchValidateFnDsa512 timeout"); + throw Program.Exception.notEnoughTime("call BatchValidateFnDsa512 precompile method"); } for (Future future : futures) { @@ -2743,7 +2798,7 @@ private static boolean verifyOne(byte[] sig, byte[] pk, byte[] hash, if (!DataWord.equalAddressByteArray(derived, expectedAddr)) { return false; } - return FNDSA.verify(pk, hash, sig); + return FNDSA512.verify(pk, hash, sig); } catch (Throwable t) { return false; } 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 dd0e2126bac..6c781d419aa 100755 --- a/chainbase/src/main/java/org/tron/core/capsule/BlockCapsule.java +++ b/chainbase/src/main/java/org/tron/core/capsule/BlockCapsule.java @@ -175,7 +175,9 @@ public void sign(byte[] privateKey) { ByteString sig = ByteString.copyFrom(ecKeyEngine.Base64toBytes(ecKeyEngine.signHash(getRawHash() .getBytes()))); - BlockHeader blockHeader = this.block.getBlockHeader().toBuilder().setWitnessSignature(sig) + BlockHeader blockHeader = this.block.getBlockHeader().toBuilder() + .clearPqAuthSig() + .setWitnessSignature(sig) .build(); this.block = this.block.toBuilder().setBlockHeader(blockHeader).build(); @@ -184,6 +186,7 @@ public void sign(byte[] privateKey) { public void setPqAuthSig(PQAuthSig pqAuthSig) { BlockHeader blockHeader = this.block.getBlockHeader().toBuilder() + .clearWitnessSignature() .setPqAuthSig(pqAuthSig).build(); this.block = this.block.toBuilder().setBlockHeader(blockHeader).build(); } @@ -201,10 +204,7 @@ public boolean validateSignature(DynamicPropertiesStore dynamicPropertiesStore, AccountStore accountStore) throws ValidateSignatureException { BlockHeader header = block.getBlockHeader(); boolean hasLegacy = !header.getWitnessSignature().isEmpty(); - PQAuthSig pqAuthSig = header.getPqAuthSig(); - boolean hasPq = pqAuthSig != null - && pqAuthSig.getSignature() != null - && !pqAuthSig.getSignature().isEmpty(); + boolean hasPq = header.hasPqAuthSig(); if (hasLegacy && hasPq) { throw new ValidateSignatureException( @@ -217,7 +217,7 @@ public boolean validateSignature(DynamicPropertiesStore dynamicPropertiesStore, byte[] witnessAccountAddress = header.getRawData().getWitnessAddress().toByteArray(); if (hasPq) { return validatePQSignature(dynamicPropertiesStore, accountStore, - witnessAccountAddress, pqAuthSig); + witnessAccountAddress, header.getPqAuthSig()); } return validateLegacySignature(dynamicPropertiesStore, accountStore, witnessAccountAddress); } @@ -233,8 +233,11 @@ private boolean validateLegacySignature(DynamicPropertiesStore dynamicProperties if (dynamicPropertiesStore.getAllowMultiSign() != 1) { return Arrays.equals(sigAddress, witnessAccountAddress); } - byte[] witnessPermissionAddress = accountStore.get(witnessAccountAddress) - .getWitnessPermissionAddress(); + AccountCapsule witnessAccount = accountStore.get(witnessAccountAddress); + if (witnessAccount == null) { + throw new ValidateSignatureException("witness account does not exist"); + } + byte[] witnessPermissionAddress = witnessAccount.getWitnessPermissionAddress(); return Arrays.equals(sigAddress, witnessPermissionAddress); } catch (SignatureException e) { throw new ValidateSignatureException(e.getMessage()); 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 24fe9fd9964..21f0f1adc59 100755 --- a/chainbase/src/main/java/org/tron/core/capsule/TransactionCapsule.java +++ b/chainbase/src/main/java/org/tron/core/capsule/TransactionCapsule.java @@ -794,7 +794,8 @@ public boolean validateSignature(AccountStore accountStore, if (!ArrayUtils.isEmpty(owner)) { //transfer from transparent address validatePubSignature(accountStore, dynamicPropertiesStore); } else { //transfer from shielded address - if (this.transaction.getSignatureCount() > 0) { + if (this.transaction.getSignatureCount() > 0 + || this.transaction.getPqAuthSigCount() > 0) { throw new ValidateSignatureException("there should be no signatures signed by " + "transparent address when transfer from shielded address"); } diff --git a/consensus/src/main/java/org/tron/consensus/base/Param.java b/consensus/src/main/java/org/tron/consensus/base/Param.java index a2692cf4c55..71f23ce23ae 100644 --- a/consensus/src/main/java/org/tron/consensus/base/Param.java +++ b/consensus/src/main/java/org/tron/consensus/base/Param.java @@ -54,6 +54,14 @@ public static Param getInstance() { return param; } + /** Signing key family carried by a {@link Miner}. */ + public enum MinerType { + /** Legacy ECDSA / SM2 witness; signs blocks via {@code BlockCapsule.sign}. */ + ECDSA, + /** Post-quantum witness; signs blocks via {@code signWitnessAuth}. */ + PQ + } + public class Miner { @Getter @@ -76,6 +84,15 @@ public class Miner { @Setter private PQScheme pqScheme; + /** + * Explicit signing-family marker so the block producer doesn't have to infer + * key type from {@code privateKey == null}. Defaults to {@link MinerType#ECDSA}; + * PQ-only miners must call {@link #setType(MinerType)} with {@link MinerType#PQ}. + */ + @Getter + @Setter + private MinerType type = MinerType.ECDSA; + public Miner(byte[] privateKey, ByteString privateKeyAddress, ByteString witnessAddress) { this.privateKey = privateKey; this.privateKeyAddress = privateKeyAddress; diff --git a/crypto/src/main/java/org/tron/common/crypto/pqc/FNDSA.java b/crypto/src/main/java/org/tron/common/crypto/pqc/FNDSA512.java similarity index 83% rename from crypto/src/main/java/org/tron/common/crypto/pqc/FNDSA.java rename to crypto/src/main/java/org/tron/common/crypto/pqc/FNDSA512.java index 3a9e22316c5..080eaa3a8c3 100644 --- a/crypto/src/main/java/org/tron/common/crypto/pqc/FNDSA.java +++ b/crypto/src/main/java/org/tron/common/crypto/pqc/FNDSA512.java @@ -1,5 +1,6 @@ package org.tron.common.crypto.pqc; +import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import org.bouncycastle.crypto.AsymmetricCipherKeyPair; import org.bouncycastle.crypto.params.ParametersWithRandom; @@ -24,7 +25,7 @@ * {@code <= SIGNATURE_LENGTH}. BouncyCastle 1.79's {@code FalconNIST.CRYPTO_BYTES} * for Falcon-512 is 690 bytes, well below the 752-byte protocol cap. */ -public final class FNDSA implements PQSignature { +public final class FNDSA512 implements PQSignature { /** * Falcon-512 encoded private key from BC: f || g || F, where f and g are each @@ -58,13 +59,13 @@ public final class FNDSA implements PQSignature { private final byte[] privateKey; private final byte[] publicKey; - public FNDSA() { + public FNDSA512() { AsymmetricCipherKeyPair kp = generateKeyPair(new SecureRandom()); this.privateKey = ((FalconPrivateKeyParameters) kp.getPrivate()).getEncoded(); this.publicKey = ((FalconPublicKeyParameters) kp.getPublic()).getH(); } - public FNDSA(byte[] seed) { + public FNDSA512(byte[] seed) { if (seed == null || seed.length != SEED_LENGTH) { throw new IllegalArgumentException("FN-DSA seed length must be " + SEED_LENGTH); } @@ -73,7 +74,7 @@ public FNDSA(byte[] seed) { this.publicKey = ((FalconPublicKeyParameters) kp.getPublic()).getH(); } - public FNDSA(byte[] privateKey, byte[] publicKey) { + public FNDSA512(byte[] privateKey, byte[] publicKey) { if (privateKey == null || privateKey.length != PRIVATE_KEY_LENGTH) { throw new IllegalArgumentException( "FN-DSA private key length must be " + PRIVATE_KEY_LENGTH); @@ -82,6 +83,7 @@ public FNDSA(byte[] privateKey, byte[] publicKey) { throw new IllegalArgumentException( "FN-DSA public key length must be " + PUBLIC_KEY_LENGTH); } + requireConsistent(privateKey, publicKey); this.privateKey = privateKey.clone(); this.publicKey = publicKey.clone(); } @@ -90,10 +92,10 @@ public FNDSA(byte[] privateKey, byte[] publicKey) { * Builds an instance from the extended private key encoding {@code f ‖ g ‖ F ‖ h} * ({@link #PRIVATE_KEY_WITH_PUBLIC_KEY_LENGTH} bytes), as produced by * {@link #getPrivateKeyWithPublicKey()}. Provided as a static factory rather - * than an additional {@code FNDSA(byte[])} constructor because Java cannot - * overload {@link #FNDSA(byte[]) the seed constructor} on length alone. + * than an additional {@code FNDSA512(byte[])} constructor because Java cannot + * overload {@link #FNDSA512(byte[]) the seed constructor} on length alone. */ - public static FNDSA fromPrivateKeyWithPublicKey(byte[] extendedPrivateKey) { + public static FNDSA512 fromPrivateKeyWithPublicKey(byte[] extendedPrivateKey) { if (extendedPrivateKey == null || extendedPrivateKey.length != PRIVATE_KEY_WITH_PUBLIC_KEY_LENGTH) { throw new IllegalArgumentException( @@ -104,7 +106,7 @@ public static FNDSA fromPrivateKeyWithPublicKey(byte[] extendedPrivateKey) { byte[] pk = new byte[PUBLIC_KEY_LENGTH]; System.arraycopy(extendedPrivateKey, 0, sk, 0, PRIVATE_KEY_LENGTH); System.arraycopy(extendedPrivateKey, PRIVATE_KEY_LENGTH, pk, 0, PUBLIC_KEY_LENGTH); - return new FNDSA(sk, pk); + return new FNDSA512(sk, pk); } @Override @@ -133,6 +135,16 @@ public byte[] getPrivateKey() { return privateKey.clone(); } + /** + * FN-DSA accepts the bare {@link #PRIVATE_KEY_LENGTH} form as well as the + * extended {@link #PRIVATE_KEY_WITH_PUBLIC_KEY_LENGTH} form used for local + * witness config. Override of {@link PQSignature#validatePrivateKey}. + */ + @Override + public void validatePrivateKey(byte[] privateKey) { + validatePrivateKeyBytes(privateKey); + } + /** * Returns the private key with the 896-byte public key {@code h} appended: * {@code f ‖ g ‖ F ‖ h} (total {@link #PRIVATE_KEY_WITH_PUBLIC_KEY_LENGTH} bytes). @@ -247,6 +259,33 @@ private static AsymmetricCipherKeyPair generateKeyPair(SecureRandom random) { return generator.generateKeyPair(); } + /** + * Domain-separated probe used by {@link #requireConsistent}; not a security + * boundary (Falcon hashes the message internally), the constant just makes the + * keypair self-check searchable in logs/stack traces. + */ + private static final byte[] CONSISTENCY_PROBE = + "tron:FN-DSA-512:keypair-consistency-probe".getBytes(StandardCharsets.UTF_8); + + /** + * Probe that the supplied (sk, pk) actually form a keypair. Falcon has no + * public API to derive {@code h} from {@code (f, g)} alone (bcgit/bc-java#2297), + * so we sign and verify a fixed probe message. Runs once per witness load and + * costs a few ms on Falcon-512 — acceptable for a startup-time misconfiguration + * check, and avoids advertising an address that signatures will never satisfy. + */ + private static void requireConsistent(byte[] privateKey, byte[] publicKey) { + byte[] sig; + try { + sig = sign(privateKey, CONSISTENCY_PROBE); + } catch (RuntimeException e) { + throw new IllegalArgumentException("FN-DSA private/public key mismatch", e); + } + if (!verify(publicKey, CONSISTENCY_PROBE, sig)) { + throw new IllegalArgumentException("FN-DSA private/public key mismatch"); + } + } + private static void validatePrivateKeyBytes(byte[] privateKey) { if (privateKey == null || (privateKey.length != PRIVATE_KEY_LENGTH diff --git a/crypto/src/main/java/org/tron/common/crypto/pqc/PQSchemeRegistry.java b/crypto/src/main/java/org/tron/common/crypto/pqc/PQSchemeRegistry.java index b3965ac4cb8..c57d7e702f2 100644 --- a/crypto/src/main/java/org/tron/common/crypto/pqc/PQSchemeRegistry.java +++ b/crypto/src/main/java/org/tron/common/crypto/pqc/PQSchemeRegistry.java @@ -75,28 +75,28 @@ private static final class SchemeInfo { static { EnumMap m = new EnumMap<>(PQScheme.class); m.put(PQScheme.FN_DSA_512, new SchemeInfo( - FNDSA.PRIVATE_KEY_LENGTH, FNDSA.PUBLIC_KEY_LENGTH, - FNDSA.SIGNATURE_LENGTH, FNDSA.SEED_LENGTH, + FNDSA512.PRIVATE_KEY_LENGTH, FNDSA512.PUBLIC_KEY_LENGTH, + FNDSA512.SIGNATURE_LENGTH, FNDSA512.SEED_LENGTH, KECCAK_256, new SignatureOps() { @Override public byte[] sign(byte[] privateKey, byte[] message) { - return FNDSA.sign(privateKey, message); + return FNDSA512.sign(privateKey, message); } @Override public boolean verify(byte[] publicKey, byte[] message, byte[] signature) { - return FNDSA.verify(publicKey, message, signature); + return FNDSA512.verify(publicKey, message, signature); } @Override public PQSignature fromSeed(byte[] seed) { - return new FNDSA(seed); + return new FNDSA512(seed); } @Override public PQSignature fromKeypair(byte[] privateKey, byte[] publicKey) { - return new FNDSA(privateKey, publicKey); + return new FNDSA512(privateKey, publicKey); } })); SCHEMES = Collections.unmodifiableMap(m); diff --git a/framework/src/main/java/org/tron/core/config/args/Args.java b/framework/src/main/java/org/tron/core/config/args/Args.java index de874f90dde..a4b6e9a4998 100644 --- a/framework/src/main/java/org/tron/core/config/args/Args.java +++ b/framework/src/main/java/org/tron/core/config/args/Args.java @@ -935,24 +935,35 @@ private static void initLocalWitnesses(Config config, CLIParameter cmd) { return; } + LocalWitnessConfig lwConfig = LocalWitnessConfig.fromConfig(config); + boolean hasCliPriv = StringUtils.isNotBlank(cmd.privateKey); + boolean hasCfgPriv = !lwConfig.getPrivateKeys().isEmpty(); + boolean hasKeystore = !lwConfig.getKeystores().isEmpty(); + boolean hasPqKeys = config.hasPath(ConfigKey.LOCAL_WITNESS_PQ_KEYS) + && !config.getStringList(ConfigKey.LOCAL_WITNESS_PQ_KEYS).isEmpty(); + if (hasPqKeys && (hasCliPriv || hasCfgPriv || hasKeystore)) { + throw new TronError( + "legacy witness keys (CLI --private-key, localwitness, localwitnesskeystore) " + + "and " + ConfigKey.LOCAL_WITNESS_PQ_KEYS + " are mutually exclusive", + TronError.ErrCode.WITNESS_INIT); + } + // path 1: CLI --private-key - if (StringUtils.isNotBlank(cmd.privateKey)) { + if (hasCliPriv) { localWitnesses = WitnessInitializer.initFromCLIPrivateKey( cmd.privateKey, cmd.witnessAddress); return; } - LocalWitnessConfig lwConfig = LocalWitnessConfig.fromConfig(config); - // path 2: config localwitness (private key list) - if (!lwConfig.getPrivateKeys().isEmpty()) { + if (hasCfgPriv) { localWitnesses = WitnessInitializer.initFromCFGPrivateKey( lwConfig.getPrivateKeys(), lwConfig.getAccountAddress()); return; } // path 3: config localwitnesskeystore + password - if (!lwConfig.getKeystores().isEmpty()) { + if (hasKeystore) { localWitnesses = WitnessInitializer.initFromKeystore( lwConfig.getKeystores(), cmd.password, lwConfig.getAccountAddress()); return; @@ -988,7 +999,10 @@ private static void initLocalWitnesses(Config config, CLIParameter cmd) { List pqPublicKeys = new ArrayList<>(pqEntries.size()); for (int i = 0; i < pqEntries.size(); i++) { String hex = pqEntries.get(i); - String stripped = hex != null && hex.startsWith("0x") ? hex.substring(2) : hex; + String stripped = hex; + if (hex != null && (hex.startsWith("0x") || hex.startsWith("0X"))) { + stripped = hex.substring(2); + } if (stripped == null || stripped.length() != extHexLen) { throw new TronError(String.format( "%s[%d] must be %d hex chars (extended priv‖pub for %s), actual: %d", diff --git a/framework/src/main/java/org/tron/core/config/args/WitnessInitializer.java b/framework/src/main/java/org/tron/core/config/args/WitnessInitializer.java index 9251f2151c7..52819df0843 100644 --- a/framework/src/main/java/org/tron/core/config/args/WitnessInitializer.java +++ b/framework/src/main/java/org/tron/core/config/args/WitnessInitializer.java @@ -128,6 +128,16 @@ public static LocalWitnesses initFromPQOnly(PQScheme scheme, "PQ public keys must be set for PQ-only witness nodes", TronError.ErrCode.WITNESS_INIT); } + if (pqPrivateKeys == null || pqPrivateKeys.isEmpty()) { + throw new TronError( + "PQ private keys must be set for PQ-only witness nodes", + TronError.ErrCode.WITNESS_INIT); + } + if (pqPrivateKeys.size() != pqPublicKeys.size()) { + throw new TronError( + "PQ private/public key count mismatch", + TronError.ErrCode.WITNESS_INIT); + } LocalWitnesses witnesses = new LocalWitnesses(); witnesses.setPqScheme(scheme); witnesses.setPqKeypairs(pqPrivateKeys, pqPublicKeys); @@ -138,6 +148,11 @@ public static LocalWitnesses initFromPQOnly(PQScheme scheme, address = PQSchemeRegistry.computeAddress(scheme, firstPubKey); logger.debug("Derived PQ-only witness address from public key"); } else { + if (pqPublicKeys.size() != 1) { + throw new TronError( + "LocalWitnessAccountAddress can only be set when there is only one PQ keypair", + TronError.ErrCode.WITNESS_INIT); + } address = Commons.decodeFromBase58Check(witnessAccountAddress); if (address == null) { throw new TronError( @@ -146,7 +161,7 @@ public static LocalWitnesses initFromPQOnly(PQScheme scheme, } logger.debug("Got localWitnessAccountAddress from config.conf"); } - witnesses.initWitnessAccountAddress(address, false); + witnesses.setWitnessAccountAddress(address); return witnesses; } diff --git a/framework/src/main/java/org/tron/core/consensus/ConsensusService.java b/framework/src/main/java/org/tron/core/consensus/ConsensusService.java index 5f54b62b955..23cbe28e3e3 100644 --- a/framework/src/main/java/org/tron/core/consensus/ConsensusService.java +++ b/framework/src/main/java/org/tron/core/consensus/ConsensusService.java @@ -58,6 +58,11 @@ public void start() { + " private vs " + pqPublicKeys.size() + " public", TronError.ErrCode.WITNESS_INIT); } + if (!privateKeys.isEmpty() && !pqPrivateKeys.isEmpty()) { + throw new TronError( + "legacy localwitness keys and localwitness_pq_keys are mutually exclusive", + TronError.ErrCode.WITNESS_INIT); + } if (privateKeys.size() > 1) { for (String key : privateKeys) { byte[] privateKey = fromHexString(key); @@ -107,6 +112,7 @@ public void start() { miner.setPQPrivateKey(sk); miner.setPQPublicKey(pk); miner.setPqScheme(scheme); + miner.setType(Param.MinerType.PQ); miners.add(miner); logger.info("Add {} witness (from configured keypair): {}, size: {}", scheme, Hex.toHexString(pqAddress), miners.size()); @@ -146,6 +152,7 @@ private Miner buildPQOnlyMinerFromKeypair(Param param, String pqPrivateKey, miner.setPQPrivateKey(sk); miner.setPQPublicKey(pk); miner.setPqScheme(scheme); + miner.setType(Param.MinerType.PQ); logger.info("Add {} witness (from configured keypair): {}", scheme, Hex.toHexString(witnessAddress)); return miner; 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 d6bdffb62ec..7dd5a9b7a21 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -1760,11 +1760,29 @@ public BlockCapsule generateBlock(Miner miner, long blockTime, long timeout) { } private void signBlockCapsule(BlockCapsule blockCapsule, Miner miner) { - PQScheme scheme = resolveWitnessScheme(miner); - if (scheme != null && PQSchemeRegistry.contains(scheme)) { - signWitnessAuth(blockCapsule, miner, scheme); - } else { - blockCapsule.sign(miner.getPrivateKey()); + switch (miner.getType()) { + case PQ: + PQScheme scheme = resolveWitnessScheme(miner); + if (scheme == null) { + // PQ-only miner whose configured scheme is not currently usable + // (proposal not activated, scheme allow flag flipped, witness + // permission missing, etc.). Surface a clear cause; DposTask's + // Throwable handler will log and the witness will miss this slot, + // but the producer thread keeps running. + throw new IllegalStateException( + "PQ-only miner " + Hex.toHexString(miner.getWitnessAddress().toByteArray()) + + " has scheme " + miner.getPqScheme() + + " configured but it is not currently usable " + + "(scheme not allowed by dynamic properties, " + + "or witness permission is missing/empty)"); + } + signWitnessAuth(blockCapsule, miner, scheme); + break; + case ECDSA: + blockCapsule.sign(miner.getPrivateKey()); + break; + default: + throw new IllegalStateException("unknown miner type: " + miner.getType()); } } @@ -1796,8 +1814,9 @@ private void signWitnessAuth(BlockCapsule blockCapsule, Miner miner, PQScheme sc byte[] pqPublicKey = miner.getPQPublicKey(); if (pqPrivateKey == null || pqPublicKey == null) { throw new IllegalStateException( - "witness permission requires " + scheme - + " but local PQ key material is not configured"); + "miner " + Hex.toHexString(miner.getWitnessAddress().toByteArray()) + + " has scheme " + scheme + + " set but local PQ key material is missing"); } byte[] digest = blockCapsule.getRawHashBytes(); byte[] signature = PQSchemeRegistry.sign(scheme, pqPrivateKey, digest); diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf index 91eb905dbf4..a0ed0877b74 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -818,7 +818,7 @@ committee = { # consensusLogicOptimization = 0 # allowOptimizedReturnValueOfChainId = 0 # allowTvmOsaka = 0 - # allowMlDsa = 0 + # allowFnDsa512 = 0 } event.subscribe = { diff --git a/framework/src/test/java/org/tron/common/crypto/pqc/FNDSAKatTest.java b/framework/src/test/java/org/tron/common/crypto/pqc/FNDSA512KatTest.java similarity index 88% rename from framework/src/test/java/org/tron/common/crypto/pqc/FNDSAKatTest.java rename to framework/src/test/java/org/tron/common/crypto/pqc/FNDSA512KatTest.java index 1c6656d8f38..14b5f6eb60a 100644 --- a/framework/src/test/java/org/tron/common/crypto/pqc/FNDSAKatTest.java +++ b/framework/src/test/java/org/tron/common/crypto/pqc/FNDSA512KatTest.java @@ -29,7 +29,7 @@ * verify is exercised per-vector and cross-vector to confirm signatures only * verify under their own key. */ -public class FNDSAKatTest { +public class FNDSA512KatTest { private static final class KatVector { final String label; @@ -46,7 +46,7 @@ private static final class KatVector { } private static byte[] seedIncrementing() { - byte[] s = new byte[FNDSA.SEED_LENGTH]; + byte[] s = new byte[FNDSA512.SEED_LENGTH]; for (int i = 0; i < s.length; i++) { s[i] = (byte) i; } @@ -54,15 +54,15 @@ private static byte[] seedIncrementing() { } private static byte[] seedDescending() { - byte[] s = new byte[FNDSA.SEED_LENGTH]; + byte[] s = new byte[FNDSA512.SEED_LENGTH]; for (int i = 0; i < s.length; i++) { - s[i] = (byte) (FNDSA.SEED_LENGTH - 1 - i); + s[i] = (byte) (FNDSA512.SEED_LENGTH - 1 - i); } return s; } private static byte[] seedFilled(int b) { - byte[] s = new byte[FNDSA.SEED_LENGTH]; + byte[] s = new byte[FNDSA512.SEED_LENGTH]; Arrays.fill(s, (byte) b); return s; } @@ -104,11 +104,11 @@ private static String hex(byte[] b) { @Test public void allVectorsDeriveExpectedPublicAndPrivateKey() { for (KatVector v : VECTORS) { - FNDSA k = new FNDSA(v.seed); + FNDSA512 k = new FNDSA512(v.seed); assertEquals(v.label + ": pk length", - FNDSA.PUBLIC_KEY_LENGTH, k.getPublicKey().length); + FNDSA512.PUBLIC_KEY_LENGTH, k.getPublicKey().length); assertEquals(v.label + ": sk length", - FNDSA.PRIVATE_KEY_LENGTH, k.getPrivateKey().length); + FNDSA512.PRIVATE_KEY_LENGTH, k.getPrivateKey().length); assertEquals(v.label + ": pk SHA-256 must match KAT vector", v.pkSha256, hex(sha256(k.getPublicKey()))); assertEquals(v.label + ": sk SHA-256 must match KAT vector", @@ -119,7 +119,7 @@ public void allVectorsDeriveExpectedPublicAndPrivateKey() { @Test public void allVectorsDeriveExpectedAddress() { for (KatVector v : VECTORS) { - FNDSA k = new FNDSA(v.seed); + FNDSA512 k = new FNDSA512(v.seed); byte[] addr = k.getAddress(); assertEquals(v.label + ": address length", 21, addr.length); @@ -134,7 +134,7 @@ public void allVectorsDeriveExpectedAddress() { @Test public void addressIsExactly0x41PlusKeccak256RightmostBytesOfPublicKey() { for (KatVector v : VECTORS) { - FNDSA k = new FNDSA(v.seed); + FNDSA512 k = new FNDSA512(v.seed); byte[] pk = k.getPublicKey(); byte[] hash = Hash.sha3(pk); byte[] expected = new byte[21]; @@ -148,8 +148,8 @@ public void addressIsExactly0x41PlusKeccak256RightmostBytesOfPublicKey() { @Test public void allVectorsAreReproducibleAcrossInstances() { for (KatVector v : VECTORS) { - FNDSA a = new FNDSA(v.seed); - FNDSA b = new FNDSA(v.seed); + FNDSA512 a = new FNDSA512(v.seed); + FNDSA512 b = new FNDSA512(v.seed); assertArrayEquals(v.label + ": pk reproducible", a.getPublicKey(), b.getPublicKey()); assertArrayEquals(v.label + ": sk reproducible", a.getPrivateKey(), b.getPrivateKey()); assertArrayEquals(v.label + ": addr reproducible", a.getAddress(), b.getAddress()); @@ -164,7 +164,7 @@ public void distinctSeedsProduceDistinctKeysAndAddresses() { for (KatVector v : VECTORS) { pkDigests.add(v.pkSha256); skDigests.add(v.skSha256); - addresses.add(hex(new FNDSA(v.seed).getAddress())); + addresses.add(hex(new FNDSA512(v.seed).getAddress())); } assertEquals("KAT pk digests must be pairwise distinct", VECTORS.length, pkDigests.size()); @@ -183,15 +183,15 @@ public void signaturesFromKatKeysVerifyUnderTheirOwnPublicKey() { new byte[1024], }; for (KatVector v : VECTORS) { - FNDSA k = new FNDSA(v.seed); + FNDSA512 k = new FNDSA512(v.seed); for (byte[] msg : messages) { byte[] sig = k.sign(msg); assertTrue(v.label + ": signature must be non-empty", sig.length > 0); assertTrue(v.label + ": signature must respect 752-byte upper bound", - sig.length <= FNDSA.SIGNATURE_LENGTH); + sig.length <= FNDSA512.SIGNATURE_LENGTH); assertTrue(v.label + ": signature must verify under its own pk", - FNDSA.verify(k.getPublicKey(), msg, sig)); + FNDSA512.verify(k.getPublicKey(), msg, sig)); assertTrue(v.label + ": registry verify must accept own signature", PQSchemeRegistry.verify( PQScheme.FN_DSA_512, k.getPublicKey(), msg, sig)); @@ -202,21 +202,21 @@ public void signaturesFromKatKeysVerifyUnderTheirOwnPublicKey() { @Test public void signatureFromVectorAFailsUnderVectorBPublicKey() { byte[] msg = "tron-fn-dsa-kat-cross".getBytes(); - FNDSA[] keys = new FNDSA[VECTORS.length]; + FNDSA512[] keys = new FNDSA512[VECTORS.length]; byte[][] sigs = new byte[VECTORS.length][]; for (int i = 0; i < VECTORS.length; i++) { - keys[i] = new FNDSA(VECTORS[i].seed); + keys[i] = new FNDSA512(VECTORS[i].seed); sigs[i] = keys[i].sign(msg); } for (int i = 0; i < VECTORS.length; i++) { for (int j = 0; j < VECTORS.length; j++) { if (i == j) { assertTrue(VECTORS[i].label + ": self-verify must succeed", - FNDSA.verify(keys[i].getPublicKey(), msg, sigs[i])); + FNDSA512.verify(keys[i].getPublicKey(), msg, sigs[i])); } else { assertFalse("signature from " + VECTORS[i].label + " must NOT verify under " + VECTORS[j].label, - FNDSA.verify(keys[j].getPublicKey(), msg, sigs[i])); + FNDSA512.verify(keys[j].getPublicKey(), msg, sigs[i])); } } } @@ -228,7 +228,7 @@ public void distinctSeedsAtRuntimeAlsoProduceDistinctRuntimePublicKeys() { // Re-derive at runtime and confirm they're still pairwise distinct. byte[][] pks = new byte[VECTORS.length][]; for (int i = 0; i < VECTORS.length; i++) { - pks[i] = new FNDSA(VECTORS[i].seed).getPublicKey(); + pks[i] = new FNDSA512(VECTORS[i].seed).getPublicKey(); } for (int i = 0; i < VECTORS.length; i++) { for (int j = i + 1; j < VECTORS.length; j++) { diff --git a/framework/src/test/java/org/tron/common/crypto/pqc/FNDSATest.java b/framework/src/test/java/org/tron/common/crypto/pqc/FNDSA512Test.java similarity index 76% rename from framework/src/test/java/org/tron/common/crypto/pqc/FNDSATest.java rename to framework/src/test/java/org/tron/common/crypto/pqc/FNDSA512Test.java index 6298b1d251b..fce3565f2ca 100644 --- a/framework/src/test/java/org/tron/common/crypto/pqc/FNDSATest.java +++ b/framework/src/test/java/org/tron/common/crypto/pqc/FNDSA512Test.java @@ -19,11 +19,11 @@ import org.junit.Test; import org.tron.protos.Protocol.PQScheme; -public class FNDSATest { +public class FNDSA512Test { private static final FalconParameters PARAMS = FalconParameters.falcon_512; - private FNDSA keypair; + private FNDSA512 keypair; private FalconPublicKeyParameters pk; private FalconPrivateKeyParameters sk; @@ -32,7 +32,7 @@ public void setUp() { AsymmetricCipherKeyPair kp = freshKeyPair(); pk = (FalconPublicKeyParameters) kp.getPublic(); sk = (FalconPrivateKeyParameters) kp.getPrivate(); - keypair = new FNDSA(sk.getEncoded(), pk.getH()); + keypair = new FNDSA512(sk.getEncoded(), pk.getH()); } private static AsymmetricCipherKeyPair freshKeyPair() { @@ -54,10 +54,10 @@ private byte[] rawSign(byte[] message) { @Test public void schemeAndLengthsMatchFips206Draft() { assertEquals(PQScheme.FN_DSA_512, keypair.getScheme()); - assertEquals(FNDSA.PUBLIC_KEY_LENGTH, keypair.getPublicKeyLength()); - assertEquals(FNDSA.SIGNATURE_LENGTH, keypair.getSignatureLength()); - assertEquals(FNDSA.PRIVATE_KEY_LENGTH, keypair.getPrivateKeyLength()); - assertEquals(FNDSA.PUBLIC_KEY_LENGTH, pk.getH().length); + assertEquals(FNDSA512.PUBLIC_KEY_LENGTH, keypair.getPublicKeyLength()); + assertEquals(FNDSA512.SIGNATURE_LENGTH, keypair.getSignatureLength()); + assertEquals(FNDSA512.PRIVATE_KEY_LENGTH, keypair.getPrivateKeyLength()); + assertEquals(FNDSA512.PUBLIC_KEY_LENGTH, pk.getH().length); } @Test @@ -65,7 +65,7 @@ public void publicKeyHasFixedLength() { for (int i = 0; i < 4; i++) { AsymmetricCipherKeyPair kp = freshKeyPair(); byte[] pkBytes = ((FalconPublicKeyParameters) kp.getPublic()).getH(); - assertEquals(FNDSA.PUBLIC_KEY_LENGTH, pkBytes.length); + assertEquals(FNDSA512.PUBLIC_KEY_LENGTH, pkBytes.length); } } @@ -74,30 +74,30 @@ public void privateKeyEncodingHasFixedLength() { for (int i = 0; i < 4; i++) { AsymmetricCipherKeyPair kp = freshKeyPair(); byte[] skBytes = ((FalconPrivateKeyParameters) kp.getPrivate()).getEncoded(); - assertEquals(FNDSA.PRIVATE_KEY_LENGTH, skBytes.length); + assertEquals(FNDSA512.PRIVATE_KEY_LENGTH, skBytes.length); } } @Test public void signProducesVerifiableSignatureWithinBound() { byte[] msg = "hello, fn-dsa".getBytes(); - byte[] sig = FNDSA.sign(sk.getEncoded(), msg); + byte[] sig = FNDSA512.sign(sk.getEncoded(), msg); assertTrue("signature must be non-empty", sig.length > 0); assertTrue( "signature must respect protocol-level upper bound", - sig.length <= FNDSA.SIGNATURE_LENGTH); - assertTrue(FNDSA.verify(pk.getH(), msg, sig)); + sig.length <= FNDSA512.SIGNATURE_LENGTH); + assertTrue(FNDSA512.verify(pk.getH(), msg, sig)); } @Test public void signatureBoundaryAtMaxAcceptedByLengthCheck() { - byte[] sig = new byte[FNDSA.SIGNATURE_LENGTH]; + byte[] sig = new byte[FNDSA512.SIGNATURE_LENGTH]; keypair.validateSignature(sig); } @Test public void signatureBoundaryAboveMaxRejected() { - byte[] sig = new byte[FNDSA.SIGNATURE_LENGTH + 1]; + byte[] sig = new byte[FNDSA512.SIGNATURE_LENGTH + 1]; try { keypair.validateSignature(sig); fail("signature longer than upper bound should be rejected"); @@ -126,9 +126,9 @@ public void emptySignatureRejectedByLengthCheck() { @Test public void verifyRejectsSignatureLongerThanUpperBound() { byte[] msg = new byte[] {1, 2, 3}; - byte[] tooLong = new byte[FNDSA.SIGNATURE_LENGTH + 1]; + byte[] tooLong = new byte[FNDSA512.SIGNATURE_LENGTH + 1]; try { - FNDSA.verify(pk.getH(), msg, tooLong); + FNDSA512.verify(pk.getH(), msg, tooLong); fail("signature exceeding upper bound should be rejected at static verify"); } catch (IllegalArgumentException expected) { assertTrue(expected.getMessage().contains("signature length")); @@ -140,7 +140,7 @@ public void verifyRejectsEmptySignature() { byte[] msg = new byte[] {1, 2, 3}; byte[] empty = new byte[0]; try { - FNDSA.verify(pk.getH(), msg, empty); + FNDSA512.verify(pk.getH(), msg, empty); fail("empty signature should be rejected at static verify"); } catch (IllegalArgumentException expected) { assertTrue(expected.getMessage().contains("signature length")); @@ -149,11 +149,11 @@ public void verifyRejectsEmptySignature() { @Test public void invalidPublicKeyLengthRejected() { - byte[] badPk = new byte[FNDSA.PUBLIC_KEY_LENGTH - 1]; + byte[] badPk = new byte[FNDSA512.PUBLIC_KEY_LENGTH - 1]; byte[] msg = new byte[] {1}; byte[] sig = new byte[16]; try { - FNDSA.verify(badPk, msg, sig); + FNDSA512.verify(badPk, msg, sig); fail("short public key should be rejected"); } catch (IllegalArgumentException expected) { assertTrue(expected.getMessage().contains("public key length")); @@ -164,7 +164,7 @@ public void invalidPublicKeyLengthRejected() { public void nullMessageRejected() { byte[] sig = new byte[16]; try { - FNDSA.verify(pk.getH(), null, sig); + FNDSA512.verify(pk.getH(), null, sig); fail("null message should be rejected"); } catch (IllegalArgumentException expected) { assertTrue(expected.getMessage().contains("message")); @@ -193,7 +193,7 @@ public void wrongPublicKeyFailsVerification() { byte[] sig = rawSign(msg); AsymmetricCipherKeyPair other = freshKeyPair(); byte[] otherPk = ((FalconPublicKeyParameters) other.getPublic()).getH(); - assertFalse(FNDSA.verify(otherPk, msg, sig)); + assertFalse(FNDSA512.verify(otherPk, msg, sig)); } @Test @@ -205,7 +205,7 @@ public void crossAlgoSignatureRejected() { for (int len : foreignLengths) { byte[] foreign = new byte[len]; try { - FNDSA.verify(pk.getH(), msg, foreign); + FNDSA512.verify(pk.getH(), msg, foreign); fail("foreign-scheme signature length " + len + " should be rejected for FN-DSA"); } catch (IllegalArgumentException expected) { assertTrue(expected.getMessage().contains("signature length")); @@ -222,52 +222,52 @@ public void emptyMessageVerifiesConsistently() { @Test public void keypairBoundInstanceSignsAndVerifies() { - FNDSA signer = new FNDSA(); + FNDSA512 signer = new FNDSA512(); byte[] msg = "keypair-bound".getBytes(); byte[] sig = signer.sign(msg); - assertTrue(sig.length > 0 && sig.length <= FNDSA.SIGNATURE_LENGTH); + assertTrue(sig.length > 0 && sig.length <= FNDSA512.SIGNATURE_LENGTH); assertTrue(signer.verify(msg, sig)); } @Test public void fromSeedIsDeterministic() { - byte[] seed = new byte[FNDSA.SEED_LENGTH]; + byte[] seed = new byte[FNDSA512.SEED_LENGTH]; for (int i = 0; i < seed.length; i++) { seed[i] = (byte) i; } - FNDSA a = new FNDSA(seed); - FNDSA b = new FNDSA(seed); + FNDSA512 a = new FNDSA512(seed); + FNDSA512 b = new FNDSA512(seed); assertArrayEquals(a.getPublicKey(), b.getPublicKey()); assertArrayEquals(a.getPrivateKey(), b.getPrivateKey()); } @Test(expected = IllegalArgumentException.class) public void invalidSeedLengthRejected() { - new FNDSA(new byte[FNDSA.SEED_LENGTH - 1]); + new FNDSA512(new byte[FNDSA512.SEED_LENGTH - 1]); } @Test(expected = UnsupportedOperationException.class) public void derivePublicKeyFromEncodedPrivateKeyUnsupported() { - FNDSA.derivePublicKey(sk.getEncoded()); + FNDSA512.derivePublicKey(sk.getEncoded()); } @Test public void computeAddressIs21Bytes() { - assertEquals(21, FNDSA.computeAddress(pk.getH()).length); + assertEquals(21, FNDSA512.computeAddress(pk.getH()).length); } @Test public void registryDispatchMatchesDirectCalls() { byte[] msg = "registry-dispatch".getBytes(); - byte[] sigDirect = FNDSA.sign(sk.getEncoded(), msg); + byte[] sigDirect = FNDSA512.sign(sk.getEncoded(), msg); assertTrue(PQSchemeRegistry.verify( PQScheme.FN_DSA_512, pk.getH(), msg, sigDirect)); byte[] sigViaRegistry = PQSchemeRegistry.sign( PQScheme.FN_DSA_512, sk.getEncoded(), msg); - assertTrue(FNDSA.verify(pk.getH(), msg, sigViaRegistry)); - assertEquals(FNDSA.PUBLIC_KEY_LENGTH, + assertTrue(FNDSA512.verify(pk.getH(), msg, sigViaRegistry)); + assertEquals(FNDSA512.PUBLIC_KEY_LENGTH, PQSchemeRegistry.getPublicKeyLength(PQScheme.FN_DSA_512)); - assertEquals(FNDSA.SIGNATURE_LENGTH, + assertEquals(FNDSA512.SIGNATURE_LENGTH, PQSchemeRegistry.getSignatureLength(PQScheme.FN_DSA_512)); } @@ -275,28 +275,28 @@ public void registryDispatchMatchesDirectCalls() { public void registryIsValidSignatureLengthRespectsUpperBound() { assertTrue(PQSchemeRegistry.isValidSignatureLength(PQScheme.FN_DSA_512, 1)); assertTrue(PQSchemeRegistry.isValidSignatureLength( - PQScheme.FN_DSA_512, FNDSA.SIGNATURE_LENGTH)); + PQScheme.FN_DSA_512, FNDSA512.SIGNATURE_LENGTH)); assertFalse(PQSchemeRegistry.isValidSignatureLength(PQScheme.FN_DSA_512, 0)); assertFalse(PQSchemeRegistry.isValidSignatureLength( - PQScheme.FN_DSA_512, FNDSA.SIGNATURE_LENGTH + 1)); + PQScheme.FN_DSA_512, FNDSA512.SIGNATURE_LENGTH + 1)); } @Test public void registryComputeAddressMatchesDirect() { assertArrayEquals( - FNDSA.computeAddress(pk.getH()), + FNDSA512.computeAddress(pk.getH()), PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, pk.getH())); } @Test(expected = IllegalArgumentException.class) public void seedConstructorRejectsNull() { - new FNDSA((byte[]) null); + new FNDSA512((byte[]) null); } @Test public void keypairConstructorRejectsNullPrivateKey() { try { - new FNDSA(null, pk.getH()); + new FNDSA512(null, pk.getH()); fail("null private key must be rejected"); } catch (IllegalArgumentException expected) { assertTrue(expected.getMessage().contains("private key length")); @@ -306,7 +306,7 @@ public void keypairConstructorRejectsNullPrivateKey() { @Test public void keypairConstructorRejectsWrongPrivateKeyLength() { try { - new FNDSA(new byte[FNDSA.PRIVATE_KEY_LENGTH - 1], pk.getH()); + new FNDSA512(new byte[FNDSA512.PRIVATE_KEY_LENGTH - 1], pk.getH()); fail("short private key must be rejected"); } catch (IllegalArgumentException expected) { assertTrue(expected.getMessage().contains("private key length")); @@ -316,7 +316,7 @@ public void keypairConstructorRejectsWrongPrivateKeyLength() { @Test public void keypairConstructorRejectsNullPublicKey() { try { - new FNDSA(sk.getEncoded(), null); + new FNDSA512(sk.getEncoded(), null); fail("null public key must be rejected"); } catch (IllegalArgumentException expected) { assertTrue(expected.getMessage().contains("public key length")); @@ -326,18 +326,30 @@ public void keypairConstructorRejectsNullPublicKey() { @Test public void keypairConstructorRejectsWrongPublicKeyLength() { try { - new FNDSA(sk.getEncoded(), new byte[FNDSA.PUBLIC_KEY_LENGTH + 1]); + new FNDSA512(sk.getEncoded(), new byte[FNDSA512.PUBLIC_KEY_LENGTH + 1]); fail("over-long public key must be rejected"); } catch (IllegalArgumentException expected) { assertTrue(expected.getMessage().contains("public key length")); } } + @Test + public void keypairConstructorRejectsMismatchedHalves() { + FalconPublicKeyParameters strangerPk = + (FalconPublicKeyParameters) freshKeyPair().getPublic(); + try { + new FNDSA512(sk.getEncoded(), strangerPk.getH()); + fail("mismatched private/public key pair must be rejected"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("mismatch")); + } + } + @Test public void extendedPrivateKeyRoundTripsThroughFromAndGetters() { byte[] extended = keypair.getPrivateKeyWithPublicKey(); - assertEquals(FNDSA.PRIVATE_KEY_WITH_PUBLIC_KEY_LENGTH, extended.length); - FNDSA restored = FNDSA.fromPrivateKeyWithPublicKey(extended); + assertEquals(FNDSA512.PRIVATE_KEY_WITH_PUBLIC_KEY_LENGTH, extended.length); + FNDSA512 restored = FNDSA512.fromPrivateKeyWithPublicKey(extended); assertArrayEquals(keypair.getPrivateKey(), restored.getPrivateKey()); assertArrayEquals(keypair.getPublicKey(), restored.getPublicKey()); // The recovered keypair must produce verifiable signatures and recover its address. @@ -349,39 +361,39 @@ public void extendedPrivateKeyRoundTripsThroughFromAndGetters() { @Test(expected = IllegalArgumentException.class) public void fromExtendedPrivateKeyRejectsNull() { - FNDSA.fromPrivateKeyWithPublicKey(null); + FNDSA512.fromPrivateKeyWithPublicKey(null); } @Test(expected = IllegalArgumentException.class) public void fromExtendedPrivateKeyRejectsWrongLength() { - FNDSA.fromPrivateKeyWithPublicKey(new byte[FNDSA.PRIVATE_KEY_LENGTH]); + FNDSA512.fromPrivateKeyWithPublicKey(new byte[FNDSA512.PRIVATE_KEY_LENGTH]); } @Test public void derivePublicKeyFromExtendedFormReturnsAppendedPublicKey() { byte[] extended = keypair.getPrivateKeyWithPublicKey(); - byte[] derived = FNDSA.derivePublicKey(extended); + byte[] derived = FNDSA512.derivePublicKey(extended); assertArrayEquals(keypair.getPublicKey(), derived); } @Test(expected = UnsupportedOperationException.class) public void derivePublicKeyRejectsNull() { - FNDSA.derivePublicKey(null); + FNDSA512.derivePublicKey(null); } @Test public void staticSignAcceptsExtendedPrivateKey() { byte[] extended = keypair.getPrivateKeyWithPublicKey(); byte[] msg = "static-sign-extended".getBytes(); - byte[] sig = FNDSA.sign(extended, msg); - assertTrue(sig.length > 0 && sig.length <= FNDSA.SIGNATURE_LENGTH); - assertTrue(FNDSA.verify(pk.getH(), msg, sig)); + byte[] sig = FNDSA512.sign(extended, msg); + assertTrue(sig.length > 0 && sig.length <= FNDSA512.SIGNATURE_LENGTH); + assertTrue(FNDSA512.verify(pk.getH(), msg, sig)); } @Test public void staticSignRejectsNullPrivateKey() { try { - FNDSA.sign(null, new byte[] {1}); + FNDSA512.sign(null, new byte[] {1}); fail("null private key must be rejected"); } catch (IllegalArgumentException expected) { assertTrue(expected.getMessage().contains("private key length")); @@ -391,7 +403,7 @@ public void staticSignRejectsNullPrivateKey() { @Test public void staticSignRejectsWrongPrivateKeyLength() { try { - FNDSA.sign(new byte[FNDSA.PRIVATE_KEY_LENGTH - 1], new byte[] {1}); + FNDSA512.sign(new byte[FNDSA512.PRIVATE_KEY_LENGTH - 1], new byte[] {1}); fail("short private key must be rejected"); } catch (IllegalArgumentException expected) { assertTrue(expected.getMessage().contains("private key length")); @@ -401,7 +413,7 @@ public void staticSignRejectsWrongPrivateKeyLength() { @Test public void staticSignRejectsNullMessage() { try { - FNDSA.sign(sk.getEncoded(), null); + FNDSA512.sign(sk.getEncoded(), null); fail("null message must be rejected"); } catch (IllegalArgumentException expected) { assertTrue(expected.getMessage().contains("message")); @@ -411,7 +423,7 @@ public void staticSignRejectsNullMessage() { @Test public void staticVerifyRejectsNullPublicKey() { try { - FNDSA.verify(null, new byte[] {1}, new byte[16]); + FNDSA512.verify(null, new byte[] {1}, new byte[16]); fail("null public key must be rejected"); } catch (IllegalArgumentException expected) { assertTrue(expected.getMessage().contains("public key length")); @@ -423,14 +435,14 @@ public void unknownPqSchemeResolvesToFnDsa512() { assertEquals(PQScheme.FN_DSA_512, PQSchemeRegistry.resolve(PQScheme.UNKNOWN_PQ_SCHEME)); assertTrue(PQSchemeRegistry.contains(PQScheme.UNKNOWN_PQ_SCHEME)); - assertEquals(FNDSA.PUBLIC_KEY_LENGTH, + assertEquals(FNDSA512.PUBLIC_KEY_LENGTH, PQSchemeRegistry.getPublicKeyLength(PQScheme.UNKNOWN_PQ_SCHEME)); - assertEquals(FNDSA.SIGNATURE_LENGTH, + assertEquals(FNDSA512.SIGNATURE_LENGTH, PQSchemeRegistry.getSignatureLength(PQScheme.UNKNOWN_PQ_SCHEME)); assertTrue(PQSchemeRegistry.isValidSignatureLength( - PQScheme.UNKNOWN_PQ_SCHEME, FNDSA.SIGNATURE_LENGTH)); + PQScheme.UNKNOWN_PQ_SCHEME, FNDSA512.SIGNATURE_LENGTH)); assertArrayEquals( - FNDSA.computeAddress(pk.getH()), + FNDSA512.computeAddress(pk.getH()), PQSchemeRegistry.computeAddress(PQScheme.UNKNOWN_PQ_SCHEME, pk.getH())); byte[] msg = "unknown-resolves-to-falcon".getBytes(); @@ -438,6 +450,6 @@ public void unknownPqSchemeResolvesToFnDsa512() { PQScheme.UNKNOWN_PQ_SCHEME, sk.getEncoded(), msg); assertTrue(PQSchemeRegistry.verify( PQScheme.UNKNOWN_PQ_SCHEME, pk.getH(), msg, sig)); - assertTrue(FNDSA.verify(pk.getH(), msg, sig)); + assertTrue(FNDSA512.verify(pk.getH(), msg, sig)); } } diff --git a/framework/src/test/java/org/tron/common/crypto/pqc/PQSchemeRegistryTest.java b/framework/src/test/java/org/tron/common/crypto/pqc/PQSchemeRegistryTest.java index 203d4625ca8..817d1dc1f07 100644 --- a/framework/src/test/java/org/tron/common/crypto/pqc/PQSchemeRegistryTest.java +++ b/framework/src/test/java/org/tron/common/crypto/pqc/PQSchemeRegistryTest.java @@ -35,37 +35,37 @@ public void containsAcceptsRegisteredScheme() { @Test public void getSeedLengthReturnsRegisteredValue() { - assertEquals(FNDSA.SEED_LENGTH, + assertEquals(FNDSA512.SEED_LENGTH, PQSchemeRegistry.getSeedLength(PQScheme.FN_DSA_512)); // UNKNOWN_PQ_SCHEME normalizes to FN_DSA_512. - assertEquals(FNDSA.SEED_LENGTH, + assertEquals(FNDSA512.SEED_LENGTH, PQSchemeRegistry.getSeedLength(PQScheme.UNKNOWN_PQ_SCHEME)); } @Test public void getPrivateKeyLengthReturnsRegisteredValue() { - assertEquals(FNDSA.PRIVATE_KEY_LENGTH, + assertEquals(FNDSA512.PRIVATE_KEY_LENGTH, PQSchemeRegistry.getPrivateKeyLength(PQScheme.FN_DSA_512)); } @Test public void fromSeedDispatchesToFalcon() { - byte[] seed = new byte[FNDSA.SEED_LENGTH]; + byte[] seed = new byte[FNDSA512.SEED_LENGTH]; Arrays.fill(seed, (byte) 0x07); PQSignature sig = PQSchemeRegistry.fromSeed(PQScheme.FN_DSA_512, seed); assertNotNull(sig); assertEquals(PQScheme.FN_DSA_512, sig.getScheme()); // Same seed must yield deterministic keypair across direct and dispatched paths. - FNDSA direct = new FNDSA(seed); + FNDSA512 direct = new FNDSA512(seed); assertArrayEquals(direct.getPublicKey(), sig.getPublicKey()); assertArrayEquals(direct.getPrivateKey(), sig.getPrivateKey()); } @Test public void fromKeypairDispatchesAndPreservesAddress() { - byte[] seed = new byte[FNDSA.SEED_LENGTH]; + byte[] seed = new byte[FNDSA512.SEED_LENGTH]; Arrays.fill(seed, (byte) 0x09); - FNDSA src = new FNDSA(seed); + FNDSA512 src = new FNDSA512(seed); PQSignature sig = PQSchemeRegistry.fromKeypair( PQScheme.FN_DSA_512, src.getPrivateKey(), src.getPublicKey()); assertArrayEquals(src.getAddress(), sig.getAddress()); @@ -88,7 +88,7 @@ public void deriveHashRejectsNullPublicKey() { public void deriveHashRejectsWrongLengthPublicKey() { try { PQSchemeRegistry.deriveHash( - PQScheme.FN_DSA_512, new byte[FNDSA.PUBLIC_KEY_LENGTH - 1]); + PQScheme.FN_DSA_512, new byte[FNDSA512.PUBLIC_KEY_LENGTH - 1]); fail("short public key must be rejected"); } catch (IllegalArgumentException expected) { assertTrue(expected.getMessage().contains("public key length")); diff --git a/framework/src/test/java/org/tron/common/crypto/pqc/PQSignatureDefaultsTest.java b/framework/src/test/java/org/tron/common/crypto/pqc/PQSignatureDefaultsTest.java index 748885e998f..03eb0b8a0fa 100644 --- a/framework/src/test/java/org/tron/common/crypto/pqc/PQSignatureDefaultsTest.java +++ b/framework/src/test/java/org/tron/common/crypto/pqc/PQSignatureDefaultsTest.java @@ -9,7 +9,7 @@ /** * Drives the {@link PQSignature} default validator branches (null and - * length-mismatch) via a minimal in-test implementation. {@link FNDSA} + * length-mismatch) via a minimal in-test implementation. {@link FNDSA512} * exposes these defaults but the cryptographic instances exercise mostly the * happy paths; the explicit fixture here forces the error legs. */ diff --git a/framework/src/test/java/org/tron/common/crypto/pqc/SignatureSchemeBenchmarkTest.java b/framework/src/test/java/org/tron/common/crypto/pqc/SignatureSchemeBenchmarkTest.java index f7fdb8d7876..7b99e7d7796 100644 --- a/framework/src/test/java/org/tron/common/crypto/pqc/SignatureSchemeBenchmarkTest.java +++ b/framework/src/test/java/org/tron/common/crypto/pqc/SignatureSchemeBenchmarkTest.java @@ -77,16 +77,16 @@ private Result benchEcKey() { private Result benchFnDsa() { for (int i = 0; i < WARMUP; i++) { - FNDSA k = new FNDSA(); + FNDSA512 k = new FNDSA512(); byte[] sig = k.sign(MESSAGE); k.verify(MESSAGE, sig); } long keygenNs = 0; - FNDSA[] keys = new FNDSA[ITERATIONS]; + FNDSA512[] keys = new FNDSA512[ITERATIONS]; for (int i = 0; i < ITERATIONS; i++) { long t0 = System.nanoTime(); - keys[i] = new FNDSA(); + keys[i] = new FNDSA512(); keygenNs += System.nanoTime() - t0; } diff --git a/framework/src/test/java/org/tron/common/crypto/pqc/program/PQClient.java b/framework/src/test/java/org/tron/common/crypto/pqc/program/PQClient.java index 6522611946c..192e4abc23b 100644 --- a/framework/src/test/java/org/tron/common/crypto/pqc/program/PQClient.java +++ b/framework/src/test/java/org/tron/common/crypto/pqc/program/PQClient.java @@ -5,15 +5,16 @@ import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import java.nio.ByteBuffer; -import java.security.MessageDigest; import java.util.Arrays; import java.util.concurrent.TimeUnit; import org.tron.api.GrpcAPI.EmptyMessage; import org.tron.api.GrpcAPI.Return; import org.tron.api.WalletGrpc; import org.tron.api.WalletGrpc.WalletBlockingStub; -import org.tron.common.crypto.pqc.FNDSA; +import org.tron.common.crypto.pqc.FNDSA512; +import org.tron.common.parameter.CommonParameter; import org.tron.common.utils.ByteArray; +import org.tron.common.utils.Sha256Hash; import org.tron.protos.Protocol.Block; import org.tron.protos.Protocol.PQAuthSig; import org.tron.protos.Protocol.Transaction; @@ -56,13 +57,13 @@ public static void main(String[] args) throws Exception { .setLevel(ch.qos.logback.classic.Level.INFO); // ── 1. Derive user keypair from same fixed seed as PQWitnessNode ───── - byte[] userSeed = new byte[FNDSA.SEED_LENGTH]; + byte[] userSeed = new byte[FNDSA512.SEED_LENGTH]; Arrays.fill(userSeed, (byte) 0x02); - FNDSA userKp = new FNDSA(userSeed); + FNDSA512 userKp = new FNDSA512(userSeed); byte[] userPub = userKp.getPublicKey(); byte[] userPriv = userKp.getPrivateKey(); - byte[] signerAddr = FNDSA.computeAddress(userPub); + byte[] signerAddr = FNDSA512.computeAddress(userPub); byte[] ownerAddr = PQWitnessNode.USER_ADDR; System.out.println("=== PQC Client ==="); @@ -83,7 +84,8 @@ public static void main(String[] args) throws Exception { Block head = stub.getNowBlock(EmptyMessage.getDefaultInstance()); byte[] headerRaw = head.getBlockHeader().getRawData().toByteArray(); long refNum = head.getBlockHeader().getRawData().getNumber(); - byte[] blockHash = sha256(headerRaw); + byte[] blockHash = Sha256Hash.of( + CommonParameter.getInstance().isECKeyCryptoEngine(), headerRaw).getBytes(); System.out.println("Reference block: #" + refNum + " hash=" + ByteArray.toHexString(Arrays.copyOfRange(blockHash, 0, 8)) + "..."); @@ -107,8 +109,10 @@ public static void main(String[] args) throws Exception { Transaction tx = Transaction.newBuilder().setRawData(rawData).build(); // ── 5. Sign with FN-DSA-512 pq_auth_sig ───────────────────────────── - byte[] txId = sha256(rawData.toByteArray()); - byte[] sig = FNDSA.sign(userPriv, txId); + byte[] txId = Sha256Hash.of( + CommonParameter.getInstance().isECKeyCryptoEngine(), + rawData.toByteArray()).getBytes(); + byte[] sig = FNDSA512.sign(userPriv, txId); // FN_DSA_512 is the launch scheme → leave scheme at proto3 default and // let PQSchemeRegistry.resolve() normalize it on the verifier side. @@ -137,10 +141,6 @@ public static void main(String[] args) throws Exception { } } - private static byte[] sha256(byte[] data) throws Exception { - return MessageDigest.getInstance("SHA-256").digest(data); - } - private static byte[] longToBytes(long value) { return ByteBuffer.allocate(8).putLong(value).array(); } diff --git a/framework/src/test/java/org/tron/common/crypto/pqc/program/PQFullNode.java b/framework/src/test/java/org/tron/common/crypto/pqc/program/PQFullNode.java index 7f628f84b70..d6bf351c772 100644 --- a/framework/src/test/java/org/tron/common/crypto/pqc/program/PQFullNode.java +++ b/framework/src/test/java/org/tron/common/crypto/pqc/program/PQFullNode.java @@ -8,7 +8,7 @@ import org.tron.common.application.Application; import org.tron.common.application.ApplicationFactory; import org.tron.common.application.TronApplicationContext; -import org.tron.common.crypto.pqc.FNDSA; +import org.tron.common.crypto.pqc.FNDSA512; import org.tron.common.utils.ByteArray; import org.tron.core.ChainBaseManager; import org.tron.core.config.DefaultConfig; @@ -56,8 +56,8 @@ public static void main(String[] args) throws Exception { .setLevel(ch.qos.logback.classic.Level.INFO); // ── 1. Derive the same deterministic keys used by PQWitnessNode ────── - FNDSA witnessKp = new FNDSA(PQWitnessNode.WITNESS_SEED); - FNDSA userKp = new FNDSA(PQWitnessNode.USER_SEED); + FNDSA512 witnessKp = new FNDSA512(PQWitnessNode.WITNESS_SEED); + FNDSA512 userKp = new FNDSA512(PQWitnessNode.USER_SEED); byte[] witnessPub = witnessKp.getPublicKey(); byte[] userPub = userKp.getPublicKey(); @@ -68,7 +68,7 @@ public static void main(String[] args) throws Exception { System.out.println("HTTP port: " + HTTP_PORT); System.out.println("P2P port: " + P2P_PORT); System.out.println("Witness address (expected): " - + ByteArray.toHexString(FNDSA.computeAddress(witnessPub))); + + ByteArray.toHexString(FNDSA512.computeAddress(witnessPub))); // ── 2. Configure node (no -w: this is a pure fullnode) ──────────────── File dbDir = Files.createTempDirectory("pqc-fullnode-").toFile(); diff --git a/framework/src/test/java/org/tron/common/crypto/pqc/program/PQTxSender.java b/framework/src/test/java/org/tron/common/crypto/pqc/program/PQTxSender.java new file mode 100644 index 00000000000..cdcaf3e72a5 --- /dev/null +++ b/framework/src/test/java/org/tron/common/crypto/pqc/program/PQTxSender.java @@ -0,0 +1,299 @@ +package org.tron.common.crypto.pqc.program; + +import com.google.protobuf.Any; +import com.google.protobuf.ByteString; +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import java.nio.ByteBuffer; +import java.security.MessageDigest; +import java.util.Arrays; +import java.util.concurrent.TimeUnit; +import org.tron.api.GrpcAPI.EmptyMessage; +import org.tron.api.GrpcAPI.Return; +import org.tron.api.WalletGrpc; +import org.tron.api.WalletGrpc.WalletBlockingStub; +import org.tron.common.crypto.pqc.FNDSA512; +import org.tron.common.math.StrictMathWrapper; +import org.tron.common.utils.ByteArray; +import org.tron.common.utils.Commons; +import org.tron.common.utils.StringUtil; +import org.tron.common.utils.client.utils.AbiUtil; +import org.tron.core.capsule.TransactionCapsule; +import org.tron.protos.Protocol.Block; +import org.tron.protos.Protocol.PQAuthSig; +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.SmartContractOuterClass.TriggerSmartContract; + +/** + * Demo client that connects to {@link PQWitnessNode} and continuously broadcasts FN-DSA-512 signed + * transfer and TRC20 transactions at 10 TPS. + *

+ * The keypair is derived from the same fixed seed used by PQWitnessNode, so no out-of-band key + * exchange is needed. + *

+ * Run from the repository root: + * ./gradlew :framework:buildFullNodeJar :framework:compileTestJava + * java -Dpqc.host=127.0.0.1 -Dpqc.port=50051 -Dpqc.transfer.tps=10 -Dpqc.trc20.tps=10 \ + * -cp "framework/build/classes/java/test:framework/build/resources/test:\ + * framework/build/libs/FullNode.jar" \ + * org.tron.common.crypto.pqc.program.PQTxSender + * + * Optional JVM args: + * -Dpqc.host=localhost + * -Dpqc.port=50051 + * -Dpqc.transfer.tps=10 + * -Dpqc.trc20.tps=10 + */ +public class PQTxSender { + + private static final String HOST = + System.getProperty("pqc.host", "localhost"); + private static final int PORT = + Integer.parseInt(System.getProperty("pqc.port", "50051")); + + /** + * Recipient of the demo transfer. + */ + private static final byte[] TO_ADDR = + Commons.decodeFromBase58Check("T9zNBvTFD97XzGsjGqvg2QHizTG8sibsHt"); + + /** + * TRC20 contract address (USDT on TRON). + */ + private static final byte[] TRC20_CONTRACT_ADDR = + Commons.decodeFromBase58Check("TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"); + + /** + * Demo TRC20 amount in base units (6 decimals = 1 token). + */ + private static final long TRC20_AMOUNT = 1L; + + /** + * Upper bound for TRC20 execution fee. + */ + private static final long TRC20_FEE_LIMIT = 1000_000_000L; + + /** + * Default send rate for transfer transactions. + */ + private static final double DEFAULT_TRANSFER_TPS = 10.0d; + /** + * Default send rate for TRC20 transactions. + */ + private static final double DEFAULT_TRC20_TPS = 10.0d; + + public static void main(String[] args) throws Exception { + // Force INFO level: logback-test.xml (on the test classpath) sets root=DEBUG + // which is far too noisy for a demo run. + ((ch.qos.logback.classic.Logger) org.slf4j.LoggerFactory + .getLogger(ch.qos.logback.classic.Logger.ROOT_LOGGER_NAME)) + .setLevel(ch.qos.logback.classic.Level.INFO); + + // ── 1. Derive user keypair from same fixed seed as PQWitnessNode ───── + byte[] userSeed = new byte[FNDSA512.SEED_LENGTH]; + Arrays.fill(userSeed, (byte) 0x02); + FNDSA512 userKp = new FNDSA512(userSeed); + + byte[] userPub = userKp.getPublicKey(); + byte[] userPriv = userKp.getPrivateKey(); + byte[] signerAddr = FNDSA512.computeAddress(userPub); + byte[] ownerAddr = Commons.decodeFromBase58Check("TJUfbazhixG4YtqJxUDmv5XisZvvy1wP91"); + double transferTps = readTps("pqc.transfer.tps", DEFAULT_TRANSFER_TPS); + double trc20Tps = readTps("pqc.trc20.tps", DEFAULT_TRC20_TPS); + + System.out.println("=== PQC Client ==="); + System.out.println("Connecting to " + HOST + ":" + PORT); + System.out.println("Owner address: " + ByteArray.toHexString(ownerAddr)); + System.out.println("Signer address: " + ByteArray.toHexString(signerAddr)); + System.out.println("Transfer TPS: " + transferTps); + System.out.println("TRC20 TPS: " + trc20Tps); + + // ── 2. Connect via gRPC ─────────────────────────────────────────────── + ManagedChannel channel = ManagedChannelBuilder + .forAddress(HOST, PORT) + .usePlaintext() + .build(); + WalletBlockingStub stub = WalletGrpc.newBlockingStub(channel); + + try { + Thread transferThread = new Thread( + () -> runTransferLoop(stub, ownerAddr, userPub, userPriv, transferTps), + "pqc-transfer-sender-grpc"); + Thread trc20Thread = new Thread( + () -> runTrc20Loop(stub, ownerAddr, userPub, userPriv, trc20Tps), + "pqc-trc20-sender-grpc"); + + transferThread.start(); + trc20Thread.start(); + transferThread.join(); + trc20Thread.join(); + } finally { + channel.shutdown(); + channel.awaitTermination(5, TimeUnit.SECONDS); + } + } + + private static byte[] sha256(byte[] data) throws Exception { + return MessageDigest.getInstance("SHA-256").digest(data); + } + + private static byte[] longToBytes(long value) { + return ByteBuffer.allocate(8).putLong(value).array(); + } + + private static void runTransferLoop(WalletBlockingStub stub, byte[] ownerAddr, + byte[] userPub, byte[] userPriv, double tps) { + if (tps <= 0) { + System.out.println("transfer sender disabled"); + return; + } + long intervalMs = tpsToIntervalMs(tps); + long counter = 1L; + while (!Thread.currentThread().isInterrupted()) { + long loopStart = System.currentTimeMillis(); + sendTransferTransaction(stub, ownerAddr, userPub, userPriv, counter++); + sleepRemaining(intervalMs, loopStart); + } + } + + private static void runTrc20Loop(WalletBlockingStub stub, byte[] ownerAddr, + byte[] userPub, byte[] userPriv, double tps) { + if (tps <= 0) { + System.out.println("trc20 sender disabled"); + return; + } + long intervalMs = tpsToIntervalMs(tps); + long counter = 1L; + while (!Thread.currentThread().isInterrupted()) { + long loopStart = System.currentTimeMillis(); + sendTrc20Transaction(stub, ownerAddr, userPub, userPriv, counter++); + sleepRemaining(intervalMs, loopStart); + } + } + + private static void sendTransferTransaction(WalletBlockingStub stub, byte[] ownerAddr, + byte[] userPub, byte[] userPriv, long seq) { + try { + WalletBlockingStub timedStub = stub.withDeadlineAfter(10, TimeUnit.SECONDS); + + // Fetch the latest block for TaPoS before every send so the demo stays valid + // even if the node advances quickly. + Block head = timedStub.getNowBlock(EmptyMessage.getDefaultInstance()); + byte[] headerRaw = head.getBlockHeader().getRawData().toByteArray(); + long refNum = head.getBlockHeader().getRawData().getNumber(); + byte[] blockHash = sha256(headerRaw); + + Transaction tx = buildTransferTransaction(ownerAddr, blockHash, refNum); + byte[] txId = sha256(tx.getRawData().toByteArray()); + byte[] sig = FNDSA512.sign(userPriv, txId); + Transaction signedTx = tx.toBuilder() + .addPqAuthSig(PQAuthSig.newBuilder() + .setPublicKey(ByteString.copyFrom(userPub)) + .setSignature(ByteString.copyFrom(sig))) + .build(); + + Return result = timedStub.broadcastTransaction(signedTx); + System.out.println("[transfer-" + seq + "] ref=#" + refNum + + " tx=" + ByteArray.toHexString(txId) + + " result=" + result.getCode() + + " msg=" + result.getMessage().toStringUtf8()); + } catch (Exception e) { + System.err.println("[transfer-" + seq + "] send failed: " + e.getMessage()); + e.printStackTrace(System.err); + } + } + + private static void sendTrc20Transaction(WalletBlockingStub stub, byte[] ownerAddr, + byte[] userPub, byte[] userPriv, long seq) { + try { + WalletBlockingStub timedStub = stub.withDeadlineAfter(10, TimeUnit.SECONDS); + + // Fetch the latest block for TaPoS before every send so the demo stays valid + // even if the node advances quickly. + Block head = timedStub.getNowBlock(EmptyMessage.getDefaultInstance()); + byte[] headerRaw = head.getBlockHeader().getRawData().toByteArray(); + long refNum = head.getBlockHeader().getRawData().getNumber(); + byte[] blockHash = sha256(headerRaw); + + Transaction tx = buildTrc20Transaction(ownerAddr, blockHash, refNum); + Transaction.raw.Builder rawBuilder = tx.getRawData().toBuilder(); + rawBuilder.setFeeLimit(TRC20_FEE_LIMIT); + tx = tx.toBuilder().setRawData(rawBuilder).build(); + + byte[] txId = sha256(tx.getRawData().toByteArray()); + byte[] sig = FNDSA512.sign(userPriv, txId); + Transaction signedTx = tx.toBuilder() + .addPqAuthSig(PQAuthSig.newBuilder() + .setPublicKey(ByteString.copyFrom(userPub)) + .setSignature(ByteString.copyFrom(sig))) + .build(); + + Return result = timedStub.broadcastTransaction(signedTx); + System.out.println("[trc20-" + seq + "] ref=#" + refNum + + " tx=" + ByteArray.toHexString(txId) + + " result=" + result.getCode() + + " msg=" + result.getMessage().toStringUtf8()); + } catch (Exception e) { + System.err.println("[trc20-" + seq + "] send failed: " + e.getMessage()); + e.printStackTrace(System.err); + } + } + + private static Transaction buildTransferTransaction(byte[] ownerAddr, byte[] blockHash, + long refNum) { + Transaction.raw rawData = Transaction.raw.newBuilder() + .addContract(Transaction.Contract.newBuilder() + .setType(ContractType.TransferContract) + .setParameter(Any.pack(TransferContract.newBuilder() + .setOwnerAddress(ByteString.copyFrom(ownerAddr)) + .setToAddress(ByteString.copyFrom(TO_ADDR)) + .setAmount(1000L) + .build())) + .setPermissionId(0)) + .setRefBlockHash(ByteString.copyFrom(Arrays.copyOfRange(blockHash, 8, 16))) + .setRefBlockBytes(ByteString.copyFrom(longToBytes(refNum), 6, 2)) + .setExpiration(System.currentTimeMillis() + 60_000L) + .build(); + return Transaction.newBuilder().setRawData(rawData).build(); + } + + private static Transaction buildTrc20Transaction(byte[] ownerAddr, byte[] blockHash, + long refNum) { + String callData = AbiUtil.parseMethod("transfer(address,uint256)", + Arrays.asList(StringUtil.encode58Check(TO_ADDR), Long.toString(TRC20_AMOUNT))); + TriggerSmartContract trigger = TriggerSmartContract.newBuilder() + .setOwnerAddress(ByteString.copyFrom(ownerAddr)) + .setContractAddress(ByteString.copyFrom(TRC20_CONTRACT_ADDR)) + .setData(ByteString.copyFrom(ByteArray.fromHexString(callData))) + .setCallValue(0L) + .build(); + TransactionCapsule trxCap = new TransactionCapsule(trigger, ContractType.TriggerSmartContract); + Transaction tx = trxCap.getInstance(); + Transaction.raw.Builder rawBuilder = tx.getRawData().toBuilder(); + rawBuilder.setRefBlockHash(ByteString.copyFrom(Arrays.copyOfRange(blockHash, 8, 16))); + rawBuilder.setRefBlockBytes(ByteString.copyFrom(longToBytes(refNum), 6, 2)); + rawBuilder.setExpiration(System.currentTimeMillis() + 60_000L); + return tx.toBuilder().setRawData(rawBuilder).build(); + } + + private static double readTps(String key, double defaultValue) { + return Double.parseDouble(System.getProperty(key, Double.toString(defaultValue))); + } + + private static long tpsToIntervalMs(double tps) { + return StrictMathWrapper.max(1L, StrictMathWrapper.round(1000.0d / tps)); + } + + private static void sleepRemaining(long intervalMs, long loopStartMs) { + long sleepMs = intervalMs - (System.currentTimeMillis() - loopStartMs); + if (sleepMs > 0) { + try { + Thread.sleep(sleepMs); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } +} diff --git a/framework/src/test/java/org/tron/common/crypto/pqc/program/PQWitnessNode.java b/framework/src/test/java/org/tron/common/crypto/pqc/program/PQWitnessNode.java index f677a44cf01..6d4e688445e 100644 --- a/framework/src/test/java/org/tron/common/crypto/pqc/program/PQWitnessNode.java +++ b/framework/src/test/java/org/tron/common/crypto/pqc/program/PQWitnessNode.java @@ -12,7 +12,7 @@ import org.tron.common.application.Application; import org.tron.common.application.ApplicationFactory; import org.tron.common.application.TronApplicationContext; -import org.tron.common.crypto.pqc.FNDSA; +import org.tron.common.crypto.pqc.FNDSA512; import org.tron.common.crypto.pqc.PQSchemeRegistry; import org.tron.common.utils.ByteArray; import org.tron.core.ChainBaseManager; @@ -73,13 +73,13 @@ public static void main(String[] args) throws Exception { .setLevel(ch.qos.logback.classic.Level.INFO); // ── 1. Derive deterministic keypairs ────────────────────────────────── - FNDSA witnessKp = new FNDSA(WITNESS_SEED); - FNDSA userKp = new FNDSA(USER_SEED); + FNDSA512 witnessKp = new FNDSA512(WITNESS_SEED); + FNDSA512 userKp = new FNDSA512(USER_SEED); byte[] witnessPub = witnessKp.getPublicKey(); - byte[] witnessAddr = FNDSA.computeAddress(witnessPub); + byte[] witnessAddr = FNDSA512.computeAddress(witnessPub); byte[] userPub = userKp.getPublicKey(); - byte[] signerAddr = FNDSA.computeAddress(userPub); + byte[] signerAddr = FNDSA512.computeAddress(userPub); System.out.println("=== PQC Witness Node ==="); System.out.println("Witness address (FN-DSA-512): " + ByteArray.toHexString(witnessAddr)); @@ -185,12 +185,12 @@ static void installPQGenesisState(Manager db, ChainBaseManager chain, } private static byte[] filledSeed(int value) { - byte[] seed = new byte[FNDSA.SEED_LENGTH]; + byte[] seed = new byte[FNDSA512.SEED_LENGTH]; Arrays.fill(seed, (byte) value); return seed; } - private static Path writeWitnessConfig(FNDSA witnessKp) throws java.io.IOException { + private static Path writeWitnessConfig(FNDSA512 witnessKp) throws java.io.IOException { Path conf = Files.createTempFile("pqc-witness-", ".conf"); conf.toFile().deleteOnExit(); String body = "include classpath(\"config-test.conf\")\n" diff --git a/framework/src/test/java/org/tron/common/runtime/vm/BatchValidateSignPQTest.java b/framework/src/test/java/org/tron/common/runtime/vm/BatchValidateFnDsa512Test.java similarity index 89% rename from framework/src/test/java/org/tron/common/runtime/vm/BatchValidateSignPQTest.java rename to framework/src/test/java/org/tron/common/runtime/vm/BatchValidateFnDsa512Test.java index aae387ad2b7..a08ddf41fd1 100644 --- a/framework/src/test/java/org/tron/common/runtime/vm/BatchValidateSignPQTest.java +++ b/framework/src/test/java/org/tron/common/runtime/vm/BatchValidateFnDsa512Test.java @@ -10,11 +10,11 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; -import org.tron.common.crypto.pqc.FNDSA; +import org.tron.common.crypto.pqc.FNDSA512; import org.tron.common.crypto.pqc.PQSchemeRegistry; import org.tron.common.utils.client.utils.AbiUtil; import org.tron.core.vm.PrecompiledContracts; -import org.tron.core.vm.PrecompiledContracts.BatchValidateSignPQ; +import org.tron.core.vm.PrecompiledContracts.BatchValidateFnDsa512; import org.tron.core.vm.PrecompiledContracts.PrecompiledContract; import org.tron.core.vm.config.VMConfig; import org.tron.protos.Protocol.PQScheme; @@ -22,17 +22,17 @@ /** * Unit tests for the 0x18 batch independent Falcon-512 verify precompile. * Returns a 256-bit bitmap where bit i is set iff - * {@code derive(pk_i) == expectedAddr_i && FNDSA.verify(pk_i, hash, sig_i)}. + * {@code derive(pk_i) == expectedAddr_i && FNDSA512.verify(pk_i, hash, sig_i)}. * Stateless — no chain DB. */ @Slf4j -public class BatchValidateSignPQTest { +public class BatchValidateFnDsa512Test { private static final DataWord ADDR_0X18 = new DataWord( "0000000000000000000000000000000000000000000000000000000000000018"); private static final String METHOD_SIGN = - "batchvalidatesignpq(bytes32,bytes[],bytes[],bytes32[])"; + "batchvalidatefndsa512(bytes32,bytes[],bytes[],bytes32[])"; private static final byte[] HASH; @@ -43,7 +43,7 @@ public class BatchValidateSignPQTest { } } - private final BatchValidateSignPQ contract = new BatchValidateSignPQ(); + private final BatchValidateFnDsa512 contract = new BatchValidateFnDsa512(); @Before public void enableProposal() { @@ -65,7 +65,7 @@ public void switchOff_returnsNull() { public void switchOn_returnsContract() { PrecompiledContract pc = PrecompiledContracts.getContractForAddress(ADDR_0X18); Assert.assertNotNull(pc); - Assert.assertTrue(pc instanceof BatchValidateSignPQ); + Assert.assertTrue(pc instanceof BatchValidateFnDsa512); } @Test @@ -76,7 +76,7 @@ public void constantCall_allValid_setsAllBits() { List pks = new ArrayList<>(n); List addrs = new ArrayList<>(n); for (int i = 0; i < n; i++) { - FNDSA k = new FNDSA(); + FNDSA512 k = new FNDSA512(); sigs.add(Hex.toHexString(k.sign(HASH))); pks.add(Hex.toHexString(k.getPublicKey())); addrs.add(addrAsBytes32Hex(k.getPublicKey())); @@ -93,8 +93,8 @@ public void constantCall_allValid_setsAllBits() { @Test public void constantCall_mismatchedAddress_clearsBit() { contract.setConstantCall(true); - FNDSA k1 = new FNDSA(); - FNDSA k2 = new FNDSA(); + FNDSA512 k1 = new FNDSA512(); + FNDSA512 k2 = new FNDSA512(); List sigs = Arrays.asList( Hex.toHexString(k1.sign(HASH)), Hex.toHexString(k2.sign(HASH))); @@ -114,7 +114,7 @@ public void constantCall_mismatchedAddress_clearsBit() { @Test public void constantCall_tamperedSignature_clearsBit() { contract.setConstantCall(true); - FNDSA k = new FNDSA(); + FNDSA512 k = new FNDSA512(); byte[] sig = k.sign(HASH); sig[0] ^= 0x01; List sigs = Collections1(Hex.toHexString(sig)); @@ -128,7 +128,7 @@ public void constantCall_tamperedSignature_clearsBit() { @Test public void constantCall_wrongPkLength_clearsBit() { contract.setConstantCall(true); - FNDSA k = new FNDSA(); + FNDSA512 k = new FNDSA512(); byte[] truncatedPk = Arrays.copyOf(k.getPublicKey(), k.getPublicKey().length - 1); List sigs = Collections1(Hex.toHexString(k.sign(HASH))); List pks = Collections1(Hex.toHexString(truncatedPk)); @@ -147,7 +147,7 @@ public void asyncPath_allValid_setsAllBits() { List pks = new ArrayList<>(n); List addrs = new ArrayList<>(n); for (int i = 0; i < n; i++) { - FNDSA k = new FNDSA(); + FNDSA512 k = new FNDSA512(); sigs.add(Hex.toHexString(k.sign(HASH))); pks.add(Hex.toHexString(k.getPublicKey())); addrs.add(addrAsBytes32Hex(k.getPublicKey())); @@ -161,7 +161,7 @@ public void asyncPath_allValid_setsAllBits() { @Test public void mismatchedArrayLengths_returnsZero() { contract.setConstantCall(true); - FNDSA k = new FNDSA(); + FNDSA512 k = new FNDSA512(); List sigs = Collections1(Hex.toHexString(k.sign(HASH))); List pks = Arrays.asList( Hex.toHexString(k.getPublicKey()), Hex.toHexString(k.getPublicKey())); @@ -180,7 +180,7 @@ public void overMaxSize_returnsZero() { List pks = new ArrayList<>(n); List addrs = new ArrayList<>(n); for (int i = 0; i < n; i++) { - FNDSA k = new FNDSA(); + FNDSA512 k = new FNDSA512(); sigs.add(Hex.toHexString(k.sign(HASH))); pks.add(Hex.toHexString(k.getPublicKey())); addrs.add(addrAsBytes32Hex(k.getPublicKey())); @@ -197,13 +197,13 @@ public void energyScalesWithCount() { List pks = new ArrayList<>(n); List addrs = new ArrayList<>(n); for (int i = 0; i < n; i++) { - FNDSA k = new FNDSA(); + FNDSA512 k = new FNDSA512(); sigs.add(Hex.toHexString(k.sign(HASH))); pks.add(Hex.toHexString(k.getPublicKey())); addrs.add(addrAsBytes32Hex(k.getPublicKey())); } byte[] input = encode(HASH, sigs, pks, addrs); - Assert.assertEquals(3L * 15000L, contract.getEnergyForData(input)); + Assert.assertEquals(3L * 2000L, contract.getEnergyForData(input)); } @Test @@ -221,7 +221,7 @@ public void differentHash_clearsAllBits() { List pks = new ArrayList<>(n); List addrs = new ArrayList<>(n); for (int i = 0; i < n; i++) { - FNDSA k = new FNDSA(); + FNDSA512 k = new FNDSA512(); // Sign HASH... sigs.add(Hex.toHexString(k.sign(HASH))); pks.add(Hex.toHexString(k.getPublicKey())); @@ -243,7 +243,7 @@ public void atMaxSize16_setsAllBits() { List pks = new ArrayList<>(n); List addrs = new ArrayList<>(n); for (int i = 0; i < n; i++) { - FNDSA k = new FNDSA(); + FNDSA512 k = new FNDSA512(); sigs.add(Hex.toHexString(k.sign(HASH))); pks.add(Hex.toHexString(k.getPublicKey())); addrs.add(addrAsBytes32Hex(k.getPublicKey())); @@ -267,7 +267,7 @@ public void asyncPath_mixedValidInvalid() { List pks = new ArrayList<>(n); List addrs = new ArrayList<>(n); for (int i = 0; i < n; i++) { - FNDSA k = new FNDSA(); + FNDSA512 k = new FNDSA512(); byte[] sig = k.sign(HASH); // Tamper entries 1 and 3. if (i == 1 || i == 3) { @@ -287,7 +287,7 @@ public void asyncPath_mixedValidInvalid() { @Test public void sigTooLong_clearsBit() { contract.setConstantCall(true); - FNDSA k = new FNDSA(); + FNDSA512 k = new FNDSA512(); byte[] oversized = new byte[800]; Arrays.fill(oversized, (byte) 0x99); List sigs = Collections1(Hex.toHexString(oversized)); @@ -303,7 +303,11 @@ public void sigTooLong_clearsBit() { private Pair run(byte[] hash, List sigs, List pks, List addrs) { byte[] input = encode(hash, sigs, pks, addrs); - contract.setVmShouldEndInUs(System.nanoTime() / 1000 + 5_000_000L); + // Preserve any longer budget callers set (e.g. atMaxSize16_setsAllBits and + // asyncPath_* need 10-30s for 16 parallel Falcon-512 verifies on slow CI). + if (contract.getVmShouldEndInUs() == 0) { + contract.setVmShouldEndInUs(System.nanoTime() / 1000 + 5_000_000L); + } Pair ret = contract.execute(input); logger.info("0x18 bitmap: {}", Hex.toHexString(ret.getRight())); return ret; diff --git a/framework/src/test/java/org/tron/common/runtime/vm/FnDsaPrecompileTest.java b/framework/src/test/java/org/tron/common/runtime/vm/FnDsaPrecompileTest.java index 4e21409e2ce..e8fa8bc27d0 100644 --- a/framework/src/test/java/org/tron/common/runtime/vm/FnDsaPrecompileTest.java +++ b/framework/src/test/java/org/tron/common/runtime/vm/FnDsaPrecompileTest.java @@ -5,7 +5,7 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; -import org.tron.common.crypto.pqc.FNDSA; +import org.tron.common.crypto.pqc.FNDSA512; import org.tron.core.vm.PrecompiledContracts; import org.tron.core.vm.PrecompiledContracts.PrecompiledContract; import org.tron.core.vm.config.VMConfig; @@ -50,7 +50,7 @@ public void switchOn_returnsContract() { @Test public void validSignature_returnsOne() { - FNDSA key = new FNDSA(); + FNDSA512 key = new FNDSA512(); byte[] sig = key.sign(MESSAGE_HASH); byte[] input = buildInput(MESSAGE_HASH, sig, key.getPublicKey()); @@ -59,12 +59,12 @@ public void validSignature_returnsOne() { Assert.assertTrue(result.getLeft()); Assert.assertArrayEquals(DataWord.ONE().getData(), result.getRight()); - Assert.assertEquals(2500, pc.getEnergyForData(input)); + Assert.assertEquals(4000, pc.getEnergyForData(input)); } @Test public void tamperedMessage_returnsZero() { - FNDSA key = new FNDSA(); + FNDSA512 key = new FNDSA512(); byte[] sig = key.sign(MESSAGE_HASH); byte[] tampered = MESSAGE_HASH.clone(); tampered[0] ^= 0x01; @@ -79,7 +79,7 @@ public void tamperedMessage_returnsZero() { @Test public void tamperedSignature_returnsZero() { - FNDSA key = new FNDSA(); + FNDSA512 key = new FNDSA512(); byte[] sig = key.sign(MESSAGE_HASH); sig[0] ^= 0x01; byte[] input = buildInput(MESSAGE_HASH, sig, key.getPublicKey()); @@ -93,8 +93,8 @@ public void tamperedSignature_returnsZero() { @Test public void wrongPublicKey_returnsZero() { - FNDSA signer = new FNDSA(); - FNDSA other = new FNDSA(); + FNDSA512 signer = new FNDSA512(); + FNDSA512 other = new FNDSA512(); byte[] sig = signer.sign(MESSAGE_HASH); byte[] input = buildInput(MESSAGE_HASH, sig, other.getPublicKey()); @@ -125,7 +125,7 @@ public void shortInput_returnsZero() { @Test public void zeroSigLen_returnsZero() { - FNDSA key = new FNDSA(); + FNDSA512 key = new FNDSA512(); byte[] pk = key.getPublicKey(); // sig_len = 0 is invalid (must be >= 1) // input must be >= MIN_INPUT_LEN (931 = 32 + 2 + 1 + 896) to reach the sigLen check @@ -143,8 +143,8 @@ public void zeroSigLen_returnsZero() { @Test public void oversizedSigLen_returnsZero() { - // sig_len = 753, which exceeds FNDSA.SIGNATURE_LENGTH (752) - byte[] input = new byte[32 + 2 + 753 + FNDSA.PUBLIC_KEY_LENGTH]; + // sig_len = 753, which exceeds FNDSA512.SIGNATURE_LENGTH (752) + byte[] input = new byte[32 + 2 + 753 + FNDSA512.PUBLIC_KEY_LENGTH]; input[32] = 0x02; // high byte input[33] = (byte) 0xF1; // low byte → 0x02F1 = 753 Pair result = @@ -156,7 +156,7 @@ public void oversizedSigLen_returnsZero() { @Test public void sigLenLargerThanActualData_returnsZero() { - FNDSA key = new FNDSA(); + FNDSA512 key = new FNDSA512(); byte[] sig = key.sign(MESSAGE_HASH); // claim sig is 100 bytes longer than it is byte[] input = buildInput(MESSAGE_HASH, sig, key.getPublicKey()); @@ -172,6 +172,23 @@ public void sigLenLargerThanActualData_returnsZero() { Assert.assertArrayEquals(DataWord.ZERO().getData(), result.getRight()); } + @Test + public void trailingBytes_returnsZero() { + // Strict equality (matches 0x100 P256Verify / EIP-7951): appending even one byte + // to an otherwise-valid input must be rejected to prevent non-canonical encodings. + FNDSA512 key = new FNDSA512(); + byte[] sig = key.sign(MESSAGE_HASH); + byte[] valid = buildInput(MESSAGE_HASH, sig, key.getPublicKey()); + byte[] padded = new byte[valid.length + 1]; + System.arraycopy(valid, 0, padded, 0, valid.length); + + Pair result = + PrecompiledContracts.getContractForAddress(FNDSA_ADDR).execute(padded); + + Assert.assertTrue(result.getLeft()); + Assert.assertArrayEquals(DataWord.ZERO().getData(), result.getRight()); + } + /** Encodes input as [msg 32B | sig_len 2B | sig | pk]. */ private static byte[] buildInput(byte[] msg, byte[] sig, byte[] pk) { int sigLen = sig.length; diff --git a/framework/src/test/java/org/tron/common/runtime/vm/ValidateMultiSignPQTest.java b/framework/src/test/java/org/tron/common/runtime/vm/ValidateMultiFnDsa512Test.java similarity index 95% rename from framework/src/test/java/org/tron/common/runtime/vm/ValidateMultiSignPQTest.java rename to framework/src/test/java/org/tron/common/runtime/vm/ValidateMultiFnDsa512Test.java index 37a7cd7aa02..3d820644b7c 100644 --- a/framework/src/test/java/org/tron/common/runtime/vm/ValidateMultiSignPQTest.java +++ b/framework/src/test/java/org/tron/common/runtime/vm/ValidateMultiFnDsa512Test.java @@ -14,7 +14,7 @@ import org.tron.common.BaseTest; import org.tron.common.TestConstants; import org.tron.common.crypto.ECKey; -import org.tron.common.crypto.pqc.FNDSA; +import org.tron.common.crypto.pqc.FNDSA512; import org.tron.common.crypto.pqc.PQSchemeRegistry; import org.tron.common.parameter.CommonParameter; import org.tron.common.utils.ByteArray; @@ -27,7 +27,7 @@ import org.tron.core.store.StoreFactory; import org.tron.core.vm.PrecompiledContracts; import org.tron.core.vm.PrecompiledContracts.PrecompiledContract; -import org.tron.core.vm.PrecompiledContracts.ValidateMultiSignPQ; +import org.tron.core.vm.PrecompiledContracts.ValidateMultiFnDsa512; import org.tron.core.vm.config.VMConfig; import org.tron.core.vm.repository.Repository; import org.tron.core.vm.repository.RepositoryImpl; @@ -40,7 +40,7 @@ * Falcon-512 entries alongside ECDSA against the same Permission.keys[]. */ @Slf4j -public class ValidateMultiSignPQTest extends BaseTest { +public class ValidateMultiFnDsa512Test extends BaseTest { private static final DataWord ADDR_0X17 = new DataWord( "0000000000000000000000000000000000000000000000000000000000000017"); @@ -56,7 +56,7 @@ public class ValidateMultiSignPQTest extends BaseTest { Arrays.fill(longData, (byte) 7); } - private final ValidateMultiSignPQ contract = new ValidateMultiSignPQ(); + private final ValidateMultiFnDsa512 contract = new ValidateMultiFnDsa512(); @Before public void before() { @@ -75,7 +75,7 @@ public void switchOff_returnsNull() { public void switchOn_returnsContract() { PrecompiledContract pc = PrecompiledContracts.getContractForAddress(ADDR_0X17); Assert.assertNotNull(pc); - Assert.assertTrue(pc instanceof ValidateMultiSignPQ); + Assert.assertTrue(pc instanceof ValidateMultiFnDsa512); } @Test @@ -111,8 +111,8 @@ public void pureEcdsaThresholdReached_returnsOne() { @Test public void purePqThresholdReached_returnsOne() { - FNDSA pq1 = new FNDSA(); - FNDSA pq2 = new FNDSA(); + FNDSA512 pq1 = new FNDSA512(); + FNDSA512 pq2 = new FNDSA512(); ECKey owner = new ECKey(); byte[] addr1 = PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, pq1.getPublicKey()); byte[] addr2 = PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, pq2.getPublicKey()); @@ -137,7 +137,7 @@ public void purePqThresholdReached_returnsOne() { @Test public void mixedEcdsaAndPq_returnsOne() { ECKey k1 = new ECKey(); - FNDSA pq1 = new FNDSA(); + FNDSA512 pq1 = new FNDSA512(); ECKey owner = new ECKey(); byte[] pqAddr = PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, pq1.getPublicKey()); setupPermission(owner, Collections.singletonList(k1.getAddress()), Collections.singletonList(1), @@ -157,7 +157,7 @@ public void mixedEcdsaAndPq_returnsOne() { @Test public void pqSignatureForgery_returnsZero() { - FNDSA pq1 = new FNDSA(); + FNDSA512 pq1 = new FNDSA512(); ECKey owner = new ECKey(); byte[] pqAddr = PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, pq1.getPublicKey()); setupPermission(owner, Collections.emptyList(), Collections.emptyList(), @@ -178,7 +178,7 @@ public void pqSignatureForgery_returnsZero() { @Test public void wrongPqPublicKeyLength_returnsZero() { - FNDSA pq1 = new FNDSA(); + FNDSA512 pq1 = new FNDSA512(); ECKey owner = new ECKey(); byte[] pqAddr = PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, pq1.getPublicKey()); setupPermission(owner, Collections.emptyList(), Collections.emptyList(), @@ -198,8 +198,8 @@ public void wrongPqPublicKeyLength_returnsZero() { @Test public void mismatchedPqArrayLengths_returnsZero() { - FNDSA pq1 = new FNDSA(); - FNDSA pq2 = new FNDSA(); + FNDSA512 pq1 = new FNDSA512(); + FNDSA512 pq2 = new FNDSA512(); ECKey owner = new ECKey(); byte[] addr1 = PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, pq1.getPublicKey()); setupPermission(owner, Collections.emptyList(), Collections.emptyList(), @@ -247,7 +247,7 @@ public void totalCountOverMaxSize_returnsZero() { @Test public void duplicatePqSig_doesNotDoubleCount() { - FNDSA pq1 = new FNDSA(); + FNDSA512 pq1 = new FNDSA512(); ECKey owner = new ECKey(); byte[] pqAddr = PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, pq1.getPublicKey()); setupPermission(owner, Collections.emptyList(), Collections.emptyList(), @@ -268,7 +268,7 @@ public void duplicatePqSig_doesNotDoubleCount() { @Test public void energyChargesEcdsaAndPqSeparately() { - FNDSA pq1 = new FNDSA(); + FNDSA512 pq1 = new FNDSA512(); ECKey k1 = new ECKey(); ECKey owner = new ECKey(); byte[] pqAddr = PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, pq1.getPublicKey()); @@ -283,8 +283,8 @@ public void energyChargesEcdsaAndPqSeparately() { List pqPks = Collections.singletonList(Hex.toHexString(pq1.getPublicKey())); byte[] input = encodeInput(owner.getAddress(), 2, data, ecdsaSigs, pqSigs, pqPks); - // 1 ECDSA × 1500 + 1 PQ × 15000 = 16500 - Assert.assertEquals(16500L, contract.getEnergyForData(input)); + // 1 ECDSA × 1500 + 1 PQ × 2000 = 3500 + Assert.assertEquals(3500L, contract.getEnergyForData(input)); } @Test @@ -308,8 +308,8 @@ public void thresholdNotReached_returnsZero() { @Test public void pqKeyNotInPermission_returnsZero() { - FNDSA inPerm = new FNDSA(); - FNDSA outsider = new FNDSA(); + FNDSA512 inPerm = new FNDSA512(); + FNDSA512 outsider = new FNDSA512(); ECKey owner = new ECKey(); byte[] inAddr = PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, inPerm.getPublicKey()); setupPermission(owner, Collections.emptyList(), Collections.emptyList(), @@ -330,7 +330,7 @@ public void pqKeyNotInPermission_returnsZero() { @Test public void pqSigTooLong_returnsZero() { - FNDSA pq1 = new FNDSA(); + FNDSA512 pq1 = new FNDSA512(); ECKey owner = new ECKey(); byte[] pqAddr = PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, pq1.getPublicKey()); setupPermission(owner, Collections.emptyList(), Collections.emptyList(), @@ -372,7 +372,7 @@ public void mixedFailingPqAborts_returnsZero() { // threshold. Verifies 0x17 does not silently skip a forged PQ signature. ECKey k1 = new ECKey(); ECKey k2 = new ECKey(); - FNDSA pq1 = new FNDSA(); + FNDSA512 pq1 = new FNDSA512(); ECKey owner = new ECKey(); byte[] pqAddr = PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, pq1.getPublicKey()); setupPermission(owner, diff --git a/framework/src/test/java/org/tron/common/utils/LocalWitnessesTest.java b/framework/src/test/java/org/tron/common/utils/LocalWitnessesTest.java index 22e99ef4571..b48995b89aa 100644 --- a/framework/src/test/java/org/tron/common/utils/LocalWitnessesTest.java +++ b/framework/src/test/java/org/tron/common/utils/LocalWitnessesTest.java @@ -9,7 +9,7 @@ import org.bouncycastle.util.encoders.Hex; import org.junit.BeforeClass; import org.junit.Test; -import org.tron.common.crypto.pqc.FNDSA; +import org.tron.common.crypto.pqc.FNDSA512; import org.tron.core.exception.TronError; import org.tron.protos.Protocol.PQScheme; @@ -25,8 +25,8 @@ public class LocalWitnessesTest { @BeforeClass public static void generateKeypairs() { - FNDSA k1 = new FNDSA(); - FNDSA k2 = new FNDSA(); + FNDSA512 k1 = new FNDSA512(); + FNDSA512 k2 = new FNDSA512(); priv = Hex.toHexString(k1.getPrivateKey()); pub = Hex.toHexString(k1.getPublicKey()); priv2 = Hex.toHexString(k2.getPrivateKey()); diff --git a/framework/src/test/java/org/tron/core/BandwidthProcessorTest.java b/framework/src/test/java/org/tron/core/BandwidthProcessorTest.java index f6fede77523..5467777e538 100755 --- a/framework/src/test/java/org/tron/core/BandwidthProcessorTest.java +++ b/framework/src/test/java/org/tron/core/BandwidthProcessorTest.java @@ -12,6 +12,7 @@ import org.junit.Test; import org.tron.common.BaseTest; import org.tron.common.TestConstants; +import org.tron.common.crypto.pqc.FNDSA512; import org.tron.common.runtime.RuntimeImpl; import org.tron.common.utils.ByteArray; import org.tron.core.capsule.AccountCapsule; @@ -905,8 +906,8 @@ public void pqPQAuthWitnessBytesSubtractedInCreateAccountCap() throws Exception .setAmount(100L) .build(); - byte[] fakeSig = new byte[752]; - byte[] fakePub = new byte[897]; + byte[] fakeSig = new byte[FNDSA512.SIGNATURE_LENGTH]; + byte[] fakePub = new byte[FNDSA512.PUBLIC_KEY_LENGTH]; Protocol.PQAuthSig pqAuthSig = Protocol.PQAuthSig.newBuilder() .setScheme(Protocol.PQScheme.FN_DSA_512) .setPublicKey(ByteString.copyFrom(fakePub)) @@ -967,8 +968,8 @@ public void pqPQAuthWitnessCountedInBandwidthUsage() throws Exception { .setAmount(100L) .build(); - byte[] fakeSig = new byte[752]; - byte[] fakePub = new byte[897]; + byte[] fakeSig = new byte[FNDSA512.SIGNATURE_LENGTH]; + byte[] fakePub = new byte[FNDSA512.PUBLIC_KEY_LENGTH]; Protocol.PQAuthSig pqAuthSig = Protocol.PQAuthSig.newBuilder() .setScheme(Protocol.PQScheme.FN_DSA_512) .setPublicKey(ByteString.copyFrom(fakePub)) diff --git a/framework/src/test/java/org/tron/core/actuator/utils/ProposalUtilTest.java b/framework/src/test/java/org/tron/core/actuator/utils/ProposalUtilTest.java index 1e7be605104..d26d3c51d18 100644 --- a/framework/src/test/java/org/tron/core/actuator/utils/ProposalUtilTest.java +++ b/framework/src/test/java/org/tron/core/actuator/utils/ProposalUtilTest.java @@ -808,6 +808,7 @@ public void validateAllowFnDsa512() { ThrowingRunnable proposeTwo = () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, code, 2); + forkUtils.init(dbManager.getChainBaseManager()); byte[] stats = new byte[27]; forkUtils.getManager().getDynamicPropertiesStore() .statsByVersion(ForkBlockVersionEnum.VERSION_4_8_1.getValue(), stats); diff --git a/framework/src/test/java/org/tron/core/capsule/BlockCapsulePQTest.java b/framework/src/test/java/org/tron/core/capsule/BlockCapsulePQTest.java index f224652046e..6c8b482cd51 100644 --- a/framework/src/test/java/org/tron/core/capsule/BlockCapsulePQTest.java +++ b/framework/src/test/java/org/tron/core/capsule/BlockCapsulePQTest.java @@ -8,7 +8,7 @@ import org.tron.common.BaseTest; import org.tron.common.TestConstants; import org.tron.common.crypto.ECKey; -import org.tron.common.crypto.pqc.FNDSA; +import org.tron.common.crypto.pqc.FNDSA512; import org.tron.common.crypto.pqc.PQSchemeRegistry; import org.tron.common.utils.Sha256Hash; import org.tron.core.config.args.Args; @@ -25,7 +25,7 @@ public class BlockCapsulePQTest extends BaseTest { private ECKey witnessKey; private byte[] witnessAddress; - private FNDSA pqKeypair; + private FNDSA512 pqKeypair; private byte[] pqAddress; @BeforeClass @@ -37,7 +37,7 @@ public static void init() { public void setUp() { witnessKey = new ECKey(); witnessAddress = witnessKey.getAddress(); - pqKeypair = new FNDSA(); + pqKeypair = new FNDSA512(); pqAddress = PQSchemeRegistry.computeAddress( PQScheme.FN_DSA_512, pqKeypair.getPublicKey()); } @@ -89,7 +89,7 @@ private BlockCapsule buildUnsignedBlock(byte[] parentHash) { } private byte[] signPQ(byte[] message) { - return FNDSA.sign(pqKeypair.getPrivateKey(), message); + return FNDSA512.sign(pqKeypair.getPrivateKey(), message); } private PQAuthSig buildPQAuthSig(byte[] signature) { @@ -158,6 +158,28 @@ public void pqOnlyAccepted() throws Exception { dbManager.getDynamicPropertiesStore(), dbManager.getAccountStore())); } + @Test + public void pqAuthSigWithDefaultSchemeAcceptedAsFnDsa512() throws Exception { + dbManager.getDynamicPropertiesStore().saveAllowMultiSign(1L); + dbManager.getDynamicPropertiesStore().saveAllowFnDsa512(1L); + AccountCapsule witness = buildWitnessAccount(pqAddress); + dbManager.getAccountStore().put(witnessAddress, witness); + + byte[] parentHash = new byte[32]; + BlockCapsule block = buildUnsignedBlock(parentHash); + byte[] digest = block.getRawHashBytes(); + // Omit setScheme(...) so the field stays at the proto3 default + // UNKNOWN_PQ_SCHEME; PQSchemeRegistry#resolve normalizes it to FN_DSA_512. + PQAuthSig defaultScheme = PQAuthSig.newBuilder() + .setPublicKey(ByteString.copyFrom(pqKeypair.getPublicKey())) + .setSignature(ByteString.copyFrom(signPQ(digest))) + .build(); + Assert.assertEquals(PQScheme.UNKNOWN_PQ_SCHEME, defaultScheme.getScheme()); + block.setPqAuthSig(defaultScheme); + Assert.assertTrue(block.validateSignature( + dbManager.getDynamicPropertiesStore(), dbManager.getAccountStore())); + } + @Test public void tamperedPQAuthSigFails() throws Exception { dbManager.getDynamicPropertiesStore().saveAllowMultiSign(1L); 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 078e6153f39..7c1c7356383 100644 --- a/framework/src/test/java/org/tron/core/capsule/TransactionCapsuleTest.java +++ b/framework/src/test/java/org/tron/core/capsule/TransactionCapsuleTest.java @@ -22,7 +22,7 @@ import org.tron.common.BaseTest; import org.tron.common.TestConstants; import org.tron.common.crypto.ECKey; -import org.tron.common.crypto.pqc.FNDSA; +import org.tron.common.crypto.pqc.FNDSA512; import org.tron.common.crypto.pqc.PQSchemeRegistry; import org.tron.common.utils.ByteArray; import org.tron.common.utils.Sha256Hash; @@ -180,8 +180,8 @@ public void pqAuthSigBeforeActivationRejected() { Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0).toBuilder() .addPqAuthSig(PQAuthSig.newBuilder() .setScheme(PQScheme.FN_DSA_512) - .setPublicKey(ByteString.copyFrom(new byte[FNDSA.PUBLIC_KEY_LENGTH])) - .setSignature(ByteString.copyFrom(new byte[FNDSA.SIGNATURE_LENGTH])) + .setPublicKey(ByteString.copyFrom(new byte[FNDSA512.PUBLIC_KEY_LENGTH])) + .setSignature(ByteString.copyFrom(new byte[FNDSA512.SIGNATURE_LENGTH])) .build()) .build(); TransactionCapsule cap = new TransactionCapsule(tx); @@ -216,16 +216,21 @@ public void fastVerify() { } } + private static byte[] txId(Transaction tx) { + return Sha256Hash.of(Args.getInstance().isECKeyCryptoEngine(), + tx.getRawData().toByteArray()).getBytes(); + } + @Test public void validPQAuthSigAccepted() throws Exception { dbManager.getDynamicPropertiesStore().saveAllowFnDsa512(1L); - FNDSA kp = new FNDSA(); + FNDSA512 kp = new FNDSA512(); putAccountWithPQPermission(PQ_OWNER_HEX, kp.getPublicKey(), PQScheme.FN_DSA_512); Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); - byte[] txid = Sha256Hash.of(true, tx.getRawData().toByteArray()).getBytes(); - - byte[] sig = FNDSA.sign(kp.getPrivateKey(), txid); + byte[] txid = txId(tx); + + byte[] sig = FNDSA512.sign(kp.getPrivateKey(), txid); Transaction signed = tx.toBuilder() .addPqAuthSig(PQAuthSig.newBuilder() @@ -242,13 +247,13 @@ public void validPQAuthSigAccepted() throws Exception { @Test public void duplicateSignerRejected() throws Exception { dbManager.getDynamicPropertiesStore().saveAllowFnDsa512(1L); - FNDSA kp = new FNDSA(); + FNDSA512 kp = new FNDSA512(); putAccountWithPQPermission(PQ_OWNER_HEX, kp.getPublicKey(), PQScheme.FN_DSA_512); Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); - byte[] txid = Sha256Hash.of(true, tx.getRawData().toByteArray()).getBytes(); + byte[] txid = txId(tx); - byte[] sig = FNDSA.sign(kp.getPrivateKey(), txid); + byte[] sig = FNDSA512.sign(kp.getPrivateKey(), txid); PQAuthSig w = PQAuthSig.newBuilder() .setScheme(PQScheme.FN_DSA_512) .setPublicKey(ByteString.copyFrom(kp.getPublicKey())) @@ -269,13 +274,13 @@ public void duplicateSignerRejected() throws Exception { @Test public void tamperedPQAuthSigRejected() throws Exception { dbManager.getDynamicPropertiesStore().saveAllowFnDsa512(1L); - FNDSA kp = new FNDSA(); + FNDSA512 kp = new FNDSA512(); putAccountWithPQPermission(PQ_OWNER_HEX, kp.getPublicKey(), PQScheme.FN_DSA_512); Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); - byte[] txid = Sha256Hash.of(true, tx.getRawData().toByteArray()).getBytes(); + byte[] txid = txId(tx); - byte[] sig = FNDSA.sign(kp.getPrivateKey(), txid); + byte[] sig = FNDSA512.sign(kp.getPrivateKey(), txid); sig[0] ^= 0x01; Transaction signed = tx.toBuilder() @@ -298,15 +303,15 @@ public void tamperedPQAuthSigRejected() throws Exception { @Test public void signerNotInPermissionRejected() throws Exception { dbManager.getDynamicPropertiesStore().saveAllowFnDsa512(1L); - FNDSA known = new FNDSA(); + FNDSA512 known = new FNDSA512(); putAccountWithPQPermission(PQ_OWNER_HEX, known.getPublicKey(), PQScheme.FN_DSA_512); // Sign with a *different* keypair → derived address is not in the permission. - FNDSA stranger = new FNDSA(); + FNDSA512 stranger = new FNDSA512(); Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); - byte[] txid = Sha256Hash.of(true, tx.getRawData().toByteArray()).getBytes(); + byte[] txid = txId(tx); - byte[] sig = FNDSA.sign(stranger.getPrivateKey(), txid); + byte[] sig = FNDSA512.sign(stranger.getPrivateKey(), txid); Transaction signed = tx.toBuilder() .addPqAuthSig(PQAuthSig.newBuilder() @@ -373,9 +378,9 @@ private long[][] measureSizes(Transaction baseTx) { long ecPack = ecCap.computeTrxSizeForBlockMessage(); // FN-DSA-512: variable-length signature (<= 752 bytes) + 897-byte public key - FNDSA kpFn = new FNDSA(); - byte[] txidFn = Sha256Hash.of(true, baseTx.getRawData().toByteArray()).getBytes(); - byte[] sigFn = FNDSA.sign(kpFn.getPrivateKey(), txidFn); + FNDSA512 kpFn = new FNDSA512(); + byte[] txidFn = txId(baseTx); + byte[] sigFn = FNDSA512.sign(kpFn.getPrivateKey(), txidFn); Transaction txFn = baseTx.toBuilder() .addPqAuthSig(PQAuthSig.newBuilder() .setScheme(PQScheme.FN_DSA_512) @@ -420,15 +425,15 @@ public void transactionSizeComparisonByScheme() { @Test public void pqAuthSigWrongPublicKeyLengthRejected() throws Exception { dbManager.getDynamicPropertiesStore().saveAllowFnDsa512(1L); - FNDSA kp = new FNDSA(); + FNDSA512 kp = new FNDSA512(); putAccountWithPQPermission(PQ_OWNER_HEX, kp.getPublicKey(), PQScheme.FN_DSA_512); Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); - byte[] txid = Sha256Hash.of(true, tx.getRawData().toByteArray()).getBytes(); - byte[] sig = FNDSA.sign(kp.getPrivateKey(), txid); + byte[] txid = txId(tx); + byte[] sig = FNDSA512.sign(kp.getPrivateKey(), txid); // Truncate public key by one byte to force the length-mismatch branch. - byte[] shortPub = new byte[FNDSA.PUBLIC_KEY_LENGTH - 1]; + byte[] shortPub = new byte[FNDSA512.PUBLIC_KEY_LENGTH - 1]; System.arraycopy(kp.getPublicKey(), 0, shortPub, 0, shortPub.length); Transaction signed = tx.toBuilder() @@ -451,7 +456,7 @@ public void pqAuthSigWrongPublicKeyLengthRejected() throws Exception { @Test public void pqAuthSigWrongSignatureLengthRejected() throws Exception { dbManager.getDynamicPropertiesStore().saveAllowFnDsa512(1L); - FNDSA kp = new FNDSA(); + FNDSA512 kp = new FNDSA512(); putAccountWithPQPermission(PQ_OWNER_HEX, kp.getPublicKey(), PQScheme.FN_DSA_512); Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); @@ -477,12 +482,12 @@ public void pqAuthSigWrongSignatureLengthRejected() throws Exception { @Test public void pqAuthSigUnsupportedSchemeRejected() throws Exception { dbManager.getDynamicPropertiesStore().saveAllowFnDsa512(1L); - FNDSA kp = new FNDSA(); + FNDSA512 kp = new FNDSA512(); putAccountWithPQPermission(PQ_OWNER_HEX, kp.getPublicKey(), PQScheme.FN_DSA_512); Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); - byte[] txid = Sha256Hash.of(true, tx.getRawData().toByteArray()).getBytes(); - byte[] sig = FNDSA.sign(kp.getPrivateKey(), txid); + byte[] txid = txId(tx); + byte[] sig = FNDSA512.sign(kp.getPrivateKey(), txid); // setSchemeValue(99) sets an unknown numeric tag; reading back yields // PQScheme.UNRECOGNIZED, which PQSchemeRegistry.contains() rejects. @@ -519,8 +524,8 @@ public void validatePubSignatureRejectsMissingSig() { @Test public void validatePubSignatureRejectsMissingContract() throws Exception { dbManager.getDynamicPropertiesStore().saveAllowFnDsa512(1L); - FNDSA kp = new FNDSA(); - byte[] sig = FNDSA.sign(kp.getPrivateKey(), new byte[32]); + FNDSA512 kp = new FNDSA512(); + byte[] sig = FNDSA512.sign(kp.getPrivateKey(), new byte[32]); // No contracts in raw_data, but a pq_auth_sig is attached so we get past // the "miss sig" guard and into the "miss contract" branch. @@ -548,14 +553,14 @@ public void validatePubSignatureRejectsTooManySignatures() throws Exception { int original = dbManager.getDynamicPropertiesStore().getTotalSignNum(); try { dbManager.getDynamicPropertiesStore().saveTotalSignNum(1); - FNDSA a = new FNDSA(); - FNDSA b = new FNDSA(); + FNDSA512 a = new FNDSA512(); + FNDSA512 b = new FNDSA512(); putAccountWithPQPermission(PQ_OWNER_HEX, a.getPublicKey(), PQScheme.FN_DSA_512); Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); - byte[] txid = Sha256Hash.of(true, tx.getRawData().toByteArray()).getBytes(); - byte[] sigA = FNDSA.sign(a.getPrivateKey(), txid); - byte[] sigB = FNDSA.sign(b.getPrivateKey(), txid); + byte[] txid = txId(tx); + byte[] sigA = FNDSA512.sign(a.getPrivateKey(), txid); + byte[] sigB = FNDSA512.sign(b.getPrivateKey(), txid); Transaction signed = tx.toBuilder() .addPqAuthSig(PQAuthSig.newBuilder() @@ -585,13 +590,13 @@ public void validatePubSignatureRejectsTooManySignatures() throws Exception { @Test public void fnDsaPQAuthSigRejectedWhenNotActivated() throws Exception { dbManager.getDynamicPropertiesStore().saveAllowFnDsa512(0L); - FNDSA kp = new FNDSA(); + FNDSA512 kp = new FNDSA512(); putAccountWithPQPermission(PQ_OWNER_HEX, kp.getPublicKey(), PQScheme.FN_DSA_512); Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); - byte[] txid = Sha256Hash.of(true, tx.getRawData().toByteArray()).getBytes(); + byte[] txid = txId(tx); - byte[] sig = FNDSA.sign(kp.getPrivateKey(), txid); + byte[] sig = FNDSA512.sign(kp.getPrivateKey(), txid); Transaction signed = tx.toBuilder() .addPqAuthSig(PQAuthSig.newBuilder() diff --git a/protocol/src/main/protos/core/Tron.proto b/protocol/src/main/protos/core/Tron.proto index d9ca33f7f53..208f4337314 100644 --- a/protocol/src/main/protos/core/Tron.proto +++ b/protocol/src/main/protos/core/Tron.proto @@ -258,7 +258,7 @@ message Key { // Per-signer post-quantum authentication witness for a transaction or block. // The signing public key is carried in-band; node verifies binding via -// derived_addr = 0x41 ‖ deriveHash(scheme, public_key)[0:20] +// derived_addr = 0x41 ‖ deriveHash(scheme, public_key)[12..32] // and matches against Permission.keys[].address. message PQAuthSig { PQScheme scheme = 1; From f8a0277cb4755b08dfbf99df854044df191eda4a Mon Sep 17 00:00:00 2001 From: federico Date: Thu, 14 May 2026 09:47:50 +0700 Subject: [PATCH 4/9] feat(net): support PQ signatures in HelloMessage --- .../org/tron/core/config/args/ConfigKey.java | 4 +- .../tron/core/consensus/ConsensusService.java | 4 +- .../core/net/service/relay/RelayService.java | 142 ++++++++++++++---- framework/src/main/resources/config.conf | 28 ++-- .../crypto/pqc/program/PQWitnessNode.java | 12 +- .../tron/core/exception/TronErrorTest.java | 2 +- .../core/net/services/RelayServiceTest.java | 116 ++++++++++++++ framework/src/test/resources/config-test.conf | 8 +- protocol/src/main/protos/core/Tron.proto | 7 + 9 files changed, 264 insertions(+), 59 deletions(-) diff --git a/framework/src/main/java/org/tron/core/config/args/ConfigKey.java b/framework/src/main/java/org/tron/core/config/args/ConfigKey.java index 71fb1c907a6..687ba2c8b14 100644 --- a/framework/src/main/java/org/tron/core/config/args/ConfigKey.java +++ b/framework/src/main/java/org/tron/core/config/args/ConfigKey.java @@ -4,9 +4,9 @@ public final class ConfigKey { public static final String COMMITTEE_ALLOW_FN_DSA_512 = "committee.allowFnDsa512"; - public static final String LOCAL_WITNESS_PQ_KEYS = "localwitness_pq_keys"; + public static final String LOCAL_WITNESS_PQ_KEYS = "localwitness_pq.keys"; - public static final String LOCAL_WITNESS_PQ_SCHEME = "localwitness_pq_scheme"; + public static final String LOCAL_WITNESS_PQ_SCHEME = "localwitness_pq.scheme"; private ConfigKey() { } diff --git a/framework/src/main/java/org/tron/core/consensus/ConsensusService.java b/framework/src/main/java/org/tron/core/consensus/ConsensusService.java index 23cbe28e3e3..08ad9cd8a75 100644 --- a/framework/src/main/java/org/tron/core/consensus/ConsensusService.java +++ b/framework/src/main/java/org/tron/core/consensus/ConsensusService.java @@ -54,13 +54,13 @@ public void start() { List pqPublicKeys = Args.getLocalWitnesses().getPqPublicKeys(); if (pqPublicKeys.size() != pqPrivateKeys.size()) { throw new TronError( - "localwitness_pq_keys size mismatch: " + pqPrivateKeys.size() + "localwitness_pq.keys size mismatch: " + pqPrivateKeys.size() + " private vs " + pqPublicKeys.size() + " public", TronError.ErrCode.WITNESS_INIT); } if (!privateKeys.isEmpty() && !pqPrivateKeys.isEmpty()) { throw new TronError( - "legacy localwitness keys and localwitness_pq_keys are mutually exclusive", + "legacy localwitness keys and localwitness_pq.keys are mutually exclusive", TronError.ErrCode.WITNESS_INIT); } if (privateKeys.size() > 1) { diff --git a/framework/src/main/java/org/tron/core/net/service/relay/RelayService.java b/framework/src/main/java/org/tron/core/net/service/relay/RelayService.java index 61ae6326e9f..55ac1063f5f 100644 --- a/framework/src/main/java/org/tron/core/net/service/relay/RelayService.java +++ b/framework/src/main/java/org/tron/core/net/service/relay/RelayService.java @@ -17,10 +17,12 @@ import org.tron.common.backup.BackupManager.BackupStatusEnum; import org.tron.common.crypto.SignInterface; import org.tron.common.crypto.SignUtils; +import org.tron.common.crypto.pqc.PQSchemeRegistry; import org.tron.common.es.ExecutorServiceManager; import org.tron.common.log.layout.DesensitizedConverter; import org.tron.common.parameter.CommonParameter; import org.tron.common.utils.ByteArray; +import org.tron.common.utils.LocalWitnesses; import org.tron.common.utils.Sha256Hash; import org.tron.core.ChainBaseManager; import org.tron.core.capsule.TransactionCapsule; @@ -35,6 +37,8 @@ import org.tron.core.store.WitnessScheduleStore; import org.tron.p2p.connection.Channel; import org.tron.protos.Protocol; +import org.tron.protos.Protocol.PQAuthSig; +import org.tron.protos.Protocol.PQScheme; import org.tron.protos.Protocol.ReasonCode; @Slf4j(topic = "net") @@ -68,6 +72,8 @@ public class RelayService { private final int keySize = Args.getLocalWitnesses().getPrivateKeys().size(); + private final int pqKeySize = Args.getLocalWitnesses().getPqPrivateKeys().size(); + private final ByteString witnessAddress = Args.getLocalWitnesses().getWitnessAccountAddress() != null ? ByteString .copyFrom(Args.getLocalWitnesses().getWitnessAccountAddress()) : null; @@ -79,10 +85,12 @@ public void init() { witnessScheduleStore = ctx.getBean(WitnessScheduleStore.class); backupManager = ctx.getBean(BackupManager.class); - logger.info("Fast forward config, isWitness: {}, keySize: {}, fastForwardNodes: {}", - parameter.isWitness(), keySize, fastForwardNodes.size()); + logger.info( + "Fast forward config, isWitness: {}, keySize: {}, pqKeySize: {}, fastForwardNodes: {}", + parameter.isWitness(), keySize, pqKeySize, fastForwardNodes.size()); - if (!parameter.isWitness() || keySize == 0 || fastForwardNodes.isEmpty()) { + if (!parameter.isWitness() || (keySize == 0 && pqKeySize == 0) + || fastForwardNodes.isEmpty()) { return; } @@ -105,22 +113,39 @@ public void close() { } public void fillHelloMessage(HelloMessage message, Channel channel) { - if (isActiveWitness()) { - fastForwardNodes.forEach(address -> { - if (address.getAddress().equals(channel.getInetAddress())) { - SignInterface cryptoEngine = SignUtils - .fromPrivate(ByteArray.fromHexString(Args.getLocalWitnesses().getPrivateKey()), - Args.getInstance().isECKeyCryptoEngine()); - - ByteString sig = ByteString.copyFrom(cryptoEngine.Base64toBytes(cryptoEngine - .signHash(Sha256Hash.of(CommonParameter.getInstance() - .isECKeyCryptoEngine(), ByteArray.fromLong(message - .getTimestamp())).getBytes()))); - message.setHelloMessage(message.getHelloMessage().toBuilder() - .setAddress(witnessAddress).setSignature(sig).build()); - } - }); + if (!isActiveWitness()) { + return; } + fastForwardNodes.forEach(address -> { + if (!address.getAddress().equals(channel.getInetAddress())) { + return; + } + byte[] digest = Sha256Hash.of(CommonParameter.getInstance() + .isECKeyCryptoEngine(), ByteArray.fromLong(message.getTimestamp())) + .getBytes(); + Protocol.HelloMessage.Builder builder = message.getHelloMessage().toBuilder() + .setAddress(witnessAddress); + if (keySize > 0) { + SignInterface cryptoEngine = SignUtils.fromPrivate( + ByteArray.fromHexString(Args.getLocalWitnesses().getPrivateKey()), + Args.getInstance().isECKeyCryptoEngine()); + ByteString sig = ByteString.copyFrom( + cryptoEngine.Base64toBytes(cryptoEngine.signHash(digest))); + builder.setSignature(sig).clearPqAuthSig(); + } else { + LocalWitnesses lw = Args.getLocalWitnesses(); + PQScheme scheme = lw.getPqScheme(); + byte[] privKey = ByteArray.fromHexString(lw.getPqPrivateKeys().get(0)); + byte[] pubKey = ByteArray.fromHexString(lw.getPqPublicKeys().get(0)); + byte[] sig = PQSchemeRegistry.sign(scheme, privKey, digest); + builder.setPqAuthSig(PQAuthSig.newBuilder() + .setScheme(scheme) + .setPublicKey(ByteString.copyFrom(pubKey)) + .setSignature(ByteString.copyFrom(sig))) + .clearSignature(); + } + message.setHelloMessage(builder.build()); + }); } public boolean checkHelloMessage(HelloMessage message, Channel channel) { @@ -150,20 +175,22 @@ public boolean checkHelloMessage(HelloMessage message, Channel channel) { return false; } + boolean hasLegacy = !msg.getSignature().isEmpty(); + boolean hasPq = msg.hasPqAuthSig(); + if (hasLegacy == hasPq) { + logger.warn("HelloMessage from {}, signature/pq_auth_sig must be set exclusively.", + channel.getInetAddress()); + return false; + } + boolean flag; try { - Sha256Hash hash = Sha256Hash.of(CommonParameter - .getInstance().isECKeyCryptoEngine(), ByteArray.fromLong(msg.getTimestamp())); - String sig = - TransactionCapsule.getBase64FromByteString(msg.getSignature()); - byte[] sigAddress = SignUtils.signatureToAddress(hash.getBytes(), sig, - Args.getInstance().isECKeyCryptoEngine()); - if (manager.getDynamicPropertiesStore().getAllowMultiSign() != 1) { - flag = Arrays.equals(sigAddress, msg.getAddress().toByteArray()); + byte[] digest = Sha256Hash.of(CommonParameter.getInstance().isECKeyCryptoEngine(), + ByteArray.fromLong(msg.getTimestamp())).getBytes(); + if (hasPq) { + flag = verifyPqAuthSig(digest, msg.getPqAuthSig(), msg.getAddress(), channel); } else { - byte[] witnessPermissionAddress = manager.getAccountStore() - .get(msg.getAddress().toByteArray()).getWitnessPermissionAddress(); - flag = Arrays.equals(sigAddress, witnessPermissionAddress); + flag = verifyLegacySignature(digest, msg.getSignature(), msg.getAddress()); } if (flag) { TronNetService.getP2pConfig().getTrustNodes().add(channel.getInetAddress()); @@ -177,6 +204,61 @@ public boolean checkHelloMessage(HelloMessage message, Channel channel) { } } + private boolean verifyLegacySignature(byte[] digest, ByteString signature, + ByteString witnessAddr) throws java.security.SignatureException { + String sig = TransactionCapsule.getBase64FromByteString(signature); + byte[] sigAddress = SignUtils.signatureToAddress(digest, sig, + Args.getInstance().isECKeyCryptoEngine()); + if (manager.getDynamicPropertiesStore().getAllowMultiSign() != 1) { + return Arrays.equals(sigAddress, witnessAddr.toByteArray()); + } + byte[] witnessPermissionAddress = manager.getAccountStore() + .get(witnessAddr.toByteArray()).getWitnessPermissionAddress(); + return Arrays.equals(sigAddress, witnessPermissionAddress); + } + + private boolean verifyPqAuthSig(byte[] digest, PQAuthSig pqAuthSig, + ByteString witnessAddr, Channel channel) { + PQScheme scheme = pqAuthSig.getScheme(); + if (!PQSchemeRegistry.contains(scheme)) { + logger.warn("HelloMessage from {}, pq_auth_sig scheme {} is not registered.", + channel.getInetAddress(), scheme); + return false; + } + if (!manager.getDynamicPropertiesStore().isPqSchemeAllowed(scheme)) { + logger.warn("HelloMessage from {}, pq_auth_sig scheme {} is not activated on chain.", + channel.getInetAddress(), scheme); + return false; + } + byte[] publicKey = pqAuthSig.getPublicKey().toByteArray(); + if (publicKey.length != PQSchemeRegistry.getPublicKeyLength(scheme)) { + logger.warn("HelloMessage from {}, pq_auth_sig public key length mismatch for {}.", + channel.getInetAddress(), scheme); + return false; + } + byte[] signature = pqAuthSig.getSignature().toByteArray(); + if (!PQSchemeRegistry.isValidSignatureLength(scheme, signature.length)) { + logger.warn("HelloMessage from {}, pq_auth_sig signature length mismatch for {}.", + channel.getInetAddress(), scheme); + return false; + } + + byte[] derivedAddr = PQSchemeRegistry.computeAddress(scheme, publicKey); + byte[] expected; + if (manager.getDynamicPropertiesStore().getAllowMultiSign() != 1) { + expected = witnessAddr.toByteArray(); + } else { + expected = manager.getAccountStore().get(witnessAddr.toByteArray()) + .getWitnessPermissionAddress(); + } + if (!Arrays.equals(derivedAddr, expected)) { + logger.warn("HelloMessage from {}, pq_auth_sig public key does not bind witness {}.", + channel.getInetAddress(), ByteArray.toHexString(witnessAddr.toByteArray())); + return false; + } + return PQSchemeRegistry.verify(scheme, publicKey, digest, signature); + } + private long getPeerCountByAddress(ByteString address) { return tronNetDelegate.getActivePeer().stream() .filter(peer -> peer.getAddress() != null && peer.getAddress().equals(address)) @@ -185,7 +267,7 @@ private long getPeerCountByAddress(ByteString address) { private boolean isActiveWitness() { return parameter.isWitness() - && keySize > 0 + && (keySize > 0 || pqKeySize > 0) && fastForwardNodes.size() > 0 && witnessScheduleStore.getActiveWitnesses().contains(witnessAddress) && backupManager.getStatus().equals(BackupStatusEnum.MASTER); diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf index a0ed0877b74..8e0c77f8309 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -702,22 +702,18 @@ localwitness = [ # "localwitnesskeystore.json" # ] -# Scheme used by localwitness_pq_keys. Defaults to FN_DSA_512. -# V2 first launch only allows FN_DSA_512 (Falcon-512, FIPS 206 draft). -# localwitness_pq_scheme = "FN_DSA_512" - -# Post-quantum witness signing keypairs, hex-encoded. Each entry is the -# extended private key f‖g‖F‖h (priv ‖ pub) as one hex string. For FN_DSA_512 -# the total is 2176 bytes (4352 hex chars): 1280 B Falcon-512 private key -# (f‖g‖F) followed by the 896 B public key h. Operators MUST generate the -# keypair off-line on a single platform and distribute the extended key; -# on-node keygen is intentionally bypassed because BouncyCastle's Falcon -# FFT/FPR code paths are not declared strictfp and could in theory diverge -# across JVMs/architectures. Used only after the ALLOW_FN_DSA_512 proposal -# is active and the witness Permission has been upgraded to FN_DSA_512. -# localwitness_pq_keys = [ -# "<4352 hex chars>" -# ] +# Post-quantum witness signing. `scheme` selects the PQ algorithm; only +# FN_DSA_512 is currently supported. Each `keys` entry is a hex-encoded +# extended private key (priv‖pub), 4352 hex chars for FN_DSA_512. Keypairs +# must be generated off-line — on-node keygen is intentionally bypassed. +# Effective only after the ALLOW_FN_DSA_512 proposal is active and the +# witness Permission has been upgraded to FN_DSA_512. +# localwitness_pq = { +# scheme = "FN_DSA_512" +# keys = [ +# "<4352 hex chars>" +# ] +# } block = { needSyncCheck = true diff --git a/framework/src/test/java/org/tron/common/crypto/pqc/program/PQWitnessNode.java b/framework/src/test/java/org/tron/common/crypto/pqc/program/PQWitnessNode.java index 6d4e688445e..ae538ce95e7 100644 --- a/framework/src/test/java/org/tron/common/crypto/pqc/program/PQWitnessNode.java +++ b/framework/src/test/java/org/tron/common/crypto/pqc/program/PQWitnessNode.java @@ -94,7 +94,7 @@ public static void main(String[] args) throws Exception { dbDir.deleteOnExit(); // Inject the witness keypair via a temp HOCON config that includes - // config-test.conf and overrides localwitness_pq_keys with the extended + // config-test.conf and overrides localwitness_pq.keys with the extended // priv‖pub hex derived from WITNESS_SEED (matches what PQClient derives). Path conf = writeWitnessConfig(witnessKp); @@ -194,10 +194,12 @@ private static Path writeWitnessConfig(FNDSA512 witnessKp) throws java.io.IOExce Path conf = Files.createTempFile("pqc-witness-", ".conf"); conf.toFile().deleteOnExit(); String body = "include classpath(\"config-test.conf\")\n" - + "localwitness_pq_scheme = \"FN_DSA_512\"\n" - + "localwitness_pq_keys = [\n" - + " \"" + Hex.toHexString(witnessKp.getPrivateKeyWithPublicKey()) + "\"\n" - + "]\n"; + + "localwitness_pq = {\n" + + " scheme = \"FN_DSA_512\"\n" + + " keys = [\n" + + " \"" + Hex.toHexString(witnessKp.getPrivateKeyWithPublicKey()) + "\"\n" + + " ]\n" + + "}\n"; Files.write(conf, body.getBytes(StandardCharsets.UTF_8)); return conf; } diff --git a/framework/src/test/java/org/tron/core/exception/TronErrorTest.java b/framework/src/test/java/org/tron/core/exception/TronErrorTest.java index 11538bd967e..14e4cdb4d7a 100644 --- a/framework/src/test/java/org/tron/core/exception/TronErrorTest.java +++ b/framework/src/test/java/org/tron/core/exception/TronErrorTest.java @@ -123,7 +123,7 @@ public void witnessInitTest() throws IOException { Path conf = temporaryFolder.newFile("no-witness.conf").toPath(); String content = "include classpath(\"" + TestConstants.TEST_CONF + "\")\n" + "localwitness = []\n" - + "localwitness_pq_keys = []\n"; + + "localwitness_pq.keys = []\n"; Files.write(conf, content.getBytes()); TronError thrown = assertThrows(TronError.class, () -> { Args.setParam(new String[]{"--witness"}, conf.toString()); diff --git a/framework/src/test/java/org/tron/core/net/services/RelayServiceTest.java b/framework/src/test/java/org/tron/core/net/services/RelayServiceTest.java index 8585244b941..35ff252a887 100644 --- a/framework/src/test/java/org/tron/core/net/services/RelayServiceTest.java +++ b/framework/src/test/java/org/tron/core/net/services/RelayServiceTest.java @@ -26,6 +26,8 @@ import org.tron.common.TestConstants; import org.tron.common.crypto.SignInterface; import org.tron.common.crypto.SignUtils; +import org.tron.common.crypto.pqc.FNDSA512; +import org.tron.common.crypto.pqc.PQSchemeRegistry; import org.tron.common.parameter.CommonParameter; import org.tron.common.utils.ByteArray; import org.tron.common.utils.ReflectUtils; @@ -48,6 +50,8 @@ import org.tron.p2p.discover.Node; import org.tron.p2p.utils.NetUtil; import org.tron.protos.Protocol; +import org.tron.protos.Protocol.PQAuthSig; +import org.tron.protos.Protocol.PQScheme; @Slf4j(topic = "net") public class RelayServiceTest extends BaseTest { @@ -226,6 +230,118 @@ private void testCheckHelloMessage() { } } + @Test + public void testPqHelloMessage() throws Exception { + FNDSA512 pqKeypair = new FNDSA512(); + byte[] pqAddress = PQSchemeRegistry.computeAddress( + PQScheme.FN_DSA_512, pqKeypair.getPublicKey()); + ByteString pqAddressBs = ByteString.copyFrom(pqAddress); + + // Snapshot prior active-witness list (if any) so other tests are not perturbed. + List previousActive; + try { + previousActive = new ArrayList<>( + chainBaseManager.getWitnessScheduleStore().getActiveWitnesses()); + } catch (Exception ignored) { + previousActive = null; + } + List active = previousActive == null + ? new ArrayList<>() : new ArrayList<>(previousActive); + if (!active.contains(pqAddressBs)) { + active.add(pqAddressBs); + } + chainBaseManager.getWitnessScheduleStore().saveActiveWitnesses(active); + + // Activate FN-DSA-512 on chain so verifyPqAuthSig accepts the scheme. + long previousAllowFnDsa = chainBaseManager.getDynamicPropertiesStore().getAllowFnDsa512(); + chainBaseManager.getDynamicPropertiesStore().saveAllowFnDsa512(1L); + + Args.getInstance().fastForward = true; + + InetSocketAddress addr = new InetSocketAddress("127.0.0.1", 10001); + Node node = new Node(NetUtil.getNodeId(), addr.getAddress().getHostAddress(), + null, addr.getPort()); + HelloMessage helloMessage = new HelloMessage(node, System.currentTimeMillis(), + ChainBaseManager.getChainBaseManager()); + byte[] digest = Sha256Hash.of(CommonParameter.getInstance().isECKeyCryptoEngine(), + ByteArray.fromLong(helloMessage.getTimestamp())).getBytes(); + byte[] pqSig = FNDSA512.sign(pqKeypair.getPrivateKey(), digest); + PQAuthSig pqAuthSig = PQAuthSig.newBuilder() + .setScheme(PQScheme.FN_DSA_512) + .setPublicKey(ByteString.copyFrom(pqKeypair.getPublicKey())) + .setSignature(ByteString.copyFrom(pqSig)) + .build(); + + Protocol.HelloMessage base = helloMessage.getHelloMessage().toBuilder() + .setAddress(pqAddressBs) + .clearSignature() + .setPqAuthSig(pqAuthSig) + .build(); + helloMessage.setHelloMessage(base); + + Channel channel = mock(Channel.class); + Mockito.when(channel.getInetSocketAddress()).thenReturn(addr); + Mockito.when(channel.getInetAddress()).thenReturn(addr.getAddress()); + PeerManager.add((ApplicationContext) ReflectUtils.getFieldObject(p2pEventHandler, "ctx"), + channel).setAddress(pqAddressBs); + + ReflectUtils.setFieldValue(tronNetService, "p2pConfig", new P2pConfig()); + Field scheduleField = service.getClass().getDeclaredField("witnessScheduleStore"); + scheduleField.setAccessible(true); + scheduleField.set(service, chainBaseManager.getWitnessScheduleStore()); + Field managerField = service.getClass().getDeclaredField("manager"); + managerField.setAccessible(true); + managerField.set(service, dbManager); + + try { + // Happy path: valid PQ-only signature. + Assert.assertTrue(service.checkHelloMessage(helloMessage, channel)); + + // Both legacy signature and pq_auth_sig set → mutex rejects. + helloMessage.setHelloMessage(base.toBuilder() + .setSignature(ByteString.copyFrom(new byte[]{0x01})) + .build()); + Assert.assertFalse(service.checkHelloMessage(helloMessage, channel)); + + // Neither legacy signature nor pq_auth_sig set → mutex rejects. + helloMessage.setHelloMessage(base.toBuilder() + .clearSignature() + .clearPqAuthSig() + .build()); + Assert.assertFalse(service.checkHelloMessage(helloMessage, channel)); + + // PQ public key length mismatch → reject. + helloMessage.setHelloMessage(base.toBuilder() + .setPqAuthSig(pqAuthSig.toBuilder() + .setPublicKey(ByteString.copyFrom(new byte[]{0x00}))) + .build()); + Assert.assertFalse(service.checkHelloMessage(helloMessage, channel)); + + // Derived PQ address does not match the claimed witness address → reject. + FNDSA512 strayKeypair = new FNDSA512(); + byte[] strayDigest = Sha256Hash.of(CommonParameter.getInstance().isECKeyCryptoEngine(), + ByteArray.fromLong(helloMessage.getTimestamp())).getBytes(); + byte[] straySig = FNDSA512.sign(strayKeypair.getPrivateKey(), strayDigest); + helloMessage.setHelloMessage(base.toBuilder() + .setPqAuthSig(PQAuthSig.newBuilder() + .setScheme(PQScheme.FN_DSA_512) + .setPublicKey(ByteString.copyFrom(strayKeypair.getPublicKey())) + .setSignature(ByteString.copyFrom(straySig))) + .build()); + Assert.assertFalse(service.checkHelloMessage(helloMessage, channel)); + + // Scheme not activated on chain → reject. + helloMessage.setHelloMessage(base); + chainBaseManager.getDynamicPropertiesStore().saveAllowFnDsa512(0L); + Assert.assertFalse(service.checkHelloMessage(helloMessage, channel)); + } finally { + chainBaseManager.getDynamicPropertiesStore().saveAllowFnDsa512(previousAllowFnDsa); + if (previousActive != null) { + chainBaseManager.getWitnessScheduleStore().saveActiveWitnesses(previousActive); + } + } + } + @Test public void testNullWitnessAddress() { try { diff --git a/framework/src/test/resources/config-test.conf b/framework/src/test/resources/config-test.conf index 99afce48263..ce3652af4a3 100644 --- a/framework/src/test/resources/config-test.conf +++ b/framework/src/test/resources/config-test.conf @@ -354,9 +354,11 @@ genesis.block = { localwitness = [ ] -localwitness_pq_scheme = "FN_DSA_512" -localwitness_pq_keys = [ -] +localwitness_pq = { + scheme = "FN_DSA_512" + keys = [ + ] +} block = { needSyncCheck = true # first node : false, other : true diff --git a/protocol/src/main/protos/core/Tron.proto b/protocol/src/main/protos/core/Tron.proto index 208f4337314..1745a73a928 100644 --- a/protocol/src/main/protos/core/Tron.proto +++ b/protocol/src/main/protos/core/Tron.proto @@ -657,10 +657,17 @@ message HelloMessage { BlockId solidBlockId = 5; BlockId headBlockId = 6; bytes address = 7; + // Legacy ECDSA signature over Sha256Hash(timestamp). Mutually exclusive + // with pq_auth_sig — exactly one of the two must be set by an active + // witness when fast-forward is enabled. bytes signature = 8; int32 nodeType = 9; int64 lowestBlockNum = 10; bytes codeVersion = 11; + // Post-quantum auth signature over Sha256Hash(timestamp). Set instead of + // `signature` when the local witness is PQ-only. Verifier only accepts + // this field after ALLOW_FN_DSA_512 is activated on chain. + PQAuthSig pq_auth_sig = 12; } message InternalTransaction { From 7c9715f9d7b9d39ea5facc350cb7bf47e93fde26 Mon Sep 17 00:00:00 2001 From: federico Date: Thu, 14 May 2026 10:04:08 +0700 Subject: [PATCH 5/9] feat(consensus): support PQ signatures in PBFT messages --- .../pbft/message/PbftBaseMessage.java | 37 ++- .../consensus/pbft/message/PbftMessage.java | 34 ++- .../net/messagehandler/PbftMsgHandler.java | 15 ++ .../org/tron/core/pbft/PbftPQMessageTest.java | 233 ++++++++++++++++++ protocol/src/main/protos/core/Tron.proto | 6 + 5 files changed, 316 insertions(+), 9 deletions(-) create mode 100644 framework/src/test/java/org/tron/core/pbft/PbftPQMessageTest.java diff --git a/consensus/src/main/java/org/tron/consensus/pbft/message/PbftBaseMessage.java b/consensus/src/main/java/org/tron/consensus/pbft/message/PbftBaseMessage.java index 4eb61f3e22e..82768f0ef33 100644 --- a/consensus/src/main/java/org/tron/consensus/pbft/message/PbftBaseMessage.java +++ b/consensus/src/main/java/org/tron/consensus/pbft/message/PbftBaseMessage.java @@ -6,6 +6,7 @@ import java.util.stream.Collectors; import org.bouncycastle.util.encoders.Hex; import org.tron.common.crypto.ECKey; +import org.tron.common.crypto.pqc.PQSchemeRegistry; import org.tron.common.overlay.message.Message; import org.tron.common.utils.ByteUtil; import org.tron.common.utils.Sha256Hash; @@ -14,6 +15,8 @@ import org.tron.core.exception.P2pException; import org.tron.protos.Protocol.PBFTMessage; import org.tron.protos.Protocol.PBFTMessage.DataType; +import org.tron.protos.Protocol.PQAuthSig; +import org.tron.protos.Protocol.PQScheme; import org.tron.protos.Protocol.SRL; public abstract class PbftBaseMessage extends Message { @@ -96,8 +99,38 @@ public DataType getDataType() { public void analyzeSignature() throws SignatureException { byte[] hash = Sha256Hash.hash(true, getPbftMessage().getRawData().toByteArray()); - publicKey = ECKey.signatureToAddress(hash, TransactionCapsule - .getBase64FromByteString(getPbftMessage().getSignature())); + boolean hasLegacy = !getPbftMessage().getSignature().isEmpty(); + boolean hasPq = getPbftMessage().hasPqAuthSig(); + if (hasLegacy == hasPq) { + throw new SignatureException( + "pbft message must set exactly one of signature / pq_auth_sig"); + } + if (hasPq) { + publicKey = verifyPqAuthSig(hash, getPbftMessage().getPqAuthSig()); + } else { + publicKey = ECKey.signatureToAddress(hash, TransactionCapsule + .getBase64FromByteString(getPbftMessage().getSignature())); + } + } + + private static byte[] verifyPqAuthSig(byte[] hash, PQAuthSig pqAuthSig) + throws SignatureException { + PQScheme scheme = pqAuthSig.getScheme(); + if (!PQSchemeRegistry.contains(scheme)) { + throw new SignatureException("pbft pq_auth_sig scheme not registered: " + scheme); + } + byte[] publicKey = pqAuthSig.getPublicKey().toByteArray(); + if (publicKey.length != PQSchemeRegistry.getPublicKeyLength(scheme)) { + throw new SignatureException("pbft pq_auth_sig public key length mismatch for " + scheme); + } + byte[] signature = pqAuthSig.getSignature().toByteArray(); + if (!PQSchemeRegistry.isValidSignatureLength(scheme, signature.length)) { + throw new SignatureException("pbft pq_auth_sig signature length mismatch for " + scheme); + } + if (!PQSchemeRegistry.verify(scheme, publicKey, hash, signature)) { + throw new SignatureException("pbft pq_auth_sig verification failed for " + scheme); + } + return PQSchemeRegistry.computeAddress(scheme, publicKey); } @Override diff --git a/consensus/src/main/java/org/tron/consensus/pbft/message/PbftMessage.java b/consensus/src/main/java/org/tron/consensus/pbft/message/PbftMessage.java index b6de49ee878..170c14b80eb 100644 --- a/consensus/src/main/java/org/tron/consensus/pbft/message/PbftMessage.java +++ b/consensus/src/main/java/org/tron/consensus/pbft/message/PbftMessage.java @@ -4,15 +4,19 @@ import java.util.List; import org.tron.common.crypto.ECKey; import org.tron.common.crypto.ECKey.ECDSASignature; +import org.tron.common.crypto.pqc.PQSchemeRegistry; import org.tron.common.utils.Sha256Hash; import org.tron.consensus.base.Param; import org.tron.consensus.base.Param.Miner; +import org.tron.consensus.base.Param.MinerType; import org.tron.core.capsule.BlockCapsule; import org.tron.core.net.message.MessageTypes; import org.tron.protos.Protocol.PBFTMessage; import org.tron.protos.Protocol.PBFTMessage.DataType; import org.tron.protos.Protocol.PBFTMessage.MsgType; import org.tron.protos.Protocol.PBFTMessage.Raw; +import org.tron.protos.Protocol.PQAuthSig; +import org.tron.protos.Protocol.PQScheme; import org.tron.protos.Protocol.SRL; public class PbftMessage extends PbftBaseMessage { @@ -56,15 +60,13 @@ public static PbftMessage fullNodePrePrepareSRLMsg(BlockCapsule block, private static PbftMessage buildCommon(DataType dataType, ByteString data, BlockCapsule block, long epoch, long viewN, Miner miner) { PbftMessage pbftMessage = new PbftMessage(); - ECKey ecKey = ECKey.fromPrivate(miner.getPrivateKey()); Raw.Builder rawBuilder = Raw.newBuilder(); PBFTMessage.Builder builder = PBFTMessage.newBuilder(); rawBuilder.setViewN(viewN).setEpoch(epoch).setDataType(dataType) .setMsgType(MsgType.PREPREPARE).setData(data); Raw raw = rawBuilder.build(); byte[] hash = Sha256Hash.hash(true, raw.toByteArray()); - ECDSASignature signature = ecKey.sign(hash); - builder.setRawData(raw).setSignature(ByteString.copyFrom(signature.toByteArray())); + signRaw(builder, raw, hash, miner); PBFTMessage message = builder.build(); pbftMessage.setType(MessageTypes.PBFT_MSG.asByte()) .setPbftMessage(message).setData(message.toByteArray()).setSwitch(block.isSwitch()); @@ -96,7 +98,6 @@ public PbftMessage buildCommitMessage(Miner miner) { private PbftMessage buildMessageCapsule(MsgType type, Miner miner) { PbftMessage pbftMessage = new PbftMessage(); - ECKey ecKey = ECKey.fromPrivate(miner.getPrivateKey()); PBFTMessage.Builder builder = PBFTMessage.newBuilder(); Raw.Builder rawBuilder = Raw.newBuilder(); rawBuilder.setViewN(getPbftMessage().getRawData().getViewN()) @@ -105,11 +106,30 @@ private PbftMessage buildMessageCapsule(MsgType type, Miner miner) { .setData(getPbftMessage().getRawData().getData()); Raw raw = rawBuilder.build(); byte[] hash = Sha256Hash.hash(true, raw.toByteArray()); - ECDSASignature signature = ecKey.sign(hash); - builder.setRawData(raw).setSignature(ByteString.copyFrom(signature.toByteArray())); + signRaw(builder, raw, hash, miner); PBFTMessage message = builder.build(); pbftMessage.setType(getType().asByte()) .setPbftMessage(message).setData(message.toByteArray()); return pbftMessage; } -} \ No newline at end of file + + private static void signRaw(PBFTMessage.Builder builder, Raw raw, byte[] hash, Miner miner) { + builder.setRawData(raw); + if (miner.getType() == MinerType.PQ) { + PQScheme scheme = miner.getPqScheme(); + byte[] sk = miner.getPQPrivateKey(); + byte[] pk = miner.getPQPublicKey(); + byte[] sig = PQSchemeRegistry.sign(scheme, sk, hash); + builder.setPqAuthSig(PQAuthSig.newBuilder() + .setScheme(scheme) + .setPublicKey(ByteString.copyFrom(pk)) + .setSignature(ByteString.copyFrom(sig))) + .clearSignature(); + } else { + ECKey ecKey = ECKey.fromPrivate(miner.getPrivateKey()); + ECDSASignature signature = ecKey.sign(hash); + builder.setSignature(ByteString.copyFrom(signature.toByteArray())) + .clearPqAuthSig(); + } + } +} diff --git a/framework/src/main/java/org/tron/core/net/messagehandler/PbftMsgHandler.java b/framework/src/main/java/org/tron/core/net/messagehandler/PbftMsgHandler.java index d086cc28b6c..ec0648c4d2e 100644 --- a/framework/src/main/java/org/tron/core/net/messagehandler/PbftMsgHandler.java +++ b/framework/src/main/java/org/tron/core/net/messagehandler/PbftMsgHandler.java @@ -5,19 +5,23 @@ import com.google.common.util.concurrent.Striped; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.tron.consensus.base.Param; import org.tron.consensus.pbft.PbftManager; import org.tron.consensus.pbft.message.PbftBaseMessage; import org.tron.consensus.pbft.message.PbftMessage; +import org.tron.core.ChainBaseManager; import org.tron.core.config.args.Args; import org.tron.core.exception.P2pException; import org.tron.core.net.TronNetDelegate; import org.tron.core.net.TronNetService; import org.tron.core.net.peer.PeerConnection; import org.tron.protos.Protocol.PBFTMessage.DataType; +import org.tron.protos.Protocol.PQScheme; +@Slf4j(topic = "pbft") @Component public class PbftMsgHandler { @@ -32,6 +36,9 @@ public class PbftMsgHandler { @Autowired private TronNetDelegate tronNetDelegate; + @Autowired + private ChainBaseManager chainBaseManager; + public void processMessage(PeerConnection peer, PbftMessage msg) throws Exception { if (!tronNetDelegate.allowPBFT()) { return; @@ -50,6 +57,14 @@ public void processMessage(PeerConnection peer, PbftMessage msg) throws Exceptio && currentEpoch - msg.getEpoch() > expireEpoch) { return; } + if (msg.getPbftMessage().hasPqAuthSig()) { + PQScheme scheme = msg.getPbftMessage().getPqAuthSig().getScheme(); + if (!chainBaseManager.getDynamicPropertiesStore().isPqSchemeAllowed(scheme)) { + logger.warn("Pbft message from {}, pq_auth_sig scheme {} is not activated on chain.", + peer.getInetAddress(), scheme); + return; + } + } msg.analyzeSignature(); String key = buildKey(msg); Lock lock = striped.get(key); diff --git a/framework/src/test/java/org/tron/core/pbft/PbftPQMessageTest.java b/framework/src/test/java/org/tron/core/pbft/PbftPQMessageTest.java new file mode 100644 index 00000000000..a3247b448cd --- /dev/null +++ b/framework/src/test/java/org/tron/core/pbft/PbftPQMessageTest.java @@ -0,0 +1,233 @@ +package org.tron.core.pbft; + +import static org.junit.Assert.assertArrayEquals; +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 com.google.protobuf.ByteString; +import java.security.SignatureException; +import org.bouncycastle.util.encoders.Hex; +import org.junit.Test; +import org.tron.common.crypto.ECKey; +import org.tron.common.crypto.pqc.FNDSA512; +import org.tron.common.crypto.pqc.PQSchemeRegistry; +import org.tron.common.utils.ByteArray; +import org.tron.common.utils.Sha256Hash; +import org.tron.consensus.base.Param; +import org.tron.consensus.base.Param.Miner; +import org.tron.consensus.base.Param.MinerType; +import org.tron.consensus.pbft.message.PbftMessage; +import org.tron.core.capsule.BlockCapsule; +import org.tron.protos.Protocol.Block; +import org.tron.protos.Protocol.PBFTMessage; +import org.tron.protos.Protocol.PQAuthSig; +import org.tron.protos.Protocol.PQScheme; + +public class PbftPQMessageTest { + + private static Miner pqMiner(FNDSA512 kp) { + byte[] address = PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, kp.getPublicKey()); + ByteString addressBs = ByteString.copyFrom(address); + Miner miner = Param.getInstance().new Miner(null, addressBs, addressBs); + miner.setPQPrivateKey(kp.getPrivateKey()); + miner.setPQPublicKey(kp.getPublicKey()); + miner.setPqScheme(PQScheme.FN_DSA_512); + miner.setType(MinerType.PQ); + return miner; + } + + private static Miner ecdsaMiner() { + ECKey key = new ECKey(); + ByteString addressBs = ByteString.copyFrom(key.getAddress()); + return Param.getInstance().new Miner( + key.getPrivKeyBytes(), addressBs, addressBs); + } + + private static BlockCapsule emptyBlock() { + return new BlockCapsule(Block.getDefaultInstance()); + } + + /** ECDSA path is unchanged: analyzeSignature recovers the signer address. */ + @Test + public void testEcdsaHappyPath() throws Exception { + Miner miner = ecdsaMiner(); + PbftMessage msg = PbftMessage.prePrepareBlockMsg(emptyBlock(), 1, miner); + assertFalse(msg.getPbftMessage().getSignature().isEmpty()); + assertFalse(msg.getPbftMessage().hasPqAuthSig()); + msg.analyzeSignature(); + assertArrayEquals(miner.getWitnessAddress().toByteArray(), msg.getPublicKey()); + } + + /** PQ miner produces a pbft message with pq_auth_sig populated and signature cleared. */ + @Test + public void testPqHappyPath() throws Exception { + FNDSA512 kp = new FNDSA512(); + Miner miner = pqMiner(kp); + + PbftMessage msg = PbftMessage.prePrepareBlockMsg(emptyBlock(), 1, miner); + assertTrue(msg.getPbftMessage().getSignature().isEmpty()); + assertTrue(msg.getPbftMessage().hasPqAuthSig()); + PQAuthSig pqAuthSig = msg.getPbftMessage().getPqAuthSig(); + assertEquals(PQScheme.FN_DSA_512, pqAuthSig.getScheme()); + assertArrayEquals(kp.getPublicKey(), pqAuthSig.getPublicKey().toByteArray()); + + msg.analyzeSignature(); + assertArrayEquals(miner.getWitnessAddress().toByteArray(), msg.getPublicKey()); + } + + /** PREPARE / COMMIT round-trip also signs with the PQ key. */ + @Test + public void testPqPrepareAndCommit() throws Exception { + FNDSA512 kp = new FNDSA512(); + Miner miner = pqMiner(kp); + PbftMessage pre = PbftMessage.prePrepareBlockMsg(emptyBlock(), 1, miner); + + PbftMessage prepare = pre.buildPrePareMessage(miner); + assertTrue(prepare.getPbftMessage().hasPqAuthSig()); + prepare.analyzeSignature(); + assertArrayEquals(miner.getWitnessAddress().toByteArray(), prepare.getPublicKey()); + + PbftMessage commit = pre.buildCommitMessage(miner); + assertTrue(commit.getPbftMessage().hasPqAuthSig()); + commit.analyzeSignature(); + assertArrayEquals(miner.getWitnessAddress().toByteArray(), commit.getPublicKey()); + } + + /** Both signature and pq_auth_sig present → reject. */ + @Test + public void testMutexBothSet() throws Exception { + FNDSA512 kp = new FNDSA512(); + Miner miner = pqMiner(kp); + PbftMessage msg = PbftMessage.prePrepareBlockMsg(emptyBlock(), 1, miner); + + PBFTMessage tampered = msg.getPbftMessage().toBuilder() + .setSignature(ByteString.copyFrom(new byte[65])) + .build(); + PbftMessage rebuilt = rebuild(msg, tampered); + SignatureException ex = assertThrows(SignatureException.class, rebuilt::analyzeSignature); + assertTrue(ex.getMessage().contains("exactly one")); + } + + /** Neither signature nor pq_auth_sig present → reject. */ + @Test + public void testMutexNeitherSet() throws Exception { + Miner miner = ecdsaMiner(); + PbftMessage msg = PbftMessage.prePrepareBlockMsg(emptyBlock(), 1, miner); + + PBFTMessage tampered = msg.getPbftMessage().toBuilder() + .clearSignature() + .clearPqAuthSig() + .build(); + PbftMessage rebuilt = rebuild(msg, tampered); + SignatureException ex = assertThrows(SignatureException.class, rebuilt::analyzeSignature); + assertTrue(ex.getMessage().contains("exactly one")); + } + + /** Scheme not registered → reject. */ + @Test + public void testPqSchemeNotRegistered() throws Exception { + FNDSA512 kp = new FNDSA512(); + Miner miner = pqMiner(kp); + PbftMessage msg = PbftMessage.prePrepareBlockMsg(emptyBlock(), 1, miner); + + PBFTMessage tampered = msg.getPbftMessage().toBuilder() + .setPqAuthSig(msg.getPbftMessage().getPqAuthSig().toBuilder() + .setScheme(PQScheme.UNKNOWN_PQ_SCHEME)) + .build(); + PbftMessage rebuilt = rebuild(msg, tampered); + SignatureException ex = assertThrows(SignatureException.class, rebuilt::analyzeSignature); + assertTrue(ex.getMessage().contains("scheme not registered")); + } + + /** Public-key length mismatch → reject. */ + @Test + public void testPqBadPublicKeyLength() throws Exception { + FNDSA512 kp = new FNDSA512(); + Miner miner = pqMiner(kp); + PbftMessage msg = PbftMessage.prePrepareBlockMsg(emptyBlock(), 1, miner); + + byte[] shortPk = new byte[FNDSA512.PUBLIC_KEY_LENGTH - 1]; + PBFTMessage tampered = msg.getPbftMessage().toBuilder() + .setPqAuthSig(msg.getPbftMessage().getPqAuthSig().toBuilder() + .setPublicKey(ByteString.copyFrom(shortPk))) + .build(); + PbftMessage rebuilt = rebuild(msg, tampered); + SignatureException ex = assertThrows(SignatureException.class, rebuilt::analyzeSignature); + assertTrue(ex.getMessage().contains("public key length mismatch")); + } + + /** Signature length above protocol cap → reject. */ + @Test + public void testPqBadSignatureLength() throws Exception { + FNDSA512 kp = new FNDSA512(); + Miner miner = pqMiner(kp); + PbftMessage msg = PbftMessage.prePrepareBlockMsg(emptyBlock(), 1, miner); + + byte[] oversized = new byte[FNDSA512.SIGNATURE_LENGTH + 1]; + PBFTMessage tampered = msg.getPbftMessage().toBuilder() + .setPqAuthSig(msg.getPbftMessage().getPqAuthSig().toBuilder() + .setSignature(ByteString.copyFrom(oversized))) + .build(); + PbftMessage rebuilt = rebuild(msg, tampered); + SignatureException ex = assertThrows(SignatureException.class, rebuilt::analyzeSignature); + assertTrue(ex.getMessage().contains("signature length mismatch")); + } + + /** Public key replaced with a stray keypair → verify fails. */ + @Test + public void testPqSignatureFromWrongKey() throws Exception { + FNDSA512 kp = new FNDSA512(); + Miner miner = pqMiner(kp); + PbftMessage msg = PbftMessage.prePrepareBlockMsg(emptyBlock(), 1, miner); + + FNDSA512 stranger = new FNDSA512(); + PBFTMessage tampered = msg.getPbftMessage().toBuilder() + .setPqAuthSig(msg.getPbftMessage().getPqAuthSig().toBuilder() + .setPublicKey(ByteString.copyFrom(stranger.getPublicKey()))) + .build(); + PbftMessage rebuilt = rebuild(msg, tampered); + SignatureException ex = assertThrows(SignatureException.class, rebuilt::analyzeSignature); + assertTrue(ex.getMessage().contains("verification failed")); + } + + /** Hand-built PQ signature recovers the derived witness address. */ + @Test + public void testManualPqAuthSig() throws Exception { + FNDSA512 kp = new FNDSA512(); + byte[] expected = PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, kp.getPublicKey()); + + PBFTMessage.Raw raw = PBFTMessage.Raw.newBuilder() + .setViewN(1) + .setEpoch(1) + .setDataType(PBFTMessage.DataType.BLOCK) + .setMsgType(PBFTMessage.MsgType.PREPREPARE) + .setData(ByteString.copyFrom(ByteArray.fromHexString("abcd"))) + .build(); + byte[] hash = Sha256Hash.hash(true, raw.toByteArray()); + byte[] sig = PQSchemeRegistry.sign(PQScheme.FN_DSA_512, kp.getPrivateKey(), hash); + + PBFTMessage message = PBFTMessage.newBuilder() + .setRawData(raw) + .setPqAuthSig(PQAuthSig.newBuilder() + .setScheme(PQScheme.FN_DSA_512) + .setPublicKey(ByteString.copyFrom(kp.getPublicKey())) + .setSignature(ByteString.copyFrom(sig))) + .build(); + PbftMessage pbft = new PbftMessage(); + pbft.setPbftMessage(message); + pbft.setData(message.toByteArray()); + pbft.analyzeSignature(); + assertEquals(Hex.toHexString(expected), Hex.toHexString(pbft.getPublicKey())); + } + + private static PbftMessage rebuild(PbftMessage original, PBFTMessage replacement) { + PbftMessage rebuilt = new PbftMessage(); + rebuilt.setType(original.getType().asByte()); + rebuilt.setPbftMessage(replacement); + rebuilt.setData(replacement.toByteArray()); + rebuilt.setSwitch(original.isSwitch()); + return rebuilt; + } +} diff --git a/protocol/src/main/protos/core/Tron.proto b/protocol/src/main/protos/core/Tron.proto index 1745a73a928..512b2898aea 100644 --- a/protocol/src/main/protos/core/Tron.proto +++ b/protocol/src/main/protos/core/Tron.proto @@ -920,7 +920,13 @@ message PBFTMessage { bytes data = 5; } Raw raw_data = 1; + // Legacy ECDSA signature over Sha256Hash(raw_data). Mutually exclusive with + // pq_auth_sig — exactly one of the two must be set per pbft message. bytes signature = 2; + // Post-quantum auth signature over Sha256Hash(raw_data). Set instead of + // `signature` when the local witness is PQ-only. Verifier only accepts + // this field after the corresponding PQ scheme is activated on chain. + PQAuthSig pq_auth_sig = 3; } message PBFTCommitResult { From fe78125b1e938405b57880e8a7e3c2167cd0bf05 Mon Sep 17 00:00:00 2001 From: federico Date: Thu, 14 May 2026 12:14:22 +0800 Subject: [PATCH 6/9] feat(consensus): aggregate PQ commit signatures and verify on sync --- .../tron/core/capsule/PbftSignCapsule.java | 16 +- .../consensus/pbft/PbftMessageAction.java | 9 +- .../consensus/pbft/PbftMessageHandle.java | 23 +- .../messagehandler/PbftDataSyncHandler.java | 120 ++++++--- .../PbftDataSyncHandlerPQTest.java | 230 ++++++++++++++++++ protocol/src/main/protos/core/Tron.proto | 8 + 6 files changed, 370 insertions(+), 36 deletions(-) create mode 100644 framework/src/test/java/org/tron/core/net/messagehandler/PbftDataSyncHandlerPQTest.java diff --git a/chainbase/src/main/java/org/tron/core/capsule/PbftSignCapsule.java b/chainbase/src/main/java/org/tron/core/capsule/PbftSignCapsule.java index 14835cb01b5..7594e8add41 100644 --- a/chainbase/src/main/java/org/tron/core/capsule/PbftSignCapsule.java +++ b/chainbase/src/main/java/org/tron/core/capsule/PbftSignCapsule.java @@ -2,11 +2,13 @@ import com.google.protobuf.ByteString; import com.google.protobuf.InvalidProtocolBufferException; +import java.util.Collections; import java.util.Deque; import java.util.List; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.tron.protos.Protocol.PBFTCommitResult; +import org.tron.protos.Protocol.PQAuthSig; @Slf4j(topic = "pbft") public class PbftSignCapsule implements ProtoCapsule { @@ -23,8 +25,18 @@ public PbftSignCapsule(byte[] data) { } public PbftSignCapsule(ByteString data, List signList) { - PBFTCommitResult.Builder builder = PBFTCommitResult.newBuilder(); - builder.setData(data).addAllSignature(signList); + this(data, signList, Collections.emptyList()); + } + + public PbftSignCapsule(ByteString data, List signList, + List pqSignList) { + PBFTCommitResult.Builder builder = PBFTCommitResult.newBuilder().setData(data); + if (signList != null && !signList.isEmpty()) { + builder.addAllSignature(signList); + } + if (pqSignList != null && !pqSignList.isEmpty()) { + builder.addAllPqSignature(pqSignList); + } pbftCommitResult = builder.build(); } diff --git a/consensus/src/main/java/org/tron/consensus/pbft/PbftMessageAction.java b/consensus/src/main/java/org/tron/consensus/pbft/PbftMessageAction.java index c4ee235ff2d..ad0c108a98c 100644 --- a/consensus/src/main/java/org/tron/consensus/pbft/PbftMessageAction.java +++ b/consensus/src/main/java/org/tron/consensus/pbft/PbftMessageAction.java @@ -10,6 +10,7 @@ import org.tron.core.ChainBaseManager; import org.tron.core.capsule.PbftSignCapsule; import org.tron.protos.Protocol.PBFTMessage.Raw; +import org.tron.protos.Protocol.PQAuthSig; @Slf4j(topic = "pbft") @Component @@ -18,14 +19,16 @@ public class PbftMessageAction { @Autowired private ChainBaseManager chainBaseManager; - public void action(PbftMessage message, List dataSignList) { + public void action(PbftMessage message, List dataSignList, + List pqSignList) { switch (message.getDataType()) { case BLOCK: { long blockNum = message.getNumber(); chainBaseManager.getCommonDataBase().saveLatestPbftBlockNum(blockNum); Raw raw = message.getPbftMessage().getRawData(); chainBaseManager.getPbftSignDataStore() - .putBlockSignData(blockNum, new PbftSignCapsule(raw.toByteString(), dataSignList)); + .putBlockSignData(blockNum, + new PbftSignCapsule(raw.toByteString(), dataSignList, pqSignList)); logger.info("commit msg block num is:{}", blockNum); } break; @@ -33,7 +36,7 @@ public void action(PbftMessage message, List dataSignList) { try { Raw raw = message.getPbftMessage().getRawData(); chainBaseManager.getPbftSignDataStore().putSrSignData(message.getEpoch(), - new PbftSignCapsule(raw.toByteString(), dataSignList)); + new PbftSignCapsule(raw.toByteString(), dataSignList, pqSignList)); logger.info("sr commit msg :{}, epoch:{}", message.getNumber(), message.getEpoch()); } catch (Exception e) { logger.error("process the sr list error!", e); diff --git a/consensus/src/main/java/org/tron/consensus/pbft/PbftMessageHandle.java b/consensus/src/main/java/org/tron/consensus/pbft/PbftMessageHandle.java index 523ffac4d61..18462cff3cb 100644 --- a/consensus/src/main/java/org/tron/consensus/pbft/PbftMessageHandle.java +++ b/consensus/src/main/java/org/tron/consensus/pbft/PbftMessageHandle.java @@ -32,6 +32,7 @@ import org.tron.consensus.pbft.message.PbftMessage; import org.tron.core.ChainBaseManager; import org.tron.protos.Protocol.PBFTMessage.DataType; +import org.tron.protos.Protocol.PQAuthSig; @Slf4j(topic = "pbft") @Component @@ -64,6 +65,15 @@ public List load(String s) throws Exception { } }); + private LoadingCache> pqSignCache = CacheBuilder.newBuilder() + .initialCapacity(100).maximumSize(1000).expireAfterWrite(2, TimeUnit.MINUTES).build( + new CacheLoader>() { + @Override + public List load(String s) throws Exception { + return new ArrayList<>(); + } + }); + private PbftMessage srPbftMessage; private Timer timer = new Timer("pbft-timer"); @@ -205,14 +215,21 @@ public synchronized void onCommit(PbftMessage message) { commitVoteMap.put(key, message); //The number of votes plus 1 long agCou = agreeCommit.incrementAndGet(message.getDataKey()); - dataSignCache.getUnchecked(message.getDataKey()) - .add(message.getPbftMessage().getSignature()); + if (message.getPbftMessage().hasPqAuthSig()) { + pqSignCache.getUnchecked(message.getDataKey()) + .add(message.getPbftMessage().getPqAuthSig()); + } else { + dataSignCache.getUnchecked(message.getDataKey()) + .add(message.getPbftMessage().getSignature()); + } if (agCou >= Param.getInstance().getAgreeNodeCount()) { srPbftMessage = null; remove(message.getNo()); //commit, if (!isSyncing()) { - pbftMessageAction.action(message, dataSignCache.getUnchecked(message.getDataKey())); + pbftMessageAction.action(message, + dataSignCache.getUnchecked(message.getDataKey()), + pqSignCache.getUnchecked(message.getDataKey())); } } } diff --git a/framework/src/main/java/org/tron/core/net/messagehandler/PbftDataSyncHandler.java b/framework/src/main/java/org/tron/core/net/messagehandler/PbftDataSyncHandler.java index d66fa6d41f7..0d9171442b2 100644 --- a/framework/src/main/java/org/tron/core/net/messagehandler/PbftDataSyncHandler.java +++ b/framework/src/main/java/org/tron/core/net/messagehandler/PbftDataSyncHandler.java @@ -19,6 +19,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.tron.common.crypto.ECKey; +import org.tron.common.crypto.pqc.PQSchemeRegistry; import org.tron.common.es.ExecutorServiceManager; import org.tron.common.utils.ByteArray; import org.tron.common.utils.Sha256Hash; @@ -33,6 +34,8 @@ import org.tron.core.net.peer.PeerConnection; import org.tron.protos.Protocol.PBFTMessage.DataType; import org.tron.protos.Protocol.PBFTMessage.Raw; +import org.tron.protos.Protocol.PQAuthSig; +import org.tron.protos.Protocol.PQScheme; @Slf4j(topic = "pbft-data-sync") @Service @@ -102,6 +105,7 @@ private void processPBFTCommitMessage(PbftCommitMessage pbftCommitMessage) { PbftSignDataStore pbftSignDataStore = chainBaseManager.getPbftSignDataStore(); Raw raw = Raw.parseFrom(pbftCommitMessage.getPBFTCommitResult().getData()); if (!validPbftSign(raw, pbftCommitMessage.getPBFTCommitResult().getSignatureList(), + pbftCommitMessage.getPBFTCommitResult().getPqSignatureList(), chainBaseManager.getWitnesses())) { return; } @@ -120,37 +124,44 @@ private void processPBFTCommitMessage(PbftCommitMessage pbftCommitMessage) { } private boolean validPbftSign(Raw raw, List srSignList, - List currentSrList) { - //valid sr list - if (srSignList.size() != 0) { - Set srSignSet = new ConcurrentSet(); - srSignSet.addAll(srSignList); - if (srSignSet.size() < Param.getInstance().getAgreeNodeCount()) { - logger.error("sr sign count {} < sr count * 2/3 + 1 == {}", srSignSet.size(), - Param.getInstance().getAgreeNodeCount()); - return false; - } - byte[] dataHash = Sha256Hash.hash(true, raw.toByteArray()); - Set srSet = Sets.newHashSet(currentSrList); - List> futureList = new ArrayList<>(); - for (ByteString sign : srSignList) { - futureList.add(executorService.submit( - new ValidPbftSignTask(raw.getViewN(), srSignSet, dataHash, srSet, sign))); - } - for (Future future : futureList) { - try { - if (!future.get()) { - return false; - } - } catch (Exception e) { - logger.error("", e); + List pqSignList, List currentSrList) { + int totalSigs = srSignList.size() + pqSignList.size(); + if (totalSigs == 0) { + return true; + } + Set srSignSet = new ConcurrentSet(); + srSignSet.addAll(srSignList); + Set pqSignSet = new ConcurrentSet(); + for (PQAuthSig pqSign : pqSignList) { + pqSignSet.add(pqSign.toByteString()); + } + int uniqueSigs = srSignSet.size() + pqSignSet.size(); + if (uniqueSigs < Param.getInstance().getAgreeNodeCount()) { + logger.error("sr sign count {} < sr count * 2/3 + 1 == {}", uniqueSigs, + Param.getInstance().getAgreeNodeCount()); + return false; + } + byte[] dataHash = Sha256Hash.hash(true, raw.toByteArray()); + Set srSet = Sets.newHashSet(currentSrList); + List> futureList = new ArrayList<>(); + for (ByteString sign : srSignList) { + futureList.add(executorService.submit( + new ValidPbftSignTask(raw.getViewN(), srSignSet, dataHash, srSet, sign))); + } + for (PQAuthSig pqSign : pqSignList) { + futureList.add(executorService.submit( + new ValidPqPbftSignTask(raw.getViewN(), pqSignSet, dataHash, srSet, pqSign))); + } + for (Future future : futureList) { + try { + if (!future.get()) { + return false; } - } - if (srSignSet.size() != 0) { - return false; + } catch (Exception e) { + logger.error("", e); } } - return true; + return srSignSet.isEmpty() && pqSignSet.isEmpty(); } private class ValidPbftSignTask implements Callable { @@ -189,4 +200,57 @@ public Boolean call() throws Exception { } } + private class ValidPqPbftSignTask implements Callable { + + private final long viewN; + private final Set pqSignSet; + private final byte[] dataHash; + private final Set srSet; + private final PQAuthSig pqAuthSig; + + ValidPqPbftSignTask(long viewN, Set pqSignSet, + byte[] dataHash, Set srSet, PQAuthSig pqAuthSig) { + this.viewN = viewN; + this.pqSignSet = pqSignSet; + this.dataHash = dataHash; + this.srSet = srSet; + this.pqAuthSig = pqAuthSig; + } + + @Override + public Boolean call() { + PQScheme scheme = pqAuthSig.getScheme(); + if (!chainBaseManager.getDynamicPropertiesStore().isPqSchemeAllowed(scheme)) { + logger.error("viewN {} pq scheme {} not activated on chain", viewN, scheme); + return false; + } + if (!PQSchemeRegistry.contains(scheme)) { + logger.error("viewN {} pq scheme {} not registered locally", viewN, scheme); + return false; + } + byte[] publicKey = pqAuthSig.getPublicKey().toByteArray(); + if (publicKey.length != PQSchemeRegistry.getPublicKeyLength(scheme)) { + logger.error("viewN {} pq public key length mismatch for {}", viewN, scheme); + return false; + } + byte[] signature = pqAuthSig.getSignature().toByteArray(); + if (!PQSchemeRegistry.isValidSignatureLength(scheme, signature.length)) { + logger.error("viewN {} pq signature length mismatch for {}", viewN, scheme); + return false; + } + if (!PQSchemeRegistry.verify(scheme, publicKey, dataHash, signature)) { + logger.error("viewN {} pq signature verification failed for {}", viewN, scheme); + return false; + } + byte[] srAddress = PQSchemeRegistry.computeAddress(scheme, publicKey); + if (!srSet.contains(ByteString.copyFrom(srAddress))) { + logger.error("valid sr pq signature fail, error sr address:{}", + ByteArray.toHexString(srAddress)); + return false; + } + pqSignSet.remove(pqAuthSig.toByteString()); + return true; + } + } + } diff --git a/framework/src/test/java/org/tron/core/net/messagehandler/PbftDataSyncHandlerPQTest.java b/framework/src/test/java/org/tron/core/net/messagehandler/PbftDataSyncHandlerPQTest.java new file mode 100644 index 00000000000..890b0097b43 --- /dev/null +++ b/framework/src/test/java/org/tron/core/net/messagehandler/PbftDataSyncHandlerPQTest.java @@ -0,0 +1,230 @@ +package org.tron.core.net.messagehandler; + +import com.google.protobuf.ByteString; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.tron.common.crypto.ECKey; +import org.tron.common.crypto.pqc.FNDSA512; +import org.tron.common.crypto.pqc.PQSchemeRegistry; +import org.tron.common.utils.Sha256Hash; +import org.tron.consensus.base.Param; +import org.tron.core.ChainBaseManager; +import org.tron.core.store.DynamicPropertiesStore; +import org.tron.protos.Protocol.PBFTMessage.DataType; +import org.tron.protos.Protocol.PBFTMessage.MsgType; +import org.tron.protos.Protocol.PBFTMessage.Raw; +import org.tron.protos.Protocol.PQAuthSig; +import org.tron.protos.Protocol.PQScheme; + +/** + * Focused tests for {@link PbftDataSyncHandler#validPbftSign} covering PQ and + * mixed ECDSA/PQ quorums on the commit-sync path. + */ +public class PbftDataSyncHandlerPQTest { + + private PbftDataSyncHandler handler; + private ChainBaseManager chainBaseManager; + private DynamicPropertiesStore dynamicPropertiesStore; + private int previousAgreeNodeCount; + private boolean previousEnable; + + @Before + public void setUp() throws Exception { + handler = new PbftDataSyncHandler(); + chainBaseManager = Mockito.mock(ChainBaseManager.class); + dynamicPropertiesStore = Mockito.mock(DynamicPropertiesStore.class); + Mockito.when(chainBaseManager.getDynamicPropertiesStore()).thenReturn(dynamicPropertiesStore); + Mockito.when(dynamicPropertiesStore.isPqSchemeAllowed(PQScheme.FN_DSA_512)).thenReturn(true); + Mockito.when(dynamicPropertiesStore.isPqSchemeAllowed(PQScheme.UNKNOWN_PQ_SCHEME)) + .thenReturn(false); + + java.lang.reflect.Field field = PbftDataSyncHandler.class.getDeclaredField("chainBaseManager"); + field.setAccessible(true); + field.set(handler, chainBaseManager); + + previousAgreeNodeCount = Param.getInstance().getAgreeNodeCount(); + previousEnable = Param.getInstance().isEnable(); + } + + @After + public void tearDown() { + Param.getInstance().setAgreeNodeCount(previousAgreeNodeCount); + Param.getInstance().setEnable(previousEnable); + handler.close(); + } + + @Test + public void emptySignatureListsValidate() throws Exception { + Param.getInstance().setAgreeNodeCount(1); + Raw raw = buildRaw(1); + Assert.assertTrue(invokeValid(raw, Collections.emptyList(), Collections.emptyList(), + Collections.emptyList())); + } + + @Test + public void pqQuorumValidates() throws Exception { + Param.getInstance().setAgreeNodeCount(2); + Raw raw = buildRaw(1); + byte[] hash = Sha256Hash.hash(true, raw.toByteArray()); + + FNDSA512 kp1 = new FNDSA512(); + FNDSA512 kp2 = new FNDSA512(); + PQAuthSig sig1 = pqSign(kp1, hash); + PQAuthSig sig2 = pqSign(kp2, hash); + + List witnesses = Arrays.asList( + ByteString.copyFrom(PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, kp1.getPublicKey())), + ByteString.copyFrom(PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, kp2.getPublicKey()))); + + Assert.assertTrue(invokeValid(raw, Collections.emptyList(), + Arrays.asList(sig1, sig2), witnesses)); + } + + @Test + public void mixedEcdsaAndPqQuorumValidates() throws Exception { + Param.getInstance().setAgreeNodeCount(2); + Raw raw = buildRaw(2); + byte[] hash = Sha256Hash.hash(true, raw.toByteArray()); + + ECKey ec = new ECKey(); + byte[] ecSig = ec.sign(hash).toByteArray(); + + FNDSA512 kp = new FNDSA512(); + PQAuthSig pq = pqSign(kp, hash); + + List witnesses = Arrays.asList( + ByteString.copyFrom(ec.getAddress()), + ByteString.copyFrom(PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, kp.getPublicKey()))); + + Assert.assertTrue(invokeValid(raw, + Collections.singletonList(ByteString.copyFrom(ecSig)), + Collections.singletonList(pq), + witnesses)); + } + + @Test + public void underQuorumFails() throws Exception { + Param.getInstance().setAgreeNodeCount(3); + Raw raw = buildRaw(3); + byte[] hash = Sha256Hash.hash(true, raw.toByteArray()); + + FNDSA512 kp = new FNDSA512(); + PQAuthSig pq = pqSign(kp, hash); + List witnesses = Collections.singletonList( + ByteString.copyFrom(PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, kp.getPublicKey()))); + + Assert.assertFalse(invokeValid(raw, Collections.emptyList(), + Collections.singletonList(pq), witnesses)); + } + + @Test + public void pqSchemeNotActivatedFails() throws Exception { + Param.getInstance().setAgreeNodeCount(1); + Raw raw = buildRaw(4); + byte[] hash = Sha256Hash.hash(true, raw.toByteArray()); + + FNDSA512 kp = new FNDSA512(); + PQAuthSig pq = pqSign(kp, hash); + List witnesses = Collections.singletonList( + ByteString.copyFrom(PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, kp.getPublicKey()))); + + Mockito.when(dynamicPropertiesStore.isPqSchemeAllowed(PQScheme.FN_DSA_512)).thenReturn(false); + Assert.assertFalse(invokeValid(raw, Collections.emptyList(), + Collections.singletonList(pq), witnesses)); + } + + @Test + public void pqPublicKeyLengthMismatchFails() throws Exception { + Param.getInstance().setAgreeNodeCount(1); + Raw raw = buildRaw(5); + byte[] hash = Sha256Hash.hash(true, raw.toByteArray()); + + FNDSA512 kp = new FNDSA512(); + byte[] sig = PQSchemeRegistry.sign(PQScheme.FN_DSA_512, kp.getPrivateKey(), hash); + PQAuthSig pq = PQAuthSig.newBuilder() + .setScheme(PQScheme.FN_DSA_512) + .setPublicKey(ByteString.copyFrom(new byte[FNDSA512.PUBLIC_KEY_LENGTH - 1])) + .setSignature(ByteString.copyFrom(sig)) + .build(); + List witnesses = Collections.singletonList( + ByteString.copyFrom(PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, kp.getPublicKey()))); + + Assert.assertFalse(invokeValid(raw, Collections.emptyList(), + Collections.singletonList(pq), witnesses)); + } + + @Test + public void pqSignerNotInWitnessSetFails() throws Exception { + Param.getInstance().setAgreeNodeCount(1); + Raw raw = buildRaw(6); + byte[] hash = Sha256Hash.hash(true, raw.toByteArray()); + + FNDSA512 kp = new FNDSA512(); + PQAuthSig pq = pqSign(kp, hash); + FNDSA512 stranger = new FNDSA512(); + List witnesses = Collections.singletonList( + ByteString.copyFrom( + PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, stranger.getPublicKey()))); + + Assert.assertFalse(invokeValid(raw, Collections.emptyList(), + Collections.singletonList(pq), witnesses)); + } + + @Test + public void pqBadSignatureFails() throws Exception { + Param.getInstance().setAgreeNodeCount(1); + Raw raw = buildRaw(7); + byte[] hash = Sha256Hash.hash(true, raw.toByteArray()); + + FNDSA512 kp = new FNDSA512(); + byte[] goodSig = PQSchemeRegistry.sign(PQScheme.FN_DSA_512, kp.getPrivateKey(), hash); + byte[] tampered = Arrays.copyOf(goodSig, goodSig.length); + tampered[tampered.length - 1] ^= 0x01; + PQAuthSig pq = PQAuthSig.newBuilder() + .setScheme(PQScheme.FN_DSA_512) + .setPublicKey(ByteString.copyFrom(kp.getPublicKey())) + .setSignature(ByteString.copyFrom(tampered)) + .build(); + List witnesses = Collections.singletonList( + ByteString.copyFrom(PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, kp.getPublicKey()))); + + Assert.assertFalse(invokeValid(raw, Collections.emptyList(), + Collections.singletonList(pq), witnesses)); + } + + private static Raw buildRaw(long viewN) { + return Raw.newBuilder() + .setViewN(viewN) + .setEpoch(0) + .setDataType(DataType.BLOCK) + .setMsgType(MsgType.COMMIT) + .setData(ByteString.copyFromUtf8("payload-" + viewN)) + .build(); + } + + private static PQAuthSig pqSign(FNDSA512 kp, byte[] hash) { + byte[] sig = PQSchemeRegistry.sign(PQScheme.FN_DSA_512, kp.getPrivateKey(), hash); + return PQAuthSig.newBuilder() + .setScheme(PQScheme.FN_DSA_512) + .setPublicKey(ByteString.copyFrom(kp.getPublicKey())) + .setSignature(ByteString.copyFrom(sig)) + .build(); + } + + private boolean invokeValid(Raw raw, List srSignList, + List pqSignList, List witnesses) throws Exception { + Method m = PbftDataSyncHandler.class.getDeclaredMethod("validPbftSign", + Raw.class, List.class, List.class, List.class); + m.setAccessible(true); + return (Boolean) m.invoke(handler, raw, new ArrayList<>(srSignList), + new ArrayList<>(pqSignList), witnesses); + } +} diff --git a/protocol/src/main/protos/core/Tron.proto b/protocol/src/main/protos/core/Tron.proto index 512b2898aea..2b3b1733e5d 100644 --- a/protocol/src/main/protos/core/Tron.proto +++ b/protocol/src/main/protos/core/Tron.proto @@ -931,7 +931,15 @@ message PBFTMessage { message PBFTCommitResult { bytes data = 1; + // Legacy ECDSA commit signatures recoverable to a SR address via the + // signed `data` hash. May coexist with `pq_signature` when a quorum + // includes both ECDSA and PQ-only witnesses; the total of the two + // lists must meet the agreement threshold. repeated bytes signature = 2; + // Post-quantum commit signatures contributed by PQ-only witnesses. + // Verifiers must reject any pq_signature entry whose scheme is not + // activated on chain. + repeated PQAuthSig pq_signature = 3; } message SRL { From 2c4db1f39d8a36b7682b69049054add7070f6110 Mon Sep 17 00:00:00 2001 From: federico Date: Thu, 14 May 2026 15:16:35 +0800 Subject: [PATCH 7/9] fix(consensus): count pbft quorum by unique signer address --- .../org/tron/core/capsule/BlockCapsule.java | 7 +- .../messagehandler/PbftDataSyncHandler.java | 50 +++-- .../common/crypto/pqc/program/PQTxSender.java | 182 +++++++++++++++--- .../tron/core/capsule/BlockCapsulePQTest.java | 29 ++- .../PbftDataSyncHandlerPQTest.java | 42 +++- .../org/tron/core/pbft/PbftPQMessageTest.java | 5 +- 6 files changed, 246 insertions(+), 69 deletions(-) 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 6c781d419aa..9324f47a08e 100755 --- a/chainbase/src/main/java/org/tron/core/capsule/BlockCapsule.java +++ b/chainbase/src/main/java/org/tron/core/capsule/BlockCapsule.java @@ -421,11 +421,8 @@ public long getTimeStamp() { public boolean hasWitnessSignature() { BlockHeader header = getInstance().getBlockHeader(); - if (!header.getWitnessSignature().isEmpty()) { - return true; - } - PQAuthSig auth = header.getPqAuthSig(); - return auth != null && !auth.getSignature().isEmpty(); + return !header.getWitnessSignature().isEmpty() + || !header.getPqAuthSig().getSignature().isEmpty(); } @Override diff --git a/framework/src/main/java/org/tron/core/net/messagehandler/PbftDataSyncHandler.java b/framework/src/main/java/org/tron/core/net/messagehandler/PbftDataSyncHandler.java index 0d9171442b2..78674dda47f 100644 --- a/framework/src/main/java/org/tron/core/net/messagehandler/PbftDataSyncHandler.java +++ b/framework/src/main/java/org/tron/core/net/messagehandler/PbftDataSyncHandler.java @@ -5,13 +5,13 @@ import com.google.common.collect.Sets; import com.google.protobuf.ByteString; import com.google.protobuf.InvalidProtocolBufferException; -import io.netty.util.internal.ConcurrentSet; import java.io.Closeable; import java.security.SignatureException; import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; @@ -129,28 +129,17 @@ private boolean validPbftSign(Raw raw, List srSignList, if (totalSigs == 0) { return true; } - Set srSignSet = new ConcurrentSet(); - srSignSet.addAll(srSignList); - Set pqSignSet = new ConcurrentSet(); - for (PQAuthSig pqSign : pqSignList) { - pqSignSet.add(pqSign.toByteString()); - } - int uniqueSigs = srSignSet.size() + pqSignSet.size(); - if (uniqueSigs < Param.getInstance().getAgreeNodeCount()) { - logger.error("sr sign count {} < sr count * 2/3 + 1 == {}", uniqueSigs, - Param.getInstance().getAgreeNodeCount()); - return false; - } byte[] dataHash = Sha256Hash.hash(true, raw.toByteArray()); Set srSet = Sets.newHashSet(currentSrList); + Set verifiedSigners = ConcurrentHashMap.newKeySet(); List> futureList = new ArrayList<>(); for (ByteString sign : srSignList) { futureList.add(executorService.submit( - new ValidPbftSignTask(raw.getViewN(), srSignSet, dataHash, srSet, sign))); + new ValidPbftSignTask(raw.getViewN(), verifiedSigners, dataHash, srSet, sign))); } for (PQAuthSig pqSign : pqSignList) { futureList.add(executorService.submit( - new ValidPqPbftSignTask(raw.getViewN(), pqSignSet, dataHash, srSet, pqSign))); + new ValidPqPbftSignTask(raw.getViewN(), verifiedSigners, dataHash, srSet, pqSign))); } for (Future future : futureList) { try { @@ -159,23 +148,30 @@ private boolean validPbftSign(Raw raw, List srSignList, } } catch (Exception e) { logger.error("", e); + return false; } } - return srSignSet.isEmpty() && pqSignSet.isEmpty(); + int unique = verifiedSigners.size(); + if (unique < Param.getInstance().getAgreeNodeCount()) { + logger.error("sr sign count {} < sr count * 2/3 + 1 == {}", unique, + Param.getInstance().getAgreeNodeCount()); + return false; + } + return true; } private class ValidPbftSignTask implements Callable { private long viewN; - private Set srSignSet; + private Set verifiedSigners; private byte[] dataHash; private Set srSet; private ByteString sign; - ValidPbftSignTask(long viewN, Set srSignSet, + ValidPbftSignTask(long viewN, Set verifiedSigners, byte[] dataHash, Set srSet, ByteString sign) { this.viewN = viewN; - this.srSignSet = srSignSet; + this.verifiedSigners = verifiedSigners; this.dataHash = dataHash; this.srSet = srSet; this.sign = sign; @@ -186,12 +182,13 @@ public Boolean call() throws Exception { try { byte[] srAddress = ECKey.signatureToAddress(dataHash, TransactionCapsule.getBase64FromByteString(sign)); - if (!srSet.contains(ByteString.copyFrom(srAddress))) { + ByteString addressKey = ByteString.copyFrom(srAddress); + if (!srSet.contains(addressKey)) { logger.error("valid sr signature fail,error sr address:{}", ByteArray.toHexString(srAddress)); return false; } - srSignSet.remove(sign); + verifiedSigners.add(addressKey); } catch (SignatureException e) { logger.error("viewN {} valid sr list sign fail!", viewN, e); return false; @@ -203,15 +200,15 @@ public Boolean call() throws Exception { private class ValidPqPbftSignTask implements Callable { private final long viewN; - private final Set pqSignSet; + private final Set verifiedSigners; private final byte[] dataHash; private final Set srSet; private final PQAuthSig pqAuthSig; - ValidPqPbftSignTask(long viewN, Set pqSignSet, + ValidPqPbftSignTask(long viewN, Set verifiedSigners, byte[] dataHash, Set srSet, PQAuthSig pqAuthSig) { this.viewN = viewN; - this.pqSignSet = pqSignSet; + this.verifiedSigners = verifiedSigners; this.dataHash = dataHash; this.srSet = srSet; this.pqAuthSig = pqAuthSig; @@ -243,12 +240,13 @@ public Boolean call() { return false; } byte[] srAddress = PQSchemeRegistry.computeAddress(scheme, publicKey); - if (!srSet.contains(ByteString.copyFrom(srAddress))) { + ByteString addressKey = ByteString.copyFrom(srAddress); + if (!srSet.contains(addressKey)) { logger.error("valid sr pq signature fail, error sr address:{}", ByteArray.toHexString(srAddress)); return false; } - pqSignSet.remove(pqAuthSig.toByteString()); + verifiedSigners.add(addressKey); return true; } } diff --git a/framework/src/test/java/org/tron/common/crypto/pqc/program/PQTxSender.java b/framework/src/test/java/org/tron/common/crypto/pqc/program/PQTxSender.java index cdcaf3e72a5..7ffaa3ca02e 100644 --- a/framework/src/test/java/org/tron/common/crypto/pqc/program/PQTxSender.java +++ b/framework/src/test/java/org/tron/common/crypto/pqc/program/PQTxSender.java @@ -12,10 +12,14 @@ import org.tron.api.GrpcAPI.Return; import org.tron.api.WalletGrpc; import org.tron.api.WalletGrpc.WalletBlockingStub; +import org.tron.common.crypto.ECKey; +import org.tron.common.crypto.ECKey.ECDSASignature; import org.tron.common.crypto.pqc.FNDSA512; import org.tron.common.math.StrictMathWrapper; +import org.tron.common.parameter.CommonParameter; import org.tron.common.utils.ByteArray; import org.tron.common.utils.Commons; +import org.tron.common.utils.Sha256Hash; import org.tron.common.utils.StringUtil; import org.tron.common.utils.client.utils.AbiUtil; import org.tron.core.capsule.TransactionCapsule; @@ -27,17 +31,19 @@ import org.tron.protos.contract.SmartContractOuterClass.TriggerSmartContract; /** - * Demo client that connects to {@link PQWitnessNode} and continuously broadcasts FN-DSA-512 signed - * transfer and TRC20 transactions at 10 TPS. + * Demo client that connects to {@link PQWitnessNode} and continuously broadcasts transfer and + * TRC20 transactions signed by FN-DSA-512 and ECDSA. *

- * The keypair is derived from the same fixed seed used by PQWitnessNode, so no out-of-band key - * exchange is needed. + * The FN-DSA-512 keypair is derived from the same fixed seed used by PQWitnessNode, so no + * out-of-band key exchange is needed. ECDSA transactions use -Decdsa.private.key. *

* Run from the repository root: * ./gradlew :framework:buildFullNodeJar :framework:compileTestJava + * CP="framework/build/classes/java/test:framework/build/resources/test" + * CP="$CP:framework/build/libs/FullNode.jar" * java -Dpqc.host=127.0.0.1 -Dpqc.port=50051 -Dpqc.transfer.tps=10 -Dpqc.trc20.tps=10 \ - * -cp "framework/build/classes/java/test:framework/build/resources/test:\ - * framework/build/libs/FullNode.jar" \ + * -Decdsa.private.key=HEX_PRIVATE_KEY -Decdsa.transfer.tps=10 -Decdsa.trc20.tps=10 \ + * -cp "$CP" \ * org.tron.common.crypto.pqc.program.PQTxSender * * Optional JVM args: @@ -45,6 +51,9 @@ * -Dpqc.port=50051 * -Dpqc.transfer.tps=10 * -Dpqc.trc20.tps=10 + * -Decdsa.private.key=1234567890123456789012345678901234567890123456789012345678901234 + * -Decdsa.transfer.tps=10 + * -Decdsa.trc20.tps=10 */ public class PQTxSender { @@ -57,7 +66,7 @@ public class PQTxSender { * Recipient of the demo transfer. */ private static final byte[] TO_ADDR = - Commons.decodeFromBase58Check("T9zNBvTFD97XzGsjGqvg2QHizTG8sibsHt"); + Commons.decodeFromBase58Check("TKmyxLsRR2FWMVEHaQA2pZh1xB7oXPXzG1"); /** * TRC20 contract address (USDT on TRON). @@ -76,13 +85,27 @@ public class PQTxSender { private static final long TRC20_FEE_LIMIT = 1000_000_000L; /** - * Default send rate for transfer transactions. + * Default demo ECDSA private key. Override it with -Decdsa.private.key for a funded account. + */ + private static final String DEFAULT_ECDSA_PRIVATE_KEY = + "1234567890123456789012345678901234567890123456789012345678901234"; + + /** + * Default send rate for FN-DSA-512 transfer transactions. */ private static final double DEFAULT_TRANSFER_TPS = 10.0d; /** - * Default send rate for TRC20 transactions. + * Default send rate for FN-DSA-512 TRC20 transactions. */ private static final double DEFAULT_TRC20_TPS = 10.0d; + /** + * Default send rate for ECDSA transfer transactions. + */ + private static final double DEFAULT_ECDSA_TRANSFER_TPS = 10.0d; + /** + * Default send rate for ECDSA TRC20 transactions. + */ + private static final double DEFAULT_ECDSA_TRC20_TPS = 10.0d; public static void main(String[] args) throws Exception { // Force INFO level: logback-test.xml (on the test classpath) sets root=DEBUG @@ -100,15 +123,24 @@ public static void main(String[] args) throws Exception { byte[] userPriv = userKp.getPrivateKey(); byte[] signerAddr = FNDSA512.computeAddress(userPub); byte[] ownerAddr = Commons.decodeFromBase58Check("TJUfbazhixG4YtqJxUDmv5XisZvvy1wP91"); + ECKey ecdsaKey = ECKey.fromPrivate( + ByteArray.fromHexString(System.getProperty("ecdsa.private.key", + DEFAULT_ECDSA_PRIVATE_KEY))); + byte[] ecdsaOwnerAddr = ecdsaKey.getAddress(); double transferTps = readTps("pqc.transfer.tps", DEFAULT_TRANSFER_TPS); double trc20Tps = readTps("pqc.trc20.tps", DEFAULT_TRC20_TPS); + double ecdsaTransferTps = readTps("ecdsa.transfer.tps", DEFAULT_ECDSA_TRANSFER_TPS); + double ecdsaTrc20Tps = readTps("ecdsa.trc20.tps", DEFAULT_ECDSA_TRC20_TPS); - System.out.println("=== PQC Client ==="); + System.out.println("=== PQC/ECDSA Tx Sender ==="); System.out.println("Connecting to " + HOST + ":" + PORT); - System.out.println("Owner address: " + ByteArray.toHexString(ownerAddr)); - System.out.println("Signer address: " + ByteArray.toHexString(signerAddr)); - System.out.println("Transfer TPS: " + transferTps); - System.out.println("TRC20 TPS: " + trc20Tps); + System.out.println("PQC owner address: " + ByteArray.toHexString(ownerAddr)); + System.out.println("PQC signer address: " + ByteArray.toHexString(signerAddr)); + System.out.println("PQC transfer TPS: " + transferTps); + System.out.println("PQC TRC20 TPS: " + trc20Tps); + System.out.println("ECDSA owner address: " + ByteArray.toHexString(ecdsaOwnerAddr)); + System.out.println("ECDSA transfer TPS: " + ecdsaTransferTps); + System.out.println("ECDSA TRC20 TPS: " + ecdsaTrc20Tps); // ── 2. Connect via gRPC ─────────────────────────────────────────────── ManagedChannel channel = ManagedChannelBuilder @@ -124,11 +156,21 @@ public static void main(String[] args) throws Exception { Thread trc20Thread = new Thread( () -> runTrc20Loop(stub, ownerAddr, userPub, userPriv, trc20Tps), "pqc-trc20-sender-grpc"); + Thread ecdsaTransferThread = new Thread( + () -> runEcdsaTransferLoop(stub, ecdsaOwnerAddr, ecdsaKey, ecdsaTransferTps), + "ecdsa-transfer-sender-grpc"); + Thread ecdsaTrc20Thread = new Thread( + () -> runEcdsaTrc20Loop(stub, ecdsaOwnerAddr, ecdsaKey, ecdsaTrc20Tps), + "ecdsa-trc20-sender-grpc"); transferThread.start(); trc20Thread.start(); + ecdsaTransferThread.start(); + ecdsaTrc20Thread.start(); transferThread.join(); trc20Thread.join(); + ecdsaTransferThread.join(); + ecdsaTrc20Thread.join(); } finally { channel.shutdown(); channel.awaitTermination(5, TimeUnit.SECONDS); @@ -139,6 +181,11 @@ private static byte[] sha256(byte[] data) throws Exception { return MessageDigest.getInstance("SHA-256").digest(data); } + private static byte[] ecdsaTxId(Transaction tx) { + return Sha256Hash.hash(CommonParameter.getInstance().isECKeyCryptoEngine(), + tx.getRawData().toByteArray()); + } + private static byte[] longToBytes(long value) { return ByteBuffer.allocate(8).putLong(value).array(); } @@ -146,7 +193,7 @@ private static byte[] longToBytes(long value) { private static void runTransferLoop(WalletBlockingStub stub, byte[] ownerAddr, byte[] userPub, byte[] userPriv, double tps) { if (tps <= 0) { - System.out.println("transfer sender disabled"); + System.out.println("pqc transfer sender disabled"); return; } long intervalMs = tpsToIntervalMs(tps); @@ -161,7 +208,7 @@ private static void runTransferLoop(WalletBlockingStub stub, byte[] ownerAddr, private static void runTrc20Loop(WalletBlockingStub stub, byte[] ownerAddr, byte[] userPub, byte[] userPriv, double tps) { if (tps <= 0) { - System.out.println("trc20 sender disabled"); + System.out.println("pqc trc20 sender disabled"); return; } long intervalMs = tpsToIntervalMs(tps); @@ -173,6 +220,36 @@ private static void runTrc20Loop(WalletBlockingStub stub, byte[] ownerAddr, } } + private static void runEcdsaTransferLoop(WalletBlockingStub stub, byte[] ownerAddr, + ECKey ecdsaKey, double tps) { + if (tps <= 0) { + System.out.println("ecdsa transfer sender disabled"); + return; + } + long intervalMs = tpsToIntervalMs(tps); + long counter = 1L; + while (!Thread.currentThread().isInterrupted()) { + long loopStart = System.currentTimeMillis(); + sendEcdsaTransferTransaction(stub, ownerAddr, ecdsaKey, counter++); + sleepRemaining(intervalMs, loopStart); + } + } + + private static void runEcdsaTrc20Loop(WalletBlockingStub stub, byte[] ownerAddr, + ECKey ecdsaKey, double tps) { + if (tps <= 0) { + System.out.println("ecdsa trc20 sender disabled"); + return; + } + long intervalMs = tpsToIntervalMs(tps); + long counter = 1L; + while (!Thread.currentThread().isInterrupted()) { + long loopStart = System.currentTimeMillis(); + sendEcdsaTrc20Transaction(stub, ownerAddr, ecdsaKey, counter++); + sleepRemaining(intervalMs, loopStart); + } + } + private static void sendTransferTransaction(WalletBlockingStub stub, byte[] ownerAddr, byte[] userPub, byte[] userPriv, long seq) { try { @@ -195,12 +272,11 @@ private static void sendTransferTransaction(WalletBlockingStub stub, byte[] owne .build(); Return result = timedStub.broadcastTransaction(signedTx); - System.out.println("[transfer-" + seq + "] ref=#" + refNum + System.out.println("[pqc-transfer-" + seq + "] ref=#" + refNum + " tx=" + ByteArray.toHexString(txId) - + " result=" + result.getCode() - + " msg=" + result.getMessage().toStringUtf8()); + + " result=" + result.getCode()); } catch (Exception e) { - System.err.println("[transfer-" + seq + "] send failed: " + e.getMessage()); + System.err.println("[pqc-transfer-" + seq + "] send failed: " + e.getMessage()); e.printStackTrace(System.err); } } @@ -231,16 +307,74 @@ private static void sendTrc20Transaction(WalletBlockingStub stub, byte[] ownerAd .build(); Return result = timedStub.broadcastTransaction(signedTx); - System.out.println("[trc20-" + seq + "] ref=#" + refNum + System.out.println("[pqc-trc20-" + seq + "] ref=#" + refNum + + " tx=" + ByteArray.toHexString(txId) + + " result=" + result.getCode()); + } catch (Exception e) { + System.err.println("[pqc-trc20-" + seq + "] send failed: " + e.getMessage()); + e.printStackTrace(System.err); + } + } + + private static void sendEcdsaTransferTransaction(WalletBlockingStub stub, byte[] ownerAddr, + ECKey ecdsaKey, long seq) { + try { + WalletBlockingStub timedStub = stub.withDeadlineAfter(10, TimeUnit.SECONDS); + + Block head = timedStub.getNowBlock(EmptyMessage.getDefaultInstance()); + byte[] headerRaw = head.getBlockHeader().getRawData().toByteArray(); + long refNum = head.getBlockHeader().getRawData().getNumber(); + byte[] blockHash = sha256(headerRaw); + + Transaction tx = buildTransferTransaction(ownerAddr, blockHash, refNum); + byte[] txId = ecdsaTxId(tx); + Transaction signedTx = signWithEcdsa(tx, ecdsaKey, txId); + + Return result = timedStub.broadcastTransaction(signedTx); + System.out.println("[ecdsa-transfer-" + seq + "] ref=#" + refNum + + " tx=" + ByteArray.toHexString(txId) + + " result=" + result.getCode()); + } catch (Exception e) { + System.err.println("[ecdsa-transfer-" + seq + "] send failed: " + e.getMessage()); + e.printStackTrace(System.err); + } + } + + private static void sendEcdsaTrc20Transaction(WalletBlockingStub stub, byte[] ownerAddr, + ECKey ecdsaKey, long seq) { + try { + WalletBlockingStub timedStub = stub.withDeadlineAfter(10, TimeUnit.SECONDS); + + Block head = timedStub.getNowBlock(EmptyMessage.getDefaultInstance()); + byte[] headerRaw = head.getBlockHeader().getRawData().toByteArray(); + long refNum = head.getBlockHeader().getRawData().getNumber(); + byte[] blockHash = sha256(headerRaw); + + Transaction tx = buildTrc20Transaction(ownerAddr, blockHash, refNum); + Transaction.raw.Builder rawBuilder = tx.getRawData().toBuilder(); + rawBuilder.setFeeLimit(TRC20_FEE_LIMIT); + tx = tx.toBuilder().setRawData(rawBuilder).build(); + + byte[] txId = ecdsaTxId(tx); + Transaction signedTx = signWithEcdsa(tx, ecdsaKey, txId); + + Return result = timedStub.broadcastTransaction(signedTx); + System.out.println("[ecdsa-trc20-" + seq + "] ref=#" + refNum + " tx=" + ByteArray.toHexString(txId) - + " result=" + result.getCode() - + " msg=" + result.getMessage().toStringUtf8()); + + " result=" + result.getCode()); } catch (Exception e) { - System.err.println("[trc20-" + seq + "] send failed: " + e.getMessage()); + System.err.println("[ecdsa-trc20-" + seq + "] send failed: " + e.getMessage()); e.printStackTrace(System.err); } } + private static Transaction signWithEcdsa(Transaction tx, ECKey ecdsaKey, byte[] txId) { + ECDSASignature signature = ecdsaKey.sign(txId); + return tx.toBuilder() + .addSignature(ByteString.copyFrom(signature.toByteArray())) + .build(); + } + private static Transaction buildTransferTransaction(byte[] ownerAddr, byte[] blockHash, long refNum) { Transaction.raw rawData = Transaction.raw.newBuilder() diff --git a/framework/src/test/java/org/tron/core/capsule/BlockCapsulePQTest.java b/framework/src/test/java/org/tron/core/capsule/BlockCapsulePQTest.java index 6c8b482cd51..c20df122995 100644 --- a/framework/src/test/java/org/tron/core/capsule/BlockCapsulePQTest.java +++ b/framework/src/test/java/org/tron/core/capsule/BlockCapsulePQTest.java @@ -15,6 +15,8 @@ import org.tron.core.exception.ValidateSignatureException; import org.tron.protos.Protocol.Account; import org.tron.protos.Protocol.AccountType; +import org.tron.protos.Protocol.Block; +import org.tron.protos.Protocol.BlockHeader; import org.tron.protos.Protocol.Key; import org.tron.protos.Protocol.PQAuthSig; import org.tron.protos.Protocol.PQScheme; @@ -100,6 +102,20 @@ private PQAuthSig buildPQAuthSig(byte[] signature) { .build(); } + /** + * {@link BlockCapsule#hasWitnessSignature()} is the apply-vs-pack discriminator + * in {@code Manager#processTransaction}; a PQ-only block must read as signed so + * it follows the same apply/trace-check path as ECDSA blocks. + */ + @Test + public void hasWitnessSignatureTrueForPqOnlyBlock() { + byte[] parentHash = new byte[32]; + BlockCapsule block = buildUnsignedBlock(parentHash); + Assert.assertFalse(block.hasWitnessSignature()); + block.setPqAuthSig(buildPQAuthSig(signPQ(block.getRawHashBytes()))); + Assert.assertTrue(block.hasWitnessSignature()); + } + @Test public void legacyValidateWithoutPQAuthSigAcceptedBeforeActivation() throws Exception { dbManager.getDynamicPropertiesStore().saveAllowMultiSign(1L); @@ -136,9 +152,16 @@ public void bothLegacyAndPQAuthSigRejected() throws Exception { dbManager.getAccountStore().put(witnessAddress, witness); byte[] parentHash = new byte[32]; - BlockCapsule block = buildSignedBlock(parentHash); - byte[] digest = block.getRawHashBytes(); - block.setPqAuthSig(buildPQAuthSig(signPQ(digest))); + BlockCapsule signed = buildSignedBlock(parentHash); + byte[] digest = signed.getRawHashBytes(); + // Bypass BlockCapsule#setPqAuthSig (which clears witness_signature) so the + // resulting block carries BOTH legacy ECDSA + PQ signatures — the wire shape + // that the mutual-exclusion check in validateSignature must reject. + BlockHeader dualHeader = signed.getInstance().getBlockHeader().toBuilder() + .setPqAuthSig(buildPQAuthSig(signPQ(digest))) + .build(); + Block dual = signed.getInstance().toBuilder().setBlockHeader(dualHeader).build(); + BlockCapsule block = new BlockCapsule(dual); block.validateSignature( dbManager.getDynamicPropertiesStore(), dbManager.getAccountStore()); } diff --git a/framework/src/test/java/org/tron/core/net/messagehandler/PbftDataSyncHandlerPQTest.java b/framework/src/test/java/org/tron/core/net/messagehandler/PbftDataSyncHandlerPQTest.java index 890b0097b43..365abbd891b 100644 --- a/framework/src/test/java/org/tron/core/net/messagehandler/PbftDataSyncHandlerPQTest.java +++ b/framework/src/test/java/org/tron/core/net/messagehandler/PbftDataSyncHandlerPQTest.java @@ -81,8 +81,8 @@ public void pqQuorumValidates() throws Exception { PQAuthSig sig2 = pqSign(kp2, hash); List witnesses = Arrays.asList( - ByteString.copyFrom(PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, kp1.getPublicKey())), - ByteString.copyFrom(PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, kp2.getPublicKey()))); + pqAddress(kp1), + pqAddress(kp2)); Assert.assertTrue(invokeValid(raw, Collections.emptyList(), Arrays.asList(sig1, sig2), witnesses)); @@ -102,7 +102,7 @@ public void mixedEcdsaAndPqQuorumValidates() throws Exception { List witnesses = Arrays.asList( ByteString.copyFrom(ec.getAddress()), - ByteString.copyFrom(PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, kp.getPublicKey()))); + pqAddress(kp)); Assert.assertTrue(invokeValid(raw, Collections.singletonList(ByteString.copyFrom(ecSig)), @@ -119,7 +119,7 @@ public void underQuorumFails() throws Exception { FNDSA512 kp = new FNDSA512(); PQAuthSig pq = pqSign(kp, hash); List witnesses = Collections.singletonList( - ByteString.copyFrom(PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, kp.getPublicKey()))); + pqAddress(kp)); Assert.assertFalse(invokeValid(raw, Collections.emptyList(), Collections.singletonList(pq), witnesses)); @@ -134,7 +134,7 @@ public void pqSchemeNotActivatedFails() throws Exception { FNDSA512 kp = new FNDSA512(); PQAuthSig pq = pqSign(kp, hash); List witnesses = Collections.singletonList( - ByteString.copyFrom(PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, kp.getPublicKey()))); + pqAddress(kp)); Mockito.when(dynamicPropertiesStore.isPqSchemeAllowed(PQScheme.FN_DSA_512)).thenReturn(false); Assert.assertFalse(invokeValid(raw, Collections.emptyList(), @@ -155,7 +155,7 @@ public void pqPublicKeyLengthMismatchFails() throws Exception { .setSignature(ByteString.copyFrom(sig)) .build(); List witnesses = Collections.singletonList( - ByteString.copyFrom(PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, kp.getPublicKey()))); + pqAddress(kp)); Assert.assertFalse(invokeValid(raw, Collections.emptyList(), Collections.singletonList(pq), witnesses)); @@ -170,14 +170,31 @@ public void pqSignerNotInWitnessSetFails() throws Exception { FNDSA512 kp = new FNDSA512(); PQAuthSig pq = pqSign(kp, hash); FNDSA512 stranger = new FNDSA512(); - List witnesses = Collections.singletonList( - ByteString.copyFrom( - PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, stranger.getPublicKey()))); + List witnesses = Collections.singletonList(pqAddress(stranger)); Assert.assertFalse(invokeValid(raw, Collections.emptyList(), Collections.singletonList(pq), witnesses)); } + @Test + public void duplicatePqSignerDoesNotInflateQuorum() throws Exception { + Param.getInstance().setAgreeNodeCount(2); + Raw raw = buildRaw(8); + byte[] hash = Sha256Hash.hash(true, raw.toByteArray()); + + FNDSA512 kp = new FNDSA512(); + PQAuthSig sig1 = pqSign(kp, hash); + PQAuthSig sig2 = pqSign(kp, hash); + Assert.assertNotEquals("Falcon should produce randomized signatures", + sig1.getSignature(), sig2.getSignature()); + + List witnesses = Collections.singletonList( + pqAddress(kp)); + + Assert.assertFalse(invokeValid(raw, Collections.emptyList(), + Arrays.asList(sig1, sig2), witnesses)); + } + @Test public void pqBadSignatureFails() throws Exception { Param.getInstance().setAgreeNodeCount(1); @@ -194,7 +211,7 @@ public void pqBadSignatureFails() throws Exception { .setSignature(ByteString.copyFrom(tampered)) .build(); List witnesses = Collections.singletonList( - ByteString.copyFrom(PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, kp.getPublicKey()))); + pqAddress(kp)); Assert.assertFalse(invokeValid(raw, Collections.emptyList(), Collections.singletonList(pq), witnesses)); @@ -210,6 +227,11 @@ private static Raw buildRaw(long viewN) { .build(); } + private static ByteString pqAddress(FNDSA512 kp) { + return ByteString.copyFrom( + PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, kp.getPublicKey())); + } + private static PQAuthSig pqSign(FNDSA512 kp, byte[] hash) { byte[] sig = PQSchemeRegistry.sign(PQScheme.FN_DSA_512, kp.getPrivateKey(), hash); return PQAuthSig.newBuilder() diff --git a/framework/src/test/java/org/tron/core/pbft/PbftPQMessageTest.java b/framework/src/test/java/org/tron/core/pbft/PbftPQMessageTest.java index a3247b448cd..16cab0b9920 100644 --- a/framework/src/test/java/org/tron/core/pbft/PbftPQMessageTest.java +++ b/framework/src/test/java/org/tron/core/pbft/PbftPQMessageTest.java @@ -132,9 +132,12 @@ public void testPqSchemeNotRegistered() throws Exception { Miner miner = pqMiner(kp); PbftMessage msg = PbftMessage.prePrepareBlockMsg(emptyBlock(), 1, miner); + // UNKNOWN_PQ_SCHEME (0) is normalized to FN_DSA_512 by PQSchemeRegistry#resolve + // for proto3 default-zero compatibility, so use setSchemeValue() to inject a + // truly unrecognized scheme value that bypasses the enum and the normalizer. PBFTMessage tampered = msg.getPbftMessage().toBuilder() .setPqAuthSig(msg.getPbftMessage().getPqAuthSig().toBuilder() - .setScheme(PQScheme.UNKNOWN_PQ_SCHEME)) + .setSchemeValue(999)) .build(); PbftMessage rebuilt = rebuild(msg, tampered); SignatureException ex = assertThrows(SignatureException.class, rebuilt::analyzeSignature); From 26e37e396acd96f9e782d47677c64e99912a32a5 Mon Sep 17 00:00:00 2001 From: federico Date: Thu, 14 May 2026 20:49:17 +0800 Subject: [PATCH 8/9] fix: address PQ PR review feedback --- .../main/java/org/tron/core/vm/PrecompiledContracts.java | 2 -- .../main/java/org/tron/common/utils/LocalWitnesses.java | 7 +++---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/actuator/src/main/java/org/tron/core/vm/PrecompiledContracts.java b/actuator/src/main/java/org/tron/core/vm/PrecompiledContracts.java index f5e0f63dc59..5fe58cb9e14 100644 --- a/actuator/src/main/java/org/tron/core/vm/PrecompiledContracts.java +++ b/actuator/src/main/java/org/tron/core/vm/PrecompiledContracts.java @@ -2497,7 +2497,6 @@ public Pair execute(byte[] data) { boolean ok = FNDSA512.verify(pk, msg, sig); return Pair.of(true, ok ? DataWord.ONE().getData() : DataWord.ZERO().getData()); } catch (Throwable t) { - logger.info("VerifyFnDsa512 error:{}", t.getMessage()); return Pair.of(true, DataWord.ZERO().getData()); } } @@ -2655,7 +2654,6 @@ public Pair execute(byte[] rawData) { if (t instanceof OutOfTimeException) { throw t; } - logger.info("ValidateMultiSign(0x17) error:{}", t.getMessage()); } return Pair.of(true, DATA_FALSE); } diff --git a/chainbase/src/main/java/org/tron/common/utils/LocalWitnesses.java b/chainbase/src/main/java/org/tron/common/utils/LocalWitnesses.java index dc8f4f0c4be..bdd0fad4269 100644 --- a/chainbase/src/main/java/org/tron/common/utils/LocalWitnesses.java +++ b/chainbase/src/main/java/org/tron/common/utils/LocalWitnesses.java @@ -142,12 +142,11 @@ public void addPrivateKeys(String privateKey) { */ public void setPqKeypairs(final List pqPrivateKeys, final List pqPublicKeys) { - if (CollectionUtils.isEmpty(pqPrivateKeys) - && CollectionUtils.isEmpty(pqPublicKeys)) { + int privCount = CollectionUtils.isEmpty(pqPrivateKeys) ? 0 : pqPrivateKeys.size(); + int pubCount = CollectionUtils.isEmpty(pqPublicKeys) ? 0 : pqPublicKeys.size(); + if (privCount == 0 && pubCount == 0) { return; } - int privCount = pqPrivateKeys == null ? 0 : pqPrivateKeys.size(); - int pubCount = pqPublicKeys == null ? 0 : pqPublicKeys.size(); if (privCount != pubCount) { throw new TronError(String.format( "PQ keypair list size mismatch: priv=%d, pub=%d", privCount, pubCount), From 6b7c1391e8d1af83e110175e75923fdfd4327a54 Mon Sep 17 00:00:00 2001 From: federico Date: Fri, 15 May 2026 17:46:23 +0800 Subject: [PATCH 9/9] feat(actuator): include pq signatures in getTransactionSignWeight --- .../org/tron/core/utils/TransactionUtil.java | 22 +++++++++++++++---- .../tron/core/capsule/TransactionCapsule.java | 10 ++++++--- 2 files changed, 25 insertions(+), 7 deletions(-) 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 53d6caf5691..e487373951e 100644 --- a/actuator/src/main/java/org/tron/core/utils/TransactionUtil.java +++ b/actuator/src/main/java/org/tron/core/utils/TransactionUtil.java @@ -37,6 +37,7 @@ import org.tron.api.GrpcAPI.TransactionExtention; import org.tron.api.GrpcAPI.TransactionSignWeight; import org.tron.api.GrpcAPI.TransactionSignWeight.Result; +import org.tron.common.math.StrictMathWrapper; import org.tron.common.parameter.CommonParameter; import org.tron.common.utils.Sha256Hash; import org.tron.core.ChainBaseManager; @@ -221,11 +222,24 @@ public TransactionSignWeight getTransactionSignWeight(Transaction trx) { } } tswBuilder.setPermission(permission); - if (trx.getSignatureCount() > 0) { + if (trx.getSignatureCount() > 0 || trx.getPqAuthSigCount() > 0) { List approveList = new ArrayList<>(); - long currentWeight = TransactionCapsule.checkWeight(permission, trx.getSignatureList(), - Sha256Hash.hash(CommonParameter.getInstance() - .isECKeyCryptoEngine(), trx.getRawData().toByteArray()), approveList); + long currentWeight = 0L; + if (trx.getSignatureCount() > 0) { + currentWeight = TransactionCapsule.checkWeight(permission, trx.getSignatureList(), + Sha256Hash.hash(CommonParameter.getInstance() + .isECKeyCryptoEngine(), trx.getRawData().toByteArray()), approveList); + } + if (trx.getPqAuthSigCount() > 0) { + java.util.Set signedAddresses = new java.util.HashSet<>(approveList); + try { + currentWeight = StrictMathWrapper.addExact(currentWeight, + TransactionCapsule.validatePQSignature(trx, permission, signedAddresses, + chainBaseManager.getDynamicPropertiesStore(), approveList)); + } catch (ArithmeticException e) { + throw new PermissionException("weight overflow"); + } + } tswBuilder.addAllApprovedList(approveList); tswBuilder.setCurrentWeight(currentWeight); } 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 21f0f1adc59..0dd908a1853 100755 --- a/chainbase/src/main/java/org/tron/core/capsule/TransactionCapsule.java +++ b/chainbase/src/main/java/org/tron/core/capsule/TransactionCapsule.java @@ -504,7 +504,7 @@ public static boolean validateSignature(Transaction transaction, try { weight = StrictMathWrapper.addExact(weight, validatePQSignature(transaction, permission, signedAddresses, - dynamicPropertiesStore)); + dynamicPropertiesStore, approveList)); } catch (ArithmeticException e) { throw new PermissionException("weight overflow"); } @@ -723,9 +723,10 @@ void logSlowSigVerify(long startNs) { * part of {@code raw_data}. * */ - static long validatePQSignature(Transaction transaction, Permission permission, + public static long validatePQSignature(Transaction transaction, Permission permission, java.util.Set signedAddresses, - DynamicPropertiesStore dynamicPropertiesStore) + DynamicPropertiesStore dynamicPropertiesStore, + List approveList) throws PermissionException { byte[] digest = computeRawHash(transaction).getBytes(); @@ -770,6 +771,9 @@ static long validatePQSignature(Transaction transaction, Permission permission, } catch (ArithmeticException e) { throw new PermissionException("weight overflow"); } + if (approveList != null) { + approveList.add(addrBs); + } } return weight; }