From 02af25ccf62e64527e986a3c06a5cdac9230df18 Mon Sep 17 00:00:00 2001 From: federico Date: Wed, 22 Apr 2026 18:46:31 +0800 Subject: [PATCH 01/12] feat: add ML-DSA post-quantum signature support --- .../AccountPermissionUpdateActuator.java | 73 +++++ .../org/tron/core/utils/ProposalUtil.java | 14 +- .../org/tron/core/capsule/BlockCapsule.java | 114 +++++++- .../tron/core/capsule/TransactionCapsule.java | 125 ++++++++- .../org/tron/core/db/BandwidthProcessor.java | 11 +- .../core/store/DynamicPropertiesStore.java | 17 ++ .../common/parameter/CommonParameter.java | 4 + .../src/main/java/org/tron/core/Constant.java | 8 + .../java/org/tron/consensus/base/Param.java | 8 + .../tron/common/crypto/pqc/MLDSA44Signer.java | 45 +++ .../common/crypto/pqc/MLDSA44Verifier.java | 46 +++ .../tron/common/crypto/pqc/MLDSA65Signer.java | 45 +++ .../common/crypto/pqc/MLDSA65Verifier.java | 46 +++ .../tron/common/crypto/pqc/PqAuthDigest.java | 71 +++++ .../common/crypto/pqc/SignatureVerifier.java | 45 +++ .../crypto/pqc/SignatureVerifierRegistry.java | 39 +++ .../crypto/pqc/MLDSA44VerifierTest.java | 134 +++++++++ .../crypto/pqc/MLDSA65VerifierTest.java | 149 ++++++++++ .../common/crypto/pqc/PqAuthDigestTest.java | 133 +++++++++ .../pqc/SignatureVerifierRegistryTest.java | 62 +++++ .../tron/core/consensus/ProposalService.java | 4 + .../main/java/org/tron/core/db/Manager.java | 50 +++- framework/src/main/resources/config.conf | 11 + .../org/tron/core/BandwidthProcessorTest.java | 118 ++++++++ .../AccountPermissionUpdateActuatorTest.java | 263 ++++++++++++++++++ .../core/actuator/utils/ProposalUtilTest.java | 25 ++ .../tron/core/capsule/BlockCapsulePqTest.java | 260 +++++++++++++++++ .../core/capsule/TransactionCapsuleTest.java | 230 +++++++++++++++ .../core/services/ProposalServiceTest.java | 17 ++ .../org/tron/core/services/http/UtilTest.java | 32 +++ protocol/src/main/protos/core/Tron.proto | 39 +++ 31 files changed, 2222 insertions(+), 16 deletions(-) create mode 100644 crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA44Signer.java create mode 100644 crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA44Verifier.java create mode 100644 crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA65Signer.java create mode 100644 crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA65Verifier.java create mode 100644 crypto/src/main/java/org/tron/common/crypto/pqc/PqAuthDigest.java create mode 100644 crypto/src/main/java/org/tron/common/crypto/pqc/SignatureVerifier.java create mode 100644 crypto/src/main/java/org/tron/common/crypto/pqc/SignatureVerifierRegistry.java create mode 100644 crypto/src/test/java/org/tron/common/crypto/pqc/MLDSA44VerifierTest.java create mode 100644 crypto/src/test/java/org/tron/common/crypto/pqc/MLDSA65VerifierTest.java create mode 100644 crypto/src/test/java/org/tron/common/crypto/pqc/PqAuthDigestTest.java create mode 100644 crypto/src/test/java/org/tron/common/crypto/pqc/SignatureVerifierRegistryTest.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..e319aaab8a6 100644 --- a/actuator/src/main/java/org/tron/core/actuator/AccountPermissionUpdateActuator.java +++ b/actuator/src/main/java/org/tron/core/actuator/AccountPermissionUpdateActuator.java @@ -16,9 +16,12 @@ import org.tron.core.exception.ContractValidateException; import org.tron.core.store.AccountStore; import org.tron.core.store.DynamicPropertiesStore; +import org.tron.common.crypto.pqc.MLDSA44Verifier; +import org.tron.common.crypto.pqc.MLDSA65Verifier; import org.tron.protos.Protocol.Key; import org.tron.protos.Protocol.Permission; import org.tron.protos.Protocol.Permission.PermissionType; +import org.tron.protos.Protocol.SignatureScheme; import org.tron.protos.Protocol.Transaction.Contract.ContractType; import org.tron.protos.Protocol.Transaction.Result.code; import org.tron.protos.contract.AccountContract.AccountPermissionUpdateContract; @@ -102,6 +105,23 @@ private boolean checkPermission(Permission permission) throws ContractValidateEx throw new ContractValidateException( "address should be distinct in permission " + permission.getType()); } + validatePermissionScheme(permission); + + List publicKeyList = permission.getKeysList() + .stream() + .map(Key::getPublicKey) + .filter(pk -> !pk.isEmpty()) + .distinct() + .collect(toList()); + long nonEmptyPublicKeyCount = permission.getKeysList().stream() + .map(Key::getPublicKey) + .filter(pk -> !pk.isEmpty()) + .count(); + if (publicKeyList.size() != nonEmptyPublicKeyCount) { + throw new ContractValidateException( + "public_key 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 +257,57 @@ public ByteString getOwnerAddress() throws InvalidProtocolBufferException { public long calcFee() { return chainBaseManager.getDynamicPropertiesStore().getUpdateAccountPermissionFee(); } + + private void validatePermissionScheme(Permission permission) throws ContractValidateException { + DynamicPropertiesStore dynamicStore = chainBaseManager.getDynamicPropertiesStore(); + boolean mlDsaAllowed = dynamicStore.allowMlDsa(); + + SignatureScheme first = permission.getKeysList().get(0).getScheme(); + for (Key key : permission.getKeysList()) { + SignatureScheme scheme = key.getScheme(); + if (scheme != first) { + throw new ContractValidateException( + "all keys in a permission must use the same scheme"); + } + if (scheme == SignatureScheme.UNKNOWN_SIG_SCHEME) { + if (!key.getPublicKey().isEmpty()) { + throw new ContractValidateException( + "public_key must be empty when scheme is UNKNOWN_SIG_SCHEME"); + } + } else { + if (!mlDsaAllowed) { + throw new ContractValidateException( + "ML-DSA is not activated, scheme " + scheme + " is not allowed"); + } + int expected = expectedPublicKeyLength(scheme); + if (expected < 0) { + throw new ContractValidateException( + "unsupported signature scheme: " + scheme); + } + if (key.getPublicKey().size() != expected) { + throw new ContractValidateException( + "public_key length for " + scheme + " must be " + expected + " bytes, got " + + key.getPublicKey().size()); + } + } + } + + if (permission.getType() == PermissionType.Witness + && first != SignatureScheme.UNKNOWN_SIG_SCHEME + && first != SignatureScheme.ML_DSA_65) { + throw new ContractValidateException( + "Witness permission only supports ML_DSA_65 or legacy scheme, got " + first); + } + } + + private static int expectedPublicKeyLength(SignatureScheme scheme) { + switch (scheme) { + case ML_DSA_44: + return MLDSA44Verifier.PUBLIC_KEY_LENGTH; + case ML_DSA_65: + return MLDSA65Verifier.PUBLIC_KEY_LENGTH; + default: + return -1; + } + } } 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 cd42d7a9010..383bdd37879 100644 --- a/actuator/src/main/java/org/tron/core/utils/ProposalUtil.java +++ b/actuator/src/main/java/org/tron/core/utils/ProposalUtil.java @@ -886,6 +886,17 @@ public static void validator(DynamicPropertiesStore dynamicPropertiesStore, } break; } + case ALLOW_ML_DSA: { + if (dynamicPropertiesStore.getAllowMlDsa() == 1) { + throw new ContractValidateException( + "[ALLOW_ML_DSA] has been valid, no need to propose again"); + } + if (value != 1) { + throw new ContractValidateException( + "This value[ALLOW_ML_DSA] is only allowed to be 1"); + } + break; + } default: break; } @@ -971,7 +982,8 @@ public enum ProposalType { // current value, value range ALLOW_TVM_BLOB(89), // 0, 1 PROPOSAL_EXPIRE_TIME(92), // (0, 31536003000) ALLOW_TVM_SELFDESTRUCT_RESTRICTION(94), // 0, 1 - ALLOW_TVM_OSAKA(96); // 0, 1 + ALLOW_TVM_OSAKA(96), // 0, 1 + ALLOW_ML_DSA(97); // 0, 1 private long code; 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 01ff7fb5365..a783d3b01e9 100755 --- a/chainbase/src/main/java/org/tron/core/capsule/BlockCapsule.java +++ b/chainbase/src/main/java/org/tron/core/capsule/BlockCapsule.java @@ -31,6 +31,9 @@ import org.tron.common.bloom.Bloom; import org.tron.common.crypto.SignInterface; import org.tron.common.crypto.SignUtils; +import org.tron.common.crypto.pqc.PqAuthDigest; +import org.tron.common.crypto.pqc.SignatureVerifier; +import org.tron.common.crypto.pqc.SignatureVerifierRegistry; import org.tron.common.parameter.CommonParameter; import org.tron.common.utils.ByteArray; import org.tron.common.utils.Sha256Hash; @@ -41,8 +44,12 @@ import org.tron.core.exception.ValidateSignatureException; import org.tron.core.store.AccountStore; import org.tron.core.store.DynamicPropertiesStore; +import org.tron.protos.Protocol.AuthWitness; import org.tron.protos.Protocol.Block; import org.tron.protos.Protocol.BlockHeader; +import org.tron.protos.Protocol.Key; +import org.tron.protos.Protocol.Permission; +import org.tron.protos.Protocol.SignatureScheme; import org.tron.protos.Protocol.Transaction; @Slf4j(topic = "capsule") @@ -173,6 +180,16 @@ public void sign(byte[] privateKey) { } + public void setWitnessAuth(AuthWitness authWitness) { + BlockHeader blockHeader = this.block.getBlockHeader().toBuilder() + .setWitnessAuth(authWitness).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()); @@ -180,27 +197,101 @@ private Sha256Hash getRawHash() { public boolean validateSignature(DynamicPropertiesStore dynamicPropertiesStore, AccountStore accountStore) throws ValidateSignatureException { + BlockHeader header = block.getBlockHeader(); + boolean hasLegacy = !header.getWitnessSignature().isEmpty(); + AuthWitness witnessAuth = header.getWitnessAuth(); + boolean hasAuth = witnessAuth != null + && witnessAuth.getSignature() != null + && !witnessAuth.getSignature().isEmpty(); + + if (hasLegacy && hasAuth) { + throw new ValidateSignatureException( + "witness_signature and witness_auth are mutually exclusive"); + } + if (!hasLegacy && !hasAuth) { + throw new ValidateSignatureException("missing witness signature"); + } + + byte[] witnessAccountAddress = header.getRawData().getWitnessAddress().toByteArray(); + if (hasAuth) { + return validateWitnessAuth(dynamicPropertiesStore, accountStore, + witnessAccountAddress, witnessAuth); + } + return validateLegacySignature(dynamicPropertiesStore, accountStore, witnessAccountAddress); + } + + private boolean validateLegacySignature(DynamicPropertiesStore dynamicPropertiesStore, + AccountStore accountStore, byte[] witnessAccountAddress) + throws ValidateSignatureException { + if (dynamicPropertiesStore.allowMlDsa()) { + AccountCapsule accountCapsule = accountStore.get(witnessAccountAddress); + if (accountCapsule != null && accountCapsule.getInstance().hasWitnessPermission()) { + Permission witnessPermission = accountCapsule.getInstance().getWitnessPermission(); + if (witnessPermission.getKeysCount() > 0 + && witnessPermission.getKeys(0).getScheme() == SignatureScheme.ML_DSA_65) { + throw new ValidateSignatureException( + "witness permission requires ML_DSA_65 but witness_signature is legacy"); + } + } + } 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()); } } + private boolean validateWitnessAuth(DynamicPropertiesStore dynamicPropertiesStore, + AccountStore accountStore, byte[] witnessAccountAddress, AuthWitness witnessAuth) + throws ValidateSignatureException { + if (!dynamicPropertiesStore.allowMlDsa()) { + throw new ValidateSignatureException( + "witness_auth present but ML-DSA 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( + "witness_auth present but witness permission is not configured"); + } + SignatureScheme scheme = witnessPermission.getKeys(0).getScheme(); + if (scheme != SignatureScheme.ML_DSA_65) { + throw new ValidateSignatureException( + "witness permission scheme " + scheme + " is not allowed for block signing"); + } + + byte[] signerAddr = witnessAuth.getSignerAddress().toByteArray(); + Key matched = null; + for (Key k : witnessPermission.getKeysList()) { + if (Arrays.equals(k.getAddress().toByteArray(), signerAddr)) { + matched = k; + break; + } + } + if (matched == null) { + throw new ValidateSignatureException( + "witness_auth signer not found in witness permission"); + } + SignatureVerifier verifier = SignatureVerifierRegistry.get(scheme); + byte[] publicKey = matched.getPublicKey().toByteArray(); + byte[] signature = witnessAuth.getSignature().toByteArray(); + byte[] rawHdrHash = getRawHash().getBytes(); + byte[] digest = PqAuthDigest.block(rawHdrHash, signerAddr); + return verifier.verify(publicKey, digest, signature); + } + public BlockId getBlockId() { if (blockId.equals(Sha256Hash.ZERO_HASH)) { blockId = @@ -308,7 +399,12 @@ public long getTimeStamp() { } public boolean hasWitnessSignature() { - return !getInstance().getBlockHeader().getWitnessSignature().isEmpty(); + BlockHeader header = getInstance().getBlockHeader(); + if (!header.getWitnessSignature().isEmpty()) { + return true; + } + AuthWitness auth = header.getWitnessAuth(); + 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 b11c6b1e0a4..72b6f3b5245 100755 --- a/chainbase/src/main/java/org/tron/core/capsule/TransactionCapsule.java +++ b/chainbase/src/main/java/org/tron/core/capsule/TransactionCapsule.java @@ -44,7 +44,11 @@ import org.tron.common.crypto.Rsv; import org.tron.common.crypto.SignInterface; import org.tron.common.crypto.SignUtils; +import org.tron.common.crypto.pqc.PqAuthDigest; +import org.tron.common.crypto.pqc.SignatureVerifier; +import org.tron.common.crypto.pqc.SignatureVerifierRegistry; 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; @@ -64,9 +68,11 @@ import org.tron.core.exception.ValidateSignatureException; import org.tron.core.store.AccountStore; import org.tron.core.store.DynamicPropertiesStore; +import org.tron.protos.Protocol.AuthWitness; import org.tron.protos.Protocol.Key; import org.tron.protos.Protocol.Permission; import org.tron.protos.Protocol.Permission.PermissionType; +import org.tron.protos.Protocol.SignatureScheme; import org.tron.protos.Protocol.Transaction; import org.tron.protos.Protocol.Transaction.Contract.ContractType; import org.tron.protos.Protocol.Transaction.Result; @@ -484,6 +490,11 @@ public static boolean validateSignature(Transaction transaction, throw new PermissionException("permission isn't exit"); } checkPermission(permissionId, permission, contract); + if (permission.getKeysCount() > 0 + && permission.getKeysList().get(0).getScheme() != SignatureScheme.UNKNOWN_SIG_SCHEME) { + throw new PermissionException( + "permission uses PQ scheme, auth_witness is required"); + } long weight = checkWeight(permission, transaction.getSignatureList(), hash, null); if (weight >= permission.getThreshold()) { return true; @@ -637,12 +648,41 @@ public boolean validatePubSignature(AccountStore accountStore, DynamicPropertiesStore dynamicPropertiesStore) throws ValidateSignatureException { if (!isVerified) { - if (this.transaction.getSignatureCount() <= 0 - || this.transaction.getRawData().getContractCount() <= 0) { + int legacyCount = this.transaction.getSignatureCount(); + int pqCount = this.transaction.getAuthWitnessCount(); + + if (pqCount > 0 && !dynamicPropertiesStore.allowMlDsa()) { + throw new ValidateSignatureException("auth_witness not allowed: ML-DSA not activated"); + } + if (legacyCount > 0 && pqCount > 0) { + throw new ValidateSignatureException( + "signature and auth_witness are mutually exclusive"); + } + if (legacyCount == 0 && pqCount == 0) { + throw new ValidateSignatureException("miss sig or contract"); + } + if (this.transaction.getRawData().getContractCount() <= 0) { throw new ValidateSignatureException("miss sig or contract"); } - if (this.transaction.getSignatureCount() > dynamicPropertiesStore - .getTotalSignNum()) { + if (pqCount > 0) { + if (pqCount > dynamicPropertiesStore.getTotalSignNum()) { + throw new ValidateSignatureException("too many signatures"); + } + try { + if (!validateStructuredSignature( + this.transaction, accountStore, dynamicPropertiesStore)) { + isVerified = false; + throw new ValidateSignatureException("sig error"); + } + } catch (PermissionException e) { + isVerified = false; + throw new ValidateSignatureException(e.getMessage()); + } + isVerified = true; + return true; + } + + if (legacyCount > dynamicPropertiesStore.getTotalSignNum()) { throw new ValidateSignatureException("too many signatures"); } @@ -662,6 +702,83 @@ public boolean validatePubSignature(AccountStore accountStore, return true; } + static boolean validateStructuredSignature(Transaction transaction, + AccountStore accountStore, DynamicPropertiesStore dynamicPropertiesStore) + throws PermissionException { + Transaction.Contract contract = transaction.getRawData().getContractList().get(0); + int permissionId = contract.getPermissionId(); + byte[] owner = getOwner(contract); + AccountCapsule account = accountStore.get(owner); + Permission permission = null; + if (account == null) { + if (permissionId == 0) { + permission = AccountCapsule.getDefaultPermission(ByteString.copyFrom(owner)); + } + if (permissionId == 2) { + permission = AccountCapsule + .createDefaultActivePermission(ByteString.copyFrom(owner), dynamicPropertiesStore); + } + } else { + permission = account.getPermissionById(permissionId); + } + if (permission == null) { + throw new PermissionException("permission isn't exit"); + } + checkPermission(permissionId, permission, contract); + + if (permission.getKeysCount() == 0 + || permission.getKeysList().get(0).getScheme() == SignatureScheme.UNKNOWN_SIG_SCHEME) { + throw new PermissionException( + "permission uses legacy scheme, auth_witness is not allowed"); + } + + byte[] txid = computeRawHash(transaction).getBytes(); + List witnesses = transaction.getAuthWitnessList(); + java.util.Set seen = new java.util.HashSet<>(); + long weight = 0L; + for (AuthWitness aw : witnesses) { + ByteString signer = aw.getSignerAddress(); + if (!seen.add(signer)) { + throw new PermissionException("duplicate signer in auth_witness"); + } + Key key = findKeyByAddress(permission, signer); + if (key == null) { + throw new PermissionException("signer is not in permission"); + } + SignatureScheme scheme = key.getScheme(); + if (!SignatureVerifierRegistry.contains(scheme)) { + throw new PermissionException("unsupported scheme: " + scheme); + } + SignatureVerifier verifier = SignatureVerifierRegistry.get(scheme); + byte[] digest = PqAuthDigest.tx(txid, permissionId, signer.toByteArray()); + byte[] pk = key.getPublicKey().toByteArray(); + byte[] sig = aw.getSignature().toByteArray(); + if (pk.length != verifier.getPublicKeyLength() + || sig.length != verifier.getSignatureLength()) { + throw new PermissionException("public key or signature length mismatch"); + } + if (!verifier.verify(pk, digest, sig)) { + throw new PermissionException("pq sig invalid"); + } + weight = StrictMathWrapper.addExact(weight, key.getWeight()); + } + return weight >= permission.getThreshold(); + } + + private static Sha256Hash computeRawHash(Transaction transaction) { + return Sha256Hash.of(CommonParameter.getInstance().isECKeyCryptoEngine(), + transaction.getRawData().toByteArray()); + } + + private static Key findKeyByAddress(Permission permission, ByteString address) { + for (Key k : permission.getKeysList()) { + if (k.getAddress().equals(address)) { + return k; + } + } + return null; + } + /** * 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 2488686bfb0..a3529fb0c78 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().getAuthWitnessCount() > 0) { + long authWitnessBytes = 0L; + for (org.tron.protos.Protocol.AuthWitness aw + : trx.getInstance().getAuthWitnessList()) { + authWitnessBytes += aw.getSerializedSize(); + } + sigOverhead = authWitnessBytes; + } 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 e0adb0d444a..e713cd51a4c 100644 --- a/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java +++ b/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java @@ -240,6 +240,8 @@ public class DynamicPropertiesStore extends TronStoreWithRevoking private static final byte[] ALLOW_TVM_OSAKA = "ALLOW_TVM_OSAKA".getBytes(); + private static final byte[] ALLOW_ML_DSA = "ALLOW_ML_DSA".getBytes(); + @Autowired private DynamicPropertiesStore(@Value("properties") String dbName) { super(dbName); @@ -2993,6 +2995,21 @@ public void saveAllowTvmOsaka(long value) { this.put(ALLOW_TVM_OSAKA, new BytesCapsule(ByteArray.fromLong(value))); } + public long getAllowMlDsa() { + return Optional.ofNullable(getUnchecked(ALLOW_ML_DSA)) + .map(BytesCapsule::getData) + .map(ByteArray::toLong) + .orElse(CommonParameter.getInstance().getAllowMlDsa()); + } + + public void saveAllowMlDsa(long value) { + this.put(ALLOW_ML_DSA, new BytesCapsule(ByteArray.fromLong(value))); + } + + public boolean allowMlDsa() { + return getAllowMlDsa() == 1L; + } + 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 a73158a718a..047213a2738 100644 --- a/common/src/main/java/org/tron/common/parameter/CommonParameter.java +++ b/common/src/main/java/org/tron/common/parameter/CommonParameter.java @@ -637,6 +637,10 @@ public class CommonParameter { @Setter public long allowTvmOsaka; + @Getter + @Setter + public long allowMlDsa; + 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..aacf31b4005 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 (ML-DSA / FIPS 204) signature constants + public static final int ML_DSA_44_PUBLIC_KEY_LENGTH = 1312; + public static final int ML_DSA_44_SIGNATURE_LENGTH = 2420; + public static final int ML_DSA_65_PUBLIC_KEY_LENGTH = 1952; + public static final int ML_DSA_65_SIGNATURE_LENGTH = 3309; + 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/consensus/src/main/java/org/tron/consensus/base/Param.java b/consensus/src/main/java/org/tron/consensus/base/Param.java index f7b7de3d084..060d1627f53 100644 --- a/consensus/src/main/java/org/tron/consensus/base/Param.java +++ b/consensus/src/main/java/org/tron/consensus/base/Param.java @@ -67,6 +67,14 @@ public class Miner { @Setter private ByteString witnessAddress; + @Getter + @Setter + private byte[] pqPrivateKey; + + @Getter + @Setter + private byte[] pqPublicKey; + 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/MLDSA44Signer.java b/crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA44Signer.java new file mode 100644 index 00000000000..9ea743ed3a1 --- /dev/null +++ b/crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA44Signer.java @@ -0,0 +1,45 @@ +package org.tron.common.crypto.pqc; + +import org.bouncycastle.pqc.crypto.mldsa.MLDSAParameters; +import org.bouncycastle.pqc.crypto.mldsa.MLDSAPrivateKeyParameters; +import org.bouncycastle.pqc.crypto.mldsa.MLDSAPublicKeyParameters; +import org.bouncycastle.pqc.crypto.mldsa.MLDSASigner; + +public final class MLDSA44Signer { + + public static final int PRIVATE_KEY_LENGTH = 2560; + + private MLDSA44Signer() { + } + + public static byte[] sign(byte[] privateKey, byte[] message) { + if (privateKey == null || privateKey.length != PRIVATE_KEY_LENGTH) { + throw new IllegalArgumentException( + "ML-DSA-44 private key length must be " + PRIVATE_KEY_LENGTH); + } + if (message == null) { + throw new IllegalArgumentException("message must not be null"); + } + MLDSAPrivateKeyParameters sk = new MLDSAPrivateKeyParameters( + MLDSAParameters.ml_dsa_44, privateKey); + MLDSASigner signer = new MLDSASigner(); + signer.init(true, sk); + signer.update(message, 0, message.length); + try { + return signer.generateSignature(); + } catch (Exception e) { + throw new IllegalStateException("ML-DSA-44 signing failed", e); + } + } + + public static byte[] derivePublicKey(byte[] privateKey) { + if (privateKey == null || privateKey.length != PRIVATE_KEY_LENGTH) { + throw new IllegalArgumentException( + "ML-DSA-44 private key length must be " + PRIVATE_KEY_LENGTH); + } + MLDSAPrivateKeyParameters sk = new MLDSAPrivateKeyParameters( + MLDSAParameters.ml_dsa_44, privateKey); + MLDSAPublicKeyParameters pk = sk.getPublicKeyParameters(); + return pk.getEncoded(); + } +} diff --git a/crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA44Verifier.java b/crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA44Verifier.java new file mode 100644 index 00000000000..7e46fb1fb30 --- /dev/null +++ b/crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA44Verifier.java @@ -0,0 +1,46 @@ +package org.tron.common.crypto.pqc; + +import org.bouncycastle.pqc.crypto.mldsa.MLDSAParameters; +import org.bouncycastle.pqc.crypto.mldsa.MLDSAPublicKeyParameters; +import org.bouncycastle.pqc.crypto.mldsa.MLDSASigner; +import org.tron.protos.Protocol.SignatureScheme; + +/** + * FIPS 204 ML-DSA-44 verifier. Consumes raw public key and signature bytes — + * no SubjectPublicKeyInfo, PEM, or Base64 wrapping. + */ +public class MLDSA44Verifier implements SignatureVerifier { + + public static final int PUBLIC_KEY_LENGTH = 1312; + public static final int SIGNATURE_LENGTH = 2420; + + @Override + public SignatureScheme getScheme() { + return SignatureScheme.ML_DSA_44; + } + + @Override + public int getPublicKeyLength() { + return PUBLIC_KEY_LENGTH; + } + + @Override + public int getSignatureLength() { + return SIGNATURE_LENGTH; + } + + @Override + public boolean verify(byte[] publicKey, byte[] message, byte[] signature) { + validatePublicKey(publicKey); + validateSignature(signature); + if (message == null) { + throw new IllegalArgumentException("message must not be null"); + } + MLDSAPublicKeyParameters pk = new MLDSAPublicKeyParameters( + MLDSAParameters.ml_dsa_44, publicKey); + MLDSASigner signer = new MLDSASigner(); + signer.init(false, pk); + signer.update(message, 0, message.length); + return signer.verifySignature(signature); + } +} diff --git a/crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA65Signer.java b/crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA65Signer.java new file mode 100644 index 00000000000..1365243ad8b --- /dev/null +++ b/crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA65Signer.java @@ -0,0 +1,45 @@ +package org.tron.common.crypto.pqc; + +import org.bouncycastle.pqc.crypto.mldsa.MLDSAParameters; +import org.bouncycastle.pqc.crypto.mldsa.MLDSAPrivateKeyParameters; +import org.bouncycastle.pqc.crypto.mldsa.MLDSAPublicKeyParameters; +import org.bouncycastle.pqc.crypto.mldsa.MLDSASigner; + +public final class MLDSA65Signer { + + public static final int PRIVATE_KEY_LENGTH = 4032; + + private MLDSA65Signer() { + } + + public static byte[] sign(byte[] privateKey, byte[] message) { + if (privateKey == null || privateKey.length != PRIVATE_KEY_LENGTH) { + throw new IllegalArgumentException( + "ML-DSA-65 private key length must be " + PRIVATE_KEY_LENGTH); + } + if (message == null) { + throw new IllegalArgumentException("message must not be null"); + } + MLDSAPrivateKeyParameters sk = new MLDSAPrivateKeyParameters( + MLDSAParameters.ml_dsa_65, privateKey); + MLDSASigner signer = new MLDSASigner(); + signer.init(true, sk); + signer.update(message, 0, message.length); + try { + return signer.generateSignature(); + } catch (Exception e) { + throw new IllegalStateException("ML-DSA-65 signing failed", e); + } + } + + public static byte[] derivePublicKey(byte[] privateKey) { + if (privateKey == null || privateKey.length != PRIVATE_KEY_LENGTH) { + throw new IllegalArgumentException( + "ML-DSA-65 private key length must be " + PRIVATE_KEY_LENGTH); + } + MLDSAPrivateKeyParameters sk = new MLDSAPrivateKeyParameters( + MLDSAParameters.ml_dsa_65, privateKey); + MLDSAPublicKeyParameters pk = sk.getPublicKeyParameters(); + return pk.getEncoded(); + } +} diff --git a/crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA65Verifier.java b/crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA65Verifier.java new file mode 100644 index 00000000000..816bfd367a8 --- /dev/null +++ b/crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA65Verifier.java @@ -0,0 +1,46 @@ +package org.tron.common.crypto.pqc; + +import org.bouncycastle.pqc.crypto.mldsa.MLDSAParameters; +import org.bouncycastle.pqc.crypto.mldsa.MLDSAPublicKeyParameters; +import org.bouncycastle.pqc.crypto.mldsa.MLDSASigner; +import org.tron.protos.Protocol.SignatureScheme; + +/** + * FIPS 204 ML-DSA-65 verifier. Consumes raw public key and signature bytes — + * no SubjectPublicKeyInfo, PEM, or Base64 wrapping. + */ +public class MLDSA65Verifier implements SignatureVerifier { + + public static final int PUBLIC_KEY_LENGTH = 1952; + public static final int SIGNATURE_LENGTH = 3309; + + @Override + public SignatureScheme getScheme() { + return SignatureScheme.ML_DSA_65; + } + + @Override + public int getPublicKeyLength() { + return PUBLIC_KEY_LENGTH; + } + + @Override + public int getSignatureLength() { + return SIGNATURE_LENGTH; + } + + @Override + public boolean verify(byte[] publicKey, byte[] message, byte[] signature) { + validatePublicKey(publicKey); + validateSignature(signature); + if (message == null) { + throw new IllegalArgumentException("message must not be null"); + } + MLDSAPublicKeyParameters pk = new MLDSAPublicKeyParameters( + MLDSAParameters.ml_dsa_65, publicKey); + MLDSASigner signer = new MLDSASigner(); + signer.init(false, pk); + signer.update(message, 0, message.length); + return signer.verifySignature(signature); + } +} diff --git a/crypto/src/main/java/org/tron/common/crypto/pqc/PqAuthDigest.java b/crypto/src/main/java/org/tron/common/crypto/pqc/PqAuthDigest.java new file mode 100644 index 00000000000..32ab9a8659e --- /dev/null +++ b/crypto/src/main/java/org/tron/common/crypto/pqc/PqAuthDigest.java @@ -0,0 +1,71 @@ +package org.tron.common.crypto.pqc; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import org.tron.common.utils.Sha256Hash; + +/** + * Domain-separated SHA-256 digests for post-quantum authentication. + * + *

The domain prefixes are UTF-8 string bytes concatenated before the + * context fields. They differ between transaction and block flows so a + * transaction signature can never be replayed as a block signature and vice + * versa. + */ +public final class PqAuthDigest { + + public static final String TX_DOMAIN = "TRON_TX_AUTH_V1"; + public static final String BLOCK_DOMAIN = "TRON_BLOCK_AUTH_V1"; + + static final byte[] TX_DOMAIN_BYTES = TX_DOMAIN.getBytes(StandardCharsets.UTF_8); + static final byte[] BLOCK_DOMAIN_BYTES = BLOCK_DOMAIN.getBytes(StandardCharsets.UTF_8); + + private PqAuthDigest() { + } + + /** + * Transaction-level PQ authentication digest. + * + *

digest = SHA-256("TRON_TX_AUTH_V1" || txid || permission_id_be4 || signer_address)
+ */ + public static byte[] tx(byte[] txid, int permissionId, byte[] signerAddress) { + requireNonNull(txid, "txid"); + requireNonNull(signerAddress, "signerAddress"); + MessageDigest md = Sha256Hash.newDigest(); + md.update(TX_DOMAIN_BYTES); + md.update(txid); + md.update(intToBe4(permissionId)); + md.update(signerAddress); + return md.digest(); + } + + /** + * Block-level PQ authentication digest. + * + *
digest = SHA-256("TRON_BLOCK_AUTH_V1" || block_header_raw_hash || witness_address)
+ */ + public static byte[] block(byte[] blockHeaderRawHash, byte[] witnessAddress) { + requireNonNull(blockHeaderRawHash, "blockHeaderRawHash"); + requireNonNull(witnessAddress, "witnessAddress"); + MessageDigest md = Sha256Hash.newDigest(); + md.update(BLOCK_DOMAIN_BYTES); + md.update(blockHeaderRawHash); + md.update(witnessAddress); + return md.digest(); + } + + private static byte[] intToBe4(int v) { + return new byte[] { + (byte) ((v >>> 24) & 0xff), + (byte) ((v >>> 16) & 0xff), + (byte) ((v >>> 8) & 0xff), + (byte) (v & 0xff) + }; + } + + private static void requireNonNull(byte[] b, String name) { + if (b == null) { + throw new IllegalArgumentException(name + " must not be null"); + } + } +} diff --git a/crypto/src/main/java/org/tron/common/crypto/pqc/SignatureVerifier.java b/crypto/src/main/java/org/tron/common/crypto/pqc/SignatureVerifier.java new file mode 100644 index 00000000000..a2f49b2f5b0 --- /dev/null +++ b/crypto/src/main/java/org/tron/common/crypto/pqc/SignatureVerifier.java @@ -0,0 +1,45 @@ +package org.tron.common.crypto.pqc; + +import org.tron.protos.Protocol.SignatureScheme; + +/** + * Stateless verifier for a single post-quantum signature scheme. Independent of + * the legacy {@code SignInterface} because PQ flows do not expose public-key + * recovery and have no notion of node-id / private-key handling in verification + * paths. + */ +public interface SignatureVerifier { + + SignatureScheme getScheme(); + + int getPublicKeyLength(); + + int getSignatureLength(); + + /** + * Verify a raw-byte signature against a raw-byte public key over {@code message}. + * The caller SHALL pre-validate lengths via {@link #validatePublicKey(byte[])} and + * {@link #validateSignature(byte[])}; implementations still defensively re-check. + * + * @return true iff the signature is cryptographically valid for the given inputs + */ + boolean verify(byte[] publicKey, byte[] message, byte[] signature); + + 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 void validateSignature(byte[] signature) { + if (signature == null || signature.length != getSignatureLength()) { + throw new IllegalArgumentException( + "invalid " + getScheme() + " signature length: " + + (signature == null ? "null" : signature.length) + + ", expected " + getSignatureLength()); + } + } +} diff --git a/crypto/src/main/java/org/tron/common/crypto/pqc/SignatureVerifierRegistry.java b/crypto/src/main/java/org/tron/common/crypto/pqc/SignatureVerifierRegistry.java new file mode 100644 index 00000000000..3b64cd637cd --- /dev/null +++ b/crypto/src/main/java/org/tron/common/crypto/pqc/SignatureVerifierRegistry.java @@ -0,0 +1,39 @@ +package org.tron.common.crypto.pqc; + +import java.util.Collections; +import java.util.EnumMap; +import java.util.Map; +import org.tron.protos.Protocol.SignatureScheme; + +/** + * Static registry of post-quantum {@link SignatureVerifier} instances keyed by + * {@link SignatureScheme}. Legacy schemes (ECDSA secp256k1, SM2/SM3) are NOT + * registered — they flow through the existing {@code SignInterface} path. + */ +public final class SignatureVerifierRegistry { + + private static final Map VERIFIERS; + + static { + EnumMap m = new EnumMap<>(SignatureScheme.class); + m.put(SignatureScheme.ML_DSA_44, new MLDSA44Verifier()); + m.put(SignatureScheme.ML_DSA_65, new MLDSA65Verifier()); + VERIFIERS = Collections.unmodifiableMap(m); + } + + private SignatureVerifierRegistry() { + } + + public static SignatureVerifier get(SignatureScheme scheme) { + SignatureVerifier v = VERIFIERS.get(scheme); + if (v == null) { + throw new IllegalArgumentException( + "no SignatureVerifier registered for scheme: " + scheme); + } + return v; + } + + public static boolean contains(SignatureScheme scheme) { + return VERIFIERS.containsKey(scheme); + } +} diff --git a/crypto/src/test/java/org/tron/common/crypto/pqc/MLDSA44VerifierTest.java b/crypto/src/test/java/org/tron/common/crypto/pqc/MLDSA44VerifierTest.java new file mode 100644 index 00000000000..2f59ed8855d --- /dev/null +++ b/crypto/src/test/java/org/tron/common/crypto/pqc/MLDSA44VerifierTest.java @@ -0,0 +1,134 @@ +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.pqc.crypto.mldsa.MLDSAKeyGenerationParameters; +import org.bouncycastle.pqc.crypto.mldsa.MLDSAKeyPairGenerator; +import org.bouncycastle.pqc.crypto.mldsa.MLDSAParameters; +import org.bouncycastle.pqc.crypto.mldsa.MLDSAPrivateKeyParameters; +import org.bouncycastle.pqc.crypto.mldsa.MLDSAPublicKeyParameters; +import org.bouncycastle.pqc.crypto.mldsa.MLDSASigner; +import org.junit.Before; +import org.junit.Test; +import org.tron.protos.Protocol.SignatureScheme; + +public class MLDSA44VerifierTest { + + private MLDSA44Verifier verifier; + private MLDSAPublicKeyParameters pk; + private MLDSAPrivateKeyParameters sk; + + @Before + public void setUp() { + verifier = new MLDSA44Verifier(); + MLDSAKeyPairGenerator gen = new MLDSAKeyPairGenerator(); + gen.init(new MLDSAKeyGenerationParameters(new SecureRandom(), MLDSAParameters.ml_dsa_44)); + AsymmetricCipherKeyPair kp = gen.generateKeyPair(); + pk = (MLDSAPublicKeyParameters) kp.getPublic(); + sk = (MLDSAPrivateKeyParameters) kp.getPrivate(); + } + + private byte[] sign(byte[] message) { + MLDSASigner signer = new MLDSASigner(); + signer.init(true, sk); + signer.update(message, 0, message.length); + try { + return signer.generateSignature(); + } catch (Exception e) { + throw new AssertionError("failed to sign in test setup", e); + } + } + + @Test + public void schemeAndLengthsMatchFips204() { + assertEquals(SignatureScheme.ML_DSA_44, verifier.getScheme()); + assertEquals(1312, verifier.getPublicKeyLength()); + assertEquals(2420, verifier.getSignatureLength()); + assertArrayEquals(pk.getEncoded(), pk.getEncoded()); + assertEquals(1312, pk.getEncoded().length); + } + + @Test + public void validSignatureVerifies() { + byte[] msg = "tron-pq-mldsa44".getBytes(); + byte[] sig = sign(msg); + assertEquals(2420, sig.length); + assertTrue(verifier.verify(pk.getEncoded(), msg, sig)); + } + + @Test + public void signatureBoundToMessage() { + byte[] msg = "hello".getBytes(); + byte[] sig = sign(msg); + byte[] tamperedMsg = "hellp".getBytes(); + assertFalse(verifier.verify(pk.getEncoded(), tamperedMsg, sig)); + } + + @Test + public void tamperedSignatureFailsVerification() { + byte[] msg = "payload".getBytes(); + byte[] sig = sign(msg); + sig[0] ^= 0x01; + assertFalse(verifier.verify(pk.getEncoded(), msg, sig)); + } + + @Test + public void wrongPublicKeyFailsVerification() { + byte[] msg = "payload".getBytes(); + byte[] sig = sign(msg); + MLDSAKeyPairGenerator gen = new MLDSAKeyPairGenerator(); + gen.init(new MLDSAKeyGenerationParameters(new SecureRandom(), MLDSAParameters.ml_dsa_44)); + MLDSAPublicKeyParameters otherPk = + (MLDSAPublicKeyParameters) gen.generateKeyPair().getPublic(); + assertFalse(verifier.verify(otherPk.getEncoded(), msg, sig)); + } + + @Test + public void invalidPublicKeyLengthRejected() { + byte[] badPk = new byte[1311]; + byte[] msg = new byte[] {1}; + byte[] sig = new byte[2420]; + try { + verifier.verify(badPk, msg, sig); + fail("short public key should be rejected"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("public key length")); + } + } + + @Test + public void invalidSignatureLengthRejected() { + byte[] badSig = new byte[2419]; + byte[] msg = new byte[] {1}; + try { + verifier.verify(pk.getEncoded(), msg, badSig); + fail("short signature should be rejected"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("signature length")); + } + } + + @Test + public void nullMessageRejected() { + byte[] sig = new byte[2420]; + try { + verifier.verify(pk.getEncoded(), null, sig); + fail("null message should be rejected"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("message")); + } + } + + @Test + public void emptyMessageVerifiesConsistently() { + byte[] msg = new byte[0]; + byte[] sig = sign(msg); + assertTrue(verifier.verify(pk.getEncoded(), msg, sig)); + } +} diff --git a/crypto/src/test/java/org/tron/common/crypto/pqc/MLDSA65VerifierTest.java b/crypto/src/test/java/org/tron/common/crypto/pqc/MLDSA65VerifierTest.java new file mode 100644 index 00000000000..c6ee8077df5 --- /dev/null +++ b/crypto/src/test/java/org/tron/common/crypto/pqc/MLDSA65VerifierTest.java @@ -0,0 +1,149 @@ +package org.tron.common.crypto.pqc; + +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.pqc.crypto.mldsa.MLDSAKeyGenerationParameters; +import org.bouncycastle.pqc.crypto.mldsa.MLDSAKeyPairGenerator; +import org.bouncycastle.pqc.crypto.mldsa.MLDSAParameters; +import org.bouncycastle.pqc.crypto.mldsa.MLDSAPrivateKeyParameters; +import org.bouncycastle.pqc.crypto.mldsa.MLDSAPublicKeyParameters; +import org.bouncycastle.pqc.crypto.mldsa.MLDSASigner; +import org.junit.Before; +import org.junit.Test; +import org.tron.protos.Protocol.SignatureScheme; + +public class MLDSA65VerifierTest { + + private MLDSA65Verifier verifier; + private MLDSAPublicKeyParameters pk; + private MLDSAPrivateKeyParameters sk; + + @Before + public void setUp() { + verifier = new MLDSA65Verifier(); + MLDSAKeyPairGenerator gen = new MLDSAKeyPairGenerator(); + gen.init(new MLDSAKeyGenerationParameters(new SecureRandom(), MLDSAParameters.ml_dsa_65)); + AsymmetricCipherKeyPair kp = gen.generateKeyPair(); + pk = (MLDSAPublicKeyParameters) kp.getPublic(); + sk = (MLDSAPrivateKeyParameters) kp.getPrivate(); + } + + private byte[] sign(byte[] message) { + MLDSASigner signer = new MLDSASigner(); + signer.init(true, sk); + signer.update(message, 0, message.length); + try { + return signer.generateSignature(); + } catch (Exception e) { + throw new AssertionError("failed to sign in test setup", e); + } + } + + @Test + public void schemeAndLengthsMatchFips204() { + assertEquals(SignatureScheme.ML_DSA_65, verifier.getScheme()); + assertEquals(1952, verifier.getPublicKeyLength()); + assertEquals(3309, verifier.getSignatureLength()); + assertEquals(1952, pk.getEncoded().length); + } + + @Test + public void validSignatureVerifies() { + byte[] msg = "tron-pq-mldsa65".getBytes(); + byte[] sig = sign(msg); + assertEquals(3309, sig.length); + assertTrue(verifier.verify(pk.getEncoded(), msg, sig)); + } + + @Test + public void signatureBoundToMessage() { + byte[] msg = "block-header".getBytes(); + byte[] sig = sign(msg); + byte[] tamperedMsg = "block-footer".getBytes(); + assertFalse(verifier.verify(pk.getEncoded(), tamperedMsg, sig)); + } + + @Test + public void tamperedSignatureFailsVerification() { + byte[] msg = "payload".getBytes(); + byte[] sig = sign(msg); + sig[sig.length - 1] ^= 0x01; + assertFalse(verifier.verify(pk.getEncoded(), msg, sig)); + } + + @Test + public void wrongPublicKeyFailsVerification() { + byte[] msg = "payload".getBytes(); + byte[] sig = sign(msg); + MLDSAKeyPairGenerator gen = new MLDSAKeyPairGenerator(); + gen.init(new MLDSAKeyGenerationParameters(new SecureRandom(), MLDSAParameters.ml_dsa_65)); + MLDSAPublicKeyParameters otherPk = + (MLDSAPublicKeyParameters) gen.generateKeyPair().getPublic(); + assertFalse(verifier.verify(otherPk.getEncoded(), msg, sig)); + } + + @Test + public void invalidPublicKeyLengthRejected() { + byte[] badPk = new byte[1951]; + byte[] msg = new byte[] {1}; + byte[] sig = new byte[3309]; + try { + verifier.verify(badPk, msg, sig); + fail("short public key should be rejected"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("public key length")); + } + } + + @Test + public void invalidSignatureLengthRejected() { + byte[] badSig = new byte[3310]; + byte[] msg = new byte[] {1}; + try { + verifier.verify(pk.getEncoded(), msg, badSig); + fail("wrong-length signature should be rejected"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("signature length")); + } + } + + @Test + public void nullMessageRejected() { + byte[] sig = new byte[3309]; + try { + verifier.verify(pk.getEncoded(), null, sig); + fail("null message should be rejected"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("message")); + } + } + + @Test + public void emptyMessageVerifiesConsistently() { + byte[] msg = new byte[0]; + byte[] sig = sign(msg); + assertTrue(verifier.verify(pk.getEncoded(), msg, sig)); + } + + @Test + public void crossSchemeKeyFailsVerification() { + MLDSAKeyPairGenerator gen44 = new MLDSAKeyPairGenerator(); + gen44.init(new MLDSAKeyGenerationParameters(new SecureRandom(), MLDSAParameters.ml_dsa_44)); + MLDSAPublicKeyParameters pk44 = + (MLDSAPublicKeyParameters) gen44.generateKeyPair().getPublic(); + byte[] pk44Bytes = pk44.getEncoded(); + byte[] msg = new byte[] {1}; + byte[] sig = new byte[3309]; + try { + verifier.verify(pk44Bytes, msg, sig); + fail("ML-DSA-44 key for ML-DSA-65 verifier should be rejected on length"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("public key length")); + } + } +} diff --git a/crypto/src/test/java/org/tron/common/crypto/pqc/PqAuthDigestTest.java b/crypto/src/test/java/org/tron/common/crypto/pqc/PqAuthDigestTest.java new file mode 100644 index 00000000000..c8ad1cd8c36 --- /dev/null +++ b/crypto/src/test/java/org/tron/common/crypto/pqc/PqAuthDigestTest.java @@ -0,0 +1,133 @@ +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 static org.junit.Assert.fail; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import org.junit.Test; + +public class PqAuthDigestTest { + + private static byte[] bytes(int... values) { + byte[] out = new byte[values.length]; + for (int i = 0; i < values.length; i++) { + out[i] = (byte) values[i]; + } + return out; + } + + @Test + public void txDigestEqualsExpectedSha256() throws Exception { + byte[] txid = bytes(0x11, 0x22, 0x33, 0x44); + int permissionId = 2; + byte[] signer = bytes(0xaa, 0xbb, 0xcc); + + MessageDigest md = MessageDigest.getInstance("SHA-256"); + md.update("TRON_TX_AUTH_V1".getBytes(StandardCharsets.UTF_8)); + md.update(txid); + md.update(bytes(0, 0, 0, 2)); + md.update(signer); + byte[] expected = md.digest(); + + byte[] actual = PqAuthDigest.tx(txid, permissionId, signer); + assertArrayEquals(expected, actual); + assertEquals(32, actual.length); + } + + @Test + public void blockDigestEqualsExpectedSha256() throws Exception { + byte[] hdrHash = new byte[32]; + for (int i = 0; i < hdrHash.length; i++) { + hdrHash[i] = (byte) i; + } + byte[] witness = bytes(0x41, 0x42, 0x43); + + MessageDigest md = MessageDigest.getInstance("SHA-256"); + md.update("TRON_BLOCK_AUTH_V1".getBytes(StandardCharsets.UTF_8)); + md.update(hdrHash); + md.update(witness); + byte[] expected = md.digest(); + + byte[] actual = PqAuthDigest.block(hdrHash, witness); + assertArrayEquals(expected, actual); + assertEquals(32, actual.length); + } + + @Test + public void txAndBlockDigestsDifferForSameContext() { + byte[] shared = new byte[32]; + byte[] addr = new byte[] {1, 2, 3, 4, 5}; + byte[] txDigest = PqAuthDigest.tx(shared, 0, addr); + byte[] blockDigest = PqAuthDigest.block(shared, addr); + assertFalse("tx and block digests must not collide for shared inputs", + java.util.Arrays.equals(txDigest, blockDigest)); + } + + @Test + public void differentSignersProduceDifferentTxDigest() { + byte[] txid = new byte[32]; + byte[] a = new byte[] {0x10}; + byte[] b = new byte[] {0x20}; + assertNotEquals( + new String(PqAuthDigest.tx(txid, 0, a)), + new String(PqAuthDigest.tx(txid, 0, b))); + } + + @Test + public void differentPermissionIdsProduceDifferentDigest() { + byte[] txid = new byte[32]; + byte[] addr = new byte[] {1}; + byte[] d0 = PqAuthDigest.tx(txid, 0, addr); + byte[] d1 = PqAuthDigest.tx(txid, 1, addr); + assertFalse(java.util.Arrays.equals(d0, d1)); + } + + @Test + public void differentWitnessesProduceDifferentBlockDigest() { + byte[] hdr = new byte[32]; + byte[] w1 = new byte[] {1}; + byte[] w2 = new byte[] {2}; + byte[] d1 = PqAuthDigest.block(hdr, w1); + byte[] d2 = PqAuthDigest.block(hdr, w2); + assertFalse(java.util.Arrays.equals(d1, d2)); + } + + @Test + public void domainPrefixesAreExact() { + assertEquals("TRON_TX_AUTH_V1", PqAuthDigest.TX_DOMAIN); + assertEquals("TRON_BLOCK_AUTH_V1", PqAuthDigest.BLOCK_DOMAIN); + } + + @Test + public void nullInputsRejected() { + try { + PqAuthDigest.tx(null, 0, new byte[1]); + fail("null txid should be rejected"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("txid")); + } + try { + PqAuthDigest.tx(new byte[1], 0, null); + fail("null signer should be rejected"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("signerAddress")); + } + try { + PqAuthDigest.block(null, new byte[1]); + fail("null hdr should be rejected"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("blockHeaderRawHash")); + } + try { + PqAuthDigest.block(new byte[1], null); + fail("null witness should be rejected"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("witnessAddress")); + } + } +} diff --git a/crypto/src/test/java/org/tron/common/crypto/pqc/SignatureVerifierRegistryTest.java b/crypto/src/test/java/org/tron/common/crypto/pqc/SignatureVerifierRegistryTest.java new file mode 100644 index 00000000000..769fe55ca1e --- /dev/null +++ b/crypto/src/test/java/org/tron/common/crypto/pqc/SignatureVerifierRegistryTest.java @@ -0,0 +1,62 @@ +package org.tron.common.crypto.pqc; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.junit.Test; +import org.tron.protos.Protocol.SignatureScheme; + +public class SignatureVerifierRegistryTest { + + @Test + public void mlDsa44Registered() { + SignatureVerifier v = SignatureVerifierRegistry.get(SignatureScheme.ML_DSA_44); + assertNotNull(v); + assertSame(SignatureScheme.ML_DSA_44, v.getScheme()); + assertTrue(SignatureVerifierRegistry.contains(SignatureScheme.ML_DSA_44)); + } + + @Test + public void mlDsa65Registered() { + SignatureVerifier v = SignatureVerifierRegistry.get(SignatureScheme.ML_DSA_65); + assertNotNull(v); + assertSame(SignatureScheme.ML_DSA_65, v.getScheme()); + assertTrue(SignatureVerifierRegistry.contains(SignatureScheme.ML_DSA_65)); + } + + @Test + public void ecdsaNotRegistered() { + assertFalse(SignatureVerifierRegistry.contains(SignatureScheme.ECDSA_SECP256K1)); + try { + SignatureVerifierRegistry.get(SignatureScheme.ECDSA_SECP256K1); + fail("expected IllegalArgumentException for ECDSA_SECP256K1"); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("ECDSA_SECP256K1")); + } + } + + @Test + public void sm2NotRegistered() { + assertFalse(SignatureVerifierRegistry.contains(SignatureScheme.SM2_SM3)); + try { + SignatureVerifierRegistry.get(SignatureScheme.SM2_SM3); + fail("expected IllegalArgumentException for SM2_SM3"); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("SM2_SM3")); + } + } + + @Test + public void unknownSchemeRejected() { + assertFalse(SignatureVerifierRegistry.contains(SignatureScheme.UNKNOWN_SIG_SCHEME)); + try { + SignatureVerifierRegistry.get(SignatureScheme.UNKNOWN_SIG_SCHEME); + fail("expected IllegalArgumentException for UNKNOWN_SIG_SCHEME"); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("UNKNOWN_SIG_SCHEME")); + } + } +} 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 1bec0c2bda3..0d5f63f1bed 100644 --- a/framework/src/main/java/org/tron/core/consensus/ProposalService.java +++ b/framework/src/main/java/org/tron/core/consensus/ProposalService.java @@ -396,6 +396,10 @@ public static boolean process(Manager manager, ProposalCapsule proposalCapsule) manager.getDynamicPropertiesStore().saveAllowTvmOsaka(entry.getValue()); break; } + case ALLOW_ML_DSA: { + manager.getDynamicPropertiesStore().saveAllowMlDsa(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 cd1a61c01fe..72e5b048c94 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -54,6 +54,8 @@ import org.tron.common.args.GenesisBlock; import org.tron.common.bloom.Bloom; import org.tron.common.cron.CronExpression; +import org.tron.common.crypto.pqc.MLDSA65Signer; +import org.tron.common.crypto.pqc.PqAuthDigest; import org.tron.common.es.ExecutorServiceManager; import org.tron.common.exit.ExitManager; import org.tron.common.logsfilter.EventPluginLoader; @@ -168,7 +170,9 @@ import org.tron.core.utils.TransactionRegister; import org.tron.protos.Protocol; import org.tron.protos.Protocol.AccountType; +import org.tron.protos.Protocol.AuthWitness; import org.tron.protos.Protocol.Permission; +import org.tron.protos.Protocol.SignatureScheme; import org.tron.protos.Protocol.Transaction; import org.tron.protos.Protocol.Transaction.Contract; import org.tron.protos.Protocol.TransactionInfo; @@ -1738,7 +1742,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; @@ -1754,6 +1758,50 @@ public BlockCapsule generateBlock(Miner miner, long blockTime, long timeout) { return capsule; } + private void signBlockCapsule(BlockCapsule blockCapsule, Miner miner) { + SignatureScheme scheme = resolveWitnessScheme(miner); + if (scheme == SignatureScheme.ML_DSA_65) { + signWitnessAuth(blockCapsule, miner); + } else { + blockCapsule.sign(miner.getPrivateKey()); + } + } + + private SignatureScheme resolveWitnessScheme(Miner miner) { + if (!chainBaseManager.getDynamicPropertiesStore().allowMlDsa()) { + return SignatureScheme.UNKNOWN_SIG_SCHEME; + } + byte[] witnessAddress = miner.getWitnessAddress().toByteArray(); + AccountCapsule accountCapsule = chainBaseManager.getAccountStore().get(witnessAddress); + if (accountCapsule == null || !accountCapsule.getInstance().hasWitnessPermission()) { + return SignatureScheme.UNKNOWN_SIG_SCHEME; + } + Permission witnessPermission = accountCapsule.getInstance().getWitnessPermission(); + if (witnessPermission.getKeysCount() == 0) { + return SignatureScheme.UNKNOWN_SIG_SCHEME; + } + return witnessPermission.getKeys(0).getScheme(); + } + + private void signWitnessAuth(BlockCapsule blockCapsule, Miner miner) { + byte[] witnessAddress = miner.getWitnessAddress().toByteArray(); + Permission witnessPermission = chainBaseManager.getAccountStore().get(witnessAddress) + .getInstance().getWitnessPermission(); + byte[] pqPrivateKey = miner.getPqPrivateKey(); + if (pqPrivateKey == null) { + throw new IllegalStateException( + "witness permission requires ML_DSA_65 but local PQ private key is not configured"); + } + byte[] signerAddress = witnessPermission.getKeys(0).getAddress().toByteArray(); + byte[] digest = PqAuthDigest.block(blockCapsule.getRawHashBytes(), signerAddress); + byte[] signature = MLDSA65Signer.sign(pqPrivateKey, digest); + AuthWitness witnessAuth = AuthWitness.newBuilder() + .setSignerAddress(ByteString.copyFrom(signerAddress)) + .setSignature(ByteString.copyFrom(signature)) + .build(); + blockCapsule.setWitnessAuth(witnessAuth); + } + 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 369924074bc..4a709e87e93 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -673,6 +673,17 @@ localwitness = [ # "localwitnesskeystore.json" # ] +# Post-quantum (ML-DSA-65) witness signing key. Required once the on-chain witness +# Permission has been upgraded to scheme = ML_DSA_65 and ALLOW_ML_DSA = 1 is active. +# Each entry is the 4032-byte FIPS 204 ML-DSA-65 private key encoded as a hex string +# (no SubjectPublicKeyInfo / PEM / Base64 wrapping). The node uses this key to +# produce the BlockHeader.witness_auth field during Dual-Sign block production. +# If the witness Permission is still legacy (scheme = UNKNOWN_SIG_SCHEME), leave this +# empty; the node will use `localwitness` / `localwitnesskeystore` as before. +# localwitness_pq = [ +# "0123abcd...<8064-hex-char ML-DSA-65 private key>" +# ] + block = { needSyncCheck = true maintenanceTimeInterval = 21600000 // 6 hours: 21600000(ms) diff --git a/framework/src/test/java/org/tron/core/BandwidthProcessorTest.java b/framework/src/test/java/org/tron/core/BandwidthProcessorTest.java index cf652af3650..08b061a1ea5 100755 --- a/framework/src/test/java/org/tron/core/BandwidthProcessorTest.java +++ b/framework/src/test/java/org/tron/core/BandwidthProcessorTest.java @@ -881,4 +881,122 @@ public void testCalculateGlobalNetLimit() { .calculateGlobalNetLimitV2(accountCapsule.getAllFrozenBalanceForBandwidth()); Assert.assertTrue(netLimitV2 > 0); } + + @Test + public void pqAuthWitnessBytesSubtractedInCreateAccountCap() 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[] signerAddr = ByteArray.fromHexString(OWNER_ADDRESS); + byte[] fakeSig = new byte[3309]; + Protocol.AuthWitness authWitness = Protocol.AuthWitness.newBuilder() + .setSignerAddress(ByteString.copyFrom(signerAddr)) + .setSignature(ByteString.copyFrom(fakeSig)) + .build(); + + TransactionCapsule baseTrx = new TransactionCapsule(contract, + chainBaseManager.getAccountStore()); + Transaction withAuth = baseTrx.getInstance().toBuilder() + .addAuthWitness(authWitness) + .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 auth_witness", + rawSize > cap); + + BandwidthProcessor processor = new BandwidthProcessor(chainBaseManager); + try { + processor.consume(trx, trace); + } catch (TooBigTransactionException e) { + Assert.fail("PQ auth_witness bytes should be deducted from create-account cap check"); + } catch (AccountResourceInsufficientException + | ContractValidateException + | TooBigTransactionResultException e) { + // acceptable: other code paths — we only care about the cap check + } finally { + chainBaseManager.getAccountStore().delete(ByteArray.fromHexString(OWNER_ADDRESS)); + chainBaseManager.getAccountStore().delete(ByteArray.fromHexString(TO_ADDRESS)); + } + } + + @Test + public void pqAuthWitnessCountedInBandwidthUsage() 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[] signerAddr = ByteArray.fromHexString(OWNER_ADDRESS); + byte[] fakeSig = new byte[3309]; + Protocol.AuthWitness authWitness = Protocol.AuthWitness.newBuilder() + .setSignerAddress(ByteString.copyFrom(signerAddr)) + .setSignature(ByteString.copyFrom(fakeSig)) + .build(); + + TransactionCapsule baseTrx = new TransactionCapsule(contract, + chainBaseManager.getAccountStore()); + Transaction withAuth = baseTrx.getInstance().toBuilder() + .addAuthWitness(authWitness) + .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..e2222644aad 100644 --- a/framework/src/test/java/org/tron/core/actuator/AccountPermissionUpdateActuatorTest.java +++ b/framework/src/test/java/org/tron/core/actuator/AccountPermissionUpdateActuatorTest.java @@ -24,6 +24,7 @@ import org.tron.protos.Protocol.Key; import org.tron.protos.Protocol.Permission; import org.tron.protos.Protocol.Permission.PermissionType; +import org.tron.protos.Protocol.SignatureScheme; import org.tron.protos.Protocol.Transaction.Contract.ContractType; import org.tron.protos.Protocol.Transaction.Result.code; import org.tron.protos.contract.AccountContract.AccountCreateContract; @@ -1019,4 +1020,266 @@ public void checkActiveDefaultOperations() { } + // ------------------------- ML-DSA scheme validation ------------------------- + + private static byte[] fixedBytes(int len, int seed) { + byte[] b = new byte[len]; + for (int i = 0; i < len; i++) { + b[i] = (byte) ((seed + i) & 0xff); + } + return b; + } + + private Key legacyKey(String addr) { + return Key.newBuilder() + .setAddress(ByteString.copyFrom(ByteArray.fromHexString(addr))) + .setWeight(KEY_WEIGHT) + .build(); + } + + private Key mlDsaKey(String addr, SignatureScheme scheme, int pkLen, int seed) { + return Key.newBuilder() + .setAddress(ByteString.copyFrom(ByteArray.fromHexString(addr))) + .setWeight(KEY_WEIGHT) + .setScheme(scheme) + .setPublicKey(ByteString.copyFrom(fixedBytes(pkLen, seed))) + .build(); + } + + private Permission ownerPermissionWithKeys(List keys, long threshold) { + return Permission.newBuilder() + .setType(PermissionType.Owner) + .setPermissionName("owner") + .setThreshold(threshold) + .addAllKeys(keys) + .build(); + } + + private Permission activePermissionWithKeys(List keys, long threshold) { + byte[] ops = new byte[32]; + java.util.Arrays.fill(ops, (byte) 0); + ops[0] = 0x01; + return Permission.newBuilder() + .setType(PermissionType.Active) + .setPermissionName("active") + .setThreshold(threshold) + .setOperations(ByteString.copyFrom(ops)) + .addAllKeys(keys) + .build(); + } + + private Permission witnessPermissionWithKey(Key key) { + return Permission.newBuilder() + .setType(PermissionType.Witness) + .setPermissionName("witness") + .setThreshold(1) + .addKeys(key) + .build(); + } + + private AccountPermissionUpdateActuator actuatorFor(Any any) { + return (AccountPermissionUpdateActuator) new AccountPermissionUpdateActuator() + .setChainBaseManager(dbManager.getChainBaseManager()).setAny(any); + } + + @Test + public void mlDsaPermissionRejectedWhenNotAllowed() { + dbManager.getDynamicPropertiesStore().saveAllowMlDsa(0L); + ByteString address = ByteString.copyFrom(ByteArray.fromHexString(OWNER_ADDRESS)); + Permission owner = ownerPermissionWithKeys( + java.util.Collections.singletonList( + mlDsaKey(KEY_ADDRESS, SignatureScheme.ML_DSA_44, 1312, 1)), 2); + Permission active = activePermissionWithKeys( + java.util.Collections.singletonList(legacyKey(KEY_ADDRESS1)), 2); + Any any = getContract(address, owner, null, + java.util.Collections.singletonList(active)); + + try { + actuatorFor(any).validate(); + fail("should reject ML-DSA key when ALLOW_ML_DSA = 0"); + } catch (ContractValidateException e) { + Assert.assertTrue(e.getMessage().contains("ML-DSA is not activated")); + } + } + + @Test + public void legacyKeyWithNonEmptyPublicKeyRejected() { + dbManager.getDynamicPropertiesStore().saveAllowMlDsa(1L); + ByteString address = ByteString.copyFrom(ByteArray.fromHexString(OWNER_ADDRESS)); + Key badLegacy = Key.newBuilder() + .setAddress(ByteString.copyFrom(ByteArray.fromHexString(KEY_ADDRESS))) + .setWeight(KEY_WEIGHT) + .setPublicKey(ByteString.copyFrom(new byte[] {1, 2, 3})) + .build(); + Permission owner = ownerPermissionWithKeys( + java.util.Collections.singletonList(badLegacy), 2); + Permission active = activePermissionWithKeys( + java.util.Collections.singletonList(legacyKey(KEY_ADDRESS1)), 2); + Any any = getContract(address, owner, null, + java.util.Collections.singletonList(active)); + + try { + actuatorFor(any).validate(); + fail("legacy key with non-empty public_key should be rejected"); + } catch (ContractValidateException e) { + Assert.assertTrue(e.getMessage().contains("public_key must be empty")); + } + } + + @Test + public void mixedSchemeInSamePermissionRejected() { + dbManager.getDynamicPropertiesStore().saveAllowMlDsa(1L); + ByteString address = ByteString.copyFrom(ByteArray.fromHexString(OWNER_ADDRESS)); + Permission owner = ownerPermissionWithKeys( + java.util.Arrays.asList( + mlDsaKey(KEY_ADDRESS, SignatureScheme.ML_DSA_44, 1312, 1), + legacyKey(KEY_ADDRESS1)), + 2); + Permission active = activePermissionWithKeys( + java.util.Collections.singletonList(legacyKey(KEY_ADDRESS2)), 2); + Any any = getContract(address, owner, null, + java.util.Collections.singletonList(active)); + + try { + actuatorFor(any).validate(); + fail("mixed scheme in one permission should be rejected"); + } catch (ContractValidateException e) { + Assert.assertTrue(e.getMessage().contains("same scheme")); + } + } + + @Test + public void mixedMlDsaSchemesRejected() { + dbManager.getDynamicPropertiesStore().saveAllowMlDsa(1L); + ByteString address = ByteString.copyFrom(ByteArray.fromHexString(OWNER_ADDRESS)); + Permission owner = ownerPermissionWithKeys( + java.util.Arrays.asList( + mlDsaKey(KEY_ADDRESS, SignatureScheme.ML_DSA_44, 1312, 1), + mlDsaKey(KEY_ADDRESS1, SignatureScheme.ML_DSA_65, 1952, 2)), + 2); + Permission active = activePermissionWithKeys( + java.util.Collections.singletonList(legacyKey(KEY_ADDRESS2)), 2); + Any any = getContract(address, owner, null, + java.util.Collections.singletonList(active)); + + try { + actuatorFor(any).validate(); + fail("mixed ML-DSA schemes should be rejected"); + } catch (ContractValidateException e) { + Assert.assertTrue(e.getMessage().contains("same scheme")); + } + } + + @Test + public void mlDsa44WrongPublicKeyLengthRejected() { + dbManager.getDynamicPropertiesStore().saveAllowMlDsa(1L); + ByteString address = ByteString.copyFrom(ByteArray.fromHexString(OWNER_ADDRESS)); + Permission owner = ownerPermissionWithKeys( + java.util.Collections.singletonList( + mlDsaKey(KEY_ADDRESS, SignatureScheme.ML_DSA_44, 1311, 1)), + 2); + Permission active = activePermissionWithKeys( + java.util.Collections.singletonList(legacyKey(KEY_ADDRESS1)), 2); + Any any = getContract(address, owner, null, + java.util.Collections.singletonList(active)); + + try { + actuatorFor(any).validate(); + fail("ML-DSA-44 wrong public_key length should be rejected"); + } catch (ContractValidateException e) { + Assert.assertTrue(e.getMessage().contains("public_key length")); + } + } + + @Test + public void witnessMlDsa44Rejected() { + dbManager.getDynamicPropertiesStore().saveAllowMlDsa(1L); + ByteString address = ByteString.copyFrom(ByteArray.fromHexString(WITNESS_ADDRESS)); + Permission owner = ownerPermissionWithKeys( + java.util.Collections.singletonList( + mlDsaKey(KEY_ADDRESS, SignatureScheme.ML_DSA_65, 1952, 1)), + 2); + Permission witness = witnessPermissionWithKey( + mlDsaKey(KEY_ADDRESS1, SignatureScheme.ML_DSA_44, 1312, 2)); + Permission active = activePermissionWithKeys( + java.util.Collections.singletonList(legacyKey(KEY_ADDRESS2)), 2); + Any any = getContract(address, owner, witness, + java.util.Collections.singletonList(active)); + + try { + actuatorFor(any).validate(); + fail("Witness permission with ML-DSA-44 should be rejected"); + } catch (ContractValidateException e) { + Assert.assertTrue(e.getMessage().contains("Witness permission only supports ML_DSA_65")); + } + } + + @Test + public void duplicatePublicKeyRejected() { + dbManager.getDynamicPropertiesStore().saveAllowMlDsa(1L); + ByteString address = ByteString.copyFrom(ByteArray.fromHexString(OWNER_ADDRESS)); + byte[] sharedPk = fixedBytes(1312, 1); + Key k1 = Key.newBuilder() + .setAddress(ByteString.copyFrom(ByteArray.fromHexString(KEY_ADDRESS))) + .setWeight(KEY_WEIGHT) + .setScheme(SignatureScheme.ML_DSA_44) + .setPublicKey(ByteString.copyFrom(sharedPk)) + .build(); + Key k2 = Key.newBuilder() + .setAddress(ByteString.copyFrom(ByteArray.fromHexString(KEY_ADDRESS1))) + .setWeight(KEY_WEIGHT) + .setScheme(SignatureScheme.ML_DSA_44) + .setPublicKey(ByteString.copyFrom(sharedPk)) + .build(); + Permission owner = ownerPermissionWithKeys(java.util.Arrays.asList(k1, k2), 2); + Permission active = activePermissionWithKeys( + java.util.Collections.singletonList(legacyKey(KEY_ADDRESS2)), 2); + Any any = getContract(address, owner, null, + java.util.Collections.singletonList(active)); + + try { + actuatorFor(any).validate(); + fail("duplicate public_key in same permission should be rejected"); + } catch (ContractValidateException e) { + Assert.assertTrue(e.getMessage().contains("public_key should be distinct")); + } + } + + @Test + public void validMlDsa44PermissionAccepted() throws ContractValidateException { + dbManager.getDynamicPropertiesStore().saveAllowMlDsa(1L); + ByteString address = ByteString.copyFrom(ByteArray.fromHexString(OWNER_ADDRESS)); + Permission owner = ownerPermissionWithKeys( + java.util.Collections.singletonList( + mlDsaKey(KEY_ADDRESS, SignatureScheme.ML_DSA_44, 1312, 1)), + 2); + Permission active = activePermissionWithKeys( + java.util.Collections.singletonList( + mlDsaKey(KEY_ADDRESS1, SignatureScheme.ML_DSA_44, 1312, 2)), + 2); + Any any = getContract(address, owner, null, + java.util.Collections.singletonList(active)); + + Assert.assertTrue(actuatorFor(any).validate()); + } + + @Test + public void validMlDsa65WitnessPermissionAccepted() throws ContractValidateException { + dbManager.getDynamicPropertiesStore().saveAllowMlDsa(1L); + ByteString address = ByteString.copyFrom(ByteArray.fromHexString(WITNESS_ADDRESS)); + Permission owner = ownerPermissionWithKeys( + java.util.Collections.singletonList( + mlDsaKey(KEY_ADDRESS, SignatureScheme.ML_DSA_65, 1952, 1)), + 2); + Permission witness = witnessPermissionWithKey( + mlDsaKey(KEY_ADDRESS1, SignatureScheme.ML_DSA_65, 1952, 2)); + Permission active = activePermissionWithKeys( + java.util.Collections.singletonList( + mlDsaKey(KEY_ADDRESS2, SignatureScheme.ML_DSA_65, 1952, 3)), + 2); + Any any = getContract(address, owner, witness, + java.util.Collections.singletonList(active)); + + Assert.assertTrue(actuatorFor(any).validate()); + } } \ No newline at end of file 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 f8d8e6bdd9d..50017aa1a4f 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 @@ -772,4 +772,29 @@ public void blockVersionCheck() { } } } + + @Test + public void validateAllowMlDsa() { + long code = ProposalType.ALLOW_ML_DSA.getCode(); + + ContractValidateException thrown = assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, code, 0)); + assertEquals("This value[ALLOW_ML_DSA] is only allowed to be 1", thrown.getMessage()); + + thrown = assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, code, 2)); + assertEquals("This value[ALLOW_ML_DSA] 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.saveAllowMlDsa(1L); + thrown = assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, code, 1)); + assertEquals("[ALLOW_ML_DSA] has been valid, no need to propose again", thrown.getMessage()); + dynamicPropertiesStore.saveAllowMlDsa(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..839b77b1efe --- /dev/null +++ b/framework/src/test/java/org/tron/core/capsule/BlockCapsulePqTest.java @@ -0,0 +1,260 @@ +package org.tron.core.capsule; + +import com.google.protobuf.ByteString; +import java.security.SecureRandom; +import org.bouncycastle.crypto.AsymmetricCipherKeyPair; +import org.bouncycastle.pqc.crypto.mldsa.MLDSAKeyGenerationParameters; +import org.bouncycastle.pqc.crypto.mldsa.MLDSAKeyPairGenerator; +import org.bouncycastle.pqc.crypto.mldsa.MLDSAParameters; +import org.bouncycastle.pqc.crypto.mldsa.MLDSAPrivateKeyParameters; +import org.bouncycastle.pqc.crypto.mldsa.MLDSAPublicKeyParameters; +import org.bouncycastle.pqc.crypto.mldsa.MLDSASigner; +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.PqAuthDigest; +import org.tron.common.utils.ByteArray; +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.AuthWitness; +import org.tron.protos.Protocol.Key; +import org.tron.protos.Protocol.Permission; +import org.tron.protos.Protocol.Permission.PermissionType; +import org.tron.protos.Protocol.SignatureScheme; + +public class BlockCapsulePqTest extends BaseTest { + + private ECKey witnessKey; + private byte[] witnessAddress; + private MLDSAPrivateKeyParameters pqSk; + private MLDSAPublicKeyParameters pqPk; + + @BeforeClass + public static void init() { + Args.setParam(new String[] {"-d", dbPath()}, TestConstants.TEST_CONF); + } + + @Before + public void setUp() { + witnessKey = new ECKey(); + witnessAddress = witnessKey.getAddress(); + MLDSAKeyPairGenerator gen = new MLDSAKeyPairGenerator(); + gen.init(new MLDSAKeyGenerationParameters(new SecureRandom(), MLDSAParameters.ml_dsa_65)); + AsymmetricCipherKeyPair kp = gen.generateKeyPair(); + pqPk = (MLDSAPublicKeyParameters) kp.getPublic(); + pqSk = (MLDSAPrivateKeyParameters) kp.getPrivate(); + } + + private AccountCapsule buildWitnessAccount(SignatureScheme scheme) { + Key.Builder kb = Key.newBuilder() + .setAddress(ByteString.copyFrom(witnessAddress)) + .setWeight(1) + .setScheme(scheme); + if (scheme == SignatureScheme.ML_DSA_65) { + kb.setPublicKey(ByteString.copyFrom(pqPk.getEncoded())); + } + 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) { + MLDSASigner signer = new MLDSASigner(); + signer.init(true, pqSk); + signer.update(message, 0, message.length); + try { + return signer.generateSignature(); + } catch (Exception e) { + throw new AssertionError(e); + } + } + + @Test + public void legacyValidateWithoutAuthWitnessAcceptedBeforeActivation() throws Exception { + dbManager.getDynamicPropertiesStore().saveAllowMultiSign(1L); + dbManager.getDynamicPropertiesStore().saveAllowMlDsa(0L); + AccountCapsule witness = buildWitnessAccount(SignatureScheme.UNKNOWN_SIG_SCHEME); + 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 authWitnessBeforeActivationRejected() throws Exception { + dbManager.getDynamicPropertiesStore().saveAllowMultiSign(1L); + dbManager.getDynamicPropertiesStore().saveAllowMlDsa(0L); + AccountCapsule witness = buildWitnessAccount(SignatureScheme.UNKNOWN_SIG_SCHEME); + dbManager.getAccountStore().put(witnessAddress, witness); + + byte[] parentHash = new byte[32]; + BlockCapsule block = buildUnsignedBlock(parentHash); + byte[] digest = PqAuthDigest.block(block.getRawHashBytes(), witnessAddress); + block.setWitnessAuth(AuthWitness.newBuilder() + .setSignerAddress(ByteString.copyFrom(witnessAddress)) + .setSignature(ByteString.copyFrom(signPq(digest))) + .build()); + block.validateSignature( + dbManager.getDynamicPropertiesStore(), dbManager.getAccountStore()); + } + + @Test(expected = ValidateSignatureException.class) + public void bothLegacyAndAuthWitnessRejected() throws Exception { + dbManager.getDynamicPropertiesStore().saveAllowMultiSign(1L); + dbManager.getDynamicPropertiesStore().saveAllowMlDsa(1L); + AccountCapsule witness = buildWitnessAccount(SignatureScheme.ML_DSA_65); + dbManager.getAccountStore().put(witnessAddress, witness); + + byte[] parentHash = new byte[32]; + BlockCapsule block = buildSignedBlock(parentHash); + byte[] digest = PqAuthDigest.block(block.getRawHashBytes(), witnessAddress); + block.setWitnessAuth(AuthWitness.newBuilder() + .setSignerAddress(ByteString.copyFrom(witnessAddress)) + .setSignature(ByteString.copyFrom(signPq(digest))) + .build()); + block.validateSignature( + dbManager.getDynamicPropertiesStore(), dbManager.getAccountStore()); + } + + @Test(expected = ValidateSignatureException.class) + public void mlDsaSchemeWithLegacyOnlyRejected() throws Exception { + dbManager.getDynamicPropertiesStore().saveAllowMultiSign(1L); + dbManager.getDynamicPropertiesStore().saveAllowMlDsa(1L); + AccountCapsule witness = buildWitnessAccount(SignatureScheme.ML_DSA_65); + dbManager.getAccountStore().put(witnessAddress, witness); + + byte[] parentHash = new byte[32]; + BlockCapsule block = buildSignedBlock(parentHash); + block.validateSignature( + dbManager.getDynamicPropertiesStore(), dbManager.getAccountStore()); + } + + @Test(expected = ValidateSignatureException.class) + public void legacySchemeWithAuthWitnessOnlyRejected() throws Exception { + dbManager.getDynamicPropertiesStore().saveAllowMultiSign(1L); + dbManager.getDynamicPropertiesStore().saveAllowMlDsa(1L); + AccountCapsule witness = buildWitnessAccount(SignatureScheme.UNKNOWN_SIG_SCHEME); + dbManager.getAccountStore().put(witnessAddress, witness); + + byte[] parentHash = new byte[32]; + BlockCapsule block = buildUnsignedBlock(parentHash); + byte[] digest = PqAuthDigest.block(block.getRawHashBytes(), witnessAddress); + block.setWitnessAuth(AuthWitness.newBuilder() + .setSignerAddress(ByteString.copyFrom(witnessAddress)) + .setSignature(ByteString.copyFrom(signPq(digest))) + .build()); + block.validateSignature( + dbManager.getDynamicPropertiesStore(), dbManager.getAccountStore()); + } + + @Test(expected = ValidateSignatureException.class) + public void neitherLegacyNorAuthRejected() throws Exception { + dbManager.getDynamicPropertiesStore().saveAllowMultiSign(1L); + dbManager.getDynamicPropertiesStore().saveAllowMlDsa(1L); + AccountCapsule witness = buildWitnessAccount(SignatureScheme.ML_DSA_65); + dbManager.getAccountStore().put(witnessAddress, witness); + + byte[] parentHash = new byte[32]; + BlockCapsule block = buildUnsignedBlock(parentHash); + block.validateSignature( + dbManager.getDynamicPropertiesStore(), dbManager.getAccountStore()); + } + + @Test + public void pqOnlyAccepted() throws Exception { + dbManager.getDynamicPropertiesStore().saveAllowMultiSign(1L); + dbManager.getDynamicPropertiesStore().saveAllowMlDsa(1L); + AccountCapsule witness = buildWitnessAccount(SignatureScheme.ML_DSA_65); + dbManager.getAccountStore().put(witnessAddress, witness); + + byte[] parentHash = new byte[32]; + BlockCapsule block = buildUnsignedBlock(parentHash); + byte[] digest = PqAuthDigest.block(block.getRawHashBytes(), witnessAddress); + block.setWitnessAuth(AuthWitness.newBuilder() + .setSignerAddress(ByteString.copyFrom(witnessAddress)) + .setSignature(ByteString.copyFrom(signPq(digest))) + .build()); + Assert.assertTrue(block.validateSignature( + dbManager.getDynamicPropertiesStore(), dbManager.getAccountStore())); + } + + @Test + public void tamperedAuthWitnessFails() throws Exception { + dbManager.getDynamicPropertiesStore().saveAllowMultiSign(1L); + dbManager.getDynamicPropertiesStore().saveAllowMlDsa(1L); + AccountCapsule witness = buildWitnessAccount(SignatureScheme.ML_DSA_65); + dbManager.getAccountStore().put(witnessAddress, witness); + + byte[] parentHash = new byte[32]; + BlockCapsule block = buildUnsignedBlock(parentHash); + byte[] digest = PqAuthDigest.block(block.getRawHashBytes(), witnessAddress); + byte[] pqSig = signPq(digest); + pqSig[pqSig.length - 1] ^= 0x01; + block.setWitnessAuth(AuthWitness.newBuilder() + .setSignerAddress(ByteString.copyFrom(witnessAddress)) + .setSignature(ByteString.copyFrom(pqSig)) + .build()); + Assert.assertFalse(block.validateSignature( + dbManager.getDynamicPropertiesStore(), dbManager.getAccountStore())); + } + + @Test(expected = ValidateSignatureException.class) + public void signerNotInWitnessPermissionRejected() throws Exception { + dbManager.getDynamicPropertiesStore().saveAllowMultiSign(1L); + dbManager.getDynamicPropertiesStore().saveAllowMlDsa(1L); + AccountCapsule witness = buildWitnessAccount(SignatureScheme.ML_DSA_65); + dbManager.getAccountStore().put(witnessAddress, witness); + + byte[] parentHash = new byte[32]; + BlockCapsule block = buildUnsignedBlock(parentHash); + byte[] otherAddr = ByteArray.fromHexString( + "41" + "abababababababababababababababababababab"); + byte[] digest = PqAuthDigest.block(block.getRawHashBytes(), otherAddr); + block.setWitnessAuth(AuthWitness.newBuilder() + .setSignerAddress(ByteString.copyFrom(otherAddr)) + .setSignature(ByteString.copyFrom(signPq(digest))) + .build()); + 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 70434430262..0e4c67aad45 100644 --- a/framework/src/test/java/org/tron/core/capsule/TransactionCapsuleTest.java +++ b/framework/src/test/java/org/tron/core/capsule/TransactionCapsuleTest.java @@ -4,23 +4,42 @@ import static org.tron.protos.Protocol.Transaction.Result.contractResult.PRECOMPILED_CONTRACT; import static org.tron.protos.Protocol.Transaction.Result.contractResult.SUCCESS; +import com.google.protobuf.Any; import com.google.protobuf.ByteString; +import java.security.SecureRandom; import lombok.extern.slf4j.Slf4j; +import org.bouncycastle.crypto.AsymmetricCipherKeyPair; +import org.bouncycastle.pqc.crypto.mldsa.MLDSAKeyGenerationParameters; +import org.bouncycastle.pqc.crypto.mldsa.MLDSAKeyPairGenerator; +import org.bouncycastle.pqc.crypto.mldsa.MLDSAParameters; +import org.bouncycastle.pqc.crypto.mldsa.MLDSAPrivateKeyParameters; +import org.bouncycastle.pqc.crypto.mldsa.MLDSAPublicKeyParameters; +import org.bouncycastle.pqc.crypto.mldsa.MLDSASigner; 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.pqc.PqAuthDigest; +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.AuthWitness; +import org.tron.protos.Protocol.Key; +import org.tron.protos.Protocol.Permission; +import org.tron.protos.Protocol.Permission.PermissionType; +import org.tron.protos.Protocol.SignatureScheme; 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; @Slf4j public class TransactionCapsuleTest extends BaseTest { @@ -69,4 +88,215 @@ public void testRemoveRedundantRet() { Assert.assertEquals(1, transactionCapsule.getInstance().getRetCount()); Assert.assertEquals(SUCCESS, transactionCapsule.getInstance().getRet(0).getContractRet()); } + + // --------------------- ML-DSA auth_witness verification --------------------- + + private static final String PQ_OWNER_HEX = + "41abd4b9367799eaa3197fecb144eb71de1e049abc"; + private static final String PQ_SIGNER_HEX = + "41548794500882809695a8a687866e76d4271a1abc"; + + private AsymmetricCipherKeyPair newMlDsa65KeyPair() { + MLDSAKeyPairGenerator gen = new MLDSAKeyPairGenerator(); + gen.init(new MLDSAKeyGenerationParameters(new SecureRandom(), MLDSAParameters.ml_dsa_65)); + return gen.generateKeyPair(); + } + + private byte[] sign(MLDSAPrivateKeyParameters sk, byte[] msg) throws Exception { + MLDSASigner s = new MLDSASigner(); + s.init(true, sk); + s.update(msg, 0, msg.length); + return s.generateSignature(); + } + + private Transaction buildTransferTx(String ownerHex, int permissionId) { + TransferContract transfer = TransferContract.newBuilder() + .setOwnerAddress(ByteString.copyFrom(ByteArray.fromHexString(ownerHex))) + .setToAddress(ByteString.copyFrom(ByteArray.fromHexString(PQ_SIGNER_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(); + } + + private void putAccountWithMlDsa65Permission(String ownerHex, byte[] pqPublicKey) { + byte[] addr = ByteArray.fromHexString(ownerHex); + byte[] signerAddr = ByteArray.fromHexString(PQ_SIGNER_HEX); + Key pqKey = Key.newBuilder() + .setAddress(ByteString.copyFrom(signerAddr)) + .setWeight(1L) + .setScheme(SignatureScheme.ML_DSA_65) + .setPublicKey(ByteString.copyFrom(pqPublicKey)) + .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 authWitnessBeforeActivationRejected() { + dbManager.getDynamicPropertiesStore().saveAllowMlDsa(0L); + Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0).toBuilder() + .addAuthWitness(AuthWitness.newBuilder() + .setSignerAddress(ByteString.copyFrom(ByteArray.fromHexString(PQ_SIGNER_HEX))) + .setSignature(ByteString.copyFrom(new byte[3309])) + .build()) + .build(); + TransactionCapsule cap = new TransactionCapsule(tx); + try { + cap.validatePubSignature(dbManager.getAccountStore(), + dbManager.getDynamicPropertiesStore()); + Assert.fail("should reject auth_witness before activation"); + } catch (ValidateSignatureException e) { + Assert.assertTrue(e.getMessage().contains("ML-DSA not activated")); + } + } + + @Test + public void signatureAndAuthWitnessAreMutuallyExclusive() { + dbManager.getDynamicPropertiesStore().saveAllowMlDsa(1L); + Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0).toBuilder() + .addSignature(ByteString.copyFrom(new byte[65])) + .addAuthWitness(AuthWitness.newBuilder() + .setSignerAddress(ByteString.copyFrom(ByteArray.fromHexString(PQ_SIGNER_HEX))) + .setSignature(ByteString.copyFrom(new byte[3309])) + .build()) + .build(); + TransactionCapsule cap = new TransactionCapsule(tx); + try { + cap.validatePubSignature(dbManager.getAccountStore(), + dbManager.getDynamicPropertiesStore()); + Assert.fail("should reject when both signature and auth_witness present"); + } catch (ValidateSignatureException e) { + Assert.assertTrue(e.getMessage().contains("mutually exclusive")); + } + } + + @Test + public void validAuthWitnessAccepted() throws Exception { + dbManager.getDynamicPropertiesStore().saveAllowMlDsa(1L); + AsymmetricCipherKeyPair kp = newMlDsa65KeyPair(); + byte[] pkBytes = ((MLDSAPublicKeyParameters) kp.getPublic()).getEncoded(); + MLDSAPrivateKeyParameters sk = (MLDSAPrivateKeyParameters) kp.getPrivate(); + putAccountWithMlDsa65Permission(PQ_OWNER_HEX, pkBytes); + + Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); + byte[] txid = Sha256Hash.of(true, tx.getRawData().toByteArray()).getBytes(); + byte[] signerAddr = ByteArray.fromHexString(PQ_SIGNER_HEX); + byte[] digest = PqAuthDigest.tx(txid, 0, signerAddr); + byte[] sig = sign(sk, digest); + + Transaction signed = tx.toBuilder() + .addAuthWitness(AuthWitness.newBuilder() + .setSignerAddress(ByteString.copyFrom(signerAddr)) + .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().saveAllowMlDsa(1L); + AsymmetricCipherKeyPair kp = newMlDsa65KeyPair(); + byte[] pkBytes = ((MLDSAPublicKeyParameters) kp.getPublic()).getEncoded(); + MLDSAPrivateKeyParameters sk = (MLDSAPrivateKeyParameters) kp.getPrivate(); + putAccountWithMlDsa65Permission(PQ_OWNER_HEX, pkBytes); + + Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); + byte[] txid = Sha256Hash.of(true, tx.getRawData().toByteArray()).getBytes(); + byte[] signerAddr = ByteArray.fromHexString(PQ_SIGNER_HEX); + byte[] digest = PqAuthDigest.tx(txid, 0, signerAddr); + byte[] sig = sign(sk, digest); + AuthWitness aw = AuthWitness.newBuilder() + .setSignerAddress(ByteString.copyFrom(signerAddr)) + .setSignature(ByteString.copyFrom(sig)) + .build(); + Transaction signed = tx.toBuilder().addAuthWitness(aw).addAuthWitness(aw).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("duplicate signer")); + } + } + + @Test + public void tamperedAuthWitnessRejected() throws Exception { + dbManager.getDynamicPropertiesStore().saveAllowMlDsa(1L); + AsymmetricCipherKeyPair kp = newMlDsa65KeyPair(); + byte[] pkBytes = ((MLDSAPublicKeyParameters) kp.getPublic()).getEncoded(); + MLDSAPrivateKeyParameters sk = (MLDSAPrivateKeyParameters) kp.getPrivate(); + putAccountWithMlDsa65Permission(PQ_OWNER_HEX, pkBytes); + + Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); + byte[] txid = Sha256Hash.of(true, tx.getRawData().toByteArray()).getBytes(); + byte[] signerAddr = ByteArray.fromHexString(PQ_SIGNER_HEX); + byte[] digest = PqAuthDigest.tx(txid, 0, signerAddr); + byte[] sig = sign(sk, digest); + sig[0] ^= 0x01; + + Transaction signed = tx.toBuilder() + .addAuthWitness(AuthWitness.newBuilder() + .setSignerAddress(ByteString.copyFrom(signerAddr)) + .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().saveAllowMlDsa(1L); + AsymmetricCipherKeyPair kp = newMlDsa65KeyPair(); + byte[] pkBytes = ((MLDSAPublicKeyParameters) kp.getPublic()).getEncoded(); + MLDSAPrivateKeyParameters sk = (MLDSAPrivateKeyParameters) kp.getPrivate(); + putAccountWithMlDsa65Permission(PQ_OWNER_HEX, pkBytes); + + Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); + byte[] txid = Sha256Hash.of(true, tx.getRawData().toByteArray()).getBytes(); + byte[] otherSigner = ByteArray.fromHexString( + "41BCE23C7D683B889326F762DDA2223A861EDA2E5C"); + byte[] digest = PqAuthDigest.tx(txid, 0, otherSigner); + byte[] sig = sign(sk, digest); + + Transaction signed = tx.toBuilder() + .addAuthWitness(AuthWitness.newBuilder() + .setSignerAddress(ByteString.copyFrom(otherSigner)) + .setSignature(ByteString.copyFrom(sig)) + .build()) + .build(); + TransactionCapsule cap = new TransactionCapsule(signed); + try { + cap.validatePubSignature(dbManager.getAccountStore(), + dbManager.getDynamicPropertiesStore()); + Assert.fail("unknown signer should be rejected"); + } catch (ValidateSignatureException e) { + Assert.assertTrue(e.getMessage().contains("not in permission")); + } + } } \ No newline at end of file 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..b30016d3dba 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_ML_DSA; 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 testProcessAllowMlDsa() { + dbManager.getDynamicPropertiesStore().saveAllowMlDsa(0L); + Assert.assertFalse(dbManager.getDynamicPropertiesStore().allowMlDsa()); + + Proposal proposal = Proposal.newBuilder() + .putParameters(ALLOW_ML_DSA.getCode(), 1L).build(); + ProposalCapsule proposalCapsule = new ProposalCapsule(proposal); + boolean result = ProposalService.process(dbManager, proposalCapsule); + Assert.assertTrue(result); + + Assert.assertEquals(1L, dbManager.getDynamicPropertiesStore().getAllowMlDsa()); + Assert.assertTrue(dbManager.getDynamicPropertiesStore().allowMlDsa()); + + dbManager.getDynamicPropertiesStore().saveAllowMlDsa(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 98c11fd4018..1d2852ba400 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,7 @@ import org.tron.core.config.args.Args; import org.tron.core.utils.TransactionUtil; import org.tron.protos.Protocol; +import org.tron.protos.Protocol.AuthWitness; import org.tron.protos.Protocol.Transaction; public class UtilTest extends BaseTest { @@ -166,4 +167,35 @@ public void testPackTransaction() { TransactionSignWeight txSignWeight = transactionUtil.getTransactionSignWeight(transaction); Assert.assertNotNull(txSignWeight); } + + @Test + public void roundtripAuthWitnessJson() throws Exception { + byte[] signer = ByteArray.fromHexString(OWNER_ADDRESS); + byte[] sig = new byte[3309]; + for (int i = 0; i < sig.length; i++) { + sig[i] = (byte) (i & 0xff); + } + AuthWitness authWitness = AuthWitness.newBuilder() + .setSignerAddress(ByteString.copyFrom(signer)) + .setSignature(ByteString.copyFrom(sig)) + .build(); + Transaction original = Transaction.newBuilder() + .setRawData(Transaction.raw.newBuilder().setTimestamp(1L).build()) + .addAuthWitness(authWitness) + .build(); + + String json = Util.printTransactionToJSON(original, false).toJSONString(); + Assert.assertTrue("JSON output should contain auth_witness field", + json.contains("auth_witness")); + + Transaction.Builder rebuilt = Transaction.newBuilder(); + JsonFormat.merge(json, rebuilt, false); + Transaction decoded = rebuilt.build(); + + Assert.assertEquals(1, decoded.getAuthWitnessCount()); + Assert.assertEquals(authWitness.getSignerAddress(), + decoded.getAuthWitness(0).getSignerAddress()); + Assert.assertEquals(authWitness.getSignature(), + decoded.getAuthWitness(0).getSignature()); + } } diff --git a/protocol/src/main/protos/core/Tron.proto b/protocol/src/main/protos/core/Tron.proto index 2b104b86d34..5be62364ebe 100644 --- a/protocol/src/main/protos/core/Tron.proto +++ b/protocol/src/main/protos/core/Tron.proto @@ -17,6 +17,20 @@ enum AccountType { Contract = 2; } +// Signature scheme identifier used by Permission.Key and AuthWitness. +// UNKNOWN_SIG_SCHEME (0) denotes legacy keys authenticated via the existing +// Transaction.signature / BlockHeader.witness_signature paths (ECDSA secp256k1 +// on mainnet, SM2/SM3 where applicable). Post-quantum schemes use dedicated +// auth_witness / witness_auth fields. +enum SignatureScheme { + UNKNOWN_SIG_SCHEME = 0; + ECDSA_SECP256K1 = 1; + SM2_SM3 = 2; + ML_DSA_44 = 3; + ML_DSA_65 = 4; + reserved 5 to 15; +} + // AccountId, (name, address) use name, (null, address) use address, (name, null) use name, message AccountId { bytes name = 1; @@ -243,6 +257,21 @@ message Account { message Key { bytes address = 1; int64 weight = 2; + // Signature scheme for this key. UNKNOWN_SIG_SCHEME = legacy; otherwise a + // post-quantum algorithm identifier requiring public_key to be populated. + SignatureScheme scheme = 3; + // Raw public key bytes. Empty for legacy keys. For ML-DSA-44 = 1312 bytes; + // for ML-DSA-65 = 1952 bytes. No SubjectPublicKeyInfo / PEM / Base64. + bytes public_key = 4; +} + +// Per-signer post-quantum authentication witness for a transaction or block. +// signer_address SHALL match a Permission.Key.address entry; signature SHALL +// verify against that key's scheme + public_key over the domain-separated +// digest defined in the PQ authentication spec. +message AuthWitness { + bytes signer_address = 1; + bytes signature = 2; } message DelegatedResource { @@ -449,6 +478,11 @@ message Transaction { // only support size = 1, repeated list here for muti-sig extension repeated bytes signature = 2; repeated Result ret = 5; + // Post-quantum authentication witnesses. Mutually exclusive with signature: + // a transaction against a PQ Permission MUST use auth_witness and empty + // signature; a transaction against a legacy Permission MUST use signature + // and empty auth_witness. + repeated AuthWitness auth_witness = 6; } message TransactionInfo { @@ -515,6 +549,11 @@ message BlockHeader { } raw raw_data = 1; bytes witness_signature = 2; + // Post-quantum witness authentication. When the producing SR has a + // Witness Permission with scheme = ML_DSA_65, witness_auth SHALL be + // present in addition to witness_signature (Dual-Sign). Otherwise this + // field SHALL be empty. + AuthWitness witness_auth = 3; } // block From 9ca04fbf6ee2b152b2de997a1b5dbdcbb8f7b6c0 Mon Sep 17 00:00:00 2001 From: federico Date: Fri, 24 Apr 2026 11:04:53 +0800 Subject: [PATCH 02/12] refactor(crypto): consolidate ML-DSA PQC signer/verifier into unified PqSignature API --- .../AccountPermissionUpdateActuator.java | 8 +- .../org/tron/common/utils/LocalWitnesses.java | 35 +++ .../org/tron/core/capsule/BlockCapsule.java | 6 +- .../tron/core/capsule/TransactionCapsule.java | 12 +- .../org/tron/common/crypto/pqc/MLDSA44.java | 197 +++++++++++++++ .../tron/common/crypto/pqc/MLDSA44Signer.java | 45 ---- .../common/crypto/pqc/MLDSA44Verifier.java | 46 ---- .../org/tron/common/crypto/pqc/MLDSA65.java | 197 +++++++++++++++ .../tron/common/crypto/pqc/MLDSA65Signer.java | 45 ---- .../common/crypto/pqc/MLDSA65Verifier.java | 46 ---- .../tron/common/crypto/pqc/PqSignature.java | 64 +++++ .../crypto/pqc/PqSignatureRegistry.java | 74 ++++++ .../common/crypto/pqc/SignatureVerifier.java | 45 ---- .../crypto/pqc/SignatureVerifierRegistry.java | 39 --- .../crypto/pqc/MLDSA44VerifierTest.java | 134 ---------- .../pqc/SignatureVerifierRegistryTest.java | 62 ----- .../java/org/tron/core/config/args/Args.java | 17 ++ .../org/tron/core/config/args/ConfigKey.java | 2 + .../core/config/args/WitnessInitializer.java | 39 +++ .../tron/core/consensus/ConsensusService.java | 46 ++++ .../main/java/org/tron/core/db/Manager.java | 4 +- framework/src/main/resources/config.conf | 17 +- .../tron/common/crypto/pqc/MLDSA44Test.java | 228 ++++++++++++++++++ .../tron/common/crypto/pqc/MLDSA65Test.java | 71 ++++-- .../common/crypto/pqc/PqAuthDigestTest.java | 0 .../crypto/pqc/PqSignatureRegistryTest.java | 82 +++++++ .../org/tron/common/crypto/pqc/PqcClient.java | 139 +++++++++++ .../common/crypto/pqc/PqcWitnessNode.java | 158 ++++++++++++ .../tron/core/capsule/BlockCapsulePqTest.java | 29 +-- .../core/capsule/TransactionCapsuleTest.java | 195 +++++++++++---- framework/src/test/resources/config-test.conf | 9 +- 31 files changed, 1513 insertions(+), 578 deletions(-) create mode 100644 crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA44.java delete mode 100644 crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA44Signer.java delete mode 100644 crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA44Verifier.java create mode 100644 crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA65.java delete mode 100644 crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA65Signer.java delete mode 100644 crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA65Verifier.java create mode 100644 crypto/src/main/java/org/tron/common/crypto/pqc/PqSignature.java create mode 100644 crypto/src/main/java/org/tron/common/crypto/pqc/PqSignatureRegistry.java delete mode 100644 crypto/src/main/java/org/tron/common/crypto/pqc/SignatureVerifier.java delete mode 100644 crypto/src/main/java/org/tron/common/crypto/pqc/SignatureVerifierRegistry.java delete mode 100644 crypto/src/test/java/org/tron/common/crypto/pqc/MLDSA44VerifierTest.java delete mode 100644 crypto/src/test/java/org/tron/common/crypto/pqc/SignatureVerifierRegistryTest.java create mode 100644 framework/src/test/java/org/tron/common/crypto/pqc/MLDSA44Test.java rename crypto/src/test/java/org/tron/common/crypto/pqc/MLDSA65VerifierTest.java => framework/src/test/java/org/tron/common/crypto/pqc/MLDSA65Test.java (69%) rename {crypto => framework}/src/test/java/org/tron/common/crypto/pqc/PqAuthDigestTest.java (100%) create mode 100644 framework/src/test/java/org/tron/common/crypto/pqc/PqSignatureRegistryTest.java create mode 100644 framework/src/test/java/org/tron/common/crypto/pqc/PqcClient.java create mode 100644 framework/src/test/java/org/tron/common/crypto/pqc/PqcWitnessNode.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 e319aaab8a6..5f94917e50d 100644 --- a/actuator/src/main/java/org/tron/core/actuator/AccountPermissionUpdateActuator.java +++ b/actuator/src/main/java/org/tron/core/actuator/AccountPermissionUpdateActuator.java @@ -16,8 +16,8 @@ import org.tron.core.exception.ContractValidateException; import org.tron.core.store.AccountStore; import org.tron.core.store.DynamicPropertiesStore; -import org.tron.common.crypto.pqc.MLDSA44Verifier; -import org.tron.common.crypto.pqc.MLDSA65Verifier; +import org.tron.common.crypto.pqc.MLDSA44; +import org.tron.common.crypto.pqc.MLDSA65; import org.tron.protos.Protocol.Key; import org.tron.protos.Protocol.Permission; import org.tron.protos.Protocol.Permission.PermissionType; @@ -303,9 +303,9 @@ private void validatePermissionScheme(Permission permission) throws ContractVali private static int expectedPublicKeyLength(SignatureScheme scheme) { switch (scheme) { case ML_DSA_44: - return MLDSA44Verifier.PUBLIC_KEY_LENGTH; + return MLDSA44.PUBLIC_KEY_LENGTH; case ML_DSA_65: - return MLDSA65Verifier.PUBLIC_KEY_LENGTH; + return MLDSA65.PUBLIC_KEY_LENGTH; default: return -1; } 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..039b44ba114 100644 --- a/chainbase/src/main/java/org/tron/common/utils/LocalWitnesses.java +++ b/chainbase/src/main/java/org/tron/common/utils/LocalWitnesses.java @@ -18,12 +18,14 @@ 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.MLDSA65; import org.tron.core.config.Parameter.ChainConstant; import org.tron.core.exception.TronError; @@ -33,6 +35,11 @@ public class LocalWitnesses { @Getter private List privateKeys = Lists.newArrayList(); + /** ML-DSA-65 seed values in hex format (64 hex chars = 32 bytes). */ + @Getter + private List pqSeeds = Lists.newArrayList(); + + @Setter @Getter private byte[] witnessAccountAddress; @@ -95,6 +102,34 @@ public void addPrivateKeys(String privateKey) { this.privateKeys.add(privateKey); } + /** ML-DSA-65 seed values (32 bytes = 64 hex chars). Keys are derived from seeds. */ + public void setPqSeeds(final List pqSeeds) { + if (CollectionUtils.isEmpty(pqSeeds)) { + return; + } + for (String seed : pqSeeds) { + validatePqSeed(seed); + } + this.pqSeeds = pqSeeds; + } + + private static void validatePqSeed(String seed) { + String hex = seed; + if (StringUtils.startsWithIgnoreCase(hex, "0X")) { + hex = hex.substring(2); + } + int expectedHexLen = MLDSA65.SEED_LENGTH * 2; + if (StringUtils.isBlank(hex) || hex.length() != expectedHexLen) { + throw new TronError(String.format("ML-DSA-65 seed must be %d hex chars, actual: %d", + expectedHexLen, StringUtils.isBlank(hex) ? 0 : hex.length()), + TronError.ErrCode.WITNESS_INIT); + } + if (!StringUtil.isHexadecimal(hex)) { + throw new TronError("ML-DSA-65 seed 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 a783d3b01e9..1b46cd6d8d8 100755 --- a/chainbase/src/main/java/org/tron/core/capsule/BlockCapsule.java +++ b/chainbase/src/main/java/org/tron/core/capsule/BlockCapsule.java @@ -32,8 +32,7 @@ import org.tron.common.crypto.SignInterface; import org.tron.common.crypto.SignUtils; import org.tron.common.crypto.pqc.PqAuthDigest; -import org.tron.common.crypto.pqc.SignatureVerifier; -import org.tron.common.crypto.pqc.SignatureVerifierRegistry; +import org.tron.common.crypto.pqc.PqSignatureRegistry; import org.tron.common.parameter.CommonParameter; import org.tron.common.utils.ByteArray; import org.tron.common.utils.Sha256Hash; @@ -284,12 +283,11 @@ private boolean validateWitnessAuth(DynamicPropertiesStore dynamicPropertiesStor throw new ValidateSignatureException( "witness_auth signer not found in witness permission"); } - SignatureVerifier verifier = SignatureVerifierRegistry.get(scheme); byte[] publicKey = matched.getPublicKey().toByteArray(); byte[] signature = witnessAuth.getSignature().toByteArray(); byte[] rawHdrHash = getRawHash().getBytes(); byte[] digest = PqAuthDigest.block(rawHdrHash, signerAddr); - return verifier.verify(publicKey, digest, signature); + return PqSignatureRegistry.verify(scheme, publicKey, digest, signature); } public BlockId getBlockId() { 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 72b6f3b5245..59066833a4a 100755 --- a/chainbase/src/main/java/org/tron/core/capsule/TransactionCapsule.java +++ b/chainbase/src/main/java/org/tron/core/capsule/TransactionCapsule.java @@ -45,8 +45,7 @@ import org.tron.common.crypto.SignInterface; import org.tron.common.crypto.SignUtils; import org.tron.common.crypto.pqc.PqAuthDigest; -import org.tron.common.crypto.pqc.SignatureVerifier; -import org.tron.common.crypto.pqc.SignatureVerifierRegistry; +import org.tron.common.crypto.pqc.PqSignatureRegistry; import org.tron.common.es.ExecutorServiceManager; import org.tron.common.math.StrictMathWrapper; import org.tron.common.overlay.message.Message; @@ -746,18 +745,17 @@ static boolean validateStructuredSignature(Transaction transaction, throw new PermissionException("signer is not in permission"); } SignatureScheme scheme = key.getScheme(); - if (!SignatureVerifierRegistry.contains(scheme)) { + if (!PqSignatureRegistry.contains(scheme)) { throw new PermissionException("unsupported scheme: " + scheme); } - SignatureVerifier verifier = SignatureVerifierRegistry.get(scheme); byte[] digest = PqAuthDigest.tx(txid, permissionId, signer.toByteArray()); byte[] pk = key.getPublicKey().toByteArray(); byte[] sig = aw.getSignature().toByteArray(); - if (pk.length != verifier.getPublicKeyLength() - || sig.length != verifier.getSignatureLength()) { + if (pk.length != PqSignatureRegistry.getPublicKeyLength(scheme) + || sig.length != PqSignatureRegistry.getSignatureLength(scheme)) { throw new PermissionException("public key or signature length mismatch"); } - if (!verifier.verify(pk, digest, sig)) { + if (!PqSignatureRegistry.verify(scheme, pk, digest, sig)) { throw new PermissionException("pq sig invalid"); } weight = StrictMathWrapper.addExact(weight, key.getWeight()); diff --git a/crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA44.java b/crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA44.java new file mode 100644 index 00000000000..a51c0a299ff --- /dev/null +++ b/crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA44.java @@ -0,0 +1,197 @@ +package org.tron.common.crypto.pqc; + +import java.security.SecureRandom; +import org.bouncycastle.crypto.AsymmetricCipherKeyPair; +import org.bouncycastle.crypto.prng.FixedSecureRandom; +import org.bouncycastle.pqc.crypto.mldsa.MLDSAKeyGenerationParameters; +import org.bouncycastle.pqc.crypto.mldsa.MLDSAKeyPairGenerator; +import org.bouncycastle.pqc.crypto.mldsa.MLDSAParameters; +import org.bouncycastle.pqc.crypto.mldsa.MLDSAPrivateKeyParameters; +import org.bouncycastle.pqc.crypto.mldsa.MLDSAPublicKeyParameters; +import org.bouncycastle.pqc.crypto.mldsa.MLDSASigner; +import org.tron.common.crypto.Hash; +import org.tron.protos.Protocol.SignatureScheme; + +/** + * FIPS 204 ML-DSA-44 keypair-bound signer/verifier. Instance verify and sign + * use the bound keypair; stateless dispatch is available via the static + * {@link #verify} / {@link #sign(byte[], byte[])} entry points and the + * {@link PqSignatureRegistry}. Consumes raw public key / signature bytes — + * no SubjectPublicKeyInfo, PEM, or Base64 wrapping. + */ +public final class MLDSA44 implements PqSignature { + + public static final int PRIVATE_KEY_LENGTH = 2560; + public static final int PUBLIC_KEY_LENGTH = 1312; + public static final int SIGNATURE_LENGTH = 2420; + /** FIPS 204 ML-DSA seed (ξ) is 32 bytes. */ + public static final int SEED_LENGTH = 32; + + private final byte[] privateKey; + private final byte[] publicKey; + + /** Generate a fresh keypair using a cryptographically secure random source. */ + public MLDSA44() { + this.privateKey = generatePrivateKey(); + this.publicKey = derivePublicKey(this.privateKey); + } + + /** Deterministically generate a keypair from a 32-byte seed (FIPS 204 ξ). */ + public MLDSA44(byte[] seed) { + if (seed == null || seed.length != SEED_LENGTH) { + throw new IllegalArgumentException("ML-DSA-44 seed length must be " + SEED_LENGTH); + } + this.privateKey = generatePrivateKeyFromSeed(seed); + this.publicKey = derivePublicKey(this.privateKey); + } + + /** + * Build a keypair-bound instance from an existing keypair without re-deriving the public key. + * The caller is responsible for ensuring that {@code publicKey} is the correct public key + * corresponding to {@code privateKey}; no consistency check is performed. + */ + public MLDSA44(byte[] privateKey, byte[] publicKey) { + validatePrivateKeyBytes(privateKey); + if (publicKey == null || publicKey.length != PUBLIC_KEY_LENGTH) { + throw new IllegalArgumentException( + "ML-DSA-44 public key length must be " + PUBLIC_KEY_LENGTH); + } + this.privateKey = privateKey.clone(); + this.publicKey = publicKey.clone(); + } + + /** Load an existing 2560-byte ML-DSA-44 private key; the public key is derived. */ + public static MLDSA44 fromPrivate(byte[] privateKey) { + validatePrivateKeyBytes(privateKey); + byte[] sk = privateKey.clone(); + return new MLDSA44(sk, derivePublicKey(sk)); + } + + @Override + public SignatureScheme getScheme() { + return SignatureScheme.ML_DSA_44; + } + + @Override + public int getPrivateKeyLength() { + return PRIVATE_KEY_LENGTH; + } + + @Override + public int getPublicKeyLength() { + return PUBLIC_KEY_LENGTH; + } + + @Override + public int getSignatureLength() { + return SIGNATURE_LENGTH; + } + + @Override + public byte[] getPrivateKey() { + return privateKey.clone(); + } + + @Override + public byte[] getPublicKey() { + return publicKey.clone(); + } + + /** 21-byte TRON address derived from the public key. */ + @Override + public byte[] getAddress() { + return Hash.sha3omit12(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( + "ML-DSA-44 public key length must be " + PUBLIC_KEY_LENGTH); + } + if (signature == null || signature.length != SIGNATURE_LENGTH) { + throw new IllegalArgumentException( + "ML-DSA-44 signature length must be " + SIGNATURE_LENGTH); + } + if (message == null) { + throw new IllegalArgumentException("message must not be null"); + } + MLDSAPublicKeyParameters pk = new MLDSAPublicKeyParameters( + MLDSAParameters.ml_dsa_44, publicKey); + MLDSASigner verifier = new MLDSASigner(); + verifier.init(false, pk); + verifier.update(message, 0, message.length); + return verifier.verifySignature(signature); + } + + public static byte[] sign(byte[] privateKey, byte[] message) { + validatePrivateKeyBytes(privateKey); + if (message == null) { + throw new IllegalArgumentException("message must not be null"); + } + MLDSAPrivateKeyParameters sk = new MLDSAPrivateKeyParameters( + MLDSAParameters.ml_dsa_44, privateKey); + MLDSASigner signer = new MLDSASigner(); + signer.init(true, sk); + signer.update(message, 0, message.length); + try { + return signer.generateSignature(); + } catch (Exception e) { + throw new IllegalStateException("ML-DSA-44 signing failed", e); + } + } + + /** Generate a fresh ML-DSA-44 private key; returns the NIST-encoded 2560-byte key. */ + public static byte[] generatePrivateKey() { + return generatePrivateKey(new SecureRandom()); + } + + /** Deterministically generate an ML-DSA-44 private key from a 32-byte seed. */ + public static byte[] generatePrivateKeyFromSeed(byte[] seed) { + if (seed == null || seed.length != SEED_LENGTH) { + throw new IllegalArgumentException( + "ML-DSA-44 seed length must be " + SEED_LENGTH); + } + return generatePrivateKey(new FixedSecureRandom(seed)); + } + + private static byte[] generatePrivateKey(SecureRandom random) { + MLDSAKeyPairGenerator generator = new MLDSAKeyPairGenerator(); + generator.init(new MLDSAKeyGenerationParameters(random, MLDSAParameters.ml_dsa_44)); + AsymmetricCipherKeyPair keyPair = generator.generateKeyPair(); + return ((MLDSAPrivateKeyParameters) keyPair.getPrivate()).getEncoded(); + } + + public static byte[] derivePublicKey(byte[] privateKey) { + validatePrivateKeyBytes(privateKey); + MLDSAPrivateKeyParameters sk = new MLDSAPrivateKeyParameters( + MLDSAParameters.ml_dsa_44, privateKey); + MLDSAPublicKeyParameters pk = sk.getPublicKeyParameters(); + return pk.getEncoded(); + } + + /** Derive a TRON 21-byte address from an ML-DSA-44 public key via {@code sha3omit12(pubKey)}. */ + public static byte[] computeAddress(byte[] publicKey) { + if (publicKey == null || publicKey.length != PUBLIC_KEY_LENGTH) { + throw new IllegalArgumentException( + "ML-DSA-44 public key length must be " + PUBLIC_KEY_LENGTH); + } + return Hash.sha3omit12(publicKey); + } + + private static void validatePrivateKeyBytes(byte[] privateKey) { + if (privateKey == null || privateKey.length != PRIVATE_KEY_LENGTH) { + throw new IllegalArgumentException( + "ML-DSA-44 private key length must be " + PRIVATE_KEY_LENGTH); + } + } +} diff --git a/crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA44Signer.java b/crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA44Signer.java deleted file mode 100644 index 9ea743ed3a1..00000000000 --- a/crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA44Signer.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.tron.common.crypto.pqc; - -import org.bouncycastle.pqc.crypto.mldsa.MLDSAParameters; -import org.bouncycastle.pqc.crypto.mldsa.MLDSAPrivateKeyParameters; -import org.bouncycastle.pqc.crypto.mldsa.MLDSAPublicKeyParameters; -import org.bouncycastle.pqc.crypto.mldsa.MLDSASigner; - -public final class MLDSA44Signer { - - public static final int PRIVATE_KEY_LENGTH = 2560; - - private MLDSA44Signer() { - } - - public static byte[] sign(byte[] privateKey, byte[] message) { - if (privateKey == null || privateKey.length != PRIVATE_KEY_LENGTH) { - throw new IllegalArgumentException( - "ML-DSA-44 private key length must be " + PRIVATE_KEY_LENGTH); - } - if (message == null) { - throw new IllegalArgumentException("message must not be null"); - } - MLDSAPrivateKeyParameters sk = new MLDSAPrivateKeyParameters( - MLDSAParameters.ml_dsa_44, privateKey); - MLDSASigner signer = new MLDSASigner(); - signer.init(true, sk); - signer.update(message, 0, message.length); - try { - return signer.generateSignature(); - } catch (Exception e) { - throw new IllegalStateException("ML-DSA-44 signing failed", e); - } - } - - public static byte[] derivePublicKey(byte[] privateKey) { - if (privateKey == null || privateKey.length != PRIVATE_KEY_LENGTH) { - throw new IllegalArgumentException( - "ML-DSA-44 private key length must be " + PRIVATE_KEY_LENGTH); - } - MLDSAPrivateKeyParameters sk = new MLDSAPrivateKeyParameters( - MLDSAParameters.ml_dsa_44, privateKey); - MLDSAPublicKeyParameters pk = sk.getPublicKeyParameters(); - return pk.getEncoded(); - } -} diff --git a/crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA44Verifier.java b/crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA44Verifier.java deleted file mode 100644 index 7e46fb1fb30..00000000000 --- a/crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA44Verifier.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.tron.common.crypto.pqc; - -import org.bouncycastle.pqc.crypto.mldsa.MLDSAParameters; -import org.bouncycastle.pqc.crypto.mldsa.MLDSAPublicKeyParameters; -import org.bouncycastle.pqc.crypto.mldsa.MLDSASigner; -import org.tron.protos.Protocol.SignatureScheme; - -/** - * FIPS 204 ML-DSA-44 verifier. Consumes raw public key and signature bytes — - * no SubjectPublicKeyInfo, PEM, or Base64 wrapping. - */ -public class MLDSA44Verifier implements SignatureVerifier { - - public static final int PUBLIC_KEY_LENGTH = 1312; - public static final int SIGNATURE_LENGTH = 2420; - - @Override - public SignatureScheme getScheme() { - return SignatureScheme.ML_DSA_44; - } - - @Override - public int getPublicKeyLength() { - return PUBLIC_KEY_LENGTH; - } - - @Override - public int getSignatureLength() { - return SIGNATURE_LENGTH; - } - - @Override - public boolean verify(byte[] publicKey, byte[] message, byte[] signature) { - validatePublicKey(publicKey); - validateSignature(signature); - if (message == null) { - throw new IllegalArgumentException("message must not be null"); - } - MLDSAPublicKeyParameters pk = new MLDSAPublicKeyParameters( - MLDSAParameters.ml_dsa_44, publicKey); - MLDSASigner signer = new MLDSASigner(); - signer.init(false, pk); - signer.update(message, 0, message.length); - return signer.verifySignature(signature); - } -} diff --git a/crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA65.java b/crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA65.java new file mode 100644 index 00000000000..36ed1b08ae9 --- /dev/null +++ b/crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA65.java @@ -0,0 +1,197 @@ +package org.tron.common.crypto.pqc; + +import java.security.SecureRandom; +import org.bouncycastle.crypto.AsymmetricCipherKeyPair; +import org.bouncycastle.crypto.prng.FixedSecureRandom; +import org.bouncycastle.pqc.crypto.mldsa.MLDSAKeyGenerationParameters; +import org.bouncycastle.pqc.crypto.mldsa.MLDSAKeyPairGenerator; +import org.bouncycastle.pqc.crypto.mldsa.MLDSAParameters; +import org.bouncycastle.pqc.crypto.mldsa.MLDSAPrivateKeyParameters; +import org.bouncycastle.pqc.crypto.mldsa.MLDSAPublicKeyParameters; +import org.bouncycastle.pqc.crypto.mldsa.MLDSASigner; +import org.tron.common.crypto.Hash; +import org.tron.protos.Protocol.SignatureScheme; + +/** + * FIPS 204 ML-DSA-65 keypair-bound signer/verifier. Instance verify and sign + * use the bound keypair; stateless dispatch is available via the static + * {@link #verify} / {@link #sign(byte[], byte[])} entry points and the + * {@link PqSignatureRegistry}. Consumes raw public key / signature bytes — + * no SubjectPublicKeyInfo, PEM, or Base64 wrapping. + */ +public final class MLDSA65 implements PqSignature { + + public static final int PRIVATE_KEY_LENGTH = 4032; + public static final int PUBLIC_KEY_LENGTH = 1952; + public static final int SIGNATURE_LENGTH = 3309; + /** FIPS 204 ML-DSA seed (ξ) is 32 bytes. */ + public static final int SEED_LENGTH = 32; + + private final byte[] privateKey; + private final byte[] publicKey; + + /** Generate a fresh keypair using a cryptographically secure random source. */ + public MLDSA65() { + this.privateKey = generatePrivateKey(); + this.publicKey = derivePublicKey(this.privateKey); + } + + /** Deterministically generate a keypair from a 32-byte seed (FIPS 204 ξ). */ + public MLDSA65(byte[] seed) { + if (seed == null || seed.length != SEED_LENGTH) { + throw new IllegalArgumentException("ML-DSA-65 seed length must be " + SEED_LENGTH); + } + this.privateKey = generatePrivateKeyFromSeed(seed); + this.publicKey = derivePublicKey(this.privateKey); + } + + /** + * Build a keypair-bound instance from an existing keypair without re-deriving the public key. + * The caller is responsible for ensuring that {@code publicKey} is the correct public key + * corresponding to {@code privateKey}; no consistency check is performed. + */ + public MLDSA65(byte[] privateKey, byte[] publicKey) { + validatePrivateKeyBytes(privateKey); + if (publicKey == null || publicKey.length != PUBLIC_KEY_LENGTH) { + throw new IllegalArgumentException( + "ML-DSA-65 public key length must be " + PUBLIC_KEY_LENGTH); + } + this.privateKey = privateKey.clone(); + this.publicKey = publicKey.clone(); + } + + /** Load an existing 4032-byte ML-DSA-65 private key; the public key is derived. */ + public static MLDSA65 fromPrivate(byte[] privateKey) { + validatePrivateKeyBytes(privateKey); + byte[] sk = privateKey.clone(); + return new MLDSA65(sk, derivePublicKey(sk)); + } + + @Override + public SignatureScheme getScheme() { + return SignatureScheme.ML_DSA_65; + } + + @Override + public int getPrivateKeyLength() { + return PRIVATE_KEY_LENGTH; + } + + @Override + public int getPublicKeyLength() { + return PUBLIC_KEY_LENGTH; + } + + @Override + public int getSignatureLength() { + return SIGNATURE_LENGTH; + } + + @Override + public byte[] getPrivateKey() { + return privateKey.clone(); + } + + @Override + public byte[] getPublicKey() { + return publicKey.clone(); + } + + /** 21-byte TRON address derived from the public key. */ + @Override + public byte[] getAddress() { + return Hash.sha3omit12(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( + "ML-DSA-65 public key length must be " + PUBLIC_KEY_LENGTH); + } + if (signature == null || signature.length != SIGNATURE_LENGTH) { + throw new IllegalArgumentException( + "ML-DSA-65 signature length must be " + SIGNATURE_LENGTH); + } + if (message == null) { + throw new IllegalArgumentException("message must not be null"); + } + MLDSAPublicKeyParameters pk = new MLDSAPublicKeyParameters( + MLDSAParameters.ml_dsa_65, publicKey); + MLDSASigner verifier = new MLDSASigner(); + verifier.init(false, pk); + verifier.update(message, 0, message.length); + return verifier.verifySignature(signature); + } + + public static byte[] sign(byte[] privateKey, byte[] message) { + validatePrivateKeyBytes(privateKey); + if (message == null) { + throw new IllegalArgumentException("message must not be null"); + } + MLDSAPrivateKeyParameters sk = new MLDSAPrivateKeyParameters( + MLDSAParameters.ml_dsa_65, privateKey); + MLDSASigner signer = new MLDSASigner(); + signer.init(true, sk); + signer.update(message, 0, message.length); + try { + return signer.generateSignature(); + } catch (Exception e) { + throw new IllegalStateException("ML-DSA-65 signing failed", e); + } + } + + /** Generate a fresh ML-DSA-65 private key; returns the NIST-encoded 4032-byte key. */ + public static byte[] generatePrivateKey() { + return generatePrivateKey(new SecureRandom()); + } + + /** Deterministically generate an ML-DSA-65 private key from a 32-byte seed. */ + public static byte[] generatePrivateKeyFromSeed(byte[] seed) { + if (seed == null || seed.length != SEED_LENGTH) { + throw new IllegalArgumentException( + "ML-DSA-65 seed length must be " + SEED_LENGTH); + } + return generatePrivateKey(new FixedSecureRandom(seed)); + } + + private static byte[] generatePrivateKey(SecureRandom random) { + MLDSAKeyPairGenerator generator = new MLDSAKeyPairGenerator(); + generator.init(new MLDSAKeyGenerationParameters(random, MLDSAParameters.ml_dsa_65)); + AsymmetricCipherKeyPair keyPair = generator.generateKeyPair(); + return ((MLDSAPrivateKeyParameters) keyPair.getPrivate()).getEncoded(); + } + + public static byte[] derivePublicKey(byte[] privateKey) { + validatePrivateKeyBytes(privateKey); + MLDSAPrivateKeyParameters sk = new MLDSAPrivateKeyParameters( + MLDSAParameters.ml_dsa_65, privateKey); + MLDSAPublicKeyParameters pk = sk.getPublicKeyParameters(); + return pk.getEncoded(); + } + + /** Derive a TRON 21-byte address from an ML-DSA-65 public key via {@code sha3omit12(pubKey)}. */ + public static byte[] computeAddress(byte[] publicKey) { + if (publicKey == null || publicKey.length != PUBLIC_KEY_LENGTH) { + throw new IllegalArgumentException( + "ML-DSA-65 public key length must be " + PUBLIC_KEY_LENGTH); + } + return Hash.sha3omit12(publicKey); + } + + private static void validatePrivateKeyBytes(byte[] privateKey) { + if (privateKey == null || privateKey.length != PRIVATE_KEY_LENGTH) { + throw new IllegalArgumentException( + "ML-DSA-65 private key length must be " + PRIVATE_KEY_LENGTH); + } + } +} diff --git a/crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA65Signer.java b/crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA65Signer.java deleted file mode 100644 index 1365243ad8b..00000000000 --- a/crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA65Signer.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.tron.common.crypto.pqc; - -import org.bouncycastle.pqc.crypto.mldsa.MLDSAParameters; -import org.bouncycastle.pqc.crypto.mldsa.MLDSAPrivateKeyParameters; -import org.bouncycastle.pqc.crypto.mldsa.MLDSAPublicKeyParameters; -import org.bouncycastle.pqc.crypto.mldsa.MLDSASigner; - -public final class MLDSA65Signer { - - public static final int PRIVATE_KEY_LENGTH = 4032; - - private MLDSA65Signer() { - } - - public static byte[] sign(byte[] privateKey, byte[] message) { - if (privateKey == null || privateKey.length != PRIVATE_KEY_LENGTH) { - throw new IllegalArgumentException( - "ML-DSA-65 private key length must be " + PRIVATE_KEY_LENGTH); - } - if (message == null) { - throw new IllegalArgumentException("message must not be null"); - } - MLDSAPrivateKeyParameters sk = new MLDSAPrivateKeyParameters( - MLDSAParameters.ml_dsa_65, privateKey); - MLDSASigner signer = new MLDSASigner(); - signer.init(true, sk); - signer.update(message, 0, message.length); - try { - return signer.generateSignature(); - } catch (Exception e) { - throw new IllegalStateException("ML-DSA-65 signing failed", e); - } - } - - public static byte[] derivePublicKey(byte[] privateKey) { - if (privateKey == null || privateKey.length != PRIVATE_KEY_LENGTH) { - throw new IllegalArgumentException( - "ML-DSA-65 private key length must be " + PRIVATE_KEY_LENGTH); - } - MLDSAPrivateKeyParameters sk = new MLDSAPrivateKeyParameters( - MLDSAParameters.ml_dsa_65, privateKey); - MLDSAPublicKeyParameters pk = sk.getPublicKeyParameters(); - return pk.getEncoded(); - } -} diff --git a/crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA65Verifier.java b/crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA65Verifier.java deleted file mode 100644 index 816bfd367a8..00000000000 --- a/crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA65Verifier.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.tron.common.crypto.pqc; - -import org.bouncycastle.pqc.crypto.mldsa.MLDSAParameters; -import org.bouncycastle.pqc.crypto.mldsa.MLDSAPublicKeyParameters; -import org.bouncycastle.pqc.crypto.mldsa.MLDSASigner; -import org.tron.protos.Protocol.SignatureScheme; - -/** - * FIPS 204 ML-DSA-65 verifier. Consumes raw public key and signature bytes — - * no SubjectPublicKeyInfo, PEM, or Base64 wrapping. - */ -public class MLDSA65Verifier implements SignatureVerifier { - - public static final int PUBLIC_KEY_LENGTH = 1952; - public static final int SIGNATURE_LENGTH = 3309; - - @Override - public SignatureScheme getScheme() { - return SignatureScheme.ML_DSA_65; - } - - @Override - public int getPublicKeyLength() { - return PUBLIC_KEY_LENGTH; - } - - @Override - public int getSignatureLength() { - return SIGNATURE_LENGTH; - } - - @Override - public boolean verify(byte[] publicKey, byte[] message, byte[] signature) { - validatePublicKey(publicKey); - validateSignature(signature); - if (message == null) { - throw new IllegalArgumentException("message must not be null"); - } - MLDSAPublicKeyParameters pk = new MLDSAPublicKeyParameters( - MLDSAParameters.ml_dsa_65, publicKey); - MLDSASigner signer = new MLDSASigner(); - signer.init(false, pk); - signer.update(message, 0, message.length); - return signer.verifySignature(signature); - } -} 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..e2a332c8a04 --- /dev/null +++ b/crypto/src/main/java/org/tron/common/crypto/pqc/PqSignature.java @@ -0,0 +1,64 @@ +package org.tron.common.crypto.pqc; + +import org.tron.protos.Protocol.SignatureScheme; + +/** + * 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 SignatureScheme} is provided by + * {@link PqSignatureRegistry}. + */ +public interface PqSignature { + + SignatureScheme getScheme(); + + int getPrivateKeyLength(); + + int getPublicKeyLength(); + + int getSignatureLength(); + + byte[] getPrivateKey(); + + byte[] getPublicKey(); + + /** 21-byte TRON address derived from the held public key. */ + 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 void validateSignature(byte[] signature) { + if (signature == null || signature.length != getSignatureLength()) { + throw new IllegalArgumentException( + "invalid " + getScheme() + " signature length: " + + (signature == null ? "null" : signature.length) + + ", expected " + getSignatureLength()); + } + } +} diff --git a/crypto/src/main/java/org/tron/common/crypto/pqc/PqSignatureRegistry.java b/crypto/src/main/java/org/tron/common/crypto/pqc/PqSignatureRegistry.java new file mode 100644 index 00000000000..284dba565e6 --- /dev/null +++ b/crypto/src/main/java/org/tron/common/crypto/pqc/PqSignatureRegistry.java @@ -0,0 +1,74 @@ +package org.tron.common.crypto.pqc; + +import java.util.Collections; +import java.util.EnumMap; +import java.util.Map; +import org.tron.protos.Protocol.SignatureScheme; + +/** + * Static dispatch table for post-quantum signature verification keyed by + * {@link SignatureScheme}. Each entry binds a scheme to its public-key length, + * signature length, and a stateless verify function (typically a method + * reference to the concrete implementation's {@code verify}). Legacy + * schemes (ECDSA secp256k1, SM2/SM3) are NOT registered — they flow through + * the existing {@code SignInterface} path. + */ +public final class PqSignatureRegistry { + + @FunctionalInterface + public interface Verifier { + boolean verify(byte[] publicKey, byte[] message, byte[] signature); + } + + private static final class SchemeInfo { + final int publicKeyLength; + final int signatureLength; + final Verifier verifier; + + SchemeInfo(int publicKeyLength, int signatureLength, Verifier verifier) { + this.publicKeyLength = publicKeyLength; + this.signatureLength = signatureLength; + this.verifier = verifier; + } + } + + private static final Map SCHEMES; + + static { + EnumMap m = new EnumMap<>(SignatureScheme.class); + m.put(SignatureScheme.ML_DSA_44, new SchemeInfo( + MLDSA44.PUBLIC_KEY_LENGTH, MLDSA44.SIGNATURE_LENGTH, MLDSA44::verify)); + m.put(SignatureScheme.ML_DSA_65, new SchemeInfo( + MLDSA65.PUBLIC_KEY_LENGTH, MLDSA65.SIGNATURE_LENGTH, MLDSA65::verify)); + SCHEMES = Collections.unmodifiableMap(m); + } + + private PqSignatureRegistry() { + } + + public static boolean contains(SignatureScheme scheme) { + return SCHEMES.containsKey(scheme); + } + + public static int getPublicKeyLength(SignatureScheme scheme) { + return require(scheme).publicKeyLength; + } + + public static int getSignatureLength(SignatureScheme scheme) { + return require(scheme).signatureLength; + } + + public static boolean verify( + SignatureScheme scheme, byte[] publicKey, byte[] message, byte[] signature) { + return require(scheme).verifier.verify(publicKey, message, signature); + } + + private static SchemeInfo require(SignatureScheme scheme) { + SchemeInfo info = SCHEMES.get(scheme); + 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/SignatureVerifier.java b/crypto/src/main/java/org/tron/common/crypto/pqc/SignatureVerifier.java deleted file mode 100644 index a2f49b2f5b0..00000000000 --- a/crypto/src/main/java/org/tron/common/crypto/pqc/SignatureVerifier.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.tron.common.crypto.pqc; - -import org.tron.protos.Protocol.SignatureScheme; - -/** - * Stateless verifier for a single post-quantum signature scheme. Independent of - * the legacy {@code SignInterface} because PQ flows do not expose public-key - * recovery and have no notion of node-id / private-key handling in verification - * paths. - */ -public interface SignatureVerifier { - - SignatureScheme getScheme(); - - int getPublicKeyLength(); - - int getSignatureLength(); - - /** - * Verify a raw-byte signature against a raw-byte public key over {@code message}. - * The caller SHALL pre-validate lengths via {@link #validatePublicKey(byte[])} and - * {@link #validateSignature(byte[])}; implementations still defensively re-check. - * - * @return true iff the signature is cryptographically valid for the given inputs - */ - boolean verify(byte[] publicKey, byte[] message, byte[] signature); - - 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 void validateSignature(byte[] signature) { - if (signature == null || signature.length != getSignatureLength()) { - throw new IllegalArgumentException( - "invalid " + getScheme() + " signature length: " - + (signature == null ? "null" : signature.length) - + ", expected " + getSignatureLength()); - } - } -} diff --git a/crypto/src/main/java/org/tron/common/crypto/pqc/SignatureVerifierRegistry.java b/crypto/src/main/java/org/tron/common/crypto/pqc/SignatureVerifierRegistry.java deleted file mode 100644 index 3b64cd637cd..00000000000 --- a/crypto/src/main/java/org/tron/common/crypto/pqc/SignatureVerifierRegistry.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.tron.common.crypto.pqc; - -import java.util.Collections; -import java.util.EnumMap; -import java.util.Map; -import org.tron.protos.Protocol.SignatureScheme; - -/** - * Static registry of post-quantum {@link SignatureVerifier} instances keyed by - * {@link SignatureScheme}. Legacy schemes (ECDSA secp256k1, SM2/SM3) are NOT - * registered — they flow through the existing {@code SignInterface} path. - */ -public final class SignatureVerifierRegistry { - - private static final Map VERIFIERS; - - static { - EnumMap m = new EnumMap<>(SignatureScheme.class); - m.put(SignatureScheme.ML_DSA_44, new MLDSA44Verifier()); - m.put(SignatureScheme.ML_DSA_65, new MLDSA65Verifier()); - VERIFIERS = Collections.unmodifiableMap(m); - } - - private SignatureVerifierRegistry() { - } - - public static SignatureVerifier get(SignatureScheme scheme) { - SignatureVerifier v = VERIFIERS.get(scheme); - if (v == null) { - throw new IllegalArgumentException( - "no SignatureVerifier registered for scheme: " + scheme); - } - return v; - } - - public static boolean contains(SignatureScheme scheme) { - return VERIFIERS.containsKey(scheme); - } -} diff --git a/crypto/src/test/java/org/tron/common/crypto/pqc/MLDSA44VerifierTest.java b/crypto/src/test/java/org/tron/common/crypto/pqc/MLDSA44VerifierTest.java deleted file mode 100644 index 2f59ed8855d..00000000000 --- a/crypto/src/test/java/org/tron/common/crypto/pqc/MLDSA44VerifierTest.java +++ /dev/null @@ -1,134 +0,0 @@ -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.pqc.crypto.mldsa.MLDSAKeyGenerationParameters; -import org.bouncycastle.pqc.crypto.mldsa.MLDSAKeyPairGenerator; -import org.bouncycastle.pqc.crypto.mldsa.MLDSAParameters; -import org.bouncycastle.pqc.crypto.mldsa.MLDSAPrivateKeyParameters; -import org.bouncycastle.pqc.crypto.mldsa.MLDSAPublicKeyParameters; -import org.bouncycastle.pqc.crypto.mldsa.MLDSASigner; -import org.junit.Before; -import org.junit.Test; -import org.tron.protos.Protocol.SignatureScheme; - -public class MLDSA44VerifierTest { - - private MLDSA44Verifier verifier; - private MLDSAPublicKeyParameters pk; - private MLDSAPrivateKeyParameters sk; - - @Before - public void setUp() { - verifier = new MLDSA44Verifier(); - MLDSAKeyPairGenerator gen = new MLDSAKeyPairGenerator(); - gen.init(new MLDSAKeyGenerationParameters(new SecureRandom(), MLDSAParameters.ml_dsa_44)); - AsymmetricCipherKeyPair kp = gen.generateKeyPair(); - pk = (MLDSAPublicKeyParameters) kp.getPublic(); - sk = (MLDSAPrivateKeyParameters) kp.getPrivate(); - } - - private byte[] sign(byte[] message) { - MLDSASigner signer = new MLDSASigner(); - signer.init(true, sk); - signer.update(message, 0, message.length); - try { - return signer.generateSignature(); - } catch (Exception e) { - throw new AssertionError("failed to sign in test setup", e); - } - } - - @Test - public void schemeAndLengthsMatchFips204() { - assertEquals(SignatureScheme.ML_DSA_44, verifier.getScheme()); - assertEquals(1312, verifier.getPublicKeyLength()); - assertEquals(2420, verifier.getSignatureLength()); - assertArrayEquals(pk.getEncoded(), pk.getEncoded()); - assertEquals(1312, pk.getEncoded().length); - } - - @Test - public void validSignatureVerifies() { - byte[] msg = "tron-pq-mldsa44".getBytes(); - byte[] sig = sign(msg); - assertEquals(2420, sig.length); - assertTrue(verifier.verify(pk.getEncoded(), msg, sig)); - } - - @Test - public void signatureBoundToMessage() { - byte[] msg = "hello".getBytes(); - byte[] sig = sign(msg); - byte[] tamperedMsg = "hellp".getBytes(); - assertFalse(verifier.verify(pk.getEncoded(), tamperedMsg, sig)); - } - - @Test - public void tamperedSignatureFailsVerification() { - byte[] msg = "payload".getBytes(); - byte[] sig = sign(msg); - sig[0] ^= 0x01; - assertFalse(verifier.verify(pk.getEncoded(), msg, sig)); - } - - @Test - public void wrongPublicKeyFailsVerification() { - byte[] msg = "payload".getBytes(); - byte[] sig = sign(msg); - MLDSAKeyPairGenerator gen = new MLDSAKeyPairGenerator(); - gen.init(new MLDSAKeyGenerationParameters(new SecureRandom(), MLDSAParameters.ml_dsa_44)); - MLDSAPublicKeyParameters otherPk = - (MLDSAPublicKeyParameters) gen.generateKeyPair().getPublic(); - assertFalse(verifier.verify(otherPk.getEncoded(), msg, sig)); - } - - @Test - public void invalidPublicKeyLengthRejected() { - byte[] badPk = new byte[1311]; - byte[] msg = new byte[] {1}; - byte[] sig = new byte[2420]; - try { - verifier.verify(badPk, msg, sig); - fail("short public key should be rejected"); - } catch (IllegalArgumentException expected) { - assertTrue(expected.getMessage().contains("public key length")); - } - } - - @Test - public void invalidSignatureLengthRejected() { - byte[] badSig = new byte[2419]; - byte[] msg = new byte[] {1}; - try { - verifier.verify(pk.getEncoded(), msg, badSig); - fail("short signature should be rejected"); - } catch (IllegalArgumentException expected) { - assertTrue(expected.getMessage().contains("signature length")); - } - } - - @Test - public void nullMessageRejected() { - byte[] sig = new byte[2420]; - try { - verifier.verify(pk.getEncoded(), null, sig); - fail("null message should be rejected"); - } catch (IllegalArgumentException expected) { - assertTrue(expected.getMessage().contains("message")); - } - } - - @Test - public void emptyMessageVerifiesConsistently() { - byte[] msg = new byte[0]; - byte[] sig = sign(msg); - assertTrue(verifier.verify(pk.getEncoded(), msg, sig)); - } -} diff --git a/crypto/src/test/java/org/tron/common/crypto/pqc/SignatureVerifierRegistryTest.java b/crypto/src/test/java/org/tron/common/crypto/pqc/SignatureVerifierRegistryTest.java deleted file mode 100644 index 769fe55ca1e..00000000000 --- a/crypto/src/test/java/org/tron/common/crypto/pqc/SignatureVerifierRegistryTest.java +++ /dev/null @@ -1,62 +0,0 @@ -package org.tron.common.crypto.pqc; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -import org.junit.Test; -import org.tron.protos.Protocol.SignatureScheme; - -public class SignatureVerifierRegistryTest { - - @Test - public void mlDsa44Registered() { - SignatureVerifier v = SignatureVerifierRegistry.get(SignatureScheme.ML_DSA_44); - assertNotNull(v); - assertSame(SignatureScheme.ML_DSA_44, v.getScheme()); - assertTrue(SignatureVerifierRegistry.contains(SignatureScheme.ML_DSA_44)); - } - - @Test - public void mlDsa65Registered() { - SignatureVerifier v = SignatureVerifierRegistry.get(SignatureScheme.ML_DSA_65); - assertNotNull(v); - assertSame(SignatureScheme.ML_DSA_65, v.getScheme()); - assertTrue(SignatureVerifierRegistry.contains(SignatureScheme.ML_DSA_65)); - } - - @Test - public void ecdsaNotRegistered() { - assertFalse(SignatureVerifierRegistry.contains(SignatureScheme.ECDSA_SECP256K1)); - try { - SignatureVerifierRegistry.get(SignatureScheme.ECDSA_SECP256K1); - fail("expected IllegalArgumentException for ECDSA_SECP256K1"); - } catch (IllegalArgumentException e) { - assertTrue(e.getMessage().contains("ECDSA_SECP256K1")); - } - } - - @Test - public void sm2NotRegistered() { - assertFalse(SignatureVerifierRegistry.contains(SignatureScheme.SM2_SM3)); - try { - SignatureVerifierRegistry.get(SignatureScheme.SM2_SM3); - fail("expected IllegalArgumentException for SM2_SM3"); - } catch (IllegalArgumentException e) { - assertTrue(e.getMessage().contains("SM2_SM3")); - } - } - - @Test - public void unknownSchemeRejected() { - assertFalse(SignatureVerifierRegistry.contains(SignatureScheme.UNKNOWN_SIG_SCHEME)); - try { - SignatureVerifierRegistry.get(SignatureScheme.UNKNOWN_SIG_SCHEME); - fail("expected IllegalArgumentException for UNKNOWN_SIG_SCHEME"); - } catch (IllegalArgumentException e) { - assertTrue(e.getMessage().contains("UNKNOWN_SIG_SCHEME")); - } - } -} 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 83d7fd2c63d..e0716e9be31 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 @@ -1041,6 +1041,10 @@ public static void applyConfigParams( config.hasPath(ConfigKey.COMMITTEE_ALLOW_TVM_OSAKA) ? config .getInt(ConfigKey.COMMITTEE_ALLOW_TVM_OSAKA) : 0; + PARAMETER.allowMlDsa = + config.hasPath(ConfigKey.COMMITTEE_ALLOW_ML_DSA) ? config + .getInt(ConfigKey.COMMITTEE_ALLOW_ML_DSA) : 0; + logConfig(); } @@ -1220,6 +1224,19 @@ private static void initLocalWitnesses(Config config, CLIParameter cmd) { } } + // path 4: PQ seed configuration + if (config.hasPath(ConfigKey.LOCAL_WITNESS_SEED_PQ)) { + List pqSeeds = config.getStringList(ConfigKey.LOCAL_WITNESS_SEED_PQ); + if (!pqSeeds.isEmpty()) { + localWitnesses.setPqSeeds(pqSeeds); + byte[] address = WitnessInitializer.resolvePqWitnessAddress(witnessAddr); + 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 index b21c9c440a4..ba71236a4c3 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 @@ -13,6 +13,7 @@ private ConfigKey() { public static final String LOCAL_WITNESS = "localwitness"; // private key public static final String LOCAL_WITNESS_ACCOUNT_ADDRESS = "localWitnessAccountAddress"; public static final String LOCAL_WITNESS_KEYSTORE = "localwitnesskeystore"; + public static final String LOCAL_WITNESS_SEED_PQ = "localwitness_seed_pq"; // crypto public static final String CRYPTO_ENGINE = "crypto.engine"; @@ -248,6 +249,7 @@ private ConfigKey() { public static final String COMMITTEE_ALLOW_TVM_BLOB = "committee.allowTvmBlob"; public static final String COMMITTEE_PROPOSAL_EXPIRE_TIME = "committee.proposalExpireTime"; public static final String COMMITTEE_ALLOW_TVM_OSAKA = "committee.allowTvmOsaka"; + public static final String COMMITTEE_ALLOW_ML_DSA = "committee.allowMlDsa"; public static final String ALLOW_ACCOUNT_ASSET_OPTIMIZATION = "committee.allowAccountAssetOptimization"; public static final String ALLOW_ASSET_OPTIMIZATION = "committee.allowAssetOptimization"; 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 30711eb6190..8bd81b6d1d5 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 @@ -97,6 +97,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[] resolvePqWitnessAddress(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..1f8e057f381 100644 --- a/framework/src/main/java/org/tron/core/consensus/ConsensusService.java +++ b/framework/src/main/java/org/tron/core/consensus/ConsensusService.java @@ -10,6 +10,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.tron.common.crypto.SignUtils; +import org.tron.common.crypto.pqc.MLDSA65; import org.tron.common.parameter.CommonParameter; import org.tron.consensus.Consensus; import org.tron.consensus.base.Param; @@ -46,6 +47,7 @@ public void start() { param.setAgreeNodeCount(parameter.getAgreeNodeCount()); List miners = new ArrayList<>(); List privateKeys = Args.getLocalWitnesses().getPrivateKeys(); + List pqSeeds = Args.getLocalWitnesses().getPqSeeds(); if (privateKeys.size() > 1) { for (String key : privateKeys) { byte[] privateKey = fromHexString(key); @@ -76,6 +78,27 @@ public void start() { Miner miner = param.new Miner(privateKey, ByteString.copyFrom(privateKeyAddress), ByteString.copyFrom(witnessAddress)); miners.add(miner); + } else if (pqSeeds.size() > 1) { + for (String seed : pqSeeds) { + byte[] seedBytes = fromHexString(seed); + MLDSA65 keypair = new MLDSA65(seedBytes); + byte[] sk = keypair.getPrivateKey(); + byte[] pk = keypair.getPublicKey(); + byte[] pqAddress = MLDSA65.computeAddress(pk); + 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); + miners.add(miner); + logger.info("Add ML-DSA witness (from seed): {}, size: {}", + Hex.toHexString(pqAddress), miners.size()); + } + } else if (pqSeeds.size() == 1) { + miners.add(buildPqOnlyMinerFromSeed(param, pqSeeds.get(0))); } param.setMiners(miners); @@ -85,6 +108,29 @@ public void start() { logger.info("consensus service start success"); } + private Miner buildPqOnlyMinerFromSeed(Param param, String pqSeed) { + byte[] seedBytes = fromHexString(pqSeed); + MLDSA65 keypair = new MLDSA65(seedBytes); + byte[] sk = keypair.getPrivateKey(); + byte[] pk = keypair.getPublicKey(); + byte[] pqAddress = MLDSA65.computeAddress(pk); + 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); + logger.info("Add ML-DSA witness (from seed): {}", Hex.toHexString(witnessAddress)); + return miner; + } + public void stop() { logger.info("consensus service closed start."); consensus.stop(); 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 72e5b048c94..33cf1852482 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -54,7 +54,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.MLDSA65Signer; +import org.tron.common.crypto.pqc.MLDSA65; import org.tron.common.crypto.pqc.PqAuthDigest; import org.tron.common.es.ExecutorServiceManager; import org.tron.common.exit.ExitManager; @@ -1794,7 +1794,7 @@ private void signWitnessAuth(BlockCapsule blockCapsule, Miner miner) { } byte[] signerAddress = witnessPermission.getKeys(0).getAddress().toByteArray(); byte[] digest = PqAuthDigest.block(blockCapsule.getRawHashBytes(), signerAddress); - byte[] signature = MLDSA65Signer.sign(pqPrivateKey, digest); + byte[] signature = MLDSA65.sign(pqPrivateKey, digest); AuthWitness witnessAuth = AuthWitness.newBuilder() .setSignerAddress(ByteString.copyFrom(signerAddress)) .setSignature(ByteString.copyFrom(signature)) diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf index 4a709e87e93..a3fa7c11019 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -673,15 +673,15 @@ localwitness = [ # "localwitnesskeystore.json" # ] -# Post-quantum (ML-DSA-65) witness signing key. Required once the on-chain witness +# Post-quantum (ML-DSA-65) witness signing seed. Required once the on-chain witness # Permission has been upgraded to scheme = ML_DSA_65 and ALLOW_ML_DSA = 1 is active. -# Each entry is the 4032-byte FIPS 204 ML-DSA-65 private key encoded as a hex string -# (no SubjectPublicKeyInfo / PEM / Base64 wrapping). The node uses this key to -# produce the BlockHeader.witness_auth field during Dual-Sign block production. -# If the witness Permission is still legacy (scheme = UNKNOWN_SIG_SCHEME), leave this -# empty; the node will use `localwitness` / `localwitnesskeystore` as before. -# localwitness_pq = [ -# "0123abcd...<8064-hex-char ML-DSA-65 private key>" +# Each entry is a 32-byte seed encoded as a hex string (64 hex chars). +# The ML-DSA-65 keypair is deterministically derived from the seed using FIPS 204. +# The node uses this key to produce the BlockHeader.witness_auth field during Dual-Sign +# block production. If the witness Permission is still legacy (scheme = UNKNOWN_SIG_SCHEME), +# leave this empty; the node will use `localwitness` / `localwitnesskeystore` as before. +# localwitness_seed_pq = [ +# "0101010101010101010101010101010101010101010101010101010101010101" # ] block = { @@ -771,6 +771,7 @@ committee = { # consensusLogicOptimization = 0 # allowOptimizedReturnValueOfChainId = 0 # allowTvmOsaka = 0 + # allowMlDsa = 0 } event.subscribe = { diff --git a/framework/src/test/java/org/tron/common/crypto/pqc/MLDSA44Test.java b/framework/src/test/java/org/tron/common/crypto/pqc/MLDSA44Test.java new file mode 100644 index 00000000000..5b3a6bfd168 --- /dev/null +++ b/framework/src/test/java/org/tron/common/crypto/pqc/MLDSA44Test.java @@ -0,0 +1,228 @@ +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.pqc.crypto.mldsa.MLDSAKeyGenerationParameters; +import org.bouncycastle.pqc.crypto.mldsa.MLDSAKeyPairGenerator; +import org.bouncycastle.pqc.crypto.mldsa.MLDSAParameters; +import org.bouncycastle.pqc.crypto.mldsa.MLDSAPrivateKeyParameters; +import org.bouncycastle.pqc.crypto.mldsa.MLDSAPublicKeyParameters; +import org.bouncycastle.pqc.crypto.mldsa.MLDSASigner; +import org.junit.Before; +import org.junit.Test; +import org.tron.protos.Protocol.SignatureScheme; + +public class MLDSA44Test { + + private MLDSA44 keypair; + private MLDSAPublicKeyParameters pk; + private MLDSAPrivateKeyParameters sk; + + @Before + public void setUp() { + MLDSAKeyPairGenerator gen = new MLDSAKeyPairGenerator(); + gen.init(new MLDSAKeyGenerationParameters(new SecureRandom(), MLDSAParameters.ml_dsa_44)); + AsymmetricCipherKeyPair kp = gen.generateKeyPair(); + pk = (MLDSAPublicKeyParameters) kp.getPublic(); + sk = (MLDSAPrivateKeyParameters) kp.getPrivate(); + keypair = new MLDSA44(sk.getEncoded(), pk.getEncoded()); + } + + private static byte[] freshPrivateKey() { + MLDSAKeyPairGenerator gen = new MLDSAKeyPairGenerator(); + gen.init(new MLDSAKeyGenerationParameters(new SecureRandom(), MLDSAParameters.ml_dsa_44)); + AsymmetricCipherKeyPair kp = gen.generateKeyPair(); + return ((MLDSAPrivateKeyParameters) kp.getPrivate()).getEncoded(); + } + + private byte[] rawSign(byte[] message) { + MLDSASigner signer = new MLDSASigner(); + signer.init(true, sk); + signer.update(message, 0, message.length); + try { + return signer.generateSignature(); + } catch (Exception e) { + throw new AssertionError("failed to sign in test setup", e); + } + } + + @Test + public void schemeAndLengthsMatchFips204() { + assertEquals(SignatureScheme.ML_DSA_44, keypair.getScheme()); + assertEquals(1312, keypair.getPublicKeyLength()); + assertEquals(2420, keypair.getSignatureLength()); + assertEquals(1312, pk.getEncoded().length); + } + + @Test + public void privateKeyLengthMatchesFips204() { + byte[] skBytes = freshPrivateKey(); + assertEquals(MLDSA44.PRIVATE_KEY_LENGTH, skBytes.length); + } + + @Test + public void derivedPublicKeyLengthMatchesFips204() { + byte[] skBytes = freshPrivateKey(); + byte[] pkBytes = MLDSA44.derivePublicKey(skBytes); + assertEquals(MLDSA44.PUBLIC_KEY_LENGTH, pkBytes.length); + } + + @Test + public void signProducesVerifiableSignature() { + byte[] skBytes = freshPrivateKey(); + byte[] pkBytes = MLDSA44.derivePublicKey(skBytes); + byte[] message = "hello, ml-dsa-44".getBytes(); + + byte[] sig = MLDSA44.sign(skBytes, message); + assertEquals(MLDSA44.SIGNATURE_LENGTH, sig.length); + + assertTrue(MLDSA44.verify(pkBytes, message, sig)); + } + + @Test + public void roundTripSignVerifyWithTamperRejected() { + byte[] skBytes = freshPrivateKey(); + byte[] pkBytes = MLDSA44.derivePublicKey(skBytes); + byte[] message = "roundtrip".getBytes(); + byte[] sig = MLDSA44.sign(skBytes, message); + + assertTrue(MLDSA44.verify(pkBytes, message, sig)); + + byte[] tampered = sig.clone(); + tampered[0] ^= 0x01; + if (MLDSA44.verify(pkBytes, message, tampered)) { + fail("tampered signature should not verify"); + } + } + + @Test + public void deterministicPublicKeyDerivation() { + byte[] skBytes = freshPrivateKey(); + byte[] pk1 = MLDSA44.derivePublicKey(skBytes); + byte[] pk2 = MLDSA44.derivePublicKey(skBytes); + assertArrayEquals(pk1, pk2); + } + + @Test(expected = IllegalArgumentException.class) + public void rejectsShortPrivateKey() { + MLDSA44.sign(new byte[10], new byte[4]); + } + + @Test(expected = IllegalArgumentException.class) + public void rejectsNullMessage() { + byte[] skBytes = freshPrivateKey(); + MLDSA44.sign(skBytes, null); + } + + @Test + public void validSignatureVerifiesViaInstance() { + byte[] msg = "tron-pq-mldsa44".getBytes(); + byte[] sig = rawSign(msg); + assertEquals(2420, sig.length); + assertTrue(keypair.verify(msg, sig)); + } + + @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); + MLDSAKeyPairGenerator gen = new MLDSAKeyPairGenerator(); + gen.init(new MLDSAKeyGenerationParameters(new SecureRandom(), MLDSAParameters.ml_dsa_44)); + MLDSAPublicKeyParameters otherPk = + (MLDSAPublicKeyParameters) gen.generateKeyPair().getPublic(); + assertFalse(MLDSA44.verify(otherPk.getEncoded(), msg, sig)); + } + + @Test + public void invalidPublicKeyLengthRejected() { + byte[] badPk = new byte[1311]; + byte[] msg = new byte[] {1}; + byte[] sig = new byte[2420]; + try { + MLDSA44.verify(badPk, msg, sig); + fail("short public key should be rejected"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("public key length")); + } + } + + @Test + public void invalidSignatureLengthRejected() { + byte[] badSig = new byte[2419]; + byte[] msg = new byte[] {1}; + try { + MLDSA44.verify(pk.getEncoded(), msg, badSig); + fail("short signature should be rejected"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("signature length")); + } + } + + @Test + public void nullMessageRejected() { + byte[] sig = new byte[2420]; + try { + MLDSA44.verify(pk.getEncoded(), null, sig); + fail("null message should be rejected"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("message")); + } + } + + @Test + public void emptyMessageVerifiesConsistently() { + byte[] msg = new byte[0]; + byte[] sig = rawSign(msg); + assertTrue(keypair.verify(msg, sig)); + } + + @Test + public void keypairBoundInstanceSignsAndVerifies() { + MLDSA44 signer = new MLDSA44(); + byte[] msg = "keypair-bound".getBytes(); + byte[] sig = signer.sign(msg); + assertEquals(MLDSA44.SIGNATURE_LENGTH, sig.length); + assertTrue(signer.verify(msg, sig)); + } + + @Test + public void fromSeedIsDeterministic() { + byte[] seed = new byte[32]; + for (int i = 0; i < seed.length; i++) { + seed[i] = (byte) i; + } + MLDSA44 a = new MLDSA44(seed); + MLDSA44 b = new MLDSA44(seed); + assertArrayEquals(a.getPublicKey(), b.getPublicKey()); + assertArrayEquals(a.getPrivateKey(), b.getPrivateKey()); + } + + @Test + public void computeAddressIs21Bytes() { + byte[] skBytes = freshPrivateKey(); + byte[] pkBytes = MLDSA44.derivePublicKey(skBytes); + assertEquals(21, MLDSA44.computeAddress(pkBytes).length); + } +} diff --git a/crypto/src/test/java/org/tron/common/crypto/pqc/MLDSA65VerifierTest.java b/framework/src/test/java/org/tron/common/crypto/pqc/MLDSA65Test.java similarity index 69% rename from crypto/src/test/java/org/tron/common/crypto/pqc/MLDSA65VerifierTest.java rename to framework/src/test/java/org/tron/common/crypto/pqc/MLDSA65Test.java index c6ee8077df5..370fb6772e1 100644 --- a/crypto/src/test/java/org/tron/common/crypto/pqc/MLDSA65VerifierTest.java +++ b/framework/src/test/java/org/tron/common/crypto/pqc/MLDSA65Test.java @@ -1,5 +1,6 @@ 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; @@ -17,23 +18,23 @@ import org.junit.Test; import org.tron.protos.Protocol.SignatureScheme; -public class MLDSA65VerifierTest { +public class MLDSA65Test { - private MLDSA65Verifier verifier; + private MLDSA65 keypair; private MLDSAPublicKeyParameters pk; private MLDSAPrivateKeyParameters sk; @Before public void setUp() { - verifier = new MLDSA65Verifier(); MLDSAKeyPairGenerator gen = new MLDSAKeyPairGenerator(); gen.init(new MLDSAKeyGenerationParameters(new SecureRandom(), MLDSAParameters.ml_dsa_65)); AsymmetricCipherKeyPair kp = gen.generateKeyPair(); pk = (MLDSAPublicKeyParameters) kp.getPublic(); sk = (MLDSAPrivateKeyParameters) kp.getPrivate(); + keypair = new MLDSA65(sk.getEncoded(), pk.getEncoded()); } - private byte[] sign(byte[] message) { + private byte[] rawSign(byte[] message) { MLDSASigner signer = new MLDSASigner(); signer.init(true, sk); signer.update(message, 0, message.length); @@ -46,45 +47,45 @@ private byte[] sign(byte[] message) { @Test public void schemeAndLengthsMatchFips204() { - assertEquals(SignatureScheme.ML_DSA_65, verifier.getScheme()); - assertEquals(1952, verifier.getPublicKeyLength()); - assertEquals(3309, verifier.getSignatureLength()); + assertEquals(SignatureScheme.ML_DSA_65, keypair.getScheme()); + assertEquals(1952, keypair.getPublicKeyLength()); + assertEquals(3309, keypair.getSignatureLength()); assertEquals(1952, pk.getEncoded().length); } @Test public void validSignatureVerifies() { byte[] msg = "tron-pq-mldsa65".getBytes(); - byte[] sig = sign(msg); + byte[] sig = rawSign(msg); assertEquals(3309, sig.length); - assertTrue(verifier.verify(pk.getEncoded(), msg, sig)); + assertTrue(keypair.verify(msg, sig)); } @Test public void signatureBoundToMessage() { byte[] msg = "block-header".getBytes(); - byte[] sig = sign(msg); + byte[] sig = rawSign(msg); byte[] tamperedMsg = "block-footer".getBytes(); - assertFalse(verifier.verify(pk.getEncoded(), tamperedMsg, sig)); + assertFalse(keypair.verify(tamperedMsg, sig)); } @Test public void tamperedSignatureFailsVerification() { byte[] msg = "payload".getBytes(); - byte[] sig = sign(msg); + byte[] sig = rawSign(msg); sig[sig.length - 1] ^= 0x01; - assertFalse(verifier.verify(pk.getEncoded(), msg, sig)); + assertFalse(keypair.verify(msg, sig)); } @Test public void wrongPublicKeyFailsVerification() { byte[] msg = "payload".getBytes(); - byte[] sig = sign(msg); + byte[] sig = rawSign(msg); MLDSAKeyPairGenerator gen = new MLDSAKeyPairGenerator(); gen.init(new MLDSAKeyGenerationParameters(new SecureRandom(), MLDSAParameters.ml_dsa_65)); MLDSAPublicKeyParameters otherPk = (MLDSAPublicKeyParameters) gen.generateKeyPair().getPublic(); - assertFalse(verifier.verify(otherPk.getEncoded(), msg, sig)); + assertFalse(MLDSA65.verify(otherPk.getEncoded(), msg, sig)); } @Test @@ -93,7 +94,7 @@ public void invalidPublicKeyLengthRejected() { byte[] msg = new byte[] {1}; byte[] sig = new byte[3309]; try { - verifier.verify(badPk, msg, sig); + MLDSA65.verify(badPk, msg, sig); fail("short public key should be rejected"); } catch (IllegalArgumentException expected) { assertTrue(expected.getMessage().contains("public key length")); @@ -105,7 +106,7 @@ public void invalidSignatureLengthRejected() { byte[] badSig = new byte[3310]; byte[] msg = new byte[] {1}; try { - verifier.verify(pk.getEncoded(), msg, badSig); + MLDSA65.verify(pk.getEncoded(), msg, badSig); fail("wrong-length signature should be rejected"); } catch (IllegalArgumentException expected) { assertTrue(expected.getMessage().contains("signature length")); @@ -116,7 +117,7 @@ public void invalidSignatureLengthRejected() { public void nullMessageRejected() { byte[] sig = new byte[3309]; try { - verifier.verify(pk.getEncoded(), null, sig); + MLDSA65.verify(pk.getEncoded(), null, sig); fail("null message should be rejected"); } catch (IllegalArgumentException expected) { assertTrue(expected.getMessage().contains("message")); @@ -126,8 +127,8 @@ public void nullMessageRejected() { @Test public void emptyMessageVerifiesConsistently() { byte[] msg = new byte[0]; - byte[] sig = sign(msg); - assertTrue(verifier.verify(pk.getEncoded(), msg, sig)); + byte[] sig = rawSign(msg); + assertTrue(keypair.verify(msg, sig)); } @Test @@ -140,10 +141,38 @@ public void crossSchemeKeyFailsVerification() { byte[] msg = new byte[] {1}; byte[] sig = new byte[3309]; try { - verifier.verify(pk44Bytes, msg, sig); + MLDSA65.verify(pk44Bytes, msg, sig); fail("ML-DSA-44 key for ML-DSA-65 verifier should be rejected on length"); } catch (IllegalArgumentException expected) { assertTrue(expected.getMessage().contains("public key length")); } } + + @Test + public void keypairBoundInstanceSignsAndVerifies() { + MLDSA65 signer = new MLDSA65(); + byte[] msg = "keypair-bound".getBytes(); + byte[] sig = signer.sign(msg); + assertEquals(MLDSA65.SIGNATURE_LENGTH, sig.length); + assertTrue(signer.verify(msg, sig)); + } + + @Test + public void fromSeedIsDeterministic() { + byte[] seed = new byte[32]; + for (int i = 0; i < seed.length; i++) { + seed[i] = (byte) i; + } + MLDSA65 a = new MLDSA65(seed); + MLDSA65 b = new MLDSA65(seed); + assertArrayEquals(a.getPublicKey(), b.getPublicKey()); + assertArrayEquals(a.getPrivateKey(), b.getPrivateKey()); + } + + @Test + public void computeAddressIs21Bytes() { + byte[] skBytes = MLDSA65.generatePrivateKey(); + byte[] pkBytes = MLDSA65.derivePublicKey(skBytes); + assertEquals(21, MLDSA65.computeAddress(pkBytes).length); + } } diff --git a/crypto/src/test/java/org/tron/common/crypto/pqc/PqAuthDigestTest.java b/framework/src/test/java/org/tron/common/crypto/pqc/PqAuthDigestTest.java similarity index 100% rename from crypto/src/test/java/org/tron/common/crypto/pqc/PqAuthDigestTest.java rename to framework/src/test/java/org/tron/common/crypto/pqc/PqAuthDigestTest.java diff --git a/framework/src/test/java/org/tron/common/crypto/pqc/PqSignatureRegistryTest.java b/framework/src/test/java/org/tron/common/crypto/pqc/PqSignatureRegistryTest.java new file mode 100644 index 00000000000..c9b9917994d --- /dev/null +++ b/framework/src/test/java/org/tron/common/crypto/pqc/PqSignatureRegistryTest.java @@ -0,0 +1,82 @@ +package org.tron.common.crypto.pqc; + +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 org.junit.Test; +import org.tron.protos.Protocol.SignatureScheme; + +public class PqSignatureRegistryTest { + + @Test + public void mlDsa44Registered() { + assertTrue(PqSignatureRegistry.contains(SignatureScheme.ML_DSA_44)); + assertEquals(MLDSA44.PUBLIC_KEY_LENGTH, + PqSignatureRegistry.getPublicKeyLength(SignatureScheme.ML_DSA_44)); + assertEquals(MLDSA44.SIGNATURE_LENGTH, + PqSignatureRegistry.getSignatureLength(SignatureScheme.ML_DSA_44)); + } + + @Test + public void mlDsa65Registered() { + assertTrue(PqSignatureRegistry.contains(SignatureScheme.ML_DSA_65)); + assertEquals(MLDSA65.PUBLIC_KEY_LENGTH, + PqSignatureRegistry.getPublicKeyLength(SignatureScheme.ML_DSA_65)); + assertEquals(MLDSA65.SIGNATURE_LENGTH, + PqSignatureRegistry.getSignatureLength(SignatureScheme.ML_DSA_65)); + } + + @Test + public void mlDsa44VerifyRoundTrip() { + MLDSA44 keypair = new MLDSA44(); + byte[] msg = "registry-44".getBytes(); + byte[] sig = keypair.sign(msg); + assertTrue(PqSignatureRegistry.verify( + SignatureScheme.ML_DSA_44, keypair.getPublicKey(), msg, sig)); + } + + @Test + public void mlDsa65VerifyRoundTrip() { + MLDSA65 keypair = new MLDSA65(); + byte[] msg = "registry-65".getBytes(); + byte[] sig = keypair.sign(msg); + assertTrue(PqSignatureRegistry.verify( + SignatureScheme.ML_DSA_65, keypair.getPublicKey(), msg, sig)); + } + + @Test + public void ecdsaNotRegistered() { + assertFalse(PqSignatureRegistry.contains(SignatureScheme.ECDSA_SECP256K1)); + try { + PqSignatureRegistry.getPublicKeyLength(SignatureScheme.ECDSA_SECP256K1); + fail("expected IllegalArgumentException for ECDSA_SECP256K1"); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("ECDSA_SECP256K1")); + } + } + + @Test + public void sm2NotRegistered() { + assertFalse(PqSignatureRegistry.contains(SignatureScheme.SM2_SM3)); + try { + PqSignatureRegistry.verify( + SignatureScheme.SM2_SM3, new byte[0], new byte[0], new byte[0]); + fail("expected IllegalArgumentException for SM2_SM3"); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("SM2_SM3")); + } + } + + @Test + public void unknownSchemeRejected() { + assertFalse(PqSignatureRegistry.contains(SignatureScheme.UNKNOWN_SIG_SCHEME)); + try { + PqSignatureRegistry.getSignatureLength(SignatureScheme.UNKNOWN_SIG_SCHEME); + fail("expected IllegalArgumentException for UNKNOWN_SIG_SCHEME"); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("UNKNOWN_SIG_SCHEME")); + } + } +} diff --git a/framework/src/test/java/org/tron/common/crypto/pqc/PqcClient.java b/framework/src/test/java/org/tron/common/crypto/pqc/PqcClient.java new file mode 100644 index 00000000000..66cc87533d7 --- /dev/null +++ b/framework/src/test/java/org/tron/common/crypto/pqc/PqcClient.java @@ -0,0 +1,139 @@ +package org.tron.common.crypto.pqc; + +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.utils.ByteArray; +import org.tron.protos.Protocol.AuthWitness; +import org.tron.protos.Protocol.Block; +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 PqcWitnessNode} and broadcasts an ML-DSA-44 + * signed transfer transaction. + * + * The keypair is derived from the same fixed seed used by PqcWitnessNode, 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.PqcWitnessNode + * Terminal 2 — broadcast a PQC transaction: + * ./gradlew :framework:run -PmainClass=org.tron.common.crypto.pqc.PqcClient + * + * Optional JVM args: + * -Dpqc.host=localhost (default: localhost) + * -Dpqc.port=50051 (default: 50051) + */ +public class PqcClient { + + 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 { + // ── 1. Derive user keypair from same fixed seed as PqcWitnessNode ───── + byte[] userSeed = new byte[32]; + Arrays.fill(userSeed, (byte) 0x02); + MLDSA44 userKp = new MLDSA44(userSeed); + + byte[] userPub = userKp.getPublicKey(); + byte[] userPriv = userKp.getPrivateKey(); + byte[] signerAddr = MLDSA44.computeAddress(userPub); + byte[] ownerAddr = PqcWitnessNode.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 ML-DSA-44 auth_witness ────────────────────────── + byte[] txId = sha256(rawData.toByteArray()); + byte[] digest = PqAuthDigest.tx(txId, 0, signerAddr); + byte[] sig = MLDSA44.sign(userPriv, digest); + + Transaction signedTx = tx.toBuilder() + .addAuthWitness(AuthWitness.newBuilder() + .setSignerAddress(ByteString.copyFrom(signerAddr)) + .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/PqcWitnessNode.java b/framework/src/test/java/org/tron/common/crypto/pqc/PqcWitnessNode.java new file mode 100644 index 00000000000..4c418cb96c3 --- /dev/null +++ b/framework/src/test/java/org/tron/common/crypto/pqc/PqcWitnessNode.java @@ -0,0 +1,158 @@ +package org.tron.common.crypto.pqc; + +import com.google.protobuf.ByteString; +import java.io.File; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Arrays; +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.utils.ByteArray; +import org.tron.common.utils.LocalWitnesses; +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.Permission; +import org.tron.protos.Protocol.Permission.PermissionType; +import org.tron.protos.Protocol.SignatureScheme; + +/** + * Demo witness node with ML-DSA-65 block production. + * + * Starts an in-process TRON node configured with a PQC witness keypair and + * a user account that holds an ML-DSA-44 owner permission — ready to receive + * transactions from {@link PqcClient}. + * + * Keypairs are derived from fixed seeds so PqcClient 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.PqcWitnessNode + * Terminal 2 — broadcast a PQC transaction: + * ./gradlew :framework:run -PmainClass=org.tron.common.crypto.pqc.PqcClient + */ +public class PqcWitnessNode { + + /** Fixed seed for the ML-DSA-65 witness keypair (shared with PqcClient for derivation). */ + static final byte[] WITNESS_SEED = filledSeed(0x01); + /** Fixed seed for the ML-DSA-44 user keypair (shared with PqcClient for derivation). */ + static final byte[] USER_SEED = filledSeed(0x02); + + /** gRPC port the node listens on. */ + static final int GRPC_PORT = 50051; + + /** 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 { + // ── 1. Derive deterministic keypairs ────────────────────────────────── + MLDSA65 witnessKp = new MLDSA65(WITNESS_SEED); + MLDSA44 userKp = new MLDSA44(USER_SEED); + + byte[] witnessPub = witnessKp.getPublicKey(); + byte[] witnessPriv = witnessKp.getPrivateKey(); + byte[] witnessAddr = MLDSA65.computeAddress(witnessPub); + ByteString witnessAddrBs = ByteString.copyFrom(witnessAddr); + + byte[] userPub = userKp.getPublicKey(); + byte[] signerAddr = MLDSA44.computeAddress(userPub); + ByteString signerAddrBs = ByteString.copyFrom(signerAddr); + + System.out.println("=== PQC Witness Node ==="); + System.out.println("Witness address (ML-DSA-65): " + 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); + + // ── 2. Configure node ───────────────────────────────────────────────── + File dbDir = Files.createTempDirectory("pqc-node-").toFile(); + dbDir.deleteOnExit(); + + Args.setParam(new String[]{"--output-directory", dbDir.getAbsolutePath(), "-w"}, + "config-test.conf"); + Args.getInstance().setRpcEnable(true); + Args.getInstance().setFullNodeHttpEnable(true); + Args.getInstance().setRpcPort(GRPC_PORT); + Args.getInstance().setP2pDisable(true); + Args.getInstance().setNeedSyncCheck(false); + Args.getInstance().setMinEffectiveConnection(0); + + // ── 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. Enable PQ features ───────────────────────────────────────────── + db.getDynamicPropertiesStore().saveAllowMlDsa(1L); + db.getDynamicPropertiesStore().saveAllowMultiSign(1L); + + // ── 5. Register witness account with ML-DSA-65 witness permission ───── + Permission witnessPerm = Permission.newBuilder() + .setType(PermissionType.Witness) + .setId(1).setPermissionName("witness").setThreshold(1) + .addKeys(Key.newBuilder() + .setAddress(witnessAddrBs).setWeight(1) + .setScheme(SignatureScheme.ML_DSA_65) + .setPublicKey(ByteString.copyFrom(witnessPub))) + .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); + + // ── 6. Register user account with ML-DSA-44 owner permission ───────── + Permission userOwnerPerm = Permission.newBuilder() + .setType(PermissionType.Owner).setPermissionName("owner").setThreshold(1) + .addKeys(Key.newBuilder() + .setAddress(signerAddrBs).setWeight(1) + .setScheme(SignatureScheme.ML_DSA_44) + .setPublicKey(ByteString.copyFrom(userPub))) + .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); + + // ── 7. Start consensus (DposTask auto-produces blocks) ──────────────── + context.getBean(ConsensusService.class).start(); + + // ── 8. Start gRPC server ────────────────────────────────────────────── + app.startup(); + + System.out.println("\nNode is running. Send Ctrl-C to stop."); + System.out.println("Run PqcClient in another terminal to broadcast a PQC transaction.\n"); + + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + System.out.println("Shutting down..."); + context.close(); + Args.clearParam(); + })); + + Thread.currentThread().join(); // block until Ctrl-C + } + + private static byte[] filledSeed(int value) { + byte[] seed = new byte[32]; + Arrays.fill(seed, (byte) value); + return seed; + } +} 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 839b77b1efe..43dc4dc9de1 100644 --- a/framework/src/test/java/org/tron/core/capsule/BlockCapsulePqTest.java +++ b/framework/src/test/java/org/tron/core/capsule/BlockCapsulePqTest.java @@ -1,15 +1,8 @@ package org.tron.core.capsule; import com.google.protobuf.ByteString; -import java.security.SecureRandom; -import org.bouncycastle.crypto.AsymmetricCipherKeyPair; -import org.bouncycastle.pqc.crypto.mldsa.MLDSAKeyGenerationParameters; -import org.bouncycastle.pqc.crypto.mldsa.MLDSAKeyPairGenerator; -import org.bouncycastle.pqc.crypto.mldsa.MLDSAParameters; -import org.bouncycastle.pqc.crypto.mldsa.MLDSAPrivateKeyParameters; -import org.bouncycastle.pqc.crypto.mldsa.MLDSAPublicKeyParameters; -import org.bouncycastle.pqc.crypto.mldsa.MLDSASigner; import org.junit.Assert; +import org.tron.common.crypto.pqc.MLDSA65; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; @@ -33,8 +26,7 @@ public class BlockCapsulePqTest extends BaseTest { private ECKey witnessKey; private byte[] witnessAddress; - private MLDSAPrivateKeyParameters pqSk; - private MLDSAPublicKeyParameters pqPk; + private MLDSA65 pqKeypair; @BeforeClass public static void init() { @@ -45,11 +37,7 @@ public static void init() { public void setUp() { witnessKey = new ECKey(); witnessAddress = witnessKey.getAddress(); - MLDSAKeyPairGenerator gen = new MLDSAKeyPairGenerator(); - gen.init(new MLDSAKeyGenerationParameters(new SecureRandom(), MLDSAParameters.ml_dsa_65)); - AsymmetricCipherKeyPair kp = gen.generateKeyPair(); - pqPk = (MLDSAPublicKeyParameters) kp.getPublic(); - pqSk = (MLDSAPrivateKeyParameters) kp.getPrivate(); + pqKeypair = new MLDSA65(); } private AccountCapsule buildWitnessAccount(SignatureScheme scheme) { @@ -58,7 +46,7 @@ private AccountCapsule buildWitnessAccount(SignatureScheme scheme) { .setWeight(1) .setScheme(scheme); if (scheme == SignatureScheme.ML_DSA_65) { - kb.setPublicKey(ByteString.copyFrom(pqPk.getEncoded())); + kb.setPublicKey(ByteString.copyFrom(pqKeypair.getPublicKey())); } Permission witnessPerm = Permission.newBuilder() .setType(PermissionType.Witness) @@ -97,14 +85,7 @@ private BlockCapsule buildUnsignedBlock(byte[] parentHash) { } private byte[] signPq(byte[] message) { - MLDSASigner signer = new MLDSASigner(); - signer.init(true, pqSk); - signer.update(message, 0, message.length); - try { - return signer.generateSignature(); - } catch (Exception e) { - throw new AssertionError(e); - } + return MLDSA65.sign(pqKeypair.getPrivateKey(), message); } @Test 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 0e4c67aad45..f3ee6e3e826 100644 --- a/framework/src/test/java/org/tron/core/capsule/TransactionCapsuleTest.java +++ b/framework/src/test/java/org/tron/core/capsule/TransactionCapsuleTest.java @@ -6,16 +6,11 @@ import com.google.protobuf.Any; import com.google.protobuf.ByteString; -import java.security.SecureRandom; import lombok.extern.slf4j.Slf4j; -import org.bouncycastle.crypto.AsymmetricCipherKeyPair; -import org.bouncycastle.pqc.crypto.mldsa.MLDSAKeyGenerationParameters; -import org.bouncycastle.pqc.crypto.mldsa.MLDSAKeyPairGenerator; -import org.bouncycastle.pqc.crypto.mldsa.MLDSAParameters; -import org.bouncycastle.pqc.crypto.mldsa.MLDSAPrivateKeyParameters; -import org.bouncycastle.pqc.crypto.mldsa.MLDSAPublicKeyParameters; -import org.bouncycastle.pqc.crypto.mldsa.MLDSASigner; import org.junit.Assert; +import org.tron.common.crypto.ECKey; +import org.tron.common.crypto.pqc.MLDSA44; +import org.tron.common.crypto.pqc.MLDSA65; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; @@ -40,6 +35,7 @@ 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 { @@ -96,17 +92,8 @@ public void testRemoveRedundantRet() { private static final String PQ_SIGNER_HEX = "41548794500882809695a8a687866e76d4271a1abc"; - private AsymmetricCipherKeyPair newMlDsa65KeyPair() { - MLDSAKeyPairGenerator gen = new MLDSAKeyPairGenerator(); - gen.init(new MLDSAKeyGenerationParameters(new SecureRandom(), MLDSAParameters.ml_dsa_65)); - return gen.generateKeyPair(); - } - - private byte[] sign(MLDSAPrivateKeyParameters sk, byte[] msg) throws Exception { - MLDSASigner s = new MLDSASigner(); - s.init(true, sk); - s.update(msg, 0, msg.length); - return s.generateSignature(); + private byte[] sign(MLDSA44 kp, byte[] msg) { + return MLDSA44.sign(kp.getPrivateKey(), msg); } private Transaction buildTransferTx(String ownerHex, int permissionId) { @@ -124,13 +111,14 @@ private Transaction buildTransferTx(String ownerHex, int permissionId) { return Transaction.newBuilder().setRawData(rawData).build(); } - private void putAccountWithMlDsa65Permission(String ownerHex, byte[] pqPublicKey) { + private void putAccountWithPqPermission( + String ownerHex, byte[] pqPublicKey, SignatureScheme scheme) { byte[] addr = ByteArray.fromHexString(ownerHex); byte[] signerAddr = ByteArray.fromHexString(PQ_SIGNER_HEX); Key pqKey = Key.newBuilder() .setAddress(ByteString.copyFrom(signerAddr)) .setWeight(1L) - .setScheme(SignatureScheme.ML_DSA_65) + .setScheme(scheme) .setPublicKey(ByteString.copyFrom(pqPublicKey)) .build(); Permission owner = Permission.newBuilder() @@ -151,7 +139,7 @@ public void authWitnessBeforeActivationRejected() { Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0).toBuilder() .addAuthWitness(AuthWitness.newBuilder() .setSignerAddress(ByteString.copyFrom(ByteArray.fromHexString(PQ_SIGNER_HEX))) - .setSignature(ByteString.copyFrom(new byte[3309])) + .setSignature(ByteString.copyFrom(new byte[2420])) .build()) .build(); TransactionCapsule cap = new TransactionCapsule(tx); @@ -171,7 +159,7 @@ public void signatureAndAuthWitnessAreMutuallyExclusive() { .addSignature(ByteString.copyFrom(new byte[65])) .addAuthWitness(AuthWitness.newBuilder() .setSignerAddress(ByteString.copyFrom(ByteArray.fromHexString(PQ_SIGNER_HEX))) - .setSignature(ByteString.copyFrom(new byte[3309])) + .setSignature(ByteString.copyFrom(new byte[2420])) .build()) .build(); TransactionCapsule cap = new TransactionCapsule(tx); @@ -187,16 +175,14 @@ public void signatureAndAuthWitnessAreMutuallyExclusive() { @Test public void validAuthWitnessAccepted() throws Exception { dbManager.getDynamicPropertiesStore().saveAllowMlDsa(1L); - AsymmetricCipherKeyPair kp = newMlDsa65KeyPair(); - byte[] pkBytes = ((MLDSAPublicKeyParameters) kp.getPublic()).getEncoded(); - MLDSAPrivateKeyParameters sk = (MLDSAPrivateKeyParameters) kp.getPrivate(); - putAccountWithMlDsa65Permission(PQ_OWNER_HEX, pkBytes); + MLDSA44 kp = new MLDSA44(); + putAccountWithPqPermission(PQ_OWNER_HEX, kp.getPublicKey(), SignatureScheme.ML_DSA_44); Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); byte[] txid = Sha256Hash.of(true, tx.getRawData().toByteArray()).getBytes(); byte[] signerAddr = ByteArray.fromHexString(PQ_SIGNER_HEX); byte[] digest = PqAuthDigest.tx(txid, 0, signerAddr); - byte[] sig = sign(sk, digest); + byte[] sig = sign(kp, digest); Transaction signed = tx.toBuilder() .addAuthWitness(AuthWitness.newBuilder() @@ -212,16 +198,14 @@ public void validAuthWitnessAccepted() throws Exception { @Test public void duplicateSignerRejected() throws Exception { dbManager.getDynamicPropertiesStore().saveAllowMlDsa(1L); - AsymmetricCipherKeyPair kp = newMlDsa65KeyPair(); - byte[] pkBytes = ((MLDSAPublicKeyParameters) kp.getPublic()).getEncoded(); - MLDSAPrivateKeyParameters sk = (MLDSAPrivateKeyParameters) kp.getPrivate(); - putAccountWithMlDsa65Permission(PQ_OWNER_HEX, pkBytes); + MLDSA44 kp = new MLDSA44(); + putAccountWithPqPermission(PQ_OWNER_HEX, kp.getPublicKey(), SignatureScheme.ML_DSA_44); Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); byte[] txid = Sha256Hash.of(true, tx.getRawData().toByteArray()).getBytes(); byte[] signerAddr = ByteArray.fromHexString(PQ_SIGNER_HEX); byte[] digest = PqAuthDigest.tx(txid, 0, signerAddr); - byte[] sig = sign(sk, digest); + byte[] sig = sign(kp, digest); AuthWitness aw = AuthWitness.newBuilder() .setSignerAddress(ByteString.copyFrom(signerAddr)) .setSignature(ByteString.copyFrom(sig)) @@ -241,16 +225,14 @@ public void duplicateSignerRejected() throws Exception { @Test public void tamperedAuthWitnessRejected() throws Exception { dbManager.getDynamicPropertiesStore().saveAllowMlDsa(1L); - AsymmetricCipherKeyPair kp = newMlDsa65KeyPair(); - byte[] pkBytes = ((MLDSAPublicKeyParameters) kp.getPublic()).getEncoded(); - MLDSAPrivateKeyParameters sk = (MLDSAPrivateKeyParameters) kp.getPrivate(); - putAccountWithMlDsa65Permission(PQ_OWNER_HEX, pkBytes); + MLDSA44 kp = new MLDSA44(); + putAccountWithPqPermission(PQ_OWNER_HEX, kp.getPublicKey(), SignatureScheme.ML_DSA_44); Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); byte[] txid = Sha256Hash.of(true, tx.getRawData().toByteArray()).getBytes(); byte[] signerAddr = ByteArray.fromHexString(PQ_SIGNER_HEX); byte[] digest = PqAuthDigest.tx(txid, 0, signerAddr); - byte[] sig = sign(sk, digest); + byte[] sig = sign(kp, digest); sig[0] ^= 0x01; Transaction signed = tx.toBuilder() @@ -272,17 +254,15 @@ public void tamperedAuthWitnessRejected() throws Exception { @Test public void signerNotInPermissionRejected() throws Exception { dbManager.getDynamicPropertiesStore().saveAllowMlDsa(1L); - AsymmetricCipherKeyPair kp = newMlDsa65KeyPair(); - byte[] pkBytes = ((MLDSAPublicKeyParameters) kp.getPublic()).getEncoded(); - MLDSAPrivateKeyParameters sk = (MLDSAPrivateKeyParameters) kp.getPrivate(); - putAccountWithMlDsa65Permission(PQ_OWNER_HEX, pkBytes); + MLDSA44 kp = new MLDSA44(); + putAccountWithPqPermission(PQ_OWNER_HEX, kp.getPublicKey(), SignatureScheme.ML_DSA_44); Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); byte[] txid = Sha256Hash.of(true, tx.getRawData().toByteArray()).getBytes(); byte[] otherSigner = ByteArray.fromHexString( "41BCE23C7D683B889326F762DDA2223A861EDA2E5C"); byte[] digest = PqAuthDigest.tx(txid, 0, otherSigner); - byte[] sig = sign(sk, digest); + byte[] sig = sign(kp, digest); Transaction signed = tx.toBuilder() .addAuthWitness(AuthWitness.newBuilder() @@ -299,4 +279,133 @@ public void signerNotInPermissionRejected() throws Exception { Assert.assertTrue(e.getMessage().contains("not in permission")); } } + + /** + * TRC20 transfer(address,uint256) call data: 4-byte selector + 32-byte address + 32-byte amount. + */ + private Transaction buildTrc20TransferTx(String ownerHex, int permissionId) { + // transfer(address,uint256) selector + byte[] selector = ByteArray.fromHexString("a9059cbb"); + byte[] toAddrPadded = new byte[32]; + byte[] toRaw = ByteArray.fromHexString(PQ_SIGNER_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] for ECKey, ML-DSA-44, ML-DSA-65. */ + private long[][] measureSizes(Transaction baseTx) { + final long blockLimit = 2_000_000L; + byte[] signerAddr = ByteArray.fromHexString(PQ_SIGNER_HEX); + + // 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(); + + // ML-DSA-44: 2420-byte signature in auth_witness + MLDSA44 kp44 = new MLDSA44(); + byte[] txid44 = Sha256Hash.of(true, baseTx.getRawData().toByteArray()).getBytes(); + byte[] sig44 = MLDSA44.sign(kp44.getPrivateKey(), PqAuthDigest.tx(txid44, 0, signerAddr)); + Transaction tx44 = baseTx.toBuilder() + .addAuthWitness(AuthWitness.newBuilder() + .setSignerAddress(ByteString.copyFrom(signerAddr)) + .setSignature(ByteString.copyFrom(sig44)) + .build()) + .build(); + TransactionCapsule cap44 = new TransactionCapsule(tx44); + long d44Serial = tx44.toByteArray().length; + long d44Pack = cap44.computeTrxSizeForBlockMessage(); + + // ML-DSA-65: 3309-byte signature in auth_witness + MLDSA65 kp65 = new MLDSA65(); + byte[] txid65 = Sha256Hash.of(true, baseTx.getRawData().toByteArray()).getBytes(); + byte[] sig65 = MLDSA65.sign(kp65.getPrivateKey(), PqAuthDigest.tx(txid65, 0, signerAddr)); + Transaction tx65 = baseTx.toBuilder() + .addAuthWitness(AuthWitness.newBuilder() + .setSignerAddress(ByteString.copyFrom(signerAddr)) + .setSignature(ByteString.copyFrom(sig65)) + .build()) + .build(); + TransactionCapsule cap65 = new TransactionCapsule(tx65); + long d65Serial = tx65.toByteArray().length; + long d65Pack = cap65.computeTrxSizeForBlockMessage(); + + return new long[][]{ + {ecSerial, ecPack, blockLimit / ecPack}, + {d44Serial, d44Pack, blockLimit / d44Pack}, + {d65Serial, d65Pack, blockLimit / d65Pack} + }; + } + + @Test + public void transactionSizeComparisonByScheme() { + long[][] trx = measureSizes(buildTransferTx(PQ_OWNER_HEX, 0)); + long[][] trc20 = measureSizes(buildTrc20TransferTx(PQ_OWNER_HEX, 0)); + + String[] labels = {"ECKey (ECDSA)", "ML-DSA-44", "ML-DSA-65"}; + System.out.println("=== TRX transfer ==="); + for (int i = 0; i < 3; 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 < 3; 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]); + } + + for (int i = 0; i < 2; i++) { + Assert.assertTrue(trx[i + 1][0] > trx[i][0]); + Assert.assertTrue(trc20[i + 1][0] > trc20[i][0]); + Assert.assertTrue(trx[i + 1][2] < trx[i][2]); + Assert.assertTrue(trc20[i + 1][2] < trc20[i][2]); + } + } + + @Test + public void mlDsa65AuthWitnessAlsoAccepted() throws Exception { + dbManager.getDynamicPropertiesStore().saveAllowMlDsa(1L); + MLDSA65 kp = new MLDSA65(); + putAccountWithPqPermission(PQ_OWNER_HEX, kp.getPublicKey(), SignatureScheme.ML_DSA_65); + + Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); + byte[] txid = Sha256Hash.of(true, tx.getRawData().toByteArray()).getBytes(); + byte[] signerAddr = ByteArray.fromHexString(PQ_SIGNER_HEX); + byte[] digest = PqAuthDigest.tx(txid, 0, signerAddr); + byte[] sig = MLDSA65.sign(kp.getPrivateKey(), digest); + + Transaction signed = tx.toBuilder() + .addAuthWitness(AuthWitness.newBuilder() + .setSignerAddress(ByteString.copyFrom(signerAddr)) + .setSignature(ByteString.copyFrom(sig)) + .build()) + .build(); + TransactionCapsule cap = new TransactionCapsule(signed); + Assert.assertTrue(cap.validatePubSignature(dbManager.getAccountStore(), + dbManager.getDynamicPropertiesStore())); + } } \ No newline at end of file diff --git a/framework/src/test/resources/config-test.conf b/framework/src/test/resources/config-test.conf index 71e93f84db5..a3c98f2b3a7 100644 --- a/framework/src/test/resources/config-test.conf +++ b/framework/src/test/resources/config-test.conf @@ -348,12 +348,15 @@ 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_seed_pq = [ + "0101010101010101010101010101010101010101010101010101010101010101" + ] + block = { needSyncCheck = true # first node : false, other : true } @@ -386,4 +389,4 @@ node.dynamicConfig.enable = true event.subscribe = { enable = false } -node.dynamicConfig.checkInterval = 0 \ No newline at end of file +node.dynamicConfig.checkInterval = 0 From c5a7bac6bc39392cb3c2f22fa9291deb432934b8 Mon Sep 17 00:00:00 2001 From: federico Date: Fri, 24 Apr 2026 18:34:18 +0800 Subject: [PATCH 03/12] chore(crypto): extend pq demo with fullnode sync --- .../src/main/java/org/tron/core/Wallet.java | 5 + framework/src/main/resources/config.conf | 10 +- .../common/crypto/pqc/program/PqFullNode.java | 118 ++++++++++++++++++ .../crypto/pqc/{ => program}/PqcClient.java | 14 ++- .../pqc/{ => program}/PqcWitnessNode.java | 87 ++++++++----- 5 files changed, 195 insertions(+), 39 deletions(-) create mode 100644 framework/src/test/java/org/tron/common/crypto/pqc/program/PqFullNode.java rename framework/src/test/java/org/tron/common/crypto/pqc/{ => program}/PqcClient.java (92%) rename framework/src/test/java/org/tron/common/crypto/pqc/{ => program}/PqcWitnessNode.java (75%) diff --git a/framework/src/main/java/org/tron/core/Wallet.java b/framework/src/main/java/org/tron/core/Wallet.java index 39e8f06c281..6079294e193 100755 --- a/framework/src/main/java/org/tron/core/Wallet.java +++ b/framework/src/main/java/org/tron/core/Wallet.java @@ -1514,6 +1514,11 @@ public Protocol.ChainParameters getChainParameters() { .setValue(dbManager.getDynamicPropertiesStore().getAllowTvmOsaka()) .build()); + builder.addChainParameter(Protocol.ChainParameters.ChainParameter.newBuilder() + .setKey("getAllowMlDsa") + .setValue(dbManager.getDynamicPropertiesStore().getAllowMlDsa()) + .build()); + return builder.build(); } diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf index a3fa7c11019..a011a882c97 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -673,13 +673,9 @@ localwitness = [ # "localwitnesskeystore.json" # ] -# Post-quantum (ML-DSA-65) witness signing seed. Required once the on-chain witness -# Permission has been upgraded to scheme = ML_DSA_65 and ALLOW_ML_DSA = 1 is active. -# Each entry is a 32-byte seed encoded as a hex string (64 hex chars). -# The ML-DSA-65 keypair is deterministically derived from the seed using FIPS 204. -# The node uses this key to produce the BlockHeader.witness_auth field during Dual-Sign -# block production. If the witness Permission is still legacy (scheme = UNKNOWN_SIG_SCHEME), -# leave this empty; the node will use `localwitness` / `localwitnesskeystore` as before. +# ML-DSA-65 witness signing seed (FIPS 204), 32-byte hex. Used only after +# ALLOW_ML_DSA = 1 and the witness Permission is upgraded to scheme = ML_DSA_65. +# MUST be produced by a CSPRNG; the value below is an example, never use in prod. # localwitness_seed_pq = [ # "0101010101010101010101010101010101010101010101010101010101010101" # ] 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..738064a529c --- /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.MLDSA44; +import org.tron.common.crypto.pqc.MLDSA65; +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 PqcWitnessNode} via P2P and syncs PQ-signed blocks. + * + * Both nodes share the same deterministic PQ genesis pre-state (witness account with an + * ML-DSA-65 witness permission + demo user account with an ML-DSA-44 owner permission), + * installed via {@link PqcWitnessNode#installPqGenesisState}. Once the witness produces + * a block it is broadcast over P2P; this node validates {@code BlockHeader.witness_auth} + * 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.PqcWitnessNode + * 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: PqcWitnessNode.P2P_PORT) + */ +public class PqFullNode { + + /** gRPC port (different from PqcWitnessNode so both can run on one host). */ + static final int GRPC_PORT = 50052; + /** Full-node HTTP port (different from PqcWitnessNode). */ + static final int HTTP_PORT = 8091; + /** P2P listen port (different from PqcWitnessNode). */ + 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(PqcWitnessNode.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 PqcWitnessNode ────── + MLDSA65 witnessKp = new MLDSA65(PqcWitnessNode.WITNESS_SEED); + MLDSA44 userKp = new MLDSA44(PqcWitnessNode.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(MLDSA65.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. + Args.getInstance().getSeedNode().setAddressList(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 witness_auth would fail to validate because + // this node wouldn't know the witness's ML-DSA-65 public key. + PqcWitnessNode.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/PqcClient.java b/framework/src/test/java/org/tron/common/crypto/pqc/program/PqcClient.java similarity index 92% rename from framework/src/test/java/org/tron/common/crypto/pqc/PqcClient.java rename to framework/src/test/java/org/tron/common/crypto/pqc/program/PqcClient.java index 66cc87533d7..79b8c8e4b89 100644 --- a/framework/src/test/java/org/tron/common/crypto/pqc/PqcClient.java +++ b/framework/src/test/java/org/tron/common/crypto/pqc/program/PqcClient.java @@ -1,4 +1,4 @@ -package org.tron.common.crypto.pqc; +package org.tron.common.crypto.pqc.program; import com.google.protobuf.Any; import com.google.protobuf.ByteString; @@ -12,6 +12,8 @@ import org.tron.api.GrpcAPI.Return; import org.tron.api.WalletGrpc; import org.tron.api.WalletGrpc.WalletBlockingStub; +import org.tron.common.crypto.pqc.MLDSA44; +import org.tron.common.crypto.pqc.PqAuthDigest; import org.tron.common.utils.ByteArray; import org.tron.protos.Protocol.AuthWitness; import org.tron.protos.Protocol.Block; @@ -28,9 +30,9 @@ * * Usage: * Terminal 1 — start the witness node first: - * ./gradlew :framework:run -PmainClass=org.tron.common.crypto.pqc.PqcWitnessNode + * ./gradlew :framework:run -PmainClass=org.tron.common.crypto.pqc.program.PqcWitnessNode * Terminal 2 — broadcast a PQC transaction: - * ./gradlew :framework:run -PmainClass=org.tron.common.crypto.pqc.PqcClient + * ./gradlew :framework:run -PmainClass=org.tron.common.crypto.pqc.program.PqcClient * * Optional JVM args: * -Dpqc.host=localhost (default: localhost) @@ -48,6 +50,12 @@ public class PqcClient { 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 PqcWitnessNode ───── byte[] userSeed = new byte[32]; Arrays.fill(userSeed, (byte) 0x02); diff --git a/framework/src/test/java/org/tron/common/crypto/pqc/PqcWitnessNode.java b/framework/src/test/java/org/tron/common/crypto/pqc/program/PqcWitnessNode.java similarity index 75% rename from framework/src/test/java/org/tron/common/crypto/pqc/PqcWitnessNode.java rename to framework/src/test/java/org/tron/common/crypto/pqc/program/PqcWitnessNode.java index 4c418cb96c3..01f0145d037 100644 --- a/framework/src/test/java/org/tron/common/crypto/pqc/PqcWitnessNode.java +++ b/framework/src/test/java/org/tron/common/crypto/pqc/program/PqcWitnessNode.java @@ -1,4 +1,4 @@ -package org.tron.common.crypto.pqc; +package org.tron.common.crypto.pqc.program; import com.google.protobuf.ByteString; import java.io.File; @@ -9,8 +9,9 @@ import org.tron.common.application.Application; import org.tron.common.application.ApplicationFactory; import org.tron.common.application.TronApplicationContext; +import org.tron.common.crypto.pqc.MLDSA44; +import org.tron.common.crypto.pqc.MLDSA65; import org.tron.common.utils.ByteArray; -import org.tron.common.utils.LocalWitnesses; import org.tron.core.ChainBaseManager; import org.tron.core.capsule.AccountCapsule; import org.tron.core.capsule.WitnessCapsule; @@ -37,9 +38,9 @@ * * Usage: * Terminal 1 — start this node: - * ./gradlew :framework:run -PmainClass=org.tron.common.crypto.pqc.PqcWitnessNode + * ./gradlew :framework:run -PmainClass=org.tron.common.crypto.pqc.program.PqcWitnessNode * Terminal 2 — broadcast a PQC transaction: - * ./gradlew :framework:run -PmainClass=org.tron.common.crypto.pqc.PqcClient + * ./gradlew :framework:run -PmainClass=org.tron.common.crypto.pqc.program.PqcClient */ public class PqcWitnessNode { @@ -51,29 +52,39 @@ public class PqcWitnessNode { /** 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 ────────────────────────────────── MLDSA65 witnessKp = new MLDSA65(WITNESS_SEED); MLDSA44 userKp = new MLDSA44(USER_SEED); byte[] witnessPub = witnessKp.getPublicKey(); - byte[] witnessPriv = witnessKp.getPrivateKey(); byte[] witnessAddr = MLDSA65.computeAddress(witnessPub); - ByteString witnessAddrBs = ByteString.copyFrom(witnessAddr); - byte[] userPub = userKp.getPublicKey(); byte[] signerAddr = MLDSA44.computeAddress(userPub); - ByteString signerAddrBs = ByteString.copyFrom(signerAddr); System.out.println("=== PQC Witness Node ==="); System.out.println("Witness address (ML-DSA-65): " + 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(); @@ -83,10 +94,12 @@ public static void main(String[] args) throws Exception { "config-test.conf"); Args.getInstance().setRpcEnable(true); Args.getInstance().setFullNodeHttpEnable(true); + Args.getInstance().setFullNodeHttpPort(HTTP_PORT); Args.getInstance().setRpcPort(GRPC_PORT); - Args.getInstance().setP2pDisable(true); + 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); @@ -94,11 +107,44 @@ public static void main(String[] args) throws Exception { Manager db = context.getBean(Manager.class); ChainBaseManager chain = context.getBean(ChainBaseManager.class); - // ── 4. Enable PQ features ───────────────────────────────────────────── + // ── 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 PqcClient 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 PqcWitnessNode 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 = MLDSA65.computeAddress(witnessPub); + ByteString witnessAddrBs = ByteString.copyFrom(witnessAddr); + byte[] signerAddr = MLDSA44.computeAddress(userPub); + ByteString signerAddrBs = ByteString.copyFrom(signerAddr); + + // Activate ML-DSA on the local chain params. db.getDynamicPropertiesStore().saveAllowMlDsa(1L); db.getDynamicPropertiesStore().saveAllowMultiSign(1L); - // ── 5. Register witness account with ML-DSA-65 witness permission ───── + // Witness account with ML-DSA-65 witness permission. Permission witnessPerm = Permission.newBuilder() .setType(PermissionType.Witness) .setId(1).setPermissionName("witness").setThreshold(1) @@ -118,7 +164,7 @@ public static void main(String[] args) throws Exception { chain.getWitnessScheduleStore().saveActiveWitnesses(new ArrayList<>()); chain.addWitness(witnessAddrBs); - // ── 6. Register user account with ML-DSA-44 owner permission ───────── + // User account with ML-DSA-44 owner permission. Permission userOwnerPerm = Permission.newBuilder() .setType(PermissionType.Owner).setPermissionName("owner").setThreshold(1) .addKeys(Key.newBuilder() @@ -131,23 +177,6 @@ public static void main(String[] args) throws Exception { userCapsule.setBalance(100_000_000L); // 100 TRX userCapsule.updatePermissions(userOwnerPerm, null, Collections.emptyList()); db.getAccountStore().put(USER_ADDR, userCapsule); - - // ── 7. Start consensus (DposTask auto-produces blocks) ──────────────── - context.getBean(ConsensusService.class).start(); - - // ── 8. Start gRPC server ────────────────────────────────────────────── - app.startup(); - - System.out.println("\nNode is running. Send Ctrl-C to stop."); - System.out.println("Run PqcClient in another terminal to broadcast a PQC transaction.\n"); - - Runtime.getRuntime().addShutdownHook(new Thread(() -> { - System.out.println("Shutting down..."); - context.close(); - Args.clearParam(); - })); - - Thread.currentThread().join(); // block until Ctrl-C } private static byte[] filledSeed(int value) { From a08de98249a5bc58ebd1fec6630088ce5f179079 Mon Sep 17 00:00:00 2001 From: federico Date: Sun, 26 Apr 2026 21:16:47 +0800 Subject: [PATCH 04/12] feat(crypto): configurable PQ witness scheme --- .../AccountPermissionUpdateActuator.java | 5 +- .../org/tron/common/utils/LocalWitnesses.java | 14 ++-- .../org/tron/core/capsule/BlockCapsule.java | 8 ++- .../crypto/pqc/PqSignatureRegistry.java | 65 +++++++++++++++---- .../java/org/tron/core/config/args/Args.java | 10 +++ .../org/tron/core/config/args/ConfigKey.java | 1 + .../tron/core/consensus/ConsensusService.java | 20 +++--- .../main/java/org/tron/core/db/Manager.java | 13 ++-- framework/src/main/resources/config.conf | 8 ++- .../common/crypto/pqc/program/PqFullNode.java | 9 ++- .../crypto/pqc/program/PqcWitnessNode.java | 17 +++-- .../AccountPermissionUpdateActuatorTest.java | 9 +-- framework/src/test/resources/config-test.conf | 1 + 13 files changed, 122 insertions(+), 58 deletions(-) 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 5f94917e50d..10abdffec4c 100644 --- a/actuator/src/main/java/org/tron/core/actuator/AccountPermissionUpdateActuator.java +++ b/actuator/src/main/java/org/tron/core/actuator/AccountPermissionUpdateActuator.java @@ -18,6 +18,7 @@ import org.tron.core.store.DynamicPropertiesStore; import org.tron.common.crypto.pqc.MLDSA44; import org.tron.common.crypto.pqc.MLDSA65; +import org.tron.common.crypto.pqc.PqSignatureRegistry; import org.tron.protos.Protocol.Key; import org.tron.protos.Protocol.Permission; import org.tron.protos.Protocol.Permission.PermissionType; @@ -294,9 +295,9 @@ private void validatePermissionScheme(Permission permission) throws ContractVali if (permission.getType() == PermissionType.Witness && first != SignatureScheme.UNKNOWN_SIG_SCHEME - && first != SignatureScheme.ML_DSA_65) { + && !PqSignatureRegistry.contains(first)) { throw new ContractValidateException( - "Witness permission only supports ML_DSA_65 or legacy scheme, got " + first); + "Witness permission only supports legacy or registered PQ schemes, got " + first); } } 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 039b44ba114..ad9246d5502 100644 --- a/chainbase/src/main/java/org/tron/common/utils/LocalWitnesses.java +++ b/chainbase/src/main/java/org/tron/common/utils/LocalWitnesses.java @@ -28,6 +28,7 @@ import org.tron.common.crypto.pqc.MLDSA65; import org.tron.core.config.Parameter.ChainConstant; import org.tron.core.exception.TronError; +import org.tron.protos.Protocol.SignatureScheme; @Slf4j(topic = "app") public class LocalWitnesses { @@ -35,10 +36,15 @@ public class LocalWitnesses { @Getter private List privateKeys = Lists.newArrayList(); - /** ML-DSA-65 seed values in hex format (64 hex chars = 32 bytes). */ + /** ML-DSA seed values in hex format (64 hex chars = 32 bytes). */ @Getter private List pqSeeds = Lists.newArrayList(); + /** PQ signature scheme used to derive keys from {@link #pqSeeds}. */ + @Getter + @Setter + private SignatureScheme pqScheme = SignatureScheme.ML_DSA_65; + @Setter @Getter private byte[] witnessAccountAddress; @@ -102,7 +108,7 @@ public void addPrivateKeys(String privateKey) { this.privateKeys.add(privateKey); } - /** ML-DSA-65 seed values (32 bytes = 64 hex chars). Keys are derived from seeds. */ + /** ML-DSA seed values (32 bytes = 64 hex chars). Keys are derived from seeds. */ public void setPqSeeds(final List pqSeeds) { if (CollectionUtils.isEmpty(pqSeeds)) { return; @@ -120,12 +126,12 @@ private static void validatePqSeed(String seed) { } int expectedHexLen = MLDSA65.SEED_LENGTH * 2; if (StringUtils.isBlank(hex) || hex.length() != expectedHexLen) { - throw new TronError(String.format("ML-DSA-65 seed must be %d hex chars, actual: %d", + throw new TronError(String.format("ML-DSA seed must be %d hex chars, actual: %d", expectedHexLen, StringUtils.isBlank(hex) ? 0 : hex.length()), TronError.ErrCode.WITNESS_INIT); } if (!StringUtil.isHexadecimal(hex)) { - throw new TronError("ML-DSA-65 seed must be hex string", + throw new TronError("ML-DSA seed must be hex string", TronError.ErrCode.WITNESS_INIT); } } 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 1b46cd6d8d8..b4c6e596e3d 100755 --- a/chainbase/src/main/java/org/tron/core/capsule/BlockCapsule.java +++ b/chainbase/src/main/java/org/tron/core/capsule/BlockCapsule.java @@ -227,9 +227,11 @@ private boolean validateLegacySignature(DynamicPropertiesStore dynamicProperties if (accountCapsule != null && accountCapsule.getInstance().hasWitnessPermission()) { Permission witnessPermission = accountCapsule.getInstance().getWitnessPermission(); if (witnessPermission.getKeysCount() > 0 - && witnessPermission.getKeys(0).getScheme() == SignatureScheme.ML_DSA_65) { + && PqSignatureRegistry.contains(witnessPermission.getKeys(0).getScheme())) { throw new ValidateSignatureException( - "witness permission requires ML_DSA_65 but witness_signature is legacy"); + "witness permission requires PQ scheme " + + witnessPermission.getKeys(0).getScheme() + + " but witness_signature is legacy"); } } } @@ -266,7 +268,7 @@ private boolean validateWitnessAuth(DynamicPropertiesStore dynamicPropertiesStor "witness_auth present but witness permission is not configured"); } SignatureScheme scheme = witnessPermission.getKeys(0).getScheme(); - if (scheme != SignatureScheme.ML_DSA_65) { + if (!PqSignatureRegistry.contains(scheme)) { throw new ValidateSignatureException( "witness permission scheme " + scheme + " is not allowed for block signing"); } diff --git a/crypto/src/main/java/org/tron/common/crypto/pqc/PqSignatureRegistry.java b/crypto/src/main/java/org/tron/common/crypto/pqc/PqSignatureRegistry.java index 284dba565e6..7843ad66b2d 100644 --- a/crypto/src/main/java/org/tron/common/crypto/pqc/PqSignatureRegistry.java +++ b/crypto/src/main/java/org/tron/common/crypto/pqc/PqSignatureRegistry.java @@ -6,29 +6,32 @@ import org.tron.protos.Protocol.SignatureScheme; /** - * Static dispatch table for post-quantum signature verification keyed by + * Static dispatch table for post-quantum signature schemes keyed by * {@link SignatureScheme}. Each entry binds a scheme to its public-key length, - * signature length, and a stateless verify function (typically a method - * reference to the concrete implementation's {@code verify}). Legacy + * signature length, and stateless sign/verify/keygen operations. Legacy * schemes (ECDSA secp256k1, SM2/SM3) are NOT registered — they flow through * the existing {@code SignInterface} path. */ public final class PqSignatureRegistry { - @FunctionalInterface - public interface Verifier { + /** 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); } private static final class SchemeInfo { final int publicKeyLength; final int signatureLength; - final Verifier verifier; + final SignatureOps ops; - SchemeInfo(int publicKeyLength, int signatureLength, Verifier verifier) { + SchemeInfo(int publicKeyLength, int signatureLength, SignatureOps ops) { this.publicKeyLength = publicKeyLength; this.signatureLength = signatureLength; - this.verifier = verifier; + this.ops = ops; } } @@ -37,9 +40,39 @@ private static final class SchemeInfo { static { EnumMap m = new EnumMap<>(SignatureScheme.class); m.put(SignatureScheme.ML_DSA_44, new SchemeInfo( - MLDSA44.PUBLIC_KEY_LENGTH, MLDSA44.SIGNATURE_LENGTH, MLDSA44::verify)); + MLDSA44.PUBLIC_KEY_LENGTH, MLDSA44.SIGNATURE_LENGTH, new SignatureOps() { + @Override + public byte[] sign(byte[] privateKey, byte[] message) { + return MLDSA44.sign(privateKey, message); + } + + @Override + public boolean verify(byte[] publicKey, byte[] message, byte[] signature) { + return MLDSA44.verify(publicKey, message, signature); + } + + @Override + public PqSignature fromSeed(byte[] seed) { + return new MLDSA44(seed); + } + })); m.put(SignatureScheme.ML_DSA_65, new SchemeInfo( - MLDSA65.PUBLIC_KEY_LENGTH, MLDSA65.SIGNATURE_LENGTH, MLDSA65::verify)); + MLDSA65.PUBLIC_KEY_LENGTH, MLDSA65.SIGNATURE_LENGTH, new SignatureOps() { + @Override + public byte[] sign(byte[] privateKey, byte[] message) { + return MLDSA65.sign(privateKey, message); + } + + @Override + public boolean verify(byte[] publicKey, byte[] message, byte[] signature) { + return MLDSA65.verify(publicKey, message, signature); + } + + @Override + public PqSignature fromSeed(byte[] seed) { + return new MLDSA65(seed); + } + })); SCHEMES = Collections.unmodifiableMap(m); } @@ -58,9 +91,17 @@ public static int getSignatureLength(SignatureScheme scheme) { return require(scheme).signatureLength; } + public static byte[] sign(SignatureScheme scheme, byte[] privateKey, byte[] message) { + return require(scheme).ops.sign(privateKey, message); + } + public static boolean verify( SignatureScheme scheme, byte[] publicKey, byte[] message, byte[] signature) { - return require(scheme).verifier.verify(publicKey, message, signature); + return require(scheme).ops.verify(publicKey, message, signature); + } + + public static PqSignature fromSeed(SignatureScheme scheme, byte[] seed) { + return require(scheme).ops.fromSeed(seed); } private static SchemeInfo require(SignatureScheme scheme) { @@ -71,4 +112,4 @@ private static SchemeInfo require(SignatureScheme scheme) { } return info; } -} +} \ No newline at end of file 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 e0716e9be31..3db9c81330f 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 @@ -78,6 +78,7 @@ import org.tron.p2p.dns.update.PublishConfig; import org.tron.p2p.utils.NetUtil; import org.tron.program.Version; +import org.tron.protos.Protocol.SignatureScheme; @Slf4j(topic = "app") @NoArgsConstructor @@ -1229,6 +1230,15 @@ private static void initLocalWitnesses(Config config, CLIParameter cmd) { List pqSeeds = config.getStringList(ConfigKey.LOCAL_WITNESS_SEED_PQ); if (!pqSeeds.isEmpty()) { localWitnesses.setPqSeeds(pqSeeds); + if (config.hasPath(ConfigKey.LOCAL_WITNESS_SEED_PQ_SCHEME)) { + String schemeName = config.getString(ConfigKey.LOCAL_WITNESS_SEED_PQ_SCHEME); + try { + localWitnesses.setPqScheme(SignatureScheme.valueOf(schemeName)); + } catch (IllegalArgumentException e) { + throw new TronError("invalid " + ConfigKey.LOCAL_WITNESS_SEED_PQ_SCHEME + + ": " + schemeName, TronError.ErrCode.WITNESS_INIT); + } + } byte[] address = WitnessInitializer.resolvePqWitnessAddress(witnessAddr); if (address != null) { localWitnesses.setWitnessAccountAddress(address); 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 ba71236a4c3..5f8def68eeb 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 @@ -14,6 +14,7 @@ private ConfigKey() { public static final String LOCAL_WITNESS_ACCOUNT_ADDRESS = "localWitnessAccountAddress"; public static final String LOCAL_WITNESS_KEYSTORE = "localwitnesskeystore"; public static final String LOCAL_WITNESS_SEED_PQ = "localwitness_seed_pq"; + public static final String LOCAL_WITNESS_SEED_PQ_SCHEME = "localwitness_seed_pq_scheme"; // crypto public static final String CRYPTO_ENGINE = "crypto.engine"; 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 1f8e057f381..cebae77fccc 100644 --- a/framework/src/main/java/org/tron/core/consensus/ConsensusService.java +++ b/framework/src/main/java/org/tron/core/consensus/ConsensusService.java @@ -10,7 +10,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.tron.common.crypto.SignUtils; -import org.tron.common.crypto.pqc.MLDSA65; +import org.tron.common.crypto.pqc.PqSignature; +import org.tron.common.crypto.pqc.PqSignatureRegistry; import org.tron.common.parameter.CommonParameter; import org.tron.consensus.Consensus; import org.tron.consensus.base.Param; @@ -18,6 +19,7 @@ import org.tron.core.capsule.WitnessCapsule; import org.tron.core.config.args.Args; import org.tron.core.store.WitnessStore; +import org.tron.protos.Protocol.SignatureScheme; @Slf4j(topic = "consensus") @Component @@ -79,12 +81,13 @@ public void start() { ByteString.copyFrom(witnessAddress)); miners.add(miner); } else if (pqSeeds.size() > 1) { + SignatureScheme scheme = Args.getLocalWitnesses().getPqScheme(); for (String seed : pqSeeds) { byte[] seedBytes = fromHexString(seed); - MLDSA65 keypair = new MLDSA65(seedBytes); + PqSignature keypair = PqSignatureRegistry.fromSeed(scheme, seedBytes); byte[] sk = keypair.getPrivateKey(); byte[] pk = keypair.getPublicKey(); - byte[] pqAddress = MLDSA65.computeAddress(pk); + byte[] pqAddress = keypair.getAddress(); WitnessCapsule witnessCapsule = witnessStore.get(pqAddress); if (null == witnessCapsule) { logger.warn("Witness {} is not in witnessStore.", Hex.toHexString(pqAddress)); @@ -94,8 +97,8 @@ public void start() { miner.setPqPrivateKey(sk); miner.setPqPublicKey(pk); miners.add(miner); - logger.info("Add ML-DSA witness (from seed): {}, size: {}", - Hex.toHexString(pqAddress), miners.size()); + logger.info("Add {} witness (from seed): {}, size: {}", + scheme, Hex.toHexString(pqAddress), miners.size()); } } else if (pqSeeds.size() == 1) { miners.add(buildPqOnlyMinerFromSeed(param, pqSeeds.get(0))); @@ -109,11 +112,12 @@ public void start() { } private Miner buildPqOnlyMinerFromSeed(Param param, String pqSeed) { + SignatureScheme scheme = Args.getLocalWitnesses().getPqScheme(); byte[] seedBytes = fromHexString(pqSeed); - MLDSA65 keypair = new MLDSA65(seedBytes); + PqSignature keypair = PqSignatureRegistry.fromSeed(scheme, seedBytes); byte[] sk = keypair.getPrivateKey(); byte[] pk = keypair.getPublicKey(); - byte[] pqAddress = MLDSA65.computeAddress(pk); + byte[] pqAddress = keypair.getAddress(); byte[] witnessAddress = Args.getLocalWitnesses().getWitnessAccountAddress(); if (witnessAddress == null || witnessAddress.length == 0) { witnessAddress = pqAddress; @@ -127,7 +131,7 @@ private Miner buildPqOnlyMinerFromSeed(Param param, String pqSeed) { ByteString.copyFrom(witnessAddress)); miner.setPqPrivateKey(sk); miner.setPqPublicKey(pk); - logger.info("Add ML-DSA witness (from seed): {}", Hex.toHexString(witnessAddress)); + logger.info("Add {} witness (from seed): {}", 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 33cf1852482..16b943c8ecf 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -54,8 +54,8 @@ import org.tron.common.args.GenesisBlock; import org.tron.common.bloom.Bloom; import org.tron.common.cron.CronExpression; -import org.tron.common.crypto.pqc.MLDSA65; import org.tron.common.crypto.pqc.PqAuthDigest; +import org.tron.common.crypto.pqc.PqSignatureRegistry; import org.tron.common.es.ExecutorServiceManager; import org.tron.common.exit.ExitManager; import org.tron.common.logsfilter.EventPluginLoader; @@ -1760,8 +1760,8 @@ public BlockCapsule generateBlock(Miner miner, long blockTime, long timeout) { private void signBlockCapsule(BlockCapsule blockCapsule, Miner miner) { SignatureScheme scheme = resolveWitnessScheme(miner); - if (scheme == SignatureScheme.ML_DSA_65) { - signWitnessAuth(blockCapsule, miner); + if (PqSignatureRegistry.contains(scheme)) { + signWitnessAuth(blockCapsule, miner, scheme); } else { blockCapsule.sign(miner.getPrivateKey()); } @@ -1783,18 +1783,19 @@ private SignatureScheme resolveWitnessScheme(Miner miner) { return witnessPermission.getKeys(0).getScheme(); } - private void signWitnessAuth(BlockCapsule blockCapsule, Miner miner) { + private void signWitnessAuth(BlockCapsule blockCapsule, Miner miner, SignatureScheme scheme) { byte[] witnessAddress = miner.getWitnessAddress().toByteArray(); Permission witnessPermission = chainBaseManager.getAccountStore().get(witnessAddress) .getInstance().getWitnessPermission(); byte[] pqPrivateKey = miner.getPqPrivateKey(); if (pqPrivateKey == null) { throw new IllegalStateException( - "witness permission requires ML_DSA_65 but local PQ private key is not configured"); + "witness permission requires " + scheme + + " but local PQ private key is not configured"); } byte[] signerAddress = witnessPermission.getKeys(0).getAddress().toByteArray(); byte[] digest = PqAuthDigest.block(blockCapsule.getRawHashBytes(), signerAddress); - byte[] signature = MLDSA65.sign(pqPrivateKey, digest); + byte[] signature = PqSignatureRegistry.sign(scheme, pqPrivateKey, digest); AuthWitness witnessAuth = AuthWitness.newBuilder() .setSignerAddress(ByteString.copyFrom(signerAddress)) .setSignature(ByteString.copyFrom(signature)) diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf index a011a882c97..8406785ef03 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -673,8 +673,12 @@ localwitness = [ # "localwitnesskeystore.json" # ] -# ML-DSA-65 witness signing seed (FIPS 204), 32-byte hex. Used only after -# ALLOW_ML_DSA = 1 and the witness Permission is upgraded to scheme = ML_DSA_65. +# Scheme used to derive keys from localwitness_seed_pq. Defaults to ML_DSA_65. +# Allowed values: ML_DSA_44, ML_DSA_65. +# localwitness_seed_pq_scheme = "ML_DSA_65" + +# ML-DSA witness signing seed (FIPS 204), 32-byte hex. Used only after +# ALLOW_ML_DSA = 1 and the witness Permission is upgraded to an ML-DSA scheme. # MUST be produced by a CSPRNG; the value below is an example, never use in prod. # localwitness_seed_pq = [ # "0101010101010101010101010101010101010101010101010101010101010101" 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 738064a529c..a47878281ea 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 @@ -9,7 +9,6 @@ import org.tron.common.application.ApplicationFactory; import org.tron.common.application.TronApplicationContext; import org.tron.common.crypto.pqc.MLDSA44; -import org.tron.common.crypto.pqc.MLDSA65; import org.tron.common.utils.ByteArray; import org.tron.core.ChainBaseManager; import org.tron.core.config.DefaultConfig; @@ -20,7 +19,7 @@ * Demo fullnode that dials {@link PqcWitnessNode} via P2P and syncs PQ-signed blocks. * * Both nodes share the same deterministic PQ genesis pre-state (witness account with an - * ML-DSA-65 witness permission + demo user account with an ML-DSA-44 owner permission), + * ML-DSA-44 witness permission + demo user account with an ML-DSA-44 owner permission), * installed via {@link PqcWitnessNode#installPqGenesisState}. Once the witness produces * a block it is broadcast over P2P; this node validates {@code BlockHeader.witness_auth} * against the same on-chain public key and applies the block. @@ -57,7 +56,7 @@ public static void main(String[] args) throws Exception { .setLevel(ch.qos.logback.classic.Level.INFO); // ── 1. Derive the same deterministic keys used by PqcWitnessNode ────── - MLDSA65 witnessKp = new MLDSA65(PqcWitnessNode.WITNESS_SEED); + MLDSA44 witnessKp = new MLDSA44(PqcWitnessNode.WITNESS_SEED); MLDSA44 userKp = new MLDSA44(PqcWitnessNode.USER_SEED); byte[] witnessPub = witnessKp.getPublicKey(); @@ -69,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(MLDSA65.computeAddress(witnessPub))); + + ByteArray.toHexString(MLDSA44.computeAddress(witnessPub))); // ── 2. Configure node (no -w: this is a pure fullnode) ──────────────── File dbDir = Files.createTempDirectory("pqc-fullnode-").toFile(); @@ -99,7 +98,7 @@ public static void main(String[] args) throws Exception { // ── 4. Install matching PQ genesis pre-state ────────────────────────── // Without this the incoming witness_auth would fail to validate because - // this node wouldn't know the witness's ML-DSA-65 public key. + // this node wouldn't know the witness's ML-DSA-44 public key. PqcWitnessNode.installPqGenesisState(db, chain, witnessPub, userPub); // ── 5. Start P2P + gRPC (no ConsensusService.start — we don't produce) ─ diff --git a/framework/src/test/java/org/tron/common/crypto/pqc/program/PqcWitnessNode.java b/framework/src/test/java/org/tron/common/crypto/pqc/program/PqcWitnessNode.java index 01f0145d037..bf133036e2c 100644 --- a/framework/src/test/java/org/tron/common/crypto/pqc/program/PqcWitnessNode.java +++ b/framework/src/test/java/org/tron/common/crypto/pqc/program/PqcWitnessNode.java @@ -10,7 +10,6 @@ import org.tron.common.application.ApplicationFactory; import org.tron.common.application.TronApplicationContext; import org.tron.common.crypto.pqc.MLDSA44; -import org.tron.common.crypto.pqc.MLDSA65; import org.tron.common.utils.ByteArray; import org.tron.core.ChainBaseManager; import org.tron.core.capsule.AccountCapsule; @@ -27,7 +26,7 @@ import org.tron.protos.Protocol.SignatureScheme; /** - * Demo witness node with ML-DSA-65 block production. + * Demo witness node with ML-DSA-44 block production. * * Starts an in-process TRON node configured with a PQC witness keypair and * a user account that holds an ML-DSA-44 owner permission — ready to receive @@ -44,7 +43,7 @@ */ public class PqcWitnessNode { - /** Fixed seed for the ML-DSA-65 witness keypair (shared with PqcClient for derivation). */ + /** Fixed seed for the ML-DSA-44 witness keypair (shared with PqcClient for derivation). */ static final byte[] WITNESS_SEED = filledSeed(0x01); /** Fixed seed for the ML-DSA-44 user keypair (shared with PqcClient for derivation). */ static final byte[] USER_SEED = filledSeed(0x02); @@ -70,16 +69,16 @@ public static void main(String[] args) throws Exception { .setLevel(ch.qos.logback.classic.Level.INFO); // ── 1. Derive deterministic keypairs ────────────────────────────────── - MLDSA65 witnessKp = new MLDSA65(WITNESS_SEED); + MLDSA44 witnessKp = new MLDSA44(WITNESS_SEED); MLDSA44 userKp = new MLDSA44(USER_SEED); byte[] witnessPub = witnessKp.getPublicKey(); - byte[] witnessAddr = MLDSA65.computeAddress(witnessPub); + byte[] witnessAddr = MLDSA44.computeAddress(witnessPub); byte[] userPub = userKp.getPublicKey(); byte[] signerAddr = MLDSA44.computeAddress(userPub); System.out.println("=== PQC Witness Node ==="); - System.out.println("Witness address (ML-DSA-65): " + ByteArray.toHexString(witnessAddr)); + System.out.println("Witness address (ML-DSA-44): " + 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); @@ -135,7 +134,7 @@ public static void main(String[] args) throws Exception { */ static void installPqGenesisState(Manager db, ChainBaseManager chain, byte[] witnessPub, byte[] userPub) { - byte[] witnessAddr = MLDSA65.computeAddress(witnessPub); + byte[] witnessAddr = MLDSA44.computeAddress(witnessPub); ByteString witnessAddrBs = ByteString.copyFrom(witnessAddr); byte[] signerAddr = MLDSA44.computeAddress(userPub); ByteString signerAddrBs = ByteString.copyFrom(signerAddr); @@ -144,13 +143,13 @@ static void installPqGenesisState(Manager db, ChainBaseManager chain, db.getDynamicPropertiesStore().saveAllowMlDsa(1L); db.getDynamicPropertiesStore().saveAllowMultiSign(1L); - // Witness account with ML-DSA-65 witness permission. + // Witness account with ML-DSA-44 witness permission. Permission witnessPerm = Permission.newBuilder() .setType(PermissionType.Witness) .setId(1).setPermissionName("witness").setThreshold(1) .addKeys(Key.newBuilder() .setAddress(witnessAddrBs).setWeight(1) - .setScheme(SignatureScheme.ML_DSA_65) + .setScheme(SignatureScheme.ML_DSA_44) .setPublicKey(ByteString.copyFrom(witnessPub))) .build(); db.getAccountStore().put(witnessAddr, new AccountCapsule(Account.newBuilder() 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 e2222644aad..2a359425023 100644 --- a/framework/src/test/java/org/tron/core/actuator/AccountPermissionUpdateActuatorTest.java +++ b/framework/src/test/java/org/tron/core/actuator/AccountPermissionUpdateActuatorTest.java @@ -1192,7 +1192,7 @@ public void mlDsa44WrongPublicKeyLengthRejected() { } @Test - public void witnessMlDsa44Rejected() { + public void witnessMlDsa44Accepted() throws ContractValidateException { dbManager.getDynamicPropertiesStore().saveAllowMlDsa(1L); ByteString address = ByteString.copyFrom(ByteArray.fromHexString(WITNESS_ADDRESS)); Permission owner = ownerPermissionWithKeys( @@ -1206,12 +1206,7 @@ public void witnessMlDsa44Rejected() { Any any = getContract(address, owner, witness, java.util.Collections.singletonList(active)); - try { - actuatorFor(any).validate(); - fail("Witness permission with ML-DSA-44 should be rejected"); - } catch (ContractValidateException e) { - Assert.assertTrue(e.getMessage().contains("Witness permission only supports ML_DSA_65")); - } + actuatorFor(any).validate(); } @Test diff --git a/framework/src/test/resources/config-test.conf b/framework/src/test/resources/config-test.conf index a3c98f2b3a7..f920e10253f 100644 --- a/framework/src/test/resources/config-test.conf +++ b/framework/src/test/resources/config-test.conf @@ -353,6 +353,7 @@ genesis.block = { localwitness = [ ] +localwitness_seed_pq_scheme = "ML_DSA_44" localwitness_seed_pq = [ "0101010101010101010101010101010101010101010101010101010101010101" ] From 51af6b6e91af8502d9b170c71c3b5cc6f114d6b3 Mon Sep 17 00:00:00 2001 From: federico Date: Sun, 26 Apr 2026 21:54:05 +0800 Subject: [PATCH 05/12] fix(crypto): pq witness init hardening and test fixes --- .../org/tron/common/utils/LocalWitnesses.java | 13 ++++++++++-- .../tron/core/capsule/TransactionCapsule.java | 6 +++++- .../java/org/tron/consensus/base/Param.java | 20 +++++++++++++++---- .../java/org/tron/core/config/args/Args.java | 1 + .../tron/core/consensus/ConsensusService.java | 10 ++++++++++ .../common/crypto/pqc/program/PqFullNode.java | 5 +++-- .../tron/core/capsule/BlockCapsulePqTest.java | 2 +- .../core/capsule/TransactionCapsuleTest.java | 6 +++--- .../tron/core/exception/TronErrorTest.java | 12 +++++++++-- 9 files changed, 60 insertions(+), 15 deletions(-) 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 ad9246d5502..f078d2113a9 100644 --- a/chainbase/src/main/java/org/tron/common/utils/LocalWitnesses.java +++ b/chainbase/src/main/java/org/tron/common/utils/LocalWitnesses.java @@ -26,6 +26,7 @@ import org.tron.common.crypto.SignInterface; import org.tron.common.crypto.SignUtils; import org.tron.common.crypto.pqc.MLDSA65; +import org.tron.common.crypto.pqc.PqSignatureRegistry; import org.tron.core.config.Parameter.ChainConstant; import org.tron.core.exception.TronError; import org.tron.protos.Protocol.SignatureScheme; @@ -42,9 +43,16 @@ public class LocalWitnesses { /** PQ signature scheme used to derive keys from {@link #pqSeeds}. */ @Getter - @Setter private SignatureScheme pqScheme = SignatureScheme.ML_DSA_65; + public void setPqScheme(SignatureScheme pqScheme) { + if (pqScheme == null || !PqSignatureRegistry.contains(pqScheme)) { + throw new TronError("unsupported PQ signature scheme: " + pqScheme, + TronError.ErrCode.WITNESS_INIT); + } + this.pqScheme = pqScheme; + } + @Setter @Getter private byte[] witnessAccountAddress; @@ -121,7 +129,8 @@ public void setPqSeeds(final List pqSeeds) { private static void validatePqSeed(String seed) { String hex = seed; - if (StringUtils.startsWithIgnoreCase(hex, "0X")) { + // Match downstream ByteArray.fromHexString, which only strips lowercase "0x". + if (StringUtils.startsWith(hex, "0x")) { hex = hex.substring(2); } int expectedHexLen = MLDSA65.SEED_LENGTH * 2; 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 59066833a4a..0a853242923 100755 --- a/chainbase/src/main/java/org/tron/core/capsule/TransactionCapsule.java +++ b/chainbase/src/main/java/org/tron/core/capsule/TransactionCapsule.java @@ -758,7 +758,11 @@ static boolean validateStructuredSignature(Transaction transaction, if (!PqSignatureRegistry.verify(scheme, pk, digest, sig)) { throw new PermissionException("pq sig invalid"); } - weight = StrictMathWrapper.addExact(weight, key.getWeight()); + try { + weight = StrictMathWrapper.addExact(weight, key.getWeight()); + } catch (ArithmeticException e) { + throw new PermissionException("weight overflow"); + } } return weight >= permission.getThreshold(); } 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 060d1627f53..c82afded31f 100644 --- a/consensus/src/main/java/org/tron/consensus/base/Param.java +++ b/consensus/src/main/java/org/tron/consensus/base/Param.java @@ -67,12 +67,8 @@ public class Miner { @Setter private ByteString witnessAddress; - @Getter - @Setter private byte[] pqPrivateKey; - @Getter - @Setter private byte[] pqPublicKey; public Miner(byte[] privateKey, ByteString privateKeyAddress, ByteString witnessAddress) { @@ -80,6 +76,22 @@ public Miner(byte[] privateKey, ByteString privateKeyAddress, ByteString witness 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/framework/src/main/java/org/tron/core/config/args/Args.java b/framework/src/main/java/org/tron/core/config/args/Args.java index 3db9c81330f..2e749e3e39c 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 @@ -1229,6 +1229,7 @@ private static void initLocalWitnesses(Config config, CLIParameter cmd) { if (config.hasPath(ConfigKey.LOCAL_WITNESS_SEED_PQ)) { List pqSeeds = config.getStringList(ConfigKey.LOCAL_WITNESS_SEED_PQ); if (!pqSeeds.isEmpty()) { + localWitnesses = new LocalWitnesses(); localWitnesses.setPqSeeds(pqSeeds); if (config.hasPath(ConfigKey.LOCAL_WITNESS_SEED_PQ_SCHEME)) { String schemeName = config.getString(ConfigKey.LOCAL_WITNESS_SEED_PQ_SCHEME); 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 cebae77fccc..69b6fe4f0f7 100644 --- a/framework/src/main/java/org/tron/core/consensus/ConsensusService.java +++ b/framework/src/main/java/org/tron/core/consensus/ConsensusService.java @@ -18,6 +18,7 @@ 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.SignatureScheme; @@ -82,6 +83,7 @@ public void start() { miners.add(miner); } else if (pqSeeds.size() > 1) { SignatureScheme scheme = Args.getLocalWitnesses().getPqScheme(); + requireSupportedPqScheme(scheme); for (String seed : pqSeeds) { byte[] seedBytes = fromHexString(seed); PqSignature keypair = PqSignatureRegistry.fromSeed(scheme, seedBytes); @@ -113,6 +115,7 @@ public void start() { private Miner buildPqOnlyMinerFromSeed(Param param, String pqSeed) { SignatureScheme scheme = Args.getLocalWitnesses().getPqScheme(); + requireSupportedPqScheme(scheme); byte[] seedBytes = fromHexString(pqSeed); PqSignature keypair = PqSignatureRegistry.fromSeed(scheme, seedBytes); byte[] sk = keypair.getPrivateKey(); @@ -135,6 +138,13 @@ private Miner buildPqOnlyMinerFromSeed(Param param, String pqSeed) { return miner; } + private static void requireSupportedPqScheme(SignatureScheme scheme) { + if (!PqSignatureRegistry.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/test/java/org/tron/common/crypto/pqc/program/PqFullNode.java b/framework/src/test/java/org/tron/common/crypto/pqc/program/PqFullNode.java index a47878281ea..2a71ef6f794 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 @@ -87,8 +87,9 @@ public static void main(String[] args) throws Exception { Args.getInstance().genesisBlock.setWitnesses(new ArrayList<>()); // Point to the witness node as the only seed peer. - Args.getInstance().getSeedNode().setAddressList(Collections.singletonList( - new InetSocketAddress(WITNESS_HOST, WITNESS_P2P_PORT))); + // 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); 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 43dc4dc9de1..dddb9ad6675 100644 --- a/framework/src/test/java/org/tron/core/capsule/BlockCapsulePqTest.java +++ b/framework/src/test/java/org/tron/core/capsule/BlockCapsulePqTest.java @@ -2,13 +2,13 @@ import com.google.protobuf.ByteString; import org.junit.Assert; -import org.tron.common.crypto.pqc.MLDSA65; 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.MLDSA65; import org.tron.common.crypto.pqc.PqAuthDigest; import org.tron.common.utils.ByteArray; import org.tron.common.utils.Sha256Hash; 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 f3ee6e3e826..a9a662a69ce 100644 --- a/framework/src/test/java/org/tron/core/capsule/TransactionCapsuleTest.java +++ b/framework/src/test/java/org/tron/core/capsule/TransactionCapsuleTest.java @@ -8,14 +8,14 @@ import com.google.protobuf.ByteString; import lombok.extern.slf4j.Slf4j; import org.junit.Assert; -import org.tron.common.crypto.ECKey; -import org.tron.common.crypto.pqc.MLDSA44; -import org.tron.common.crypto.pqc.MLDSA65; 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.MLDSA44; +import org.tron.common.crypto.pqc.MLDSA65; import org.tron.common.crypto.pqc.PqAuthDigest; import org.tron.common.utils.ByteArray; import org.tron.common.utils.Sha256Hash; 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 e965ae3fd60..9ca052a54e4 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; @@ -109,9 +110,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_seed_pq = []\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()); } From b94123ee1088ae0868748873de4cc0df94146767 Mon Sep 17 00:00:00 2001 From: federico Date: Mon, 27 Apr 2026 13:29:58 +0800 Subject: [PATCH 06/12] test(crypto): add ML-DSA vs ECKey signature scheme benchmark Micro-benchmark comparing keygen/sign/verify latency for secp256k1 ECKey, ML-DSA-44 and ML-DSA-65, for tracking PQ signature integration performance characteristics across releases. --- .../pqc/SignatureSchemeBenchmarkTest.java | 166 ++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 framework/src/test/java/org/tron/common/crypto/pqc/SignatureSchemeBenchmarkTest.java 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..b8210017fbf --- /dev/null +++ b/framework/src/test/java/org/tron/common/crypto/pqc/SignatureSchemeBenchmarkTest.java @@ -0,0 +1,166 @@ +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), ML-DSA-44 and ML-DSA-65. 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 mldsa44 = benchMlDsa44(); + Result mldsa65 = benchMlDsa65(); + + 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(mldsa44); + printResult(mldsa65); + } + + 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 benchMlDsa44() { + for (int i = 0; i < WARMUP; i++) { + MLDSA44 k = new MLDSA44(); + byte[] sig = k.sign(MESSAGE); + k.verify(MESSAGE, sig); + } + + long keygenNs = 0; + MLDSA44[] keys = new MLDSA44[ITERATIONS]; + for (int i = 0; i < ITERATIONS; i++) { + long t0 = System.nanoTime(); + keys[i] = new MLDSA44(); + 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("ML-DSA-44", keygenNs, signNs, verifyNs); + } + + private Result benchMlDsa65() { + for (int i = 0; i < WARMUP; i++) { + MLDSA65 k = new MLDSA65(); + byte[] sig = k.sign(MESSAGE); + k.verify(MESSAGE, sig); + } + + long keygenNs = 0; + MLDSA65[] keys = new MLDSA65[ITERATIONS]; + for (int i = 0; i < ITERATIONS; i++) { + long t0 = System.nanoTime(); + keys[i] = new MLDSA65(); + 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("ML-DSA-65", 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; + } + } +} From 22b0341217f25ad9a82814f66a72cd002184255a Mon Sep 17 00:00:00 2001 From: federico Date: Mon, 27 Apr 2026 15:33:12 +0800 Subject: [PATCH 07/12] feat(vm): add ML-DSA verify precompiles at 0x12 and 0x14 Introduce two TVM precompiles for post-quantum signature verification, gated on the existing ALLOW_ML_DSA proposal: - 0x12 VerifyMlDsa44: ML-DSA-44 (FIPS-204, SHAKE256), 4500 energy. Compatible with EIP-8051 0x12 at the algorithm level; uses raw 1312-byte public keys (BouncyCastle-native form) rather than the 20512-byte expanded form, so wire bytes differ. - 0x14 VerifyMlDsa65: ML-DSA-65 (TRON extension), 7000 energy. Input layout for both: [msg 32B | signature | publicKey], output is a 32-byte word (1 on success, 0 otherwise). Wires VMConfig.allowMlDsa() through ConfigLoader so the flag is loaded from the store on startup. --- .../tron/core/vm/PrecompiledContracts.java | 89 ++++++++ .../org/tron/core/vm/config/ConfigLoader.java | 1 + .../org/tron/core/vm/config/VMConfig.java | 10 + .../runtime/vm/MlDsaPrecompileTest.java | 193 ++++++++++++++++++ 4 files changed, 293 insertions(+) create mode 100644 framework/src/test/java/org/tron/common/runtime/vm/MlDsaPrecompileTest.java 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 634f7f2d3d1..eb7f3f0305b 100644 --- a/actuator/src/main/java/org/tron/core/vm/PrecompiledContracts.java +++ b/actuator/src/main/java/org/tron/core/vm/PrecompiledContracts.java @@ -45,6 +45,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.MLDSA44; +import org.tron.common.crypto.pqc.MLDSA65; import org.tron.common.crypto.zksnark.BN128; import org.tron.common.crypto.zksnark.BN128Fp; import org.tron.common.crypto.zksnark.BN128G1; @@ -107,6 +109,9 @@ public class PrecompiledContracts { private static final EthRipemd160 ethRipemd160 = new EthRipemd160(); private static final Blake2F blake2F = new Blake2F(); + private static final VerifyMlDsa44 verifyMlDsa44 = new VerifyMlDsa44(); + private static final VerifyMlDsa65 verifyMlDsa65 = new VerifyMlDsa65(); + // FreezeV2 PrecompileContracts private static final GetChainParameter getChainParameter = new GetChainParameter(); private static final AvailableUnfreezeV2Size availableUnfreezeV2Size = new AvailableUnfreezeV2Size(); @@ -200,6 +205,15 @@ public class PrecompiledContracts { private static final DataWord blake2FAddr = new DataWord( "0000000000000000000000000000000000000000000000000000000000020009"); + // EIP-8051 0x12: ML-DSA-44 verify (FIPS-204, SHAKE256). Uses raw 1312-byte public key + // rather than EIP-8051's 20512-byte expanded form; signatures produced for 0x12 on + // EIP-8051-compliant chains are not byte-compatible with this precompile's input. + private static final DataWord verifyMlDsa44Addr = new DataWord( + "0000000000000000000000000000000000000000000000000000000000000012"); + // 0x14: ML-DSA-65 verify (TRON extension, FIPS-204 / SHAKE256, raw 1952-byte public key). + private static final DataWord verifyMlDsa65Addr = new DataWord( + "0000000000000000000000000000000000000000000000000000000000000014"); + public static PrecompiledContract getOptimizedContractForConstant(PrecompiledContract contract) { try { Constructor constructor = contract.getClass().getDeclaredConstructor(); @@ -282,6 +296,13 @@ public static PrecompiledContract getContractForAddress(DataWord address) { return blake2F; } + if (VMConfig.allowMlDsa() && address.equals(verifyMlDsa44Addr)) { + return verifyMlDsa44; + } + if (VMConfig.allowMlDsa() && address.equals(verifyMlDsa65Addr)) { + return verifyMlDsa65; + } + if (VMConfig.allowTvmFreezeV2()) { if (address.equals(getChainParameterAddr)) { return getChainParameter; @@ -2221,4 +2242,72 @@ public Pair execute(byte[] data) { } } + /** + * Verifies an ML-DSA-44 signature (FIPS-204, SHAKE256). Input layout (right-padded with + * zeros if shorter): [msg 32B | signature 2420B | publicKey 1312B] = 3764B. Returns a + * 32-byte word: 1 on success, 0 on failure or malformed input. + */ + public static class VerifyMlDsa44 extends PrecompiledContract { + + private static final int MSG_LEN = 32; + private static final int SIG_LEN = MLDSA44.SIGNATURE_LENGTH; + private static final int PK_LEN = MLDSA44.PUBLIC_KEY_LENGTH; + private static final int INPUT_LEN = MSG_LEN + SIG_LEN + PK_LEN; + + @Override + public long getEnergyForData(byte[] data) { + return 4500; + } + + @Override + public Pair execute(byte[] data) { + if (data == null || data.length < INPUT_LEN) { + return Pair.of(true, DataWord.ZERO().getData()); + } + try { + byte[] msg = copyOfRange(data, 0, MSG_LEN); + byte[] sig = copyOfRange(data, MSG_LEN, MSG_LEN + SIG_LEN); + byte[] pk = copyOfRange(data, MSG_LEN + SIG_LEN, INPUT_LEN); + boolean ok = MLDSA44.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()); + } + } + } + + /** + * Verifies an ML-DSA-65 signature (FIPS-204, SHAKE256). Input layout: + * [msg 32B | signature 3309B | publicKey 1952B] = 5293B. TRON extension; not part of + * EIP-8051. Returns a 32-byte word: 1 on success, 0 otherwise. + */ + public static class VerifyMlDsa65 extends PrecompiledContract { + + private static final int MSG_LEN = 32; + private static final int SIG_LEN = MLDSA65.SIGNATURE_LENGTH; + private static final int PK_LEN = MLDSA65.PUBLIC_KEY_LENGTH; + private static final int INPUT_LEN = MSG_LEN + SIG_LEN + PK_LEN; + + @Override + public long getEnergyForData(byte[] data) { + return 7000; + } + + @Override + public Pair execute(byte[] data) { + if (data == null || data.length < INPUT_LEN) { + return Pair.of(true, DataWord.ZERO().getData()); + } + try { + byte[] msg = copyOfRange(data, 0, MSG_LEN); + byte[] sig = copyOfRange(data, MSG_LEN, MSG_LEN + SIG_LEN); + byte[] pk = copyOfRange(data, MSG_LEN + SIG_LEN, INPUT_LEN); + boolean ok = MLDSA65.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()); + } + } + } + } 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 e099101912b..a7d74418f6c 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 @@ -46,6 +46,7 @@ public static void load(StoreFactory storeFactory) { VMConfig.initAllowTvmBlob(ds.getAllowTvmBlob()); VMConfig.initAllowTvmSelfdestructRestriction(ds.getAllowTvmSelfdestructRestriction()); VMConfig.initAllowTvmOsaka(ds.getAllowTvmOsaka()); + VMConfig.initAllowMlDsa(ds.getAllowMlDsa()); } } } 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 1a7f0c058a4..1a45b6ed786 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 @@ -63,6 +63,8 @@ public class VMConfig { private static boolean ALLOW_TVM_OSAKA = false; + private static boolean ALLOW_ML_DSA = false; + private VMConfig() { } @@ -178,6 +180,10 @@ public static void initAllowTvmOsaka(long allow) { ALLOW_TVM_OSAKA = allow == 1; } + public static void initAllowMlDsa(long allow) { + ALLOW_ML_DSA = allow == 1; + } + public static boolean getEnergyLimitHardFork() { return CommonParameter.ENERGY_LIMIT_HARD_FORK; } @@ -281,4 +287,8 @@ public static boolean allowTvmSelfdestructRestriction() { public static boolean allowTvmOsaka() { return ALLOW_TVM_OSAKA; } + + public static boolean allowMlDsa() { + return ALLOW_ML_DSA; + } } diff --git a/framework/src/test/java/org/tron/common/runtime/vm/MlDsaPrecompileTest.java b/framework/src/test/java/org/tron/common/runtime/vm/MlDsaPrecompileTest.java new file mode 100644 index 00000000000..51a77e4d8b7 --- /dev/null +++ b/framework/src/test/java/org/tron/common/runtime/vm/MlDsaPrecompileTest.java @@ -0,0 +1,193 @@ +package org.tron.common.runtime.vm; + +import java.util.Arrays; +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.MLDSA44; +import org.tron.common.crypto.pqc.MLDSA65; +import org.tron.core.vm.PrecompiledContracts; +import org.tron.core.vm.PrecompiledContracts.PrecompiledContract; +import org.tron.core.vm.config.VMConfig; + +/** + * Unit tests for the ML-DSA-44 (0x12) and ML-DSA-65 (0x14) verify precompiles. + * These tests are stateless — no chain DB needed. + */ +public class MlDsaPrecompileTest { + + private static final DataWord MLDSA44_ADDR = new DataWord( + "0000000000000000000000000000000000000000000000000000000000000012"); + private static final DataWord MLDSA65_ADDR = new DataWord( + "0000000000000000000000000000000000000000000000000000000000000014"); + + 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.initAllowMlDsa(1L); + } + + @After + public void disableProposal() { + VMConfig.initAllowMlDsa(0L); + } + + @Test + public void switchOff_returnsNull() { + VMConfig.initAllowMlDsa(0L); + Assert.assertNull(PrecompiledContracts.getContractForAddress(MLDSA44_ADDR)); + Assert.assertNull(PrecompiledContracts.getContractForAddress(MLDSA65_ADDR)); + } + + @Test + public void switchOn_returnsContracts() { + Assert.assertNotNull(PrecompiledContracts.getContractForAddress(MLDSA44_ADDR)); + Assert.assertNotNull(PrecompiledContracts.getContractForAddress(MLDSA65_ADDR)); + } + + @Test + public void mldsa44_validSignature_returnsOne() { + MLDSA44 key = new MLDSA44(); + byte[] sig = key.sign(MESSAGE_HASH); + byte[] input = concat(MESSAGE_HASH, sig, key.getPublicKey()); + + PrecompiledContract pc = PrecompiledContracts.getContractForAddress(MLDSA44_ADDR); + Pair result = pc.execute(input); + + Assert.assertTrue(result.getLeft()); + Assert.assertArrayEquals(DataWord.ONE().getData(), result.getRight()); + Assert.assertEquals(4500, pc.getEnergyForData(input)); + } + + @Test + public void mldsa44_tamperedMessage_returnsZero() { + MLDSA44 key = new MLDSA44(); + byte[] sig = key.sign(MESSAGE_HASH); + byte[] tampered = MESSAGE_HASH.clone(); + tampered[0] ^= 0x01; + byte[] input = concat(tampered, sig, key.getPublicKey()); + + Pair result = + PrecompiledContracts.getContractForAddress(MLDSA44_ADDR).execute(input); + + Assert.assertTrue(result.getLeft()); + Assert.assertArrayEquals(DataWord.ZERO().getData(), result.getRight()); + } + + @Test + public void mldsa44_tamperedSignature_returnsZero() { + MLDSA44 key = new MLDSA44(); + byte[] sig = key.sign(MESSAGE_HASH); + sig[0] ^= 0x01; + byte[] input = concat(MESSAGE_HASH, sig, key.getPublicKey()); + + Pair result = + PrecompiledContracts.getContractForAddress(MLDSA44_ADDR).execute(input); + + Assert.assertTrue(result.getLeft()); + Assert.assertArrayEquals(DataWord.ZERO().getData(), result.getRight()); + } + + @Test + public void mldsa44_wrongPublicKey_returnsZero() { + MLDSA44 signer = new MLDSA44(); + MLDSA44 other = new MLDSA44(); + byte[] sig = signer.sign(MESSAGE_HASH); + byte[] input = concat(MESSAGE_HASH, sig, other.getPublicKey()); + + Pair result = + PrecompiledContracts.getContractForAddress(MLDSA44_ADDR).execute(input); + + Assert.assertTrue(result.getLeft()); + Assert.assertArrayEquals(DataWord.ZERO().getData(), result.getRight()); + } + + @Test + public void mldsa44_shortInput_returnsZero() { + byte[] tooShort = new byte[100]; + Pair result = + PrecompiledContracts.getContractForAddress(MLDSA44_ADDR).execute(tooShort); + + Assert.assertTrue(result.getLeft()); + Assert.assertArrayEquals(DataWord.ZERO().getData(), result.getRight()); + } + + @Test + public void mldsa44_nullInput_returnsZero() { + Pair result = + PrecompiledContracts.getContractForAddress(MLDSA44_ADDR).execute(null); + + Assert.assertTrue(result.getLeft()); + Assert.assertArrayEquals(DataWord.ZERO().getData(), result.getRight()); + } + + @Test + public void mldsa65_validSignature_returnsOne() { + MLDSA65 key = new MLDSA65(); + byte[] sig = key.sign(MESSAGE_HASH); + byte[] input = concat(MESSAGE_HASH, sig, key.getPublicKey()); + + PrecompiledContract pc = PrecompiledContracts.getContractForAddress(MLDSA65_ADDR); + Pair result = pc.execute(input); + + Assert.assertTrue(result.getLeft()); + Assert.assertArrayEquals(DataWord.ONE().getData(), result.getRight()); + Assert.assertEquals(7000, pc.getEnergyForData(input)); + } + + @Test + public void mldsa65_tamperedSignature_returnsZero() { + MLDSA65 key = new MLDSA65(); + byte[] sig = key.sign(MESSAGE_HASH); + sig[sig.length - 1] ^= 0x01; + byte[] input = concat(MESSAGE_HASH, sig, key.getPublicKey()); + + Pair result = + PrecompiledContracts.getContractForAddress(MLDSA65_ADDR).execute(input); + + Assert.assertTrue(result.getLeft()); + Assert.assertArrayEquals(DataWord.ZERO().getData(), result.getRight()); + } + + @Test + public void mldsa65_shortInput_returnsZero() { + Pair result = + PrecompiledContracts.getContractForAddress(MLDSA65_ADDR).execute(new byte[3000]); + + Assert.assertTrue(result.getLeft()); + Assert.assertArrayEquals(DataWord.ZERO().getData(), result.getRight()); + } + + private static byte[] concat(byte[]... parts) { + int total = 0; + for (byte[] p : parts) { + total += p.length; + } + byte[] out = new byte[total]; + int off = 0; + for (byte[] p : parts) { + System.arraycopy(p, 0, out, off, p.length); + off += p.length; + } + return out; + } + + // Sanity check: the helper above and Arrays.copyOfRange behave consistently. + @Test + public void concatHelper_works() { + byte[] a = {1, 2}; + byte[] b = {3, 4, 5}; + byte[] c = concat(a, b); + Assert.assertArrayEquals(new byte[] {1, 2, 3, 4, 5}, c); + Assert.assertArrayEquals(a, Arrays.copyOfRange(c, 0, 2)); + } +} From 99d20b9167e0d2312f79b4f6d0162e6eef69b395 Mon Sep 17 00:00:00 2001 From: federico Date: Mon, 27 Apr 2026 20:50:03 +0800 Subject: [PATCH 08/12] feat(crypto): add FN-DSA / Falcon-512 post-quantum signature support --- .../AccountPermissionUpdateActuator.java | 20 +- .../org/tron/core/utils/ProposalUtil.java | 14 +- .../org/tron/core/capsule/BlockCapsule.java | 8 +- .../tron/core/capsule/TransactionCapsule.java | 10 +- .../core/store/DynamicPropertiesStore.java | 42 +++ .../common/parameter/CommonParameter.java | 4 + .../src/main/java/org/tron/core/Constant.java | 5 + .../org/tron/common/crypto/pqc/FNDSA.java | 194 ++++++++++ .../org/tron/common/crypto/pqc/MLDSA44.java | 11 + .../org/tron/common/crypto/pqc/MLDSA65.java | 11 + .../tron/common/crypto/pqc/PqSignature.java | 8 +- .../crypto/pqc/PqSignatureRegistry.java | 31 ++ .../src/main/java/org/tron/core/Wallet.java | 5 + .../java/org/tron/core/config/args/Args.java | 17 +- .../org/tron/core/config/args/ConfigKey.java | 1 + .../tron/core/consensus/ProposalService.java | 4 + .../main/java/org/tron/core/db/Manager.java | 2 +- .../org/tron/common/crypto/pqc/FNDSATest.java | 344 ++++++++++++++++++ .../AccountPermissionUpdateActuatorTest.java | 78 ++++ .../core/capsule/TransactionCapsuleTest.java | 87 ++++- protocol/src/main/protos/core/Tron.proto | 4 +- 21 files changed, 885 insertions(+), 15 deletions(-) create mode 100644 crypto/src/main/java/org/tron/common/crypto/pqc/FNDSA.java create mode 100644 framework/src/test/java/org/tron/common/crypto/pqc/FNDSATest.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 10abdffec4c..284ae37a5d9 100644 --- a/actuator/src/main/java/org/tron/core/actuator/AccountPermissionUpdateActuator.java +++ b/actuator/src/main/java/org/tron/core/actuator/AccountPermissionUpdateActuator.java @@ -16,6 +16,7 @@ import org.tron.core.exception.ContractValidateException; import org.tron.core.store.AccountStore; import org.tron.core.store.DynamicPropertiesStore; +import org.tron.common.crypto.pqc.FNDSA; import org.tron.common.crypto.pqc.MLDSA44; import org.tron.common.crypto.pqc.MLDSA65; import org.tron.common.crypto.pqc.PqSignatureRegistry; @@ -261,7 +262,6 @@ public long calcFee() { private void validatePermissionScheme(Permission permission) throws ContractValidateException { DynamicPropertiesStore dynamicStore = chainBaseManager.getDynamicPropertiesStore(); - boolean mlDsaAllowed = dynamicStore.allowMlDsa(); SignatureScheme first = permission.getKeysList().get(0).getScheme(); for (Key key : permission.getKeysList()) { @@ -276,9 +276,9 @@ private void validatePermissionScheme(Permission permission) throws ContractVali "public_key must be empty when scheme is UNKNOWN_SIG_SCHEME"); } } else { - if (!mlDsaAllowed) { + if (!dynamicStore.isPqSchemeAllowed(scheme)) { throw new ContractValidateException( - "ML-DSA is not activated, scheme " + scheme + " is not allowed"); + schemeNotActivatedMessage(scheme) + ", scheme " + scheme + " is not allowed"); } int expected = expectedPublicKeyLength(scheme); if (expected < 0) { @@ -307,8 +307,22 @@ private static int expectedPublicKeyLength(SignatureScheme scheme) { return MLDSA44.PUBLIC_KEY_LENGTH; case ML_DSA_65: return MLDSA65.PUBLIC_KEY_LENGTH; + case FN_DSA: + return FNDSA.PUBLIC_KEY_LENGTH; default: return -1; } } + + private static String schemeNotActivatedMessage(SignatureScheme scheme) { + switch (scheme) { + case ML_DSA_44: + case ML_DSA_65: + return "ML-DSA is not activated"; + case FN_DSA: + return "FN-DSA is not activated"; + default: + return scheme + " is not activated"; + } + } } 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 383bdd37879..f092c89c14e 100644 --- a/actuator/src/main/java/org/tron/core/utils/ProposalUtil.java +++ b/actuator/src/main/java/org/tron/core/utils/ProposalUtil.java @@ -897,6 +897,17 @@ public static void validator(DynamicPropertiesStore dynamicPropertiesStore, } break; } + case ALLOW_FN_DSA: { + if (dynamicPropertiesStore.getAllowFnDsa() == 1) { + throw new ContractValidateException( + "[ALLOW_FN_DSA] has been valid, no need to propose again"); + } + if (value != 1) { + throw new ContractValidateException( + "This value[ALLOW_FN_DSA] is only allowed to be 1"); + } + break; + } default: break; } @@ -983,7 +994,8 @@ public enum ProposalType { // current value, value range PROPOSAL_EXPIRE_TIME(92), // (0, 31536003000) ALLOW_TVM_SELFDESTRUCT_RESTRICTION(94), // 0, 1 ALLOW_TVM_OSAKA(96), // 0, 1 - ALLOW_ML_DSA(97); // 0, 1 + ALLOW_ML_DSA(97), // 0, 1 + ALLOW_FN_DSA(100); // 0, 1 private long code; 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 b4c6e596e3d..4ab7b690578 100755 --- a/chainbase/src/main/java/org/tron/core/capsule/BlockCapsule.java +++ b/chainbase/src/main/java/org/tron/core/capsule/BlockCapsule.java @@ -254,9 +254,9 @@ private boolean validateLegacySignature(DynamicPropertiesStore dynamicProperties private boolean validateWitnessAuth(DynamicPropertiesStore dynamicPropertiesStore, AccountStore accountStore, byte[] witnessAccountAddress, AuthWitness witnessAuth) throws ValidateSignatureException { - if (!dynamicPropertiesStore.allowMlDsa()) { + if (!dynamicPropertiesStore.isAnyPqSchemeAllowed()) { throw new ValidateSignatureException( - "witness_auth present but ML-DSA is not activated"); + "witness_auth present but no post-quantum scheme is activated"); } AccountCapsule accountCapsule = accountStore.get(witnessAccountAddress); Permission witnessPermission = null; @@ -272,6 +272,10 @@ private boolean validateWitnessAuth(DynamicPropertiesStore dynamicPropertiesStor throw new ValidateSignatureException( "witness permission scheme " + scheme + " is not allowed for block signing"); } + if (!dynamicPropertiesStore.isPqSchemeAllowed(scheme)) { + throw new ValidateSignatureException( + "witness permission scheme " + scheme + " is not activated"); + } byte[] signerAddr = witnessAuth.getSignerAddress().toByteArray(); Key matched = null; 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 0a853242923..c5f624249ec 100755 --- a/chainbase/src/main/java/org/tron/core/capsule/TransactionCapsule.java +++ b/chainbase/src/main/java/org/tron/core/capsule/TransactionCapsule.java @@ -650,8 +650,9 @@ public boolean validatePubSignature(AccountStore accountStore, int legacyCount = this.transaction.getSignatureCount(); int pqCount = this.transaction.getAuthWitnessCount(); - if (pqCount > 0 && !dynamicPropertiesStore.allowMlDsa()) { - throw new ValidateSignatureException("auth_witness not allowed: ML-DSA not activated"); + if (pqCount > 0 && !dynamicPropertiesStore.isAnyPqSchemeAllowed()) { + throw new ValidateSignatureException( + "auth_witness not allowed: no post-quantum scheme is activated"); } if (legacyCount > 0 && pqCount > 0) { throw new ValidateSignatureException( @@ -748,11 +749,14 @@ static boolean validateStructuredSignature(Transaction transaction, if (!PqSignatureRegistry.contains(scheme)) { throw new PermissionException("unsupported scheme: " + scheme); } + if (!dynamicPropertiesStore.isPqSchemeAllowed(scheme)) { + throw new PermissionException(scheme + " is not activated"); + } byte[] digest = PqAuthDigest.tx(txid, permissionId, signer.toByteArray()); byte[] pk = key.getPublicKey().toByteArray(); byte[] sig = aw.getSignature().toByteArray(); if (pk.length != PqSignatureRegistry.getPublicKeyLength(scheme) - || sig.length != PqSignatureRegistry.getSignatureLength(scheme)) { + || !PqSignatureRegistry.isValidSignatureLength(scheme, sig.length)) { throw new PermissionException("public key or signature length mismatch"); } if (!PqSignatureRegistry.verify(scheme, pk, digest, sig)) { 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 e713cd51a4c..38aa7f81d1f 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.SignatureScheme; import org.tron.core.db.TronStoreWithRevoking; import org.tron.core.exception.BadItemException; import org.tron.core.exception.ItemNotFoundException; @@ -242,6 +243,8 @@ public class DynamicPropertiesStore extends TronStoreWithRevoking private static final byte[] ALLOW_ML_DSA = "ALLOW_ML_DSA".getBytes(); + private static final byte[] ALLOW_FN_DSA = "ALLOW_FN_DSA".getBytes(); + @Autowired private DynamicPropertiesStore(@Value("properties") String dbName) { super(dbName); @@ -3010,6 +3013,45 @@ public boolean allowMlDsa() { return getAllowMlDsa() == 1L; } + public long getAllowFnDsa() { + return Optional.ofNullable(getUnchecked(ALLOW_FN_DSA)) + .map(BytesCapsule::getData) + .map(ByteArray::toLong) + .orElse(CommonParameter.getInstance().getAllowFnDsa()); + } + + public void saveAllowFnDsa(long value) { + this.put(ALLOW_FN_DSA, new BytesCapsule(ByteArray.fromLong(value))); + } + + public boolean allowFnDsa() { + return getAllowFnDsa() == 1L; + } + + /** Returns true iff at least one post-quantum signature scheme is currently activated. */ + public boolean isAnyPqSchemeAllowed() { + return allowMlDsa() || allowFnDsa(); + } + + /** + * Per-scheme governance check. ML-DSA-44 and ML-DSA-65 are gated by a single + * {@code ALLOW_ML_DSA} flag; FN-DSA has its own flag. Non-PQ schemes return false. + */ + public boolean isPqSchemeAllowed(SignatureScheme scheme) { + if (scheme == null) { + return false; + } + switch (scheme) { + case ML_DSA_44: + case ML_DSA_65: + return allowMlDsa(); + case FN_DSA: + return allowFnDsa(); + 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 047213a2738..ee52ff7f6a2 100644 --- a/common/src/main/java/org/tron/common/parameter/CommonParameter.java +++ b/common/src/main/java/org/tron/common/parameter/CommonParameter.java @@ -641,6 +641,10 @@ public class CommonParameter { @Setter public long allowMlDsa; + @Getter + @Setter + public long allowFnDsa; + 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 aacf31b4005..e8ee8f28dc4 100644 --- a/common/src/main/java/org/tron/core/Constant.java +++ b/common/src/main/java/org/tron/core/Constant.java @@ -65,6 +65,11 @@ public class Constant { public static final int ML_DSA_44_SIGNATURE_LENGTH = 2420; public static final int ML_DSA_65_PUBLIC_KEY_LENGTH = 1952; public static final int ML_DSA_65_SIGNATURE_LENGTH = 3309; + // 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"; 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..8fe1a6154b8 --- /dev/null +++ b/crypto/src/main/java/org/tron/common/crypto/pqc/FNDSA.java @@ -0,0 +1,194 @@ +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.common.crypto.Hash; +import org.tron.protos.Protocol.SignatureScheme; + +/** + * FIPS 206 (draft) FN-DSA / Falcon-512 keypair-bound signer/verifier. Mirrors the + * {@link MLDSA44} / {@link MLDSA65} / {@link SLHDSA} shape: instance methods sign/verify + * with the bound keypair, static {@link #sign(byte[], byte[])} / {@link #verify} provide + * stateless entry points used by {@link PqSignatureRegistry}. + * + *

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}; ML-DSA / SLH-DSA override back to + * strict equality. 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; + /** 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) { + validatePrivateKeyBytes(privateKey); + 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(); + } + + @Override + public SignatureScheme getScheme() { + return SignatureScheme.FN_DSA; + } + + @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(); + } + + @Override + public byte[] getPublicKey() { + return publicKey.clone(); + } + + @Override + public byte[] getAddress() { + return Hash.sha3omit12(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; + } + } + + 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); + } + } + + public static byte[] derivePublicKey(byte[] privateKey) { + throw new UnsupportedOperationException( + "FN-DSA public key cannot be derived from the encoded private key alone; " + + "supply both halves to the (privateKey, publicKey) constructor"); + } + + public static byte[] computeAddress(byte[] publicKey) { + if (publicKey == null || publicKey.length != PUBLIC_KEY_LENGTH) { + throw new IllegalArgumentException( + "FN-DSA public key length must be " + PUBLIC_KEY_LENGTH); + } + return Hash.sha3omit12(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) { + throw new IllegalArgumentException( + "FN-DSA private key length must be " + PRIVATE_KEY_LENGTH); + } + } +} diff --git a/crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA44.java b/crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA44.java index a51c0a299ff..80faf20942c 100644 --- a/crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA44.java +++ b/crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA44.java @@ -113,6 +113,17 @@ public boolean verify(byte[] message, byte[] signature) { return verify(publicKey, message, signature); } + /** ML-DSA-44 produces fixed-length signatures; override the default upper-bound check. */ + @Override + public void validateSignature(byte[] signature) { + if (signature == null || signature.length != SIGNATURE_LENGTH) { + throw new IllegalArgumentException( + "invalid " + getScheme() + " signature length: " + + (signature == null ? "null" : signature.length) + + ", expected " + SIGNATURE_LENGTH); + } + } + public static boolean verify(byte[] publicKey, byte[] message, byte[] signature) { if (publicKey == null || publicKey.length != PUBLIC_KEY_LENGTH) { throw new IllegalArgumentException( diff --git a/crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA65.java b/crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA65.java index 36ed1b08ae9..cecc4377450 100644 --- a/crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA65.java +++ b/crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA65.java @@ -113,6 +113,17 @@ public boolean verify(byte[] message, byte[] signature) { return verify(publicKey, message, signature); } + /** ML-DSA-65 produces fixed-length signatures; override the default upper-bound check. */ + @Override + public void validateSignature(byte[] signature) { + if (signature == null || signature.length != SIGNATURE_LENGTH) { + throw new IllegalArgumentException( + "invalid " + getScheme() + " signature length: " + + (signature == null ? "null" : signature.length) + + ", expected " + SIGNATURE_LENGTH); + } + } + public static boolean verify(byte[] publicKey, byte[] message, byte[] signature) { if (publicKey == null || publicKey.length != PUBLIC_KEY_LENGTH) { throw new IllegalArgumentException( 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 index e2a332c8a04..3765649870d 100644 --- a/crypto/src/main/java/org/tron/common/crypto/pqc/PqSignature.java +++ b/crypto/src/main/java/org/tron/common/crypto/pqc/PqSignature.java @@ -53,12 +53,16 @@ default void validatePublicKey(byte[] publicKey) { } } + /** + * Default upper-bound check, sufficient for variable-length schemes (FN-DSA). + * Fixed-length schemes (ML-DSA-44 / ML-DSA-65) override this with strict equality. + */ default void validateSignature(byte[] signature) { - if (signature == null || signature.length != getSignatureLength()) { + if (signature == null || signature.length == 0 || signature.length > getSignatureLength()) { throw new IllegalArgumentException( "invalid " + getScheme() + " signature length: " + (signature == null ? "null" : signature.length) - + ", expected " + getSignatureLength()); + + ", expected 1.." + getSignatureLength()); } } } diff --git a/crypto/src/main/java/org/tron/common/crypto/pqc/PqSignatureRegistry.java b/crypto/src/main/java/org/tron/common/crypto/pqc/PqSignatureRegistry.java index 7843ad66b2d..49c123f720f 100644 --- a/crypto/src/main/java/org/tron/common/crypto/pqc/PqSignatureRegistry.java +++ b/crypto/src/main/java/org/tron/common/crypto/pqc/PqSignatureRegistry.java @@ -73,6 +73,23 @@ public PqSignature fromSeed(byte[] seed) { return new MLDSA65(seed); } })); + m.put(SignatureScheme.FN_DSA, new SchemeInfo( + FNDSA.PUBLIC_KEY_LENGTH, FNDSA.SIGNATURE_LENGTH, 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); + } + })); SCHEMES = Collections.unmodifiableMap(m); } @@ -91,6 +108,20 @@ public static int getSignatureLength(SignatureScheme scheme) { return require(scheme).signatureLength; } + /** + * Per-scheme signature-length predicate. Fixed-length schemes (ML-DSA-44 / ML-DSA-65) + * require exact equality with {@link #getSignatureLength(SignatureScheme)}; + * variable-length schemes (FN-DSA) treat that value as an upper bound and accept any + * {@code 1..max}. + */ + public static boolean isValidSignatureLength(SignatureScheme scheme, int length) { + SchemeInfo info = require(scheme); + if (scheme == SignatureScheme.FN_DSA) { + return length > 0 && length <= info.signatureLength; + } + return length == info.signatureLength; + } + public static byte[] sign(SignatureScheme scheme, byte[] privateKey, byte[] message) { return require(scheme).ops.sign(privateKey, message); } diff --git a/framework/src/main/java/org/tron/core/Wallet.java b/framework/src/main/java/org/tron/core/Wallet.java index 6079294e193..4d2457d9cfb 100755 --- a/framework/src/main/java/org/tron/core/Wallet.java +++ b/framework/src/main/java/org/tron/core/Wallet.java @@ -1519,6 +1519,11 @@ public Protocol.ChainParameters getChainParameters() { .setValue(dbManager.getDynamicPropertiesStore().getAllowMlDsa()) .build()); + builder.addChainParameter(Protocol.ChainParameters.ChainParameter.newBuilder() + .setKey("getAllowFnDsa") + .setValue(dbManager.getDynamicPropertiesStore().getAllowFnDsa()) + .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 2e749e3e39c..9f737fd849b 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 @@ -31,6 +31,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; @@ -1046,6 +1047,10 @@ public static void applyConfigParams( config.hasPath(ConfigKey.COMMITTEE_ALLOW_ML_DSA) ? config .getInt(ConfigKey.COMMITTEE_ALLOW_ML_DSA) : 0; + PARAMETER.allowFnDsa = + config.hasPath(ConfigKey.COMMITTEE_ALLOW_FN_DSA) ? config + .getInt(ConfigKey.COMMITTEE_ALLOW_FN_DSA) : 0; + logConfig(); } @@ -1189,6 +1194,10 @@ private static void applyCLIParams(CLIParameter cmd, JCommander jc) { } } + private static final EnumSet WITNESS_PQ_SEED_SCHEMES = EnumSet.of( + SignatureScheme.ML_DSA_44, SignatureScheme.ML_DSA_65, + SignatureScheme.FN_DSA); + private static void initLocalWitnesses(Config config, CLIParameter cmd) { // not a witness node, skip if (!PARAMETER.isWitness()) { @@ -1234,7 +1243,13 @@ private static void initLocalWitnesses(Config config, CLIParameter cmd) { if (config.hasPath(ConfigKey.LOCAL_WITNESS_SEED_PQ_SCHEME)) { String schemeName = config.getString(ConfigKey.LOCAL_WITNESS_SEED_PQ_SCHEME); try { - localWitnesses.setPqScheme(SignatureScheme.valueOf(schemeName)); + SignatureScheme scheme = SignatureScheme.valueOf(schemeName); + if (!WITNESS_PQ_SEED_SCHEMES.contains(scheme)) { + throw new TronError("invalid " + ConfigKey.LOCAL_WITNESS_SEED_PQ_SCHEME + + ": " + schemeName + "; valid values: " + WITNESS_PQ_SEED_SCHEMES, + TronError.ErrCode.WITNESS_INIT); + } + localWitnesses.setPqScheme(scheme); } catch (IllegalArgumentException e) { throw new TronError("invalid " + ConfigKey.LOCAL_WITNESS_SEED_PQ_SCHEME + ": " + schemeName, 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 index 5f8def68eeb..11ce71c3cae 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 @@ -251,6 +251,7 @@ private ConfigKey() { public static final String COMMITTEE_PROPOSAL_EXPIRE_TIME = "committee.proposalExpireTime"; public static final String COMMITTEE_ALLOW_TVM_OSAKA = "committee.allowTvmOsaka"; public static final String COMMITTEE_ALLOW_ML_DSA = "committee.allowMlDsa"; + public static final String COMMITTEE_ALLOW_FN_DSA = "committee.allowFnDsa"; public static final String ALLOW_ACCOUNT_ASSET_OPTIMIZATION = "committee.allowAccountAssetOptimization"; public static final String ALLOW_ASSET_OPTIMIZATION = "committee.allowAssetOptimization"; 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 0d5f63f1bed..506f11abcc8 100644 --- a/framework/src/main/java/org/tron/core/consensus/ProposalService.java +++ b/framework/src/main/java/org/tron/core/consensus/ProposalService.java @@ -400,6 +400,10 @@ public static boolean process(Manager manager, ProposalCapsule proposalCapsule) manager.getDynamicPropertiesStore().saveAllowMlDsa(entry.getValue()); break; } + case ALLOW_FN_DSA: { + manager.getDynamicPropertiesStore().saveAllowFnDsa(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 16b943c8ecf..3caf057a130 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -1768,7 +1768,7 @@ private void signBlockCapsule(BlockCapsule blockCapsule, Miner miner) { } private SignatureScheme resolveWitnessScheme(Miner miner) { - if (!chainBaseManager.getDynamicPropertiesStore().allowMlDsa()) { + if (!chainBaseManager.getDynamicPropertiesStore().isAnyPqSchemeAllowed()) { return SignatureScheme.UNKNOWN_SIG_SCHEME; } byte[] witnessAddress = miner.getWitnessAddress().toByteArray(); 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..cf863b8dfa5 --- /dev/null +++ b/framework/src/test/java/org/tron/common/crypto/pqc/FNDSATest.java @@ -0,0 +1,344 @@ +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.common.crypto.pqc.MLDSA44; +import org.tron.common.crypto.pqc.MLDSA65; +import org.tron.common.crypto.pqc.PqSignatureRegistry; +import org.tron.protos.Protocol.SignatureScheme; + +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(SignatureScheme.FN_DSA, 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(PqSignatureRegistry.verify( + SignatureScheme.FN_DSA, pk.getH(), msg, sigDirect)); + byte[] sigViaRegistry = PqSignatureRegistry.sign( + SignatureScheme.FN_DSA, sk.getEncoded(), msg); + assertTrue(FNDSA.verify(pk.getH(), msg, sigViaRegistry)); + assertEquals(FNDSA.PUBLIC_KEY_LENGTH, + PqSignatureRegistry.getPublicKeyLength(SignatureScheme.FN_DSA)); + assertEquals(FNDSA.SIGNATURE_LENGTH, + PqSignatureRegistry.getSignatureLength(SignatureScheme.FN_DSA)); + } + + @Test + public void registryIsValidSignatureLengthRespectsUpperBound() { + assertTrue(PqSignatureRegistry.isValidSignatureLength(SignatureScheme.FN_DSA, 1)); + assertTrue(PqSignatureRegistry.isValidSignatureLength( + SignatureScheme.FN_DSA, FNDSA.SIGNATURE_LENGTH)); + assertFalse(PqSignatureRegistry.isValidSignatureLength(SignatureScheme.FN_DSA, 0)); + assertFalse(PqSignatureRegistry.isValidSignatureLength( + SignatureScheme.FN_DSA, FNDSA.SIGNATURE_LENGTH + 1)); + } + + // ----- B.8 regression: fixed-length schemes still enforce strict equality ----- + + @Test + public void mlDsa44ValidateSignatureRemainsStrictEquality() { + MLDSA44 mlDsa44 = new MLDSA44(); + // exact length passes + mlDsa44.validateSignature(new byte[MLDSA44.SIGNATURE_LENGTH]); + // shorter rejected + try { + mlDsa44.validateSignature(new byte[MLDSA44.SIGNATURE_LENGTH - 1]); + fail("ML-DSA-44 must reject undersized signature"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("signature length")); + } + // longer rejected + try { + mlDsa44.validateSignature(new byte[MLDSA44.SIGNATURE_LENGTH + 1]); + fail("ML-DSA-44 must reject oversized signature"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("signature length")); + } + } + + @Test + public void mlDsa65ValidateSignatureRemainsStrictEquality() { + MLDSA65 mlDsa65 = new MLDSA65(); + mlDsa65.validateSignature(new byte[MLDSA65.SIGNATURE_LENGTH]); + try { + mlDsa65.validateSignature(new byte[MLDSA65.SIGNATURE_LENGTH - 1]); + fail("ML-DSA-65 must reject undersized signature"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("signature length")); + } + try { + mlDsa65.validateSignature(new byte[MLDSA65.SIGNATURE_LENGTH + 1]); + fail("ML-DSA-65 must reject oversized signature"); + } catch (IllegalArgumentException expected) { + assertTrue(expected.getMessage().contains("signature length")); + } + } + + @Test + public void registryIsValidSignatureLengthForFixedSchemesIsStrictEquality() { + assertTrue(PqSignatureRegistry.isValidSignatureLength( + SignatureScheme.ML_DSA_44, MLDSA44.SIGNATURE_LENGTH)); + assertFalse(PqSignatureRegistry.isValidSignatureLength( + SignatureScheme.ML_DSA_44, MLDSA44.SIGNATURE_LENGTH - 1)); + assertFalse(PqSignatureRegistry.isValidSignatureLength( + SignatureScheme.ML_DSA_44, MLDSA44.SIGNATURE_LENGTH + 1)); + + assertTrue(PqSignatureRegistry.isValidSignatureLength( + SignatureScheme.ML_DSA_65, MLDSA65.SIGNATURE_LENGTH)); + assertFalse(PqSignatureRegistry.isValidSignatureLength( + SignatureScheme.ML_DSA_65, MLDSA65.SIGNATURE_LENGTH - 1)); + assertFalse(PqSignatureRegistry.isValidSignatureLength( + SignatureScheme.ML_DSA_65, MLDSA65.SIGNATURE_LENGTH + 1)); + } +} 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 2a359425023..165bda24f73 100644 --- a/framework/src/test/java/org/tron/core/actuator/AccountPermissionUpdateActuatorTest.java +++ b/framework/src/test/java/org/tron/core/actuator/AccountPermissionUpdateActuatorTest.java @@ -1277,4 +1277,82 @@ public void validMlDsa65WitnessPermissionAccepted() throws ContractValidateExcep Assert.assertTrue(actuatorFor(any).validate()); } + + @Test + public void fnDsaPermissionRejectedWhenNotAllowed() { + dbManager.getDynamicPropertiesStore().saveAllowFnDsa(0L); + ByteString address = ByteString.copyFrom(ByteArray.fromHexString(OWNER_ADDRESS)); + Permission owner = ownerPermissionWithKeys( + java.util.Collections.singletonList( + mlDsaKey(KEY_ADDRESS, SignatureScheme.FN_DSA, 896, 1)), 2); + Permission active = activePermissionWithKeys( + java.util.Collections.singletonList(legacyKey(KEY_ADDRESS1)), 2); + Any any = getContract(address, owner, null, + java.util.Collections.singletonList(active)); + + try { + actuatorFor(any).validate(); + Assert.fail("should reject FN-DSA key when ALLOW_FN_DSA = 0"); + } catch (ContractValidateException e) { + Assert.assertTrue(e.getMessage().contains("FN-DSA is not activated")); + } + } + + @Test + public void fnDsaWrongPublicKeyLengthRejected() { + dbManager.getDynamicPropertiesStore().saveAllowFnDsa(1L); + ByteString address = ByteString.copyFrom(ByteArray.fromHexString(OWNER_ADDRESS)); + Permission owner = ownerPermissionWithKeys( + java.util.Collections.singletonList( + mlDsaKey(KEY_ADDRESS, SignatureScheme.FN_DSA, 895, 1)), 2); + Permission active = activePermissionWithKeys( + java.util.Collections.singletonList(legacyKey(KEY_ADDRESS1)), 2); + Any any = getContract(address, owner, null, + java.util.Collections.singletonList(active)); + + try { + actuatorFor(any).validate(); + Assert.fail("FN-DSA wrong public_key length should be rejected"); + } catch (ContractValidateException e) { + Assert.assertTrue(e.getMessage().contains("public_key length")); + } + } + + @Test + public void validFnDsaPermissionAccepted() throws ContractValidateException { + dbManager.getDynamicPropertiesStore().saveAllowFnDsa(1L); + ByteString address = ByteString.copyFrom(ByteArray.fromHexString(OWNER_ADDRESS)); + Permission owner = ownerPermissionWithKeys( + java.util.Collections.singletonList( + mlDsaKey(KEY_ADDRESS, SignatureScheme.FN_DSA, 896, 1)), + 2); + Permission active = activePermissionWithKeys( + java.util.Collections.singletonList( + mlDsaKey(KEY_ADDRESS1, SignatureScheme.FN_DSA, 896, 2)), + 2); + Any any = getContract(address, owner, null, + java.util.Collections.singletonList(active)); + + Assert.assertTrue(actuatorFor(any).validate()); + } + + @Test + public void validFnDsaWitnessPermissionAccepted() throws ContractValidateException { + dbManager.getDynamicPropertiesStore().saveAllowFnDsa(1L); + ByteString address = ByteString.copyFrom(ByteArray.fromHexString(WITNESS_ADDRESS)); + Permission owner = ownerPermissionWithKeys( + java.util.Collections.singletonList( + mlDsaKey(KEY_ADDRESS, SignatureScheme.FN_DSA, 896, 1)), + 2); + Permission witness = witnessPermissionWithKey( + mlDsaKey(KEY_ADDRESS1, SignatureScheme.FN_DSA, 896, 2)); + Permission active = activePermissionWithKeys( + java.util.Collections.singletonList( + mlDsaKey(KEY_ADDRESS2, SignatureScheme.FN_DSA, 896, 3)), + 2); + Any any = getContract(address, owner, witness, + java.util.Collections.singletonList(active)); + + Assert.assertTrue(actuatorFor(any).validate()); + } } \ No newline at end of file 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 a9a662a69ce..a8e98ea33bd 100644 --- a/framework/src/test/java/org/tron/core/capsule/TransactionCapsuleTest.java +++ b/framework/src/test/java/org/tron/core/capsule/TransactionCapsuleTest.java @@ -14,6 +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.MLDSA44; import org.tron.common.crypto.pqc.MLDSA65; import org.tron.common.crypto.pqc.PqAuthDigest; @@ -136,6 +137,7 @@ private void putAccountWithPqPermission( @Test public void authWitnessBeforeActivationRejected() { dbManager.getDynamicPropertiesStore().saveAllowMlDsa(0L); + dbManager.getDynamicPropertiesStore().saveAllowFnDsa(0L); Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0).toBuilder() .addAuthWitness(AuthWitness.newBuilder() .setSignerAddress(ByteString.copyFrom(ByteArray.fromHexString(PQ_SIGNER_HEX))) @@ -148,7 +150,7 @@ public void authWitnessBeforeActivationRejected() { dbManager.getDynamicPropertiesStore()); Assert.fail("should reject auth_witness before activation"); } catch (ValidateSignatureException e) { - Assert.assertTrue(e.getMessage().contains("ML-DSA not activated")); + Assert.assertTrue(e.getMessage().contains("no post-quantum scheme is activated")); } } @@ -408,4 +410,87 @@ public void mlDsa65AuthWitnessAlsoAccepted() throws Exception { Assert.assertTrue(cap.validatePubSignature(dbManager.getAccountStore(), dbManager.getDynamicPropertiesStore())); } + + @Test + public void fnDsaAuthWitnessAccepted() throws Exception { + dbManager.getDynamicPropertiesStore().saveAllowFnDsa(1L); + FNDSA kp = new FNDSA(); + putAccountWithPqPermission(PQ_OWNER_HEX, kp.getPublicKey(), SignatureScheme.FN_DSA); + + Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); + byte[] txid = Sha256Hash.of(true, tx.getRawData().toByteArray()).getBytes(); + byte[] signerAddr = ByteArray.fromHexString(PQ_SIGNER_HEX); + byte[] digest = PqAuthDigest.tx(txid, 0, signerAddr); + byte[] sig = FNDSA.sign(kp.getPrivateKey(), digest); + Assert.assertTrue("FN-DSA signature must be within protocol bound", + sig.length > 0 && sig.length <= FNDSA.SIGNATURE_LENGTH); + + Transaction signed = tx.toBuilder() + .addAuthWitness(AuthWitness.newBuilder() + .setSignerAddress(ByteString.copyFrom(signerAddr)) + .setSignature(ByteString.copyFrom(sig)) + .build()) + .build(); + TransactionCapsule cap = new TransactionCapsule(signed); + Assert.assertTrue(cap.validatePubSignature(dbManager.getAccountStore(), + dbManager.getDynamicPropertiesStore())); + } + + @Test + public void fnDsaTamperedAuthWitnessRejected() throws Exception { + dbManager.getDynamicPropertiesStore().saveAllowFnDsa(1L); + FNDSA kp = new FNDSA(); + putAccountWithPqPermission(PQ_OWNER_HEX, kp.getPublicKey(), SignatureScheme.FN_DSA); + + Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); + byte[] txid = Sha256Hash.of(true, tx.getRawData().toByteArray()).getBytes(); + byte[] signerAddr = ByteArray.fromHexString(PQ_SIGNER_HEX); + byte[] digest = PqAuthDigest.tx(txid, 0, signerAddr); + byte[] sig = FNDSA.sign(kp.getPrivateKey(), digest); + sig[0] ^= 0x01; + + Transaction signed = tx.toBuilder() + .addAuthWitness(AuthWitness.newBuilder() + .setSignerAddress(ByteString.copyFrom(signerAddr)) + .setSignature(ByteString.copyFrom(sig)) + .build()) + .build(); + TransactionCapsule cap = new TransactionCapsule(signed); + try { + cap.validatePubSignature(dbManager.getAccountStore(), + dbManager.getDynamicPropertiesStore()); + Assert.fail("tampered FN-DSA signature should be rejected"); + } catch (ValidateSignatureException e) { + Assert.assertTrue(e.getMessage().contains("pq sig invalid")); + } + } + + @Test + public void fnDsaAuthWitnessRejectedWhenNotActivated() throws Exception { + dbManager.getDynamicPropertiesStore().saveAllowMlDsa(0L); + dbManager.getDynamicPropertiesStore().saveAllowFnDsa(0L); + FNDSA kp = new FNDSA(); + putAccountWithPqPermission(PQ_OWNER_HEX, kp.getPublicKey(), SignatureScheme.FN_DSA); + + Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); + byte[] txid = Sha256Hash.of(true, tx.getRawData().toByteArray()).getBytes(); + byte[] signerAddr = ByteArray.fromHexString(PQ_SIGNER_HEX); + byte[] digest = PqAuthDigest.tx(txid, 0, signerAddr); + byte[] sig = FNDSA.sign(kp.getPrivateKey(), digest); + + Transaction signed = tx.toBuilder() + .addAuthWitness(AuthWitness.newBuilder() + .setSignerAddress(ByteString.copyFrom(signerAddr)) + .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 is 0"); + } catch (ValidateSignatureException expected) { + // accepted: rejection path triggered + } + } } \ No newline at end of file diff --git a/protocol/src/main/protos/core/Tron.proto b/protocol/src/main/protos/core/Tron.proto index 5be62364ebe..de37e555388 100644 --- a/protocol/src/main/protos/core/Tron.proto +++ b/protocol/src/main/protos/core/Tron.proto @@ -28,7 +28,9 @@ enum SignatureScheme { SM2_SM3 = 2; ML_DSA_44 = 3; ML_DSA_65 = 4; - reserved 5 to 15; + FN_DSA = 6; + reserved 5; + reserved 7 to 15; } // AccountId, (name, address) use name, (null, address) use address, (name, null) use name, From 7a5e8cd5f13d4e3633406486591aaff313fa5f60 Mon Sep 17 00:00:00 2001 From: federico Date: Mon, 27 Apr 2026 22:03:24 +0800 Subject: [PATCH 09/12] feat(vm): add FN-DSA verify precompile at 0x16 (EIP-8052) --- .../tron/core/vm/PrecompiledContracts.java | 60 ++++++ .../org/tron/core/vm/config/ConfigLoader.java | 1 + .../org/tron/core/vm/config/VMConfig.java | 10 + .../runtime/vm/FnDsaPrecompileTest.java | 185 ++++++++++++++++++ 4 files changed, 256 insertions(+) create mode 100644 framework/src/test/java/org/tron/common/runtime/vm/FnDsaPrecompileTest.java 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 eb7f3f0305b..5f5769ef60c 100644 --- a/actuator/src/main/java/org/tron/core/vm/PrecompiledContracts.java +++ b/actuator/src/main/java/org/tron/core/vm/PrecompiledContracts.java @@ -45,6 +45,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.MLDSA44; import org.tron.common.crypto.pqc.MLDSA65; import org.tron.common.crypto.zksnark.BN128; @@ -111,6 +112,7 @@ public class PrecompiledContracts { private static final VerifyMlDsa44 verifyMlDsa44 = new VerifyMlDsa44(); private static final VerifyMlDsa65 verifyMlDsa65 = new VerifyMlDsa65(); + private static final VerifyFnDsa verifyFnDsa = new VerifyFnDsa(); // FreezeV2 PrecompileContracts private static final GetChainParameter getChainParameter = new GetChainParameter(); @@ -213,6 +215,11 @@ public class PrecompiledContracts { // 0x14: ML-DSA-65 verify (TRON extension, FIPS-204 / SHAKE256, raw 1952-byte public key). private static final DataWord verifyMlDsa65Addr = new DataWord( "0000000000000000000000000000000000000000000000000000000000000014"); + // 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"); public static PrecompiledContract getOptimizedContractForConstant(PrecompiledContract contract) { try { @@ -302,6 +309,9 @@ public static PrecompiledContract getContractForAddress(DataWord address) { if (VMConfig.allowMlDsa() && address.equals(verifyMlDsa65Addr)) { return verifyMlDsa65; } + if (VMConfig.allowFnDsa() && address.equals(verifyFnDsaAddr)) { + return verifyFnDsa; + } if (VMConfig.allowTvmFreezeV2()) { if (address.equals(getChainParameterAddr)) { @@ -2310,4 +2320,54 @@ 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()); + } + } + } + } 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 a7d74418f6c..a5102462061 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.initAllowMlDsa(ds.getAllowMlDsa()); + VMConfig.initAllowFnDsa(ds.getAllowFnDsa()); } } } 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 1a45b6ed786..7b6fd492a6b 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_ML_DSA = false; + private static boolean ALLOW_FN_DSA = false; + private VMConfig() { } @@ -184,6 +186,10 @@ public static void initAllowMlDsa(long allow) { ALLOW_ML_DSA = allow == 1; } + public static void initAllowFnDsa(long allow) { + ALLOW_FN_DSA = allow == 1; + } + public static boolean getEnergyLimitHardFork() { return CommonParameter.ENERGY_LIMIT_HARD_FORK; } @@ -291,4 +297,8 @@ public static boolean allowTvmOsaka() { public static boolean allowMlDsa() { return ALLOW_ML_DSA; } + + public static boolean allowFnDsa() { + return ALLOW_FN_DSA; + } } 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..edf349c812a --- /dev/null +++ b/framework/src/test/java/org/tron/common/runtime/vm/FnDsaPrecompileTest.java @@ -0,0 +1,185 @@ +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.initAllowFnDsa(1L); + } + + @After + public void disableProposal() { + VMConfig.initAllowFnDsa(0L); + } + + @Test + public void switchOff_returnsNull() { + VMConfig.initAllowFnDsa(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) + byte[] input = new byte[32 + 2 + pk.length]; + 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; + } +} From 9f589d5565c23060118d9ef2a4ee71af0edebd98 Mon Sep 17 00:00:00 2001 From: federico Date: Tue, 28 Apr 2026 10:41:44 +0800 Subject: [PATCH 10/12] refactor(protocol): introduce PqPublicKey, PqAuthWitness and key_id-based signing --- .../AccountPermissionUpdateActuator.java | 31 +++-- .../org/tron/core/capsule/BlockCapsule.java | 60 +++++----- .../tron/core/capsule/TransactionCapsule.java | 57 +++++---- .../org/tron/core/db/BandwidthProcessor.java | 12 +- .../tron/common/crypto/pqc/PqAuthDigest.java | 22 ++-- .../main/java/org/tron/core/db/Manager.java | 14 +-- .../common/crypto/pqc/PqAuthDigestTest.java | 81 ++++++------- .../common/crypto/pqc/program/PqFullNode.java | 4 +- .../common/crypto/pqc/program/PqcClient.java | 9 +- .../crypto/pqc/program/PqcWitnessNode.java | 13 +- .../org/tron/core/BandwidthProcessorTest.java | 20 ++-- .../AccountPermissionUpdateActuatorTest.java | 23 ++-- .../tron/core/capsule/BlockCapsulePqTest.java | 54 ++++----- .../core/capsule/TransactionCapsuleTest.java | 112 ++++++++---------- .../org/tron/core/services/http/UtilTest.java | 25 ++-- protocol/src/main/protos/core/Tron.proto | 47 ++++---- 16 files changed, 287 insertions(+), 297 deletions(-) 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 284ae37a5d9..137db8fe413 100644 --- a/actuator/src/main/java/org/tron/core/actuator/AccountPermissionUpdateActuator.java +++ b/actuator/src/main/java/org/tron/core/actuator/AccountPermissionUpdateActuator.java @@ -100,10 +100,15 @@ private boolean checkPermission(Permission permission) throws ContractValidateEx long weightSum = 0; List addressList = permission.getKeysList() .stream() - .map(x -> x.getAddress()) + .map(Key::getAddress) + .filter(addr -> !addr.isEmpty()) .distinct() .collect(toList()); - if (addressList.size() != permission.getKeysList().size()) { + long nonEmptyAddrCount = permission.getKeysList().stream() + .map(Key::getAddress) + .filter(addr -> !addr.isEmpty()) + .count(); + if (addressList.size() != nonEmptyAddrCount) { throw new ContractValidateException( "address should be distinct in permission " + permission.getType()); } @@ -111,12 +116,12 @@ private boolean checkPermission(Permission permission) throws ContractValidateEx List publicKeyList = permission.getKeysList() .stream() - .map(Key::getPublicKey) + .map(k -> k.hasPqKey() ? k.getPqKey().getPublicKey() : ByteString.EMPTY) .filter(pk -> !pk.isEmpty()) .distinct() .collect(toList()); long nonEmptyPublicKeyCount = permission.getKeysList().stream() - .map(Key::getPublicKey) + .map(k -> k.hasPqKey() ? k.getPqKey().getPublicKey() : ByteString.EMPTY) .filter(pk -> !pk.isEmpty()) .count(); if (publicKeyList.size() != nonEmptyPublicKeyCount) { @@ -125,7 +130,8 @@ private boolean checkPermission(Permission permission) throws ContractValidateEx } for (Key key : permission.getKeysList()) { - if (!DecodeUtil.addressValid(key.getAddress().toByteArray())) { + if (!key.getAddress().isEmpty() + && !DecodeUtil.addressValid(key.getAddress().toByteArray())) { throw new ContractValidateException("key is not a validate address"); } if (key.getWeight() <= 0) { @@ -263,15 +269,15 @@ public long calcFee() { private void validatePermissionScheme(Permission permission) throws ContractValidateException { DynamicPropertiesStore dynamicStore = chainBaseManager.getDynamicPropertiesStore(); - SignatureScheme first = permission.getKeysList().get(0).getScheme(); + SignatureScheme first = keyScheme(permission.getKeysList().get(0)); for (Key key : permission.getKeysList()) { - SignatureScheme scheme = key.getScheme(); + SignatureScheme scheme = keyScheme(key); if (scheme != first) { throw new ContractValidateException( "all keys in a permission must use the same scheme"); } if (scheme == SignatureScheme.UNKNOWN_SIG_SCHEME) { - if (!key.getPublicKey().isEmpty()) { + if (key.hasPqKey() && !key.getPqKey().getPublicKey().isEmpty()) { throw new ContractValidateException( "public_key must be empty when scheme is UNKNOWN_SIG_SCHEME"); } @@ -285,10 +291,11 @@ private void validatePermissionScheme(Permission permission) throws ContractVali throw new ContractValidateException( "unsupported signature scheme: " + scheme); } - if (key.getPublicKey().size() != expected) { + int actual = key.hasPqKey() ? key.getPqKey().getPublicKey().size() : 0; + if (actual != expected) { throw new ContractValidateException( "public_key length for " + scheme + " must be " + expected + " bytes, got " - + key.getPublicKey().size()); + + actual); } } } @@ -301,6 +308,10 @@ private void validatePermissionScheme(Permission permission) throws ContractVali } } + private static SignatureScheme keyScheme(Key key) { + return key.hasPqKey() ? key.getPqKey().getScheme() : SignatureScheme.UNKNOWN_SIG_SCHEME; + } + private static int expectedPublicKeyLength(SignatureScheme scheme) { switch (scheme) { case ML_DSA_44: 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 4ab7b690578..aac0d62c271 100755 --- a/chainbase/src/main/java/org/tron/core/capsule/BlockCapsule.java +++ b/chainbase/src/main/java/org/tron/core/capsule/BlockCapsule.java @@ -43,7 +43,7 @@ import org.tron.core.exception.ValidateSignatureException; import org.tron.core.store.AccountStore; import org.tron.core.store.DynamicPropertiesStore; -import org.tron.protos.Protocol.AuthWitness; +import org.tron.protos.Protocol.PqAuthWitness; import org.tron.protos.Protocol.Block; import org.tron.protos.Protocol.BlockHeader; import org.tron.protos.Protocol.Key; @@ -179,9 +179,9 @@ public void sign(byte[] privateKey) { } - public void setWitnessAuth(AuthWitness authWitness) { + public void setPqWitness(PqAuthWitness pqWitness) { BlockHeader blockHeader = this.block.getBlockHeader().toBuilder() - .setWitnessAuth(authWitness).build(); + .setPqWitness(pqWitness).build(); this.block = this.block.toBuilder().setBlockHeader(blockHeader).build(); } @@ -198,14 +198,14 @@ public boolean validateSignature(DynamicPropertiesStore dynamicPropertiesStore, AccountStore accountStore) throws ValidateSignatureException { BlockHeader header = block.getBlockHeader(); boolean hasLegacy = !header.getWitnessSignature().isEmpty(); - AuthWitness witnessAuth = header.getWitnessAuth(); + PqAuthWitness witnessAuth = header.getPqWitness(); boolean hasAuth = witnessAuth != null && witnessAuth.getSignature() != null && !witnessAuth.getSignature().isEmpty(); if (hasLegacy && hasAuth) { throw new ValidateSignatureException( - "witness_signature and witness_auth are mutually exclusive"); + "witness_signature and pq_witness are mutually exclusive"); } if (!hasLegacy && !hasAuth) { throw new ValidateSignatureException("missing witness signature"); @@ -226,12 +226,15 @@ private boolean validateLegacySignature(DynamicPropertiesStore dynamicProperties AccountCapsule accountCapsule = accountStore.get(witnessAccountAddress); if (accountCapsule != null && accountCapsule.getInstance().hasWitnessPermission()) { Permission witnessPermission = accountCapsule.getInstance().getWitnessPermission(); - if (witnessPermission.getKeysCount() > 0 - && PqSignatureRegistry.contains(witnessPermission.getKeys(0).getScheme())) { - throw new ValidateSignatureException( - "witness permission requires PQ scheme " - + witnessPermission.getKeys(0).getScheme() - + " but witness_signature is legacy"); + if (witnessPermission.getKeysCount() > 0) { + Key k = witnessPermission.getKeys(0); + SignatureScheme ks = k.hasPqKey() + ? k.getPqKey().getScheme() : SignatureScheme.UNKNOWN_SIG_SCHEME; + if (PqSignatureRegistry.contains(ks)) { + throw new ValidateSignatureException( + "witness permission requires PQ scheme " + ks + + " but witness_signature is legacy"); + } } } } @@ -252,11 +255,11 @@ private boolean validateLegacySignature(DynamicPropertiesStore dynamicProperties } private boolean validateWitnessAuth(DynamicPropertiesStore dynamicPropertiesStore, - AccountStore accountStore, byte[] witnessAccountAddress, AuthWitness witnessAuth) + AccountStore accountStore, byte[] witnessAccountAddress, PqAuthWitness witnessAuth) throws ValidateSignatureException { if (!dynamicPropertiesStore.isAnyPqSchemeAllowed()) { throw new ValidateSignatureException( - "witness_auth present but no post-quantum scheme is activated"); + "pq_witness present but no post-quantum scheme is activated"); } AccountCapsule accountCapsule = accountStore.get(witnessAccountAddress); Permission witnessPermission = null; @@ -265,9 +268,18 @@ private boolean validateWitnessAuth(DynamicPropertiesStore dynamicPropertiesStor } if (witnessPermission == null || witnessPermission.getKeysCount() == 0) { throw new ValidateSignatureException( - "witness_auth present but witness permission is not configured"); + "pq_witness present but witness permission is not configured"); } - SignatureScheme scheme = witnessPermission.getKeys(0).getScheme(); + int keyId = witnessAuth.getKeyId(); + if (keyId < 0 || keyId >= witnessPermission.getKeysCount()) { + throw new ValidateSignatureException("pq_witness key_id out of range: " + keyId); + } + Key matched = witnessPermission.getKeys(keyId); + if (!matched.hasPqKey()) { + throw new ValidateSignatureException( + "witness permission key at index " + keyId + " is not a PQ key"); + } + SignatureScheme scheme = matched.getPqKey().getScheme(); if (!PqSignatureRegistry.contains(scheme)) { throw new ValidateSignatureException( "witness permission scheme " + scheme + " is not allowed for block signing"); @@ -277,22 +289,10 @@ private boolean validateWitnessAuth(DynamicPropertiesStore dynamicPropertiesStor "witness permission scheme " + scheme + " is not activated"); } - byte[] signerAddr = witnessAuth.getSignerAddress().toByteArray(); - Key matched = null; - for (Key k : witnessPermission.getKeysList()) { - if (Arrays.equals(k.getAddress().toByteArray(), signerAddr)) { - matched = k; - break; - } - } - if (matched == null) { - throw new ValidateSignatureException( - "witness_auth signer not found in witness permission"); - } - byte[] publicKey = matched.getPublicKey().toByteArray(); + byte[] publicKey = matched.getPqKey().getPublicKey().toByteArray(); byte[] signature = witnessAuth.getSignature().toByteArray(); byte[] rawHdrHash = getRawHash().getBytes(); - byte[] digest = PqAuthDigest.block(rawHdrHash, signerAddr); + byte[] digest = PqAuthDigest.block(rawHdrHash, keyId); return PqSignatureRegistry.verify(scheme, publicKey, digest, signature); } @@ -407,7 +407,7 @@ public boolean hasWitnessSignature() { if (!header.getWitnessSignature().isEmpty()) { return true; } - AuthWitness auth = header.getWitnessAuth(); + PqAuthWitness auth = header.getPqWitness(); return auth != null && !auth.getSignature().isEmpty(); } 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 c5f624249ec..86ec963bce3 100755 --- a/chainbase/src/main/java/org/tron/core/capsule/TransactionCapsule.java +++ b/chainbase/src/main/java/org/tron/core/capsule/TransactionCapsule.java @@ -67,7 +67,7 @@ import org.tron.core.exception.ValidateSignatureException; import org.tron.core.store.AccountStore; import org.tron.core.store.DynamicPropertiesStore; -import org.tron.protos.Protocol.AuthWitness; +import org.tron.protos.Protocol.PqAuthWitness; import org.tron.protos.Protocol.Key; import org.tron.protos.Protocol.Permission; import org.tron.protos.Protocol.Permission.PermissionType; @@ -489,10 +489,11 @@ public static boolean validateSignature(Transaction transaction, throw new PermissionException("permission isn't exit"); } checkPermission(permissionId, permission, contract); - if (permission.getKeysCount() > 0 - && permission.getKeysList().get(0).getScheme() != SignatureScheme.UNKNOWN_SIG_SCHEME) { + Key firstKey = permission.getKeysCount() > 0 ? permission.getKeysList().get(0) : null; + if (firstKey != null && firstKey.hasPqKey() + && firstKey.getPqKey().getScheme() != SignatureScheme.UNKNOWN_SIG_SCHEME) { throw new PermissionException( - "permission uses PQ scheme, auth_witness is required"); + "permission uses PQ scheme, pq_witness is required"); } long weight = checkWeight(permission, transaction.getSignatureList(), hash, null); if (weight >= permission.getThreshold()) { @@ -648,15 +649,15 @@ public boolean validatePubSignature(AccountStore accountStore, throws ValidateSignatureException { if (!isVerified) { int legacyCount = this.transaction.getSignatureCount(); - int pqCount = this.transaction.getAuthWitnessCount(); + int pqCount = this.transaction.getPqWitnessCount(); if (pqCount > 0 && !dynamicPropertiesStore.isAnyPqSchemeAllowed()) { throw new ValidateSignatureException( - "auth_witness not allowed: no post-quantum scheme is activated"); + "pq_witness not allowed: no post-quantum scheme is activated"); } if (legacyCount > 0 && pqCount > 0) { throw new ValidateSignatureException( - "signature and auth_witness are mutually exclusive"); + "signature and pq_witness are mutually exclusive"); } if (legacyCount == 0 && pqCount == 0) { throw new ValidateSignatureException("miss sig or contract"); @@ -727,33 +728,38 @@ static boolean validateStructuredSignature(Transaction transaction, checkPermission(permissionId, permission, contract); if (permission.getKeysCount() == 0 - || permission.getKeysList().get(0).getScheme() == SignatureScheme.UNKNOWN_SIG_SCHEME) { + || !permission.getKeysList().get(0).hasPqKey() + || permission.getKeysList().get(0).getPqKey().getScheme() + == SignatureScheme.UNKNOWN_SIG_SCHEME) { throw new PermissionException( - "permission uses legacy scheme, auth_witness is not allowed"); + "permission uses legacy scheme, pq_witness is not allowed"); } byte[] txid = computeRawHash(transaction).getBytes(); - List witnesses = transaction.getAuthWitnessList(); - java.util.Set seen = new java.util.HashSet<>(); + List witnesses = transaction.getPqWitnessList(); + java.util.Set seen = new java.util.HashSet<>(); long weight = 0L; - for (AuthWitness aw : witnesses) { - ByteString signer = aw.getSignerAddress(); - if (!seen.add(signer)) { - throw new PermissionException("duplicate signer in auth_witness"); + for (PqAuthWitness aw : witnesses) { + int keyId = aw.getKeyId(); + if (!seen.add(keyId)) { + throw new PermissionException("duplicate key_id in pq_witness"); } - Key key = findKeyByAddress(permission, signer); - if (key == null) { - throw new PermissionException("signer is not in permission"); + if (keyId < 0 || keyId >= permission.getKeysCount()) { + throw new PermissionException("key_id out of range: " + keyId); } - SignatureScheme scheme = key.getScheme(); + Key key = permission.getKeys(keyId); + if (!key.hasPqKey()) { + throw new PermissionException("key at index " + keyId + " is not a PQ key"); + } + SignatureScheme scheme = key.getPqKey().getScheme(); if (!PqSignatureRegistry.contains(scheme)) { throw new PermissionException("unsupported scheme: " + scheme); } if (!dynamicPropertiesStore.isPqSchemeAllowed(scheme)) { throw new PermissionException(scheme + " is not activated"); } - byte[] digest = PqAuthDigest.tx(txid, permissionId, signer.toByteArray()); - byte[] pk = key.getPublicKey().toByteArray(); + byte[] digest = PqAuthDigest.tx(txid, permissionId, keyId); + byte[] pk = key.getPqKey().getPublicKey().toByteArray(); byte[] sig = aw.getSignature().toByteArray(); if (pk.length != PqSignatureRegistry.getPublicKeyLength(scheme) || !PqSignatureRegistry.isValidSignatureLength(scheme, sig.length)) { @@ -776,15 +782,6 @@ private static Sha256Hash computeRawHash(Transaction transaction) { transaction.getRawData().toByteArray()); } - private static Key findKeyByAddress(Permission permission, ByteString address) { - for (Key k : permission.getKeysList()) { - if (k.getAddress().equals(address)) { - return k; - } - } - return null; - } - /** * 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 a3529fb0c78..40a43ddea85 100644 --- a/chainbase/src/main/java/org/tron/core/db/BandwidthProcessor.java +++ b/chainbase/src/main/java/org/tron/core/db/BandwidthProcessor.java @@ -141,13 +141,13 @@ public void consume(TransactionCapsule trx, TransactionTrace trace) long maxCreateAccountTxSize = dynamicPropertiesStore.getMaxCreateAccountTxSize(); int signatureCount = trx.getInstance().getSignatureCount(); long sigOverhead = signatureCount * PER_SIGN_LENGTH; - if (trx.getInstance().getAuthWitnessCount() > 0) { - long authWitnessBytes = 0L; - for (org.tron.protos.Protocol.AuthWitness aw - : trx.getInstance().getAuthWitnessList()) { - authWitnessBytes += aw.getSerializedSize(); + if (trx.getInstance().getPqWitnessCount() > 0) { + long pqWitnessBytes = 0L; + for (org.tron.protos.Protocol.PqAuthWitness aw + : trx.getInstance().getPqWitnessList()) { + pqWitnessBytes += aw.getSerializedSize(); } - sigOverhead = authWitnessBytes; + sigOverhead = pqWitnessBytes; } long createAccountBytesSize = trx.getInstance().toBuilder().clearRet() .build().getSerializedSize() - sigOverhead; diff --git a/crypto/src/main/java/org/tron/common/crypto/pqc/PqAuthDigest.java b/crypto/src/main/java/org/tron/common/crypto/pqc/PqAuthDigest.java index 32ab9a8659e..06aac5b0e4f 100644 --- a/crypto/src/main/java/org/tron/common/crypto/pqc/PqAuthDigest.java +++ b/crypto/src/main/java/org/tron/common/crypto/pqc/PqAuthDigest.java @@ -26,31 +26,35 @@ private PqAuthDigest() { /** * Transaction-level PQ authentication digest. * - *

digest = SHA-256("TRON_TX_AUTH_V1" || txid || permission_id_be4 || signer_address)
+ *
digest = SHA-256("TRON_TX_AUTH_V1" || txid || permission_id_be4 || key_id_be4)
+ * + *

{@code keyId} is the 0-based index of the signing key in the permission's key list. + * For single-key permissions the caller passes 0. */ - public static byte[] tx(byte[] txid, int permissionId, byte[] signerAddress) { + public static byte[] tx(byte[] txid, int permissionId, int keyId) { requireNonNull(txid, "txid"); - requireNonNull(signerAddress, "signerAddress"); MessageDigest md = Sha256Hash.newDigest(); md.update(TX_DOMAIN_BYTES); md.update(txid); md.update(intToBe4(permissionId)); - md.update(signerAddress); + md.update(intToBe4(keyId)); return md.digest(); } /** * Block-level PQ authentication digest. * - *

digest = SHA-256("TRON_BLOCK_AUTH_V1" || block_header_raw_hash || witness_address)
+ *
digest = SHA-256("TRON_BLOCK_AUTH_V1" || block_header_raw_hash || key_id_be4)
+ * + *

{@code keyId} is the 0-based index of the signing key in the witness permission's key list. + * For the typical single-key witness permission the caller passes 0. */ - public static byte[] block(byte[] blockHeaderRawHash, byte[] witnessAddress) { + public static byte[] block(byte[] blockHeaderRawHash, int keyId) { requireNonNull(blockHeaderRawHash, "blockHeaderRawHash"); - requireNonNull(witnessAddress, "witnessAddress"); MessageDigest md = Sha256Hash.newDigest(); md.update(BLOCK_DOMAIN_BYTES); md.update(blockHeaderRawHash); - md.update(witnessAddress); + md.update(intToBe4(keyId)); return md.digest(); } @@ -63,7 +67,7 @@ private static byte[] intToBe4(int v) { }; } - private static void requireNonNull(byte[] b, String name) { + static void requireNonNull(byte[] b, String name) { if (b == null) { throw new IllegalArgumentException(name + " must not be null"); } 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 3caf057a130..052e973c81d 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -170,7 +170,8 @@ import org.tron.core.utils.TransactionRegister; import org.tron.protos.Protocol; import org.tron.protos.Protocol.AccountType; -import org.tron.protos.Protocol.AuthWitness; +import org.tron.protos.Protocol.PqAuthWitness; +import org.tron.protos.Protocol.Key; import org.tron.protos.Protocol.Permission; import org.tron.protos.Protocol.SignatureScheme; import org.tron.protos.Protocol.Transaction; @@ -1780,7 +1781,8 @@ private SignatureScheme resolveWitnessScheme(Miner miner) { if (witnessPermission.getKeysCount() == 0) { return SignatureScheme.UNKNOWN_SIG_SCHEME; } - return witnessPermission.getKeys(0).getScheme(); + Key k = witnessPermission.getKeys(0); + return k.hasPqKey() ? k.getPqKey().getScheme() : SignatureScheme.UNKNOWN_SIG_SCHEME; } private void signWitnessAuth(BlockCapsule blockCapsule, Miner miner, SignatureScheme scheme) { @@ -1793,14 +1795,12 @@ private void signWitnessAuth(BlockCapsule blockCapsule, Miner miner, SignatureSc "witness permission requires " + scheme + " but local PQ private key is not configured"); } - byte[] signerAddress = witnessPermission.getKeys(0).getAddress().toByteArray(); - byte[] digest = PqAuthDigest.block(blockCapsule.getRawHashBytes(), signerAddress); + byte[] digest = PqAuthDigest.block(blockCapsule.getRawHashBytes(), 0); byte[] signature = PqSignatureRegistry.sign(scheme, pqPrivateKey, digest); - AuthWitness witnessAuth = AuthWitness.newBuilder() - .setSignerAddress(ByteString.copyFrom(signerAddress)) + PqAuthWitness witnessAuth = PqAuthWitness.newBuilder() .setSignature(ByteString.copyFrom(signature)) .build(); - blockCapsule.setWitnessAuth(witnessAuth); + blockCapsule.setPqWitness(witnessAuth); } private void filterOwnerAddress(TransactionCapsule transactionCapsule, Set result) { diff --git a/framework/src/test/java/org/tron/common/crypto/pqc/PqAuthDigestTest.java b/framework/src/test/java/org/tron/common/crypto/pqc/PqAuthDigestTest.java index c8ad1cd8c36..3ee5f6d008e 100644 --- a/framework/src/test/java/org/tron/common/crypto/pqc/PqAuthDigestTest.java +++ b/framework/src/test/java/org/tron/common/crypto/pqc/PqAuthDigestTest.java @@ -13,28 +13,29 @@ public class PqAuthDigestTest { - private static byte[] bytes(int... values) { - byte[] out = new byte[values.length]; - for (int i = 0; i < values.length; i++) { - out[i] = (byte) values[i]; - } - return out; + private static byte[] be4(int v) { + return new byte[] { + (byte) ((v >>> 24) & 0xff), + (byte) ((v >>> 16) & 0xff), + (byte) ((v >>> 8) & 0xff), + (byte) (v & 0xff) + }; } @Test public void txDigestEqualsExpectedSha256() throws Exception { - byte[] txid = bytes(0x11, 0x22, 0x33, 0x44); + byte[] txid = new byte[] {0x11, 0x22, 0x33, 0x44}; int permissionId = 2; - byte[] signer = bytes(0xaa, 0xbb, 0xcc); + int keyId = 3; MessageDigest md = MessageDigest.getInstance("SHA-256"); md.update("TRON_TX_AUTH_V1".getBytes(StandardCharsets.UTF_8)); md.update(txid); - md.update(bytes(0, 0, 0, 2)); - md.update(signer); + md.update(be4(permissionId)); + md.update(be4(keyId)); byte[] expected = md.digest(); - byte[] actual = PqAuthDigest.tx(txid, permissionId, signer); + byte[] actual = PqAuthDigest.tx(txid, permissionId, keyId); assertArrayEquals(expected, actual); assertEquals(32, actual.length); } @@ -45,15 +46,15 @@ public void blockDigestEqualsExpectedSha256() throws Exception { for (int i = 0; i < hdrHash.length; i++) { hdrHash[i] = (byte) i; } - byte[] witness = bytes(0x41, 0x42, 0x43); + int keyId = 0; MessageDigest md = MessageDigest.getInstance("SHA-256"); md.update("TRON_BLOCK_AUTH_V1".getBytes(StandardCharsets.UTF_8)); md.update(hdrHash); - md.update(witness); + md.update(be4(keyId)); byte[] expected = md.digest(); - byte[] actual = PqAuthDigest.block(hdrHash, witness); + byte[] actual = PqAuthDigest.block(hdrHash, keyId); assertArrayEquals(expected, actual); assertEquals(32, actual.length); } @@ -61,40 +62,34 @@ public void blockDigestEqualsExpectedSha256() throws Exception { @Test public void txAndBlockDigestsDifferForSameContext() { byte[] shared = new byte[32]; - byte[] addr = new byte[] {1, 2, 3, 4, 5}; - byte[] txDigest = PqAuthDigest.tx(shared, 0, addr); - byte[] blockDigest = PqAuthDigest.block(shared, addr); + byte[] txDigest = PqAuthDigest.tx(shared, 0, 0); + byte[] blockDigest = PqAuthDigest.block(shared, 0); assertFalse("tx and block digests must not collide for shared inputs", java.util.Arrays.equals(txDigest, blockDigest)); } @Test - public void differentSignersProduceDifferentTxDigest() { + public void differentKeyIdsProduceDifferentTxDigest() { byte[] txid = new byte[32]; - byte[] a = new byte[] {0x10}; - byte[] b = new byte[] {0x20}; assertNotEquals( - new String(PqAuthDigest.tx(txid, 0, a)), - new String(PqAuthDigest.tx(txid, 0, b))); + new String(PqAuthDigest.tx(txid, 0, 0)), + new String(PqAuthDigest.tx(txid, 0, 1))); } @Test public void differentPermissionIdsProduceDifferentDigest() { byte[] txid = new byte[32]; - byte[] addr = new byte[] {1}; - byte[] d0 = PqAuthDigest.tx(txid, 0, addr); - byte[] d1 = PqAuthDigest.tx(txid, 1, addr); + byte[] d0 = PqAuthDigest.tx(txid, 0, 0); + byte[] d1 = PqAuthDigest.tx(txid, 1, 0); assertFalse(java.util.Arrays.equals(d0, d1)); } @Test - public void differentWitnessesProduceDifferentBlockDigest() { + public void differentKeyIdsProduceDifferentBlockDigest() { byte[] hdr = new byte[32]; - byte[] w1 = new byte[] {1}; - byte[] w2 = new byte[] {2}; - byte[] d1 = PqAuthDigest.block(hdr, w1); - byte[] d2 = PqAuthDigest.block(hdr, w2); - assertFalse(java.util.Arrays.equals(d1, d2)); + byte[] d0 = PqAuthDigest.block(hdr, 0); + byte[] d1 = PqAuthDigest.block(hdr, 1); + assertFalse(java.util.Arrays.equals(d0, d1)); } @Test @@ -104,30 +99,22 @@ public void domainPrefixesAreExact() { } @Test - public void nullInputsRejected() { + public void nullTxidRejected() { try { - PqAuthDigest.tx(null, 0, new byte[1]); + PqAuthDigest.tx(null, 0, 0); fail("null txid should be rejected"); } catch (IllegalArgumentException expected) { assertTrue(expected.getMessage().contains("txid")); } + } + + @Test + public void nullBlockHeaderHashRejected() { try { - PqAuthDigest.tx(new byte[1], 0, null); - fail("null signer should be rejected"); - } catch (IllegalArgumentException expected) { - assertTrue(expected.getMessage().contains("signerAddress")); - } - try { - PqAuthDigest.block(null, new byte[1]); - fail("null hdr should be rejected"); + PqAuthDigest.block(null, 0); + fail("null blockHeaderRawHash should be rejected"); } catch (IllegalArgumentException expected) { assertTrue(expected.getMessage().contains("blockHeaderRawHash")); } - try { - PqAuthDigest.block(new byte[1], null); - fail("null witness should be rejected"); - } catch (IllegalArgumentException expected) { - assertTrue(expected.getMessage().contains("witnessAddress")); - } } } 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 2a71ef6f794..798d1c16654 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 @@ -21,7 +21,7 @@ * Both nodes share the same deterministic PQ genesis pre-state (witness account with an * ML-DSA-44 witness permission + demo user account with an ML-DSA-44 owner permission), * installed via {@link PqcWitnessNode#installPqGenesisState}. Once the witness produces - * a block it is broadcast over P2P; this node validates {@code BlockHeader.witness_auth} + * a block it is broadcast over P2P; this node validates {@code BlockHeader.pq_witness} * against the same on-chain public key and applies the block. * * Usage: @@ -98,7 +98,7 @@ public static void main(String[] args) throws Exception { ChainBaseManager chain = context.getBean(ChainBaseManager.class); // ── 4. Install matching PQ genesis pre-state ────────────────────────── - // Without this the incoming witness_auth would fail to validate because + // Without this the incoming pq_witness would fail to validate because // this node wouldn't know the witness's ML-DSA-44 public key. PqcWitnessNode.installPqGenesisState(db, chain, witnessPub, userPub); diff --git a/framework/src/test/java/org/tron/common/crypto/pqc/program/PqcClient.java b/framework/src/test/java/org/tron/common/crypto/pqc/program/PqcClient.java index 79b8c8e4b89..c4442adb9c3 100644 --- a/framework/src/test/java/org/tron/common/crypto/pqc/program/PqcClient.java +++ b/framework/src/test/java/org/tron/common/crypto/pqc/program/PqcClient.java @@ -15,7 +15,7 @@ import org.tron.common.crypto.pqc.MLDSA44; import org.tron.common.crypto.pqc.PqAuthDigest; import org.tron.common.utils.ByteArray; -import org.tron.protos.Protocol.AuthWitness; +import org.tron.protos.Protocol.PqAuthWitness; import org.tron.protos.Protocol.Block; import org.tron.protos.Protocol.Transaction; import org.tron.protos.Protocol.Transaction.Contract.ContractType; @@ -107,14 +107,13 @@ public static void main(String[] args) throws Exception { Transaction tx = Transaction.newBuilder().setRawData(rawData).build(); - // ── 5. Sign with ML-DSA-44 auth_witness ────────────────────────── + // ── 5. Sign with ML-DSA-44 pq_witness ────────────────────────── byte[] txId = sha256(rawData.toByteArray()); - byte[] digest = PqAuthDigest.tx(txId, 0, signerAddr); + byte[] digest = PqAuthDigest.tx(txId, 0, 0); byte[] sig = MLDSA44.sign(userPriv, digest); Transaction signedTx = tx.toBuilder() - .addAuthWitness(AuthWitness.newBuilder() - .setSignerAddress(ByteString.copyFrom(signerAddr)) + .addPqWitness(PqAuthWitness.newBuilder() .setSignature(ByteString.copyFrom(sig))) .build(); diff --git a/framework/src/test/java/org/tron/common/crypto/pqc/program/PqcWitnessNode.java b/framework/src/test/java/org/tron/common/crypto/pqc/program/PqcWitnessNode.java index bf133036e2c..c9fb23ca0c1 100644 --- a/framework/src/test/java/org/tron/common/crypto/pqc/program/PqcWitnessNode.java +++ b/framework/src/test/java/org/tron/common/crypto/pqc/program/PqcWitnessNode.java @@ -23,6 +23,7 @@ import org.tron.protos.Protocol.Key; import org.tron.protos.Protocol.Permission; import org.tron.protos.Protocol.Permission.PermissionType; +import org.tron.protos.Protocol.PqPublicKey; import org.tron.protos.Protocol.SignatureScheme; /** @@ -149,8 +150,10 @@ static void installPqGenesisState(Manager db, ChainBaseManager chain, .setId(1).setPermissionName("witness").setThreshold(1) .addKeys(Key.newBuilder() .setAddress(witnessAddrBs).setWeight(1) - .setScheme(SignatureScheme.ML_DSA_44) - .setPublicKey(ByteString.copyFrom(witnessPub))) + .setPqKey(PqPublicKey.newBuilder() + .setScheme(SignatureScheme.ML_DSA_44) + .setPublicKey(ByteString.copyFrom(witnessPub)) + .build())) .build(); db.getAccountStore().put(witnessAddr, new AccountCapsule(Account.newBuilder() .setAddress(witnessAddrBs).setType(AccountType.Normal) @@ -168,8 +171,10 @@ static void installPqGenesisState(Manager db, ChainBaseManager chain, .setType(PermissionType.Owner).setPermissionName("owner").setThreshold(1) .addKeys(Key.newBuilder() .setAddress(signerAddrBs).setWeight(1) - .setScheme(SignatureScheme.ML_DSA_44) - .setPublicKey(ByteString.copyFrom(userPub))) + .setPqKey(PqPublicKey.newBuilder() + .setScheme(SignatureScheme.ML_DSA_44) + .setPublicKey(ByteString.copyFrom(userPub)) + .build())) .build(); AccountCapsule userCapsule = new AccountCapsule( ByteString.copyFrom(USER_ADDR), ByteString.copyFromUtf8("pquser"), AccountType.Normal); diff --git a/framework/src/test/java/org/tron/core/BandwidthProcessorTest.java b/framework/src/test/java/org/tron/core/BandwidthProcessorTest.java index 08b061a1ea5..4fe1f911130 100755 --- a/framework/src/test/java/org/tron/core/BandwidthProcessorTest.java +++ b/framework/src/test/java/org/tron/core/BandwidthProcessorTest.java @@ -883,7 +883,7 @@ public void testCalculateGlobalNetLimit() { } @Test - public void pqAuthWitnessBytesSubtractedInCreateAccountCap() throws Exception { + public void pqPqAuthWitnessBytesSubtractedInCreateAccountCap() throws Exception { chainBaseManager.getDynamicPropertiesStore().saveLatestBlockHeaderTimestamp(1526647838000L); chainBaseManager.getDynamicPropertiesStore().saveTotalNetWeight(10_000_000L); @@ -905,17 +905,15 @@ public void pqAuthWitnessBytesSubtractedInCreateAccountCap() throws Exception { .setAmount(100L) .build(); - byte[] signerAddr = ByteArray.fromHexString(OWNER_ADDRESS); byte[] fakeSig = new byte[3309]; - Protocol.AuthWitness authWitness = Protocol.AuthWitness.newBuilder() - .setSignerAddress(ByteString.copyFrom(signerAddr)) + Protocol.PqAuthWitness pqWitness = Protocol.PqAuthWitness.newBuilder() .setSignature(ByteString.copyFrom(fakeSig)) .build(); TransactionCapsule baseTrx = new TransactionCapsule(contract, chainBaseManager.getAccountStore()); Transaction withAuth = baseTrx.getInstance().toBuilder() - .addAuthWitness(authWitness) + .addPqWitness(pqWitness) .build(); TransactionCapsule trx = new TransactionCapsule(withAuth); TransactionTrace trace = new TransactionTrace(trx, StoreFactory.getInstance(), @@ -923,14 +921,14 @@ public void pqAuthWitnessBytesSubtractedInCreateAccountCap() throws Exception { long cap = chainBaseManager.getDynamicPropertiesStore().getMaxCreateAccountTxSize(); long rawSize = trx.getInstance().toBuilder().clearRet().build().getSerializedSize(); - Assert.assertTrue("test precondition: raw tx must exceed cap with auth_witness", + Assert.assertTrue("test precondition: raw tx must exceed cap with pq_witness", rawSize > cap); BandwidthProcessor processor = new BandwidthProcessor(chainBaseManager); try { processor.consume(trx, trace); } catch (TooBigTransactionException e) { - Assert.fail("PQ auth_witness bytes should be deducted from create-account cap check"); + Assert.fail("PQ pq_witness bytes should be deducted from create-account cap check"); } catch (AccountResourceInsufficientException | ContractValidateException | TooBigTransactionResultException e) { @@ -942,7 +940,7 @@ public void pqAuthWitnessBytesSubtractedInCreateAccountCap() throws Exception { } @Test - public void pqAuthWitnessCountedInBandwidthUsage() throws Exception { + public void pqPqAuthWitnessCountedInBandwidthUsage() throws Exception { chainBaseManager.getDynamicPropertiesStore().saveLatestBlockHeaderTimestamp(1526647838000L); chainBaseManager.getDynamicPropertiesStore().saveTotalNetWeight(10_000_000L); @@ -970,17 +968,15 @@ public void pqAuthWitnessCountedInBandwidthUsage() throws Exception { .setAmount(100L) .build(); - byte[] signerAddr = ByteArray.fromHexString(OWNER_ADDRESS); byte[] fakeSig = new byte[3309]; - Protocol.AuthWitness authWitness = Protocol.AuthWitness.newBuilder() - .setSignerAddress(ByteString.copyFrom(signerAddr)) + Protocol.PqAuthWitness pqWitness = Protocol.PqAuthWitness.newBuilder() .setSignature(ByteString.copyFrom(fakeSig)) .build(); TransactionCapsule baseTrx = new TransactionCapsule(contract, chainBaseManager.getAccountStore()); Transaction withAuth = baseTrx.getInstance().toBuilder() - .addAuthWitness(authWitness) + .addPqWitness(pqWitness) .build(); TransactionCapsule trx = new TransactionCapsule(withAuth); TransactionTrace trace = new TransactionTrace(trx, StoreFactory.getInstance(), 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 165bda24f73..d96e944245f 100644 --- a/framework/src/test/java/org/tron/core/actuator/AccountPermissionUpdateActuatorTest.java +++ b/framework/src/test/java/org/tron/core/actuator/AccountPermissionUpdateActuatorTest.java @@ -24,6 +24,7 @@ import org.tron.protos.Protocol.Key; import org.tron.protos.Protocol.Permission; import org.tron.protos.Protocol.Permission.PermissionType; +import org.tron.protos.Protocol.PqPublicKey; import org.tron.protos.Protocol.SignatureScheme; import org.tron.protos.Protocol.Transaction.Contract.ContractType; import org.tron.protos.Protocol.Transaction.Result.code; @@ -1041,8 +1042,10 @@ private Key mlDsaKey(String addr, SignatureScheme scheme, int pkLen, int seed) { return Key.newBuilder() .setAddress(ByteString.copyFrom(ByteArray.fromHexString(addr))) .setWeight(KEY_WEIGHT) - .setScheme(scheme) - .setPublicKey(ByteString.copyFrom(fixedBytes(pkLen, seed))) + .setPqKey(PqPublicKey.newBuilder() + .setScheme(scheme) + .setPublicKey(ByteString.copyFrom(fixedBytes(pkLen, seed))) + .build()) .build(); } @@ -1109,7 +1112,9 @@ public void legacyKeyWithNonEmptyPublicKeyRejected() { Key badLegacy = Key.newBuilder() .setAddress(ByteString.copyFrom(ByteArray.fromHexString(KEY_ADDRESS))) .setWeight(KEY_WEIGHT) - .setPublicKey(ByteString.copyFrom(new byte[] {1, 2, 3})) + .setPqKey(PqPublicKey.newBuilder() + .setPublicKey(ByteString.copyFrom(new byte[] {1, 2, 3})) + .build()) .build(); Permission owner = ownerPermissionWithKeys( java.util.Collections.singletonList(badLegacy), 2); @@ -1217,14 +1222,18 @@ public void duplicatePublicKeyRejected() { Key k1 = Key.newBuilder() .setAddress(ByteString.copyFrom(ByteArray.fromHexString(KEY_ADDRESS))) .setWeight(KEY_WEIGHT) - .setScheme(SignatureScheme.ML_DSA_44) - .setPublicKey(ByteString.copyFrom(sharedPk)) + .setPqKey(PqPublicKey.newBuilder() + .setScheme(SignatureScheme.ML_DSA_44) + .setPublicKey(ByteString.copyFrom(sharedPk)) + .build()) .build(); Key k2 = Key.newBuilder() .setAddress(ByteString.copyFrom(ByteArray.fromHexString(KEY_ADDRESS1))) .setWeight(KEY_WEIGHT) - .setScheme(SignatureScheme.ML_DSA_44) - .setPublicKey(ByteString.copyFrom(sharedPk)) + .setPqKey(PqPublicKey.newBuilder() + .setScheme(SignatureScheme.ML_DSA_44) + .setPublicKey(ByteString.copyFrom(sharedPk)) + .build()) .build(); Permission owner = ownerPermissionWithKeys(java.util.Arrays.asList(k1, k2), 2); Permission active = activePermissionWithKeys( 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 dddb9ad6675..09ff5170198 100644 --- a/framework/src/test/java/org/tron/core/capsule/BlockCapsulePqTest.java +++ b/framework/src/test/java/org/tron/core/capsule/BlockCapsulePqTest.java @@ -16,10 +16,11 @@ import org.tron.core.exception.ValidateSignatureException; import org.tron.protos.Protocol.Account; import org.tron.protos.Protocol.AccountType; -import org.tron.protos.Protocol.AuthWitness; +import org.tron.protos.Protocol.PqAuthWitness; import org.tron.protos.Protocol.Key; import org.tron.protos.Protocol.Permission; import org.tron.protos.Protocol.Permission.PermissionType; +import org.tron.protos.Protocol.PqPublicKey; import org.tron.protos.Protocol.SignatureScheme; public class BlockCapsulePqTest extends BaseTest { @@ -43,10 +44,12 @@ public void setUp() { private AccountCapsule buildWitnessAccount(SignatureScheme scheme) { Key.Builder kb = Key.newBuilder() .setAddress(ByteString.copyFrom(witnessAddress)) - .setWeight(1) - .setScheme(scheme); + .setWeight(1); if (scheme == SignatureScheme.ML_DSA_65) { - kb.setPublicKey(ByteString.copyFrom(pqKeypair.getPublicKey())); + kb.setPqKey(PqPublicKey.newBuilder() + .setScheme(scheme) + .setPublicKey(ByteString.copyFrom(pqKeypair.getPublicKey())) + .build()); } Permission witnessPerm = Permission.newBuilder() .setType(PermissionType.Witness) @@ -89,7 +92,7 @@ private byte[] signPq(byte[] message) { } @Test - public void legacyValidateWithoutAuthWitnessAcceptedBeforeActivation() throws Exception { + public void legacyValidateWithoutPqAuthWitnessAcceptedBeforeActivation() throws Exception { dbManager.getDynamicPropertiesStore().saveAllowMultiSign(1L); dbManager.getDynamicPropertiesStore().saveAllowMlDsa(0L); AccountCapsule witness = buildWitnessAccount(SignatureScheme.UNKNOWN_SIG_SCHEME); @@ -102,7 +105,7 @@ public void legacyValidateWithoutAuthWitnessAcceptedBeforeActivation() throws Ex } @Test(expected = ValidateSignatureException.class) - public void authWitnessBeforeActivationRejected() throws Exception { + public void pqWitnessBeforeActivationRejected() throws Exception { dbManager.getDynamicPropertiesStore().saveAllowMultiSign(1L); dbManager.getDynamicPropertiesStore().saveAllowMlDsa(0L); AccountCapsule witness = buildWitnessAccount(SignatureScheme.UNKNOWN_SIG_SCHEME); @@ -110,9 +113,8 @@ public void authWitnessBeforeActivationRejected() throws Exception { byte[] parentHash = new byte[32]; BlockCapsule block = buildUnsignedBlock(parentHash); - byte[] digest = PqAuthDigest.block(block.getRawHashBytes(), witnessAddress); - block.setWitnessAuth(AuthWitness.newBuilder() - .setSignerAddress(ByteString.copyFrom(witnessAddress)) + byte[] digest = PqAuthDigest.block(block.getRawHashBytes(), 0); + block.setPqWitness(PqAuthWitness.newBuilder() .setSignature(ByteString.copyFrom(signPq(digest))) .build()); block.validateSignature( @@ -120,7 +122,7 @@ public void authWitnessBeforeActivationRejected() throws Exception { } @Test(expected = ValidateSignatureException.class) - public void bothLegacyAndAuthWitnessRejected() throws Exception { + public void bothLegacyAndPqAuthWitnessRejected() throws Exception { dbManager.getDynamicPropertiesStore().saveAllowMultiSign(1L); dbManager.getDynamicPropertiesStore().saveAllowMlDsa(1L); AccountCapsule witness = buildWitnessAccount(SignatureScheme.ML_DSA_65); @@ -128,9 +130,8 @@ public void bothLegacyAndAuthWitnessRejected() throws Exception { byte[] parentHash = new byte[32]; BlockCapsule block = buildSignedBlock(parentHash); - byte[] digest = PqAuthDigest.block(block.getRawHashBytes(), witnessAddress); - block.setWitnessAuth(AuthWitness.newBuilder() - .setSignerAddress(ByteString.copyFrom(witnessAddress)) + byte[] digest = PqAuthDigest.block(block.getRawHashBytes(), 0); + block.setPqWitness(PqAuthWitness.newBuilder() .setSignature(ByteString.copyFrom(signPq(digest))) .build()); block.validateSignature( @@ -151,7 +152,7 @@ public void mlDsaSchemeWithLegacyOnlyRejected() throws Exception { } @Test(expected = ValidateSignatureException.class) - public void legacySchemeWithAuthWitnessOnlyRejected() throws Exception { + public void legacySchemeWithPqAuthWitnessOnlyRejected() throws Exception { dbManager.getDynamicPropertiesStore().saveAllowMultiSign(1L); dbManager.getDynamicPropertiesStore().saveAllowMlDsa(1L); AccountCapsule witness = buildWitnessAccount(SignatureScheme.UNKNOWN_SIG_SCHEME); @@ -159,9 +160,8 @@ public void legacySchemeWithAuthWitnessOnlyRejected() throws Exception { byte[] parentHash = new byte[32]; BlockCapsule block = buildUnsignedBlock(parentHash); - byte[] digest = PqAuthDigest.block(block.getRawHashBytes(), witnessAddress); - block.setWitnessAuth(AuthWitness.newBuilder() - .setSignerAddress(ByteString.copyFrom(witnessAddress)) + byte[] digest = PqAuthDigest.block(block.getRawHashBytes(), 0); + block.setPqWitness(PqAuthWitness.newBuilder() .setSignature(ByteString.copyFrom(signPq(digest))) .build()); block.validateSignature( @@ -190,9 +190,8 @@ public void pqOnlyAccepted() throws Exception { byte[] parentHash = new byte[32]; BlockCapsule block = buildUnsignedBlock(parentHash); - byte[] digest = PqAuthDigest.block(block.getRawHashBytes(), witnessAddress); - block.setWitnessAuth(AuthWitness.newBuilder() - .setSignerAddress(ByteString.copyFrom(witnessAddress)) + byte[] digest = PqAuthDigest.block(block.getRawHashBytes(), 0); + block.setPqWitness(PqAuthWitness.newBuilder() .setSignature(ByteString.copyFrom(signPq(digest))) .build()); Assert.assertTrue(block.validateSignature( @@ -200,7 +199,7 @@ public void pqOnlyAccepted() throws Exception { } @Test - public void tamperedAuthWitnessFails() throws Exception { + public void tamperedPqAuthWitnessFails() throws Exception { dbManager.getDynamicPropertiesStore().saveAllowMultiSign(1L); dbManager.getDynamicPropertiesStore().saveAllowMlDsa(1L); AccountCapsule witness = buildWitnessAccount(SignatureScheme.ML_DSA_65); @@ -208,11 +207,10 @@ public void tamperedAuthWitnessFails() throws Exception { byte[] parentHash = new byte[32]; BlockCapsule block = buildUnsignedBlock(parentHash); - byte[] digest = PqAuthDigest.block(block.getRawHashBytes(), witnessAddress); + byte[] digest = PqAuthDigest.block(block.getRawHashBytes(), 0); byte[] pqSig = signPq(digest); pqSig[pqSig.length - 1] ^= 0x01; - block.setWitnessAuth(AuthWitness.newBuilder() - .setSignerAddress(ByteString.copyFrom(witnessAddress)) + block.setPqWitness(PqAuthWitness.newBuilder() .setSignature(ByteString.copyFrom(pqSig)) .build()); Assert.assertFalse(block.validateSignature( @@ -228,11 +226,9 @@ public void signerNotInWitnessPermissionRejected() throws Exception { byte[] parentHash = new byte[32]; BlockCapsule block = buildUnsignedBlock(parentHash); - byte[] otherAddr = ByteArray.fromHexString( - "41" + "abababababababababababababababababababab"); - byte[] digest = PqAuthDigest.block(block.getRawHashBytes(), otherAddr); - block.setWitnessAuth(AuthWitness.newBuilder() - .setSignerAddress(ByteString.copyFrom(otherAddr)) + byte[] digest = PqAuthDigest.block(block.getRawHashBytes(), 1); + block.setPqWitness(PqAuthWitness.newBuilder() + .setKeyId(1) .setSignature(ByteString.copyFrom(signPq(digest))) .build()); block.validateSignature( 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 a8e98ea33bd..e4bb8cbcaae 100644 --- a/framework/src/test/java/org/tron/core/capsule/TransactionCapsuleTest.java +++ b/framework/src/test/java/org/tron/core/capsule/TransactionCapsuleTest.java @@ -25,10 +25,11 @@ import org.tron.core.config.args.Args; import org.tron.core.exception.ValidateSignatureException; import org.tron.protos.Protocol.AccountType; -import org.tron.protos.Protocol.AuthWitness; +import org.tron.protos.Protocol.PqAuthWitness; import org.tron.protos.Protocol.Key; import org.tron.protos.Protocol.Permission; import org.tron.protos.Protocol.Permission.PermissionType; +import org.tron.protos.Protocol.PqPublicKey; import org.tron.protos.Protocol.SignatureScheme; import org.tron.protos.Protocol.Transaction; import org.tron.protos.Protocol.Transaction.Contract.ContractType; @@ -86,7 +87,7 @@ public void testRemoveRedundantRet() { Assert.assertEquals(SUCCESS, transactionCapsule.getInstance().getRet(0).getContractRet()); } - // --------------------- ML-DSA auth_witness verification --------------------- + // --------------------- ML-DSA pq_witness verification --------------------- private static final String PQ_OWNER_HEX = "41abd4b9367799eaa3197fecb144eb71de1e049abc"; @@ -119,8 +120,10 @@ private void putAccountWithPqPermission( Key pqKey = Key.newBuilder() .setAddress(ByteString.copyFrom(signerAddr)) .setWeight(1L) - .setScheme(scheme) - .setPublicKey(ByteString.copyFrom(pqPublicKey)) + .setPqKey(PqPublicKey.newBuilder() + .setScheme(scheme) + .setPublicKey(ByteString.copyFrom(pqPublicKey)) + .build()) .build(); Permission owner = Permission.newBuilder() .setType(PermissionType.Owner) @@ -135,12 +138,11 @@ private void putAccountWithPqPermission( } @Test - public void authWitnessBeforeActivationRejected() { + public void pqWitnessBeforeActivationRejected() { dbManager.getDynamicPropertiesStore().saveAllowMlDsa(0L); dbManager.getDynamicPropertiesStore().saveAllowFnDsa(0L); Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0).toBuilder() - .addAuthWitness(AuthWitness.newBuilder() - .setSignerAddress(ByteString.copyFrom(ByteArray.fromHexString(PQ_SIGNER_HEX))) + .addPqWitness(PqAuthWitness.newBuilder() .setSignature(ByteString.copyFrom(new byte[2420])) .build()) .build(); @@ -148,19 +150,18 @@ public void authWitnessBeforeActivationRejected() { try { cap.validatePubSignature(dbManager.getAccountStore(), dbManager.getDynamicPropertiesStore()); - Assert.fail("should reject auth_witness before activation"); + Assert.fail("should reject pq_witness before activation"); } catch (ValidateSignatureException e) { Assert.assertTrue(e.getMessage().contains("no post-quantum scheme is activated")); } } @Test - public void signatureAndAuthWitnessAreMutuallyExclusive() { + public void signatureAndPqAuthWitnessAreMutuallyExclusive() { dbManager.getDynamicPropertiesStore().saveAllowMlDsa(1L); Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0).toBuilder() .addSignature(ByteString.copyFrom(new byte[65])) - .addAuthWitness(AuthWitness.newBuilder() - .setSignerAddress(ByteString.copyFrom(ByteArray.fromHexString(PQ_SIGNER_HEX))) + .addPqWitness(PqAuthWitness.newBuilder() .setSignature(ByteString.copyFrom(new byte[2420])) .build()) .build(); @@ -168,27 +169,25 @@ public void signatureAndAuthWitnessAreMutuallyExclusive() { try { cap.validatePubSignature(dbManager.getAccountStore(), dbManager.getDynamicPropertiesStore()); - Assert.fail("should reject when both signature and auth_witness present"); + Assert.fail("should reject when both signature and pq_witness present"); } catch (ValidateSignatureException e) { Assert.assertTrue(e.getMessage().contains("mutually exclusive")); } } @Test - public void validAuthWitnessAccepted() throws Exception { + public void validPqAuthWitnessAccepted() throws Exception { dbManager.getDynamicPropertiesStore().saveAllowMlDsa(1L); MLDSA44 kp = new MLDSA44(); putAccountWithPqPermission(PQ_OWNER_HEX, kp.getPublicKey(), SignatureScheme.ML_DSA_44); Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); byte[] txid = Sha256Hash.of(true, tx.getRawData().toByteArray()).getBytes(); - byte[] signerAddr = ByteArray.fromHexString(PQ_SIGNER_HEX); - byte[] digest = PqAuthDigest.tx(txid, 0, signerAddr); + byte[] digest = PqAuthDigest.tx(txid, 0, 0); byte[] sig = sign(kp, digest); Transaction signed = tx.toBuilder() - .addAuthWitness(AuthWitness.newBuilder() - .setSignerAddress(ByteString.copyFrom(signerAddr)) + .addPqWitness(PqAuthWitness.newBuilder() .setSignature(ByteString.copyFrom(sig)) .build()) .build(); @@ -205,41 +204,37 @@ public void duplicateSignerRejected() throws Exception { Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); byte[] txid = Sha256Hash.of(true, tx.getRawData().toByteArray()).getBytes(); - byte[] signerAddr = ByteArray.fromHexString(PQ_SIGNER_HEX); - byte[] digest = PqAuthDigest.tx(txid, 0, signerAddr); + byte[] digest = PqAuthDigest.tx(txid, 0, 0); byte[] sig = sign(kp, digest); - AuthWitness aw = AuthWitness.newBuilder() - .setSignerAddress(ByteString.copyFrom(signerAddr)) + PqAuthWitness aw = PqAuthWitness.newBuilder() .setSignature(ByteString.copyFrom(sig)) .build(); - Transaction signed = tx.toBuilder().addAuthWitness(aw).addAuthWitness(aw).build(); + Transaction signed = tx.toBuilder().addPqWitness(aw).addPqWitness(aw).build(); TransactionCapsule cap = new TransactionCapsule(signed); try { cap.validatePubSignature(dbManager.getAccountStore(), dbManager.getDynamicPropertiesStore()); - Assert.fail("duplicate signer should be rejected"); + Assert.fail("duplicate key_id should be rejected"); } catch (ValidateSignatureException e) { - Assert.assertTrue(e.getMessage().contains("duplicate signer")); + Assert.assertTrue(e.getMessage().contains("duplicate key_id")); } } @Test - public void tamperedAuthWitnessRejected() throws Exception { + public void tamperedPqAuthWitnessRejected() throws Exception { dbManager.getDynamicPropertiesStore().saveAllowMlDsa(1L); MLDSA44 kp = new MLDSA44(); putAccountWithPqPermission(PQ_OWNER_HEX, kp.getPublicKey(), SignatureScheme.ML_DSA_44); Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); byte[] txid = Sha256Hash.of(true, tx.getRawData().toByteArray()).getBytes(); - byte[] signerAddr = ByteArray.fromHexString(PQ_SIGNER_HEX); - byte[] digest = PqAuthDigest.tx(txid, 0, signerAddr); + byte[] digest = PqAuthDigest.tx(txid, 0, 0); byte[] sig = sign(kp, digest); sig[0] ^= 0x01; Transaction signed = tx.toBuilder() - .addAuthWitness(AuthWitness.newBuilder() - .setSignerAddress(ByteString.copyFrom(signerAddr)) + .addPqWitness(PqAuthWitness.newBuilder() .setSignature(ByteString.copyFrom(sig)) .build()) .build(); @@ -261,14 +256,12 @@ public void signerNotInPermissionRejected() throws Exception { Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); byte[] txid = Sha256Hash.of(true, tx.getRawData().toByteArray()).getBytes(); - byte[] otherSigner = ByteArray.fromHexString( - "41BCE23C7D683B889326F762DDA2223A861EDA2E5C"); - byte[] digest = PqAuthDigest.tx(txid, 0, otherSigner); + byte[] digest = PqAuthDigest.tx(txid, 0, 1); byte[] sig = sign(kp, digest); Transaction signed = tx.toBuilder() - .addAuthWitness(AuthWitness.newBuilder() - .setSignerAddress(ByteString.copyFrom(otherSigner)) + .addPqWitness(PqAuthWitness.newBuilder() + .setKeyId(1) .setSignature(ByteString.copyFrom(sig)) .build()) .build(); @@ -276,9 +269,9 @@ public void signerNotInPermissionRejected() throws Exception { try { cap.validatePubSignature(dbManager.getAccountStore(), dbManager.getDynamicPropertiesStore()); - Assert.fail("unknown signer should be rejected"); + Assert.fail("out-of-range key_id should be rejected"); } catch (ValidateSignatureException e) { - Assert.assertTrue(e.getMessage().contains("not in permission")); + Assert.assertTrue(e.getMessage().contains("key_id out of range")); } } @@ -319,7 +312,6 @@ private Transaction buildTrc20TransferTx(String ownerHex, int permissionId) { /** Returns [serializedSize, packSize, maxTxPerBlock] for ECKey, ML-DSA-44, ML-DSA-65. */ private long[][] measureSizes(Transaction baseTx) { final long blockLimit = 2_000_000L; - byte[] signerAddr = ByteArray.fromHexString(PQ_SIGNER_HEX); // ECKey (ECDSA): 65-byte signature in `signature` field ECKey ecKey = new ECKey(); @@ -328,13 +320,12 @@ private long[][] measureSizes(Transaction baseTx) { long ecSerial = ecCap.getInstance().toByteArray().length; long ecPack = ecCap.computeTrxSizeForBlockMessage(); - // ML-DSA-44: 2420-byte signature in auth_witness + // ML-DSA-44: 2420-byte signature in pq_witness MLDSA44 kp44 = new MLDSA44(); byte[] txid44 = Sha256Hash.of(true, baseTx.getRawData().toByteArray()).getBytes(); - byte[] sig44 = MLDSA44.sign(kp44.getPrivateKey(), PqAuthDigest.tx(txid44, 0, signerAddr)); + byte[] sig44 = MLDSA44.sign(kp44.getPrivateKey(), PqAuthDigest.tx(txid44, 0, 0)); Transaction tx44 = baseTx.toBuilder() - .addAuthWitness(AuthWitness.newBuilder() - .setSignerAddress(ByteString.copyFrom(signerAddr)) + .addPqWitness(PqAuthWitness.newBuilder() .setSignature(ByteString.copyFrom(sig44)) .build()) .build(); @@ -342,13 +333,12 @@ private long[][] measureSizes(Transaction baseTx) { long d44Serial = tx44.toByteArray().length; long d44Pack = cap44.computeTrxSizeForBlockMessage(); - // ML-DSA-65: 3309-byte signature in auth_witness + // ML-DSA-65: 3309-byte signature in pq_witness MLDSA65 kp65 = new MLDSA65(); byte[] txid65 = Sha256Hash.of(true, baseTx.getRawData().toByteArray()).getBytes(); - byte[] sig65 = MLDSA65.sign(kp65.getPrivateKey(), PqAuthDigest.tx(txid65, 0, signerAddr)); + byte[] sig65 = MLDSA65.sign(kp65.getPrivateKey(), PqAuthDigest.tx(txid65, 0, 0)); Transaction tx65 = baseTx.toBuilder() - .addAuthWitness(AuthWitness.newBuilder() - .setSignerAddress(ByteString.copyFrom(signerAddr)) + .addPqWitness(PqAuthWitness.newBuilder() .setSignature(ByteString.copyFrom(sig65)) .build()) .build(); @@ -389,20 +379,18 @@ public void transactionSizeComparisonByScheme() { } @Test - public void mlDsa65AuthWitnessAlsoAccepted() throws Exception { + public void mlDsa65PqAuthWitnessAlsoAccepted() throws Exception { dbManager.getDynamicPropertiesStore().saveAllowMlDsa(1L); MLDSA65 kp = new MLDSA65(); putAccountWithPqPermission(PQ_OWNER_HEX, kp.getPublicKey(), SignatureScheme.ML_DSA_65); Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); byte[] txid = Sha256Hash.of(true, tx.getRawData().toByteArray()).getBytes(); - byte[] signerAddr = ByteArray.fromHexString(PQ_SIGNER_HEX); - byte[] digest = PqAuthDigest.tx(txid, 0, signerAddr); + byte[] digest = PqAuthDigest.tx(txid, 0, 0); byte[] sig = MLDSA65.sign(kp.getPrivateKey(), digest); Transaction signed = tx.toBuilder() - .addAuthWitness(AuthWitness.newBuilder() - .setSignerAddress(ByteString.copyFrom(signerAddr)) + .addPqWitness(PqAuthWitness.newBuilder() .setSignature(ByteString.copyFrom(sig)) .build()) .build(); @@ -412,22 +400,20 @@ public void mlDsa65AuthWitnessAlsoAccepted() throws Exception { } @Test - public void fnDsaAuthWitnessAccepted() throws Exception { + public void fnDsaPqAuthWitnessAccepted() throws Exception { dbManager.getDynamicPropertiesStore().saveAllowFnDsa(1L); FNDSA kp = new FNDSA(); putAccountWithPqPermission(PQ_OWNER_HEX, kp.getPublicKey(), SignatureScheme.FN_DSA); Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); byte[] txid = Sha256Hash.of(true, tx.getRawData().toByteArray()).getBytes(); - byte[] signerAddr = ByteArray.fromHexString(PQ_SIGNER_HEX); - byte[] digest = PqAuthDigest.tx(txid, 0, signerAddr); + byte[] digest = PqAuthDigest.tx(txid, 0, 0); byte[] sig = FNDSA.sign(kp.getPrivateKey(), digest); Assert.assertTrue("FN-DSA signature must be within protocol bound", sig.length > 0 && sig.length <= FNDSA.SIGNATURE_LENGTH); Transaction signed = tx.toBuilder() - .addAuthWitness(AuthWitness.newBuilder() - .setSignerAddress(ByteString.copyFrom(signerAddr)) + .addPqWitness(PqAuthWitness.newBuilder() .setSignature(ByteString.copyFrom(sig)) .build()) .build(); @@ -437,21 +423,19 @@ public void fnDsaAuthWitnessAccepted() throws Exception { } @Test - public void fnDsaTamperedAuthWitnessRejected() throws Exception { + public void fnDsaTamperedPqAuthWitnessRejected() throws Exception { dbManager.getDynamicPropertiesStore().saveAllowFnDsa(1L); FNDSA kp = new FNDSA(); putAccountWithPqPermission(PQ_OWNER_HEX, kp.getPublicKey(), SignatureScheme.FN_DSA); Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); byte[] txid = Sha256Hash.of(true, tx.getRawData().toByteArray()).getBytes(); - byte[] signerAddr = ByteArray.fromHexString(PQ_SIGNER_HEX); - byte[] digest = PqAuthDigest.tx(txid, 0, signerAddr); + byte[] digest = PqAuthDigest.tx(txid, 0, 0); byte[] sig = FNDSA.sign(kp.getPrivateKey(), digest); sig[0] ^= 0x01; Transaction signed = tx.toBuilder() - .addAuthWitness(AuthWitness.newBuilder() - .setSignerAddress(ByteString.copyFrom(signerAddr)) + .addPqWitness(PqAuthWitness.newBuilder() .setSignature(ByteString.copyFrom(sig)) .build()) .build(); @@ -466,7 +450,7 @@ public void fnDsaTamperedAuthWitnessRejected() throws Exception { } @Test - public void fnDsaAuthWitnessRejectedWhenNotActivated() throws Exception { + public void fnDsaPqAuthWitnessRejectedWhenNotActivated() throws Exception { dbManager.getDynamicPropertiesStore().saveAllowMlDsa(0L); dbManager.getDynamicPropertiesStore().saveAllowFnDsa(0L); FNDSA kp = new FNDSA(); @@ -474,13 +458,11 @@ public void fnDsaAuthWitnessRejectedWhenNotActivated() throws Exception { Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); byte[] txid = Sha256Hash.of(true, tx.getRawData().toByteArray()).getBytes(); - byte[] signerAddr = ByteArray.fromHexString(PQ_SIGNER_HEX); - byte[] digest = PqAuthDigest.tx(txid, 0, signerAddr); + byte[] digest = PqAuthDigest.tx(txid, 0, 0); byte[] sig = FNDSA.sign(kp.getPrivateKey(), digest); Transaction signed = tx.toBuilder() - .addAuthWitness(AuthWitness.newBuilder() - .setSignerAddress(ByteString.copyFrom(signerAddr)) + .addPqWitness(PqAuthWitness.newBuilder() .setSignature(ByteString.copyFrom(sig)) .build()) .build(); 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 1d2852ba400..a1ac825abc1 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,7 +15,7 @@ import org.tron.core.config.args.Args; import org.tron.core.utils.TransactionUtil; import org.tron.protos.Protocol; -import org.tron.protos.Protocol.AuthWitness; +import org.tron.protos.Protocol.PqAuthWitness; import org.tron.protos.Protocol.Transaction; public class UtilTest extends BaseTest { @@ -169,33 +169,32 @@ public void testPackTransaction() { } @Test - public void roundtripAuthWitnessJson() throws Exception { - byte[] signer = ByteArray.fromHexString(OWNER_ADDRESS); + public void roundtripPqAuthWitnessJson() throws Exception { byte[] sig = new byte[3309]; for (int i = 0; i < sig.length; i++) { sig[i] = (byte) (i & 0xff); } - AuthWitness authWitness = AuthWitness.newBuilder() - .setSignerAddress(ByteString.copyFrom(signer)) + PqAuthWitness pqWitness = PqAuthWitness.newBuilder() + .setKeyId(1) .setSignature(ByteString.copyFrom(sig)) .build(); Transaction original = Transaction.newBuilder() .setRawData(Transaction.raw.newBuilder().setTimestamp(1L).build()) - .addAuthWitness(authWitness) + .addPqWitness(pqWitness) .build(); String json = Util.printTransactionToJSON(original, false).toJSONString(); - Assert.assertTrue("JSON output should contain auth_witness field", - json.contains("auth_witness")); + Assert.assertTrue("JSON output should contain pq_witness field", + json.contains("pq_witness")); Transaction.Builder rebuilt = Transaction.newBuilder(); JsonFormat.merge(json, rebuilt, false); Transaction decoded = rebuilt.build(); - Assert.assertEquals(1, decoded.getAuthWitnessCount()); - Assert.assertEquals(authWitness.getSignerAddress(), - decoded.getAuthWitness(0).getSignerAddress()); - Assert.assertEquals(authWitness.getSignature(), - decoded.getAuthWitness(0).getSignature()); + Assert.assertEquals(1, decoded.getPqWitnessCount()); + Assert.assertEquals(pqWitness.getKeyId(), + decoded.getPqWitness(0).getKeyId()); + Assert.assertEquals(pqWitness.getSignature(), + decoded.getPqWitness(0).getSignature()); } } diff --git a/protocol/src/main/protos/core/Tron.proto b/protocol/src/main/protos/core/Tron.proto index de37e555388..85241032d14 100644 --- a/protocol/src/main/protos/core/Tron.proto +++ b/protocol/src/main/protos/core/Tron.proto @@ -17,11 +17,11 @@ enum AccountType { Contract = 2; } -// Signature scheme identifier used by Permission.Key and AuthWitness. +// Signature scheme identifier used by Permission.Key and PqWitness. // UNKNOWN_SIG_SCHEME (0) denotes legacy keys authenticated via the existing // Transaction.signature / BlockHeader.witness_signature paths (ECDSA secp256k1 // on mainnet, SM2/SM3 where applicable). Post-quantum schemes use dedicated -// auth_witness / witness_auth fields. +// pq_witness / pq_witness fields. enum SignatureScheme { UNKNOWN_SIG_SCHEME = 0; ECDSA_SECP256K1 = 1; @@ -256,24 +256,29 @@ message Account { int64 acquired_delegated_frozenV2_balance_for_bandwidth = 37; } +// Post-quantum public key: algorithm identifier paired with raw key bytes. +// Used inside Key.pq_key; absent on legacy (ECDSA/SM2) keys. +message PqPublicKey { + SignatureScheme scheme = 1; + // Raw key bytes. ML-DSA-44=1312B, ML-DSA-65=1952B, FN-DSA=896B. + bytes public_key = 2; +} + message Key { - bytes address = 1; - int64 weight = 2; - // Signature scheme for this key. UNKNOWN_SIG_SCHEME = legacy; otherwise a - // post-quantum algorithm identifier requiring public_key to be populated. - SignatureScheme scheme = 3; - // Raw public key bytes. Empty for legacy keys. For ML-DSA-44 = 1312 bytes; - // for ML-DSA-65 = 1952 bytes. No SubjectPublicKeyInfo / PEM / Base64. - bytes public_key = 4; + bytes address = 1; // empty for PQ-only keys + int64 weight = 2; + // Post-quantum key. Absent for legacy keys. + PqPublicKey pq_key = 3; + reserved 4; // was public_key; merged into pq_key } // Per-signer post-quantum authentication witness for a transaction or block. -// signer_address SHALL match a Permission.Key.address entry; signature SHALL -// verify against that key's scheme + public_key over the domain-separated -// digest defined in the PQ authentication spec. -message AuthWitness { - bytes signer_address = 1; - bytes signature = 2; +// key_id is the 0-based index of the signing key in the permission's key list. +// key_id = 0 is the proto3 default and is omitted on the wire — single-key +// accounts pay no overhead for this field. +message PqAuthWitness { + uint32 key_id = 1; + bytes signature = 2; } message DelegatedResource { @@ -481,10 +486,10 @@ message Transaction { repeated bytes signature = 2; repeated Result ret = 5; // Post-quantum authentication witnesses. Mutually exclusive with signature: - // a transaction against a PQ Permission MUST use auth_witness and empty + // a transaction against a PQ Permission MUST use pq_witness and empty // signature; a transaction against a legacy Permission MUST use signature - // and empty auth_witness. - repeated AuthWitness auth_witness = 6; + // and empty pq_witness. + repeated PqAuthWitness pq_witness = 6; } message TransactionInfo { @@ -552,10 +557,10 @@ message BlockHeader { raw raw_data = 1; bytes witness_signature = 2; // Post-quantum witness authentication. When the producing SR has a - // Witness Permission with scheme = ML_DSA_65, witness_auth SHALL be + // Witness Permission with scheme = ML_DSA_65, pq_witness SHALL be // present in addition to witness_signature (Dual-Sign). Otherwise this // field SHALL be empty. - AuthWitness witness_auth = 3; + PqAuthWitness pq_witness = 3; } // block From 5368957efc0508e967833036e0332217f24c19d8 Mon Sep 17 00:00:00 2001 From: federico Date: Tue, 28 Apr 2026 12:03:14 +0800 Subject: [PATCH 11/12] =?UTF-8?q?refactor(crypto):=20rename=20Pq=20?= =?UTF-8?q?=E2=86=92=20PQ=20and=20fix=20FN-DSA=20seed=20validation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AccountPermissionUpdateActuator.java | 4 +- .../org/tron/common/utils/LocalWitnesses.java | 29 ++++--- .../org/tron/core/capsule/BlockCapsule.java | 22 ++--- .../tron/core/capsule/TransactionCapsule.java | 20 ++--- .../org/tron/core/db/BandwidthProcessor.java | 2 +- .../java/org/tron/consensus/base/Param.java | 8 +- .../org/tron/common/crypto/pqc/FNDSA.java | 6 +- .../org/tron/common/crypto/pqc/MLDSA44.java | 4 +- .../org/tron/common/crypto/pqc/MLDSA65.java | 4 +- .../{PqAuthDigest.java => PQAuthDigest.java} | 4 +- .../{PqSignature.java => PQSignature.java} | 4 +- ...Registry.java => PQSignatureRegistry.java} | 33 +++++--- .../java/org/tron/core/config/args/Args.java | 3 +- .../core/config/args/WitnessInitializer.java | 2 +- .../tron/core/consensus/ConsensusService.java | 22 ++--- .../main/java/org/tron/core/db/Manager.java | 16 ++-- framework/src/main/resources/config.conf | 9 +- .../org/tron/common/crypto/pqc/FNDSATest.java | 30 +++---- ...hDigestTest.java => PQAuthDigestTest.java} | 30 +++---- ...Test.java => PQSignatureRegistryTest.java} | 30 +++---- .../program/{PqcClient.java => PQClient.java} | 22 ++--- .../{PqFullNode.java => PQFullNode.java} | 28 +++---- ...PqcWitnessNode.java => PQWitnessNode.java} | 32 +++---- .../tron/common/utils/LocalWitnessesTest.java | 68 +++++++++++++++ .../org/tron/core/BandwidthProcessorTest.java | 8 +- .../AccountPermissionUpdateActuatorTest.java | 10 +-- ...ulePqTest.java => BlockCapsulePQTest.java} | 56 ++++++------- .../core/capsule/TransactionCapsuleTest.java | 84 +++++++++---------- .../org/tron/core/services/http/UtilTest.java | 6 +- protocol/src/main/protos/core/Tron.proto | 13 ++- 30 files changed, 349 insertions(+), 260 deletions(-) rename crypto/src/main/java/org/tron/common/crypto/pqc/{PqAuthDigest.java => PQAuthDigest.java} (97%) rename crypto/src/main/java/org/tron/common/crypto/pqc/{PqSignature.java => PQSignature.java} (97%) rename crypto/src/main/java/org/tron/common/crypto/pqc/{PqSignatureRegistry.java => PQSignatureRegistry.java} (80%) rename framework/src/test/java/org/tron/common/crypto/pqc/{PqAuthDigestTest.java => PQAuthDigestTest.java} (79%) rename framework/src/test/java/org/tron/common/crypto/pqc/{PqSignatureRegistryTest.java => PQSignatureRegistryTest.java} (69%) rename framework/src/test/java/org/tron/common/crypto/pqc/program/{PqcClient.java => PQClient.java} (90%) rename framework/src/test/java/org/tron/common/crypto/pqc/program/{PqFullNode.java => PQFullNode.java} (84%) rename framework/src/test/java/org/tron/common/crypto/pqc/program/{PqcWitnessNode.java => PQWitnessNode.java} (90%) create mode 100644 framework/src/test/java/org/tron/common/utils/LocalWitnessesTest.java rename framework/src/test/java/org/tron/core/capsule/{BlockCapsulePqTest.java => BlockCapsulePQTest.java} (83%) 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 137db8fe413..3a780fd5222 100644 --- a/actuator/src/main/java/org/tron/core/actuator/AccountPermissionUpdateActuator.java +++ b/actuator/src/main/java/org/tron/core/actuator/AccountPermissionUpdateActuator.java @@ -19,7 +19,7 @@ import org.tron.common.crypto.pqc.FNDSA; import org.tron.common.crypto.pqc.MLDSA44; import org.tron.common.crypto.pqc.MLDSA65; -import org.tron.common.crypto.pqc.PqSignatureRegistry; +import org.tron.common.crypto.pqc.PQSignatureRegistry; import org.tron.protos.Protocol.Key; import org.tron.protos.Protocol.Permission; import org.tron.protos.Protocol.Permission.PermissionType; @@ -302,7 +302,7 @@ private void validatePermissionScheme(Permission permission) throws ContractVali if (permission.getType() == PermissionType.Witness && first != SignatureScheme.UNKNOWN_SIG_SCHEME - && !PqSignatureRegistry.contains(first)) { + && !PQSignatureRegistry.contains(first)) { throw new ContractValidateException( "Witness permission only supports legacy or registered PQ schemes, got " + first); } 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 f078d2113a9..60be6440276 100644 --- a/chainbase/src/main/java/org/tron/common/utils/LocalWitnesses.java +++ b/chainbase/src/main/java/org/tron/common/utils/LocalWitnesses.java @@ -25,8 +25,7 @@ import org.tron.common.crypto.ECKey; import org.tron.common.crypto.SignInterface; import org.tron.common.crypto.SignUtils; -import org.tron.common.crypto.pqc.MLDSA65; -import org.tron.common.crypto.pqc.PqSignatureRegistry; +import org.tron.common.crypto.pqc.PQSignatureRegistry; import org.tron.core.config.Parameter.ChainConstant; import org.tron.core.exception.TronError; import org.tron.protos.Protocol.SignatureScheme; @@ -37,7 +36,11 @@ public class LocalWitnesses { @Getter private List privateKeys = Lists.newArrayList(); - /** ML-DSA seed values in hex format (64 hex chars = 32 bytes). */ + /** + * PQ seed values in hex format. The expected byte length depends on + * {@link #pqScheme}: 32 bytes (64 hex chars) for ML-DSA-44 / ML-DSA-65, + * 48 bytes (96 hex chars) for FN-DSA. + */ @Getter private List pqSeeds = Lists.newArrayList(); @@ -46,7 +49,7 @@ public class LocalWitnesses { private SignatureScheme pqScheme = SignatureScheme.ML_DSA_65; public void setPqScheme(SignatureScheme pqScheme) { - if (pqScheme == null || !PqSignatureRegistry.contains(pqScheme)) { + if (pqScheme == null || !PQSignatureRegistry.contains(pqScheme)) { throw new TronError("unsupported PQ signature scheme: " + pqScheme, TronError.ErrCode.WITNESS_INIT); } @@ -116,31 +119,37 @@ public void addPrivateKeys(String privateKey) { this.privateKeys.add(privateKey); } - /** ML-DSA seed values (32 bytes = 64 hex chars). Keys are derived from seeds. */ + /** + * PQ seed values used to derive signing keys under {@link #pqScheme}. Each seed must + * be a hex string whose byte length matches the scheme's required seed size; callers + * must therefore set the scheme via {@link #setPqScheme(SignatureScheme)} before + * calling this method when targeting a non-default scheme. + */ public void setPqSeeds(final List pqSeeds) { if (CollectionUtils.isEmpty(pqSeeds)) { return; } + int expectedSeedLen = PQSignatureRegistry.getSeedLength(pqScheme); for (String seed : pqSeeds) { - validatePqSeed(seed); + validatePqSeed(seed, expectedSeedLen); } this.pqSeeds = pqSeeds; } - private static void validatePqSeed(String seed) { + private static void validatePqSeed(String seed, int expectedSeedLen) { String hex = seed; // Match downstream ByteArray.fromHexString, which only strips lowercase "0x". if (StringUtils.startsWith(hex, "0x")) { hex = hex.substring(2); } - int expectedHexLen = MLDSA65.SEED_LENGTH * 2; + int expectedHexLen = expectedSeedLen * 2; if (StringUtils.isBlank(hex) || hex.length() != expectedHexLen) { - throw new TronError(String.format("ML-DSA seed must be %d hex chars, actual: %d", + throw new TronError(String.format("PQ seed must be %d hex chars, actual: %d", expectedHexLen, StringUtils.isBlank(hex) ? 0 : hex.length()), TronError.ErrCode.WITNESS_INIT); } if (!StringUtil.isHexadecimal(hex)) { - throw new TronError("ML-DSA seed must be hex string", + throw new TronError("PQ seed must be hex string", TronError.ErrCode.WITNESS_INIT); } } 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 aac0d62c271..75cfe7cfad7 100755 --- a/chainbase/src/main/java/org/tron/core/capsule/BlockCapsule.java +++ b/chainbase/src/main/java/org/tron/core/capsule/BlockCapsule.java @@ -31,8 +31,8 @@ import org.tron.common.bloom.Bloom; import org.tron.common.crypto.SignInterface; import org.tron.common.crypto.SignUtils; -import org.tron.common.crypto.pqc.PqAuthDigest; -import org.tron.common.crypto.pqc.PqSignatureRegistry; +import org.tron.common.crypto.pqc.PQAuthDigest; +import org.tron.common.crypto.pqc.PQSignatureRegistry; import org.tron.common.parameter.CommonParameter; import org.tron.common.utils.ByteArray; import org.tron.common.utils.Sha256Hash; @@ -43,7 +43,7 @@ import org.tron.core.exception.ValidateSignatureException; import org.tron.core.store.AccountStore; import org.tron.core.store.DynamicPropertiesStore; -import org.tron.protos.Protocol.PqAuthWitness; +import org.tron.protos.Protocol.PQAuthWitness; import org.tron.protos.Protocol.Block; import org.tron.protos.Protocol.BlockHeader; import org.tron.protos.Protocol.Key; @@ -179,7 +179,7 @@ public void sign(byte[] privateKey) { } - public void setPqWitness(PqAuthWitness pqWitness) { + public void setPqWitness(PQAuthWitness pqWitness) { BlockHeader blockHeader = this.block.getBlockHeader().toBuilder() .setPqWitness(pqWitness).build(); this.block = this.block.toBuilder().setBlockHeader(blockHeader).build(); @@ -198,7 +198,7 @@ public boolean validateSignature(DynamicPropertiesStore dynamicPropertiesStore, AccountStore accountStore) throws ValidateSignatureException { BlockHeader header = block.getBlockHeader(); boolean hasLegacy = !header.getWitnessSignature().isEmpty(); - PqAuthWitness witnessAuth = header.getPqWitness(); + PQAuthWitness witnessAuth = header.getPqWitness(); boolean hasAuth = witnessAuth != null && witnessAuth.getSignature() != null && !witnessAuth.getSignature().isEmpty(); @@ -230,7 +230,7 @@ private boolean validateLegacySignature(DynamicPropertiesStore dynamicProperties Key k = witnessPermission.getKeys(0); SignatureScheme ks = k.hasPqKey() ? k.getPqKey().getScheme() : SignatureScheme.UNKNOWN_SIG_SCHEME; - if (PqSignatureRegistry.contains(ks)) { + if (PQSignatureRegistry.contains(ks)) { throw new ValidateSignatureException( "witness permission requires PQ scheme " + ks + " but witness_signature is legacy"); @@ -255,7 +255,7 @@ private boolean validateLegacySignature(DynamicPropertiesStore dynamicProperties } private boolean validateWitnessAuth(DynamicPropertiesStore dynamicPropertiesStore, - AccountStore accountStore, byte[] witnessAccountAddress, PqAuthWitness witnessAuth) + AccountStore accountStore, byte[] witnessAccountAddress, PQAuthWitness witnessAuth) throws ValidateSignatureException { if (!dynamicPropertiesStore.isAnyPqSchemeAllowed()) { throw new ValidateSignatureException( @@ -280,7 +280,7 @@ private boolean validateWitnessAuth(DynamicPropertiesStore dynamicPropertiesStor "witness permission key at index " + keyId + " is not a PQ key"); } SignatureScheme scheme = matched.getPqKey().getScheme(); - if (!PqSignatureRegistry.contains(scheme)) { + if (!PQSignatureRegistry.contains(scheme)) { throw new ValidateSignatureException( "witness permission scheme " + scheme + " is not allowed for block signing"); } @@ -292,8 +292,8 @@ private boolean validateWitnessAuth(DynamicPropertiesStore dynamicPropertiesStor byte[] publicKey = matched.getPqKey().getPublicKey().toByteArray(); byte[] signature = witnessAuth.getSignature().toByteArray(); byte[] rawHdrHash = getRawHash().getBytes(); - byte[] digest = PqAuthDigest.block(rawHdrHash, keyId); - return PqSignatureRegistry.verify(scheme, publicKey, digest, signature); + byte[] digest = PQAuthDigest.block(rawHdrHash, keyId); + return PQSignatureRegistry.verify(scheme, publicKey, digest, signature); } public BlockId getBlockId() { @@ -407,7 +407,7 @@ public boolean hasWitnessSignature() { if (!header.getWitnessSignature().isEmpty()) { return true; } - PqAuthWitness auth = header.getPqWitness(); + PQAuthWitness auth = header.getPqWitness(); return auth != null && !auth.getSignature().isEmpty(); } 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 86ec963bce3..904777ceeed 100755 --- a/chainbase/src/main/java/org/tron/core/capsule/TransactionCapsule.java +++ b/chainbase/src/main/java/org/tron/core/capsule/TransactionCapsule.java @@ -44,8 +44,8 @@ import org.tron.common.crypto.Rsv; import org.tron.common.crypto.SignInterface; import org.tron.common.crypto.SignUtils; -import org.tron.common.crypto.pqc.PqAuthDigest; -import org.tron.common.crypto.pqc.PqSignatureRegistry; +import org.tron.common.crypto.pqc.PQAuthDigest; +import org.tron.common.crypto.pqc.PQSignatureRegistry; import org.tron.common.es.ExecutorServiceManager; import org.tron.common.math.StrictMathWrapper; import org.tron.common.overlay.message.Message; @@ -67,7 +67,7 @@ import org.tron.core.exception.ValidateSignatureException; import org.tron.core.store.AccountStore; import org.tron.core.store.DynamicPropertiesStore; -import org.tron.protos.Protocol.PqAuthWitness; +import org.tron.protos.Protocol.PQAuthWitness; import org.tron.protos.Protocol.Key; import org.tron.protos.Protocol.Permission; import org.tron.protos.Protocol.Permission.PermissionType; @@ -736,10 +736,10 @@ static boolean validateStructuredSignature(Transaction transaction, } byte[] txid = computeRawHash(transaction).getBytes(); - List witnesses = transaction.getPqWitnessList(); + List witnesses = transaction.getPqWitnessList(); java.util.Set seen = new java.util.HashSet<>(); long weight = 0L; - for (PqAuthWitness aw : witnesses) { + for (PQAuthWitness aw : witnesses) { int keyId = aw.getKeyId(); if (!seen.add(keyId)) { throw new PermissionException("duplicate key_id in pq_witness"); @@ -752,20 +752,20 @@ static boolean validateStructuredSignature(Transaction transaction, throw new PermissionException("key at index " + keyId + " is not a PQ key"); } SignatureScheme scheme = key.getPqKey().getScheme(); - if (!PqSignatureRegistry.contains(scheme)) { + if (!PQSignatureRegistry.contains(scheme)) { throw new PermissionException("unsupported scheme: " + scheme); } if (!dynamicPropertiesStore.isPqSchemeAllowed(scheme)) { throw new PermissionException(scheme + " is not activated"); } - byte[] digest = PqAuthDigest.tx(txid, permissionId, keyId); + byte[] digest = PQAuthDigest.tx(txid, permissionId, keyId); byte[] pk = key.getPqKey().getPublicKey().toByteArray(); byte[] sig = aw.getSignature().toByteArray(); - if (pk.length != PqSignatureRegistry.getPublicKeyLength(scheme) - || !PqSignatureRegistry.isValidSignatureLength(scheme, sig.length)) { + if (pk.length != PQSignatureRegistry.getPublicKeyLength(scheme) + || !PQSignatureRegistry.isValidSignatureLength(scheme, sig.length)) { throw new PermissionException("public key or signature length mismatch"); } - if (!PqSignatureRegistry.verify(scheme, pk, digest, sig)) { + if (!PQSignatureRegistry.verify(scheme, pk, digest, sig)) { throw new PermissionException("pq sig invalid"); } try { 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 40a43ddea85..2072f4d2975 100644 --- a/chainbase/src/main/java/org/tron/core/db/BandwidthProcessor.java +++ b/chainbase/src/main/java/org/tron/core/db/BandwidthProcessor.java @@ -143,7 +143,7 @@ public void consume(TransactionCapsule trx, TransactionTrace trace) long sigOverhead = signatureCount * PER_SIGN_LENGTH; if (trx.getInstance().getPqWitnessCount() > 0) { long pqWitnessBytes = 0L; - for (org.tron.protos.Protocol.PqAuthWitness aw + for (org.tron.protos.Protocol.PQAuthWitness aw : trx.getInstance().getPqWitnessList()) { pqWitnessBytes += aw.getSerializedSize(); } 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 c82afded31f..22cae16b6a4 100644 --- a/consensus/src/main/java/org/tron/consensus/base/Param.java +++ b/consensus/src/main/java/org/tron/consensus/base/Param.java @@ -77,19 +77,19 @@ public Miner(byte[] privateKey, ByteString privateKeyAddress, ByteString witness this.witnessAddress = witnessAddress; } - public byte[] getPqPrivateKey() { + public byte[] getPQPrivateKey() { return pqPrivateKey == null ? null : pqPrivateKey.clone(); } - public void setPqPrivateKey(byte[] pqPrivateKey) { + public void setPQPrivateKey(byte[] pqPrivateKey) { this.pqPrivateKey = pqPrivateKey == null ? null : pqPrivateKey.clone(); } - public byte[] getPqPublicKey() { + public byte[] getPQPublicKey() { return pqPublicKey == null ? null : pqPublicKey.clone(); } - public void setPqPublicKey(byte[] pqPublicKey) { + public void setPQPublicKey(byte[] pqPublicKey) { this.pqPublicKey = pqPublicKey == null ? null : pqPublicKey.clone(); } } 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 index 8fe1a6154b8..fbbc9dbf48b 100644 --- a/crypto/src/main/java/org/tron/common/crypto/pqc/FNDSA.java +++ b/crypto/src/main/java/org/tron/common/crypto/pqc/FNDSA.java @@ -17,15 +17,15 @@ * FIPS 206 (draft) FN-DSA / Falcon-512 keypair-bound signer/verifier. Mirrors the * {@link MLDSA44} / {@link MLDSA65} / {@link SLHDSA} shape: instance methods sign/verify * with the bound keypair, static {@link #sign(byte[], byte[])} / {@link #verify} provide - * stateless entry points used by {@link PqSignatureRegistry}. + * stateless entry points used by {@link PQSignatureRegistry}. * *

Falcon signatures are variable-length: {@link #SIGNATURE_LENGTH} is - * the protocol-level upper bound, not an exact length. The {@link PqSignature#validateSignature} + * the protocol-level upper bound, not an exact length. The {@link PQSignature#validateSignature} * default treats this as {@code <= SIGNATURE_LENGTH}; ML-DSA / SLH-DSA override back to * strict equality. 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 FNDSA implements PQSignature { /** * Falcon-512 encoded private key from BC: f || g || F, where f and g are each diff --git a/crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA44.java b/crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA44.java index 80faf20942c..3d14e81c499 100644 --- a/crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA44.java +++ b/crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA44.java @@ -16,10 +16,10 @@ * FIPS 204 ML-DSA-44 keypair-bound signer/verifier. Instance verify and sign * use the bound keypair; stateless dispatch is available via the static * {@link #verify} / {@link #sign(byte[], byte[])} entry points and the - * {@link PqSignatureRegistry}. Consumes raw public key / signature bytes — + * {@link PQSignatureRegistry}. Consumes raw public key / signature bytes — * no SubjectPublicKeyInfo, PEM, or Base64 wrapping. */ -public final class MLDSA44 implements PqSignature { +public final class MLDSA44 implements PQSignature { public static final int PRIVATE_KEY_LENGTH = 2560; public static final int PUBLIC_KEY_LENGTH = 1312; diff --git a/crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA65.java b/crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA65.java index cecc4377450..2dca36ca52d 100644 --- a/crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA65.java +++ b/crypto/src/main/java/org/tron/common/crypto/pqc/MLDSA65.java @@ -16,10 +16,10 @@ * FIPS 204 ML-DSA-65 keypair-bound signer/verifier. Instance verify and sign * use the bound keypair; stateless dispatch is available via the static * {@link #verify} / {@link #sign(byte[], byte[])} entry points and the - * {@link PqSignatureRegistry}. Consumes raw public key / signature bytes — + * {@link PQSignatureRegistry}. Consumes raw public key / signature bytes — * no SubjectPublicKeyInfo, PEM, or Base64 wrapping. */ -public final class MLDSA65 implements PqSignature { +public final class MLDSA65 implements PQSignature { public static final int PRIVATE_KEY_LENGTH = 4032; public static final int PUBLIC_KEY_LENGTH = 1952; diff --git a/crypto/src/main/java/org/tron/common/crypto/pqc/PqAuthDigest.java b/crypto/src/main/java/org/tron/common/crypto/pqc/PQAuthDigest.java similarity index 97% rename from crypto/src/main/java/org/tron/common/crypto/pqc/PqAuthDigest.java rename to crypto/src/main/java/org/tron/common/crypto/pqc/PQAuthDigest.java index 06aac5b0e4f..caa122eb4ec 100644 --- a/crypto/src/main/java/org/tron/common/crypto/pqc/PqAuthDigest.java +++ b/crypto/src/main/java/org/tron/common/crypto/pqc/PQAuthDigest.java @@ -12,7 +12,7 @@ * transaction signature can never be replayed as a block signature and vice * versa. */ -public final class PqAuthDigest { +public final class PQAuthDigest { public static final String TX_DOMAIN = "TRON_TX_AUTH_V1"; public static final String BLOCK_DOMAIN = "TRON_BLOCK_AUTH_V1"; @@ -20,7 +20,7 @@ public final class PqAuthDigest { static final byte[] TX_DOMAIN_BYTES = TX_DOMAIN.getBytes(StandardCharsets.UTF_8); static final byte[] BLOCK_DOMAIN_BYTES = BLOCK_DOMAIN.getBytes(StandardCharsets.UTF_8); - private PqAuthDigest() { + private PQAuthDigest() { } /** 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 similarity index 97% rename from crypto/src/main/java/org/tron/common/crypto/pqc/PqSignature.java rename to crypto/src/main/java/org/tron/common/crypto/pqc/PQSignature.java index 3765649870d..0cb30030757 100644 --- a/crypto/src/main/java/org/tron/common/crypto/pqc/PqSignature.java +++ b/crypto/src/main/java/org/tron/common/crypto/pqc/PQSignature.java @@ -6,9 +6,9 @@ * 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 SignatureScheme} is provided by - * {@link PqSignatureRegistry}. + * {@link PQSignatureRegistry}. */ -public interface PqSignature { +public interface PQSignature { SignatureScheme getScheme(); diff --git a/crypto/src/main/java/org/tron/common/crypto/pqc/PqSignatureRegistry.java b/crypto/src/main/java/org/tron/common/crypto/pqc/PQSignatureRegistry.java similarity index 80% rename from crypto/src/main/java/org/tron/common/crypto/pqc/PqSignatureRegistry.java rename to crypto/src/main/java/org/tron/common/crypto/pqc/PQSignatureRegistry.java index 49c123f720f..aa1d1b2cf76 100644 --- a/crypto/src/main/java/org/tron/common/crypto/pqc/PqSignatureRegistry.java +++ b/crypto/src/main/java/org/tron/common/crypto/pqc/PQSignatureRegistry.java @@ -12,7 +12,7 @@ * schemes (ECDSA secp256k1, SM2/SM3) are NOT registered — they flow through * the existing {@code SignInterface} path. */ -public final class PqSignatureRegistry { +public final class PQSignatureRegistry { /** Stateless sign/verify/keygen dispatch bound to a single PQ scheme. */ public interface SignatureOps { @@ -20,17 +20,19 @@ public interface SignatureOps { boolean verify(byte[] publicKey, byte[] message, byte[] signature); - PqSignature fromSeed(byte[] seed); + PQSignature fromSeed(byte[] seed); } private static final class SchemeInfo { final int publicKeyLength; final int signatureLength; + final int seedLength; final SignatureOps ops; - SchemeInfo(int publicKeyLength, int signatureLength, SignatureOps ops) { + SchemeInfo(int publicKeyLength, int signatureLength, int seedLength, SignatureOps ops) { this.publicKeyLength = publicKeyLength; this.signatureLength = signatureLength; + this.seedLength = seedLength; this.ops = ops; } } @@ -40,7 +42,8 @@ private static final class SchemeInfo { static { EnumMap m = new EnumMap<>(SignatureScheme.class); m.put(SignatureScheme.ML_DSA_44, new SchemeInfo( - MLDSA44.PUBLIC_KEY_LENGTH, MLDSA44.SIGNATURE_LENGTH, new SignatureOps() { + MLDSA44.PUBLIC_KEY_LENGTH, MLDSA44.SIGNATURE_LENGTH, MLDSA44.SEED_LENGTH, + new SignatureOps() { @Override public byte[] sign(byte[] privateKey, byte[] message) { return MLDSA44.sign(privateKey, message); @@ -52,12 +55,13 @@ public boolean verify(byte[] publicKey, byte[] message, byte[] signature) { } @Override - public PqSignature fromSeed(byte[] seed) { + public PQSignature fromSeed(byte[] seed) { return new MLDSA44(seed); } })); m.put(SignatureScheme.ML_DSA_65, new SchemeInfo( - MLDSA65.PUBLIC_KEY_LENGTH, MLDSA65.SIGNATURE_LENGTH, new SignatureOps() { + MLDSA65.PUBLIC_KEY_LENGTH, MLDSA65.SIGNATURE_LENGTH, MLDSA65.SEED_LENGTH, + new SignatureOps() { @Override public byte[] sign(byte[] privateKey, byte[] message) { return MLDSA65.sign(privateKey, message); @@ -69,12 +73,13 @@ public boolean verify(byte[] publicKey, byte[] message, byte[] signature) { } @Override - public PqSignature fromSeed(byte[] seed) { + public PQSignature fromSeed(byte[] seed) { return new MLDSA65(seed); } })); m.put(SignatureScheme.FN_DSA, new SchemeInfo( - FNDSA.PUBLIC_KEY_LENGTH, FNDSA.SIGNATURE_LENGTH, new SignatureOps() { + FNDSA.PUBLIC_KEY_LENGTH, FNDSA.SIGNATURE_LENGTH, FNDSA.SEED_LENGTH, + new SignatureOps() { @Override public byte[] sign(byte[] privateKey, byte[] message) { return FNDSA.sign(privateKey, message); @@ -86,14 +91,14 @@ public boolean verify(byte[] publicKey, byte[] message, byte[] signature) { } @Override - public PqSignature fromSeed(byte[] seed) { + public PQSignature fromSeed(byte[] seed) { return new FNDSA(seed); } })); SCHEMES = Collections.unmodifiableMap(m); } - private PqSignatureRegistry() { + private PQSignatureRegistry() { } public static boolean contains(SignatureScheme scheme) { @@ -108,6 +113,10 @@ public static int getSignatureLength(SignatureScheme scheme) { return require(scheme).signatureLength; } + public static int getSeedLength(SignatureScheme scheme) { + return require(scheme).seedLength; + } + /** * Per-scheme signature-length predicate. Fixed-length schemes (ML-DSA-44 / ML-DSA-65) * require exact equality with {@link #getSignatureLength(SignatureScheme)}; @@ -131,7 +140,7 @@ public static boolean verify( return require(scheme).ops.verify(publicKey, message, signature); } - public static PqSignature fromSeed(SignatureScheme scheme, byte[] seed) { + public static PQSignature fromSeed(SignatureScheme scheme, byte[] seed) { return require(scheme).ops.fromSeed(seed); } @@ -139,7 +148,7 @@ private static SchemeInfo require(SignatureScheme scheme) { SchemeInfo info = SCHEMES.get(scheme); if (info == null) { throw new IllegalArgumentException( - "no PqSignature registered for scheme: " + scheme); + "no PQSignature registered for scheme: " + scheme); } return info; } 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 9f737fd849b..346bc281e11 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 @@ -1239,7 +1239,7 @@ private static void initLocalWitnesses(Config config, CLIParameter cmd) { List pqSeeds = config.getStringList(ConfigKey.LOCAL_WITNESS_SEED_PQ); if (!pqSeeds.isEmpty()) { localWitnesses = new LocalWitnesses(); - localWitnesses.setPqSeeds(pqSeeds); + // Scheme must be applied before seeds — seed-length validation depends on it. if (config.hasPath(ConfigKey.LOCAL_WITNESS_SEED_PQ_SCHEME)) { String schemeName = config.getString(ConfigKey.LOCAL_WITNESS_SEED_PQ_SCHEME); try { @@ -1255,6 +1255,7 @@ private static void initLocalWitnesses(Config config, CLIParameter cmd) { + ": " + schemeName, TronError.ErrCode.WITNESS_INIT); } } + localWitnesses.setPqSeeds(pqSeeds); byte[] address = WitnessInitializer.resolvePqWitnessAddress(witnessAddr); if (address != null) { localWitnesses.setWitnessAccountAddress(address); 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 8bd81b6d1d5..40d8ca420b1 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 @@ -101,7 +101,7 @@ 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. */ - public static LocalWitnesses initFromPqOnly(String witnessAccountAddress) { + public static LocalWitnesses initFromPQOnly(String witnessAccountAddress) { if (StringUtils.isBlank(witnessAccountAddress)) { throw new TronError( "localWitnessAccountAddress must be set for PQ-only witness nodes", 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 69b6fe4f0f7..d241ae2672b 100644 --- a/framework/src/main/java/org/tron/core/consensus/ConsensusService.java +++ b/framework/src/main/java/org/tron/core/consensus/ConsensusService.java @@ -10,8 +10,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.tron.common.crypto.SignUtils; -import org.tron.common.crypto.pqc.PqSignature; -import org.tron.common.crypto.pqc.PqSignatureRegistry; +import org.tron.common.crypto.pqc.PQSignature; +import org.tron.common.crypto.pqc.PQSignatureRegistry; import org.tron.common.parameter.CommonParameter; import org.tron.consensus.Consensus; import org.tron.consensus.base.Param; @@ -86,7 +86,7 @@ public void start() { requireSupportedPqScheme(scheme); for (String seed : pqSeeds) { byte[] seedBytes = fromHexString(seed); - PqSignature keypair = PqSignatureRegistry.fromSeed(scheme, seedBytes); + PQSignature keypair = PQSignatureRegistry.fromSeed(scheme, seedBytes); byte[] sk = keypair.getPrivateKey(); byte[] pk = keypair.getPublicKey(); byte[] pqAddress = keypair.getAddress(); @@ -96,14 +96,14 @@ public void start() { } ByteString pqAddressBs = ByteString.copyFrom(pqAddress); Miner miner = param.new Miner(null, pqAddressBs, pqAddressBs); - miner.setPqPrivateKey(sk); - miner.setPqPublicKey(pk); + miner.setPQPrivateKey(sk); + miner.setPQPublicKey(pk); miners.add(miner); logger.info("Add {} witness (from seed): {}, size: {}", scheme, Hex.toHexString(pqAddress), miners.size()); } } else if (pqSeeds.size() == 1) { - miners.add(buildPqOnlyMinerFromSeed(param, pqSeeds.get(0))); + miners.add(buildPQOnlyMinerFromSeed(param, pqSeeds.get(0))); } param.setMiners(miners); @@ -113,11 +113,11 @@ public void start() { logger.info("consensus service start success"); } - private Miner buildPqOnlyMinerFromSeed(Param param, String pqSeed) { + private Miner buildPQOnlyMinerFromSeed(Param param, String pqSeed) { SignatureScheme scheme = Args.getLocalWitnesses().getPqScheme(); requireSupportedPqScheme(scheme); byte[] seedBytes = fromHexString(pqSeed); - PqSignature keypair = PqSignatureRegistry.fromSeed(scheme, seedBytes); + PQSignature keypair = PQSignatureRegistry.fromSeed(scheme, seedBytes); byte[] sk = keypair.getPrivateKey(); byte[] pk = keypair.getPublicKey(); byte[] pqAddress = keypair.getAddress(); @@ -132,14 +132,14 @@ private Miner buildPqOnlyMinerFromSeed(Param param, String pqSeed) { // 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.setPQPrivateKey(sk); + miner.setPQPublicKey(pk); logger.info("Add {} witness (from seed): {}", scheme, Hex.toHexString(witnessAddress)); return miner; } private static void requireSupportedPqScheme(SignatureScheme scheme) { - if (!PqSignatureRegistry.contains(scheme)) { + if (!PQSignatureRegistry.contains(scheme)) { throw new TronError("unsupported PQ witness scheme: " + scheme, TronError.ErrCode.WITNESS_INIT); } 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 052e973c81d..5d0a69ea3d3 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -54,8 +54,8 @@ import org.tron.common.args.GenesisBlock; import org.tron.common.bloom.Bloom; import org.tron.common.cron.CronExpression; -import org.tron.common.crypto.pqc.PqAuthDigest; -import org.tron.common.crypto.pqc.PqSignatureRegistry; +import org.tron.common.crypto.pqc.PQAuthDigest; +import org.tron.common.crypto.pqc.PQSignatureRegistry; import org.tron.common.es.ExecutorServiceManager; import org.tron.common.exit.ExitManager; import org.tron.common.logsfilter.EventPluginLoader; @@ -170,8 +170,8 @@ import org.tron.core.utils.TransactionRegister; import org.tron.protos.Protocol; import org.tron.protos.Protocol.AccountType; -import org.tron.protos.Protocol.PqAuthWitness; import org.tron.protos.Protocol.Key; +import org.tron.protos.Protocol.PQAuthWitness; import org.tron.protos.Protocol.Permission; import org.tron.protos.Protocol.SignatureScheme; import org.tron.protos.Protocol.Transaction; @@ -1761,7 +1761,7 @@ public BlockCapsule generateBlock(Miner miner, long blockTime, long timeout) { private void signBlockCapsule(BlockCapsule blockCapsule, Miner miner) { SignatureScheme scheme = resolveWitnessScheme(miner); - if (PqSignatureRegistry.contains(scheme)) { + if (PQSignatureRegistry.contains(scheme)) { signWitnessAuth(blockCapsule, miner, scheme); } else { blockCapsule.sign(miner.getPrivateKey()); @@ -1789,15 +1789,15 @@ private void signWitnessAuth(BlockCapsule blockCapsule, Miner miner, SignatureSc byte[] witnessAddress = miner.getWitnessAddress().toByteArray(); Permission witnessPermission = chainBaseManager.getAccountStore().get(witnessAddress) .getInstance().getWitnessPermission(); - byte[] pqPrivateKey = miner.getPqPrivateKey(); + byte[] pqPrivateKey = miner.getPQPrivateKey(); if (pqPrivateKey == null) { throw new IllegalStateException( "witness permission requires " + scheme + " but local PQ private key is not configured"); } - byte[] digest = PqAuthDigest.block(blockCapsule.getRawHashBytes(), 0); - byte[] signature = PqSignatureRegistry.sign(scheme, pqPrivateKey, digest); - PqAuthWitness witnessAuth = PqAuthWitness.newBuilder() + byte[] digest = PQAuthDigest.block(blockCapsule.getRawHashBytes(), 0); + byte[] signature = PQSignatureRegistry.sign(scheme, pqPrivateKey, digest); + PQAuthWitness witnessAuth = PQAuthWitness.newBuilder() .setSignature(ByteString.copyFrom(signature)) .build(); blockCapsule.setPqWitness(witnessAuth); diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf index 8406785ef03..b9404f7ad3d 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -674,11 +674,14 @@ localwitness = [ # ] # Scheme used to derive keys from localwitness_seed_pq. Defaults to ML_DSA_65. -# Allowed values: ML_DSA_44, ML_DSA_65. +# Allowed values: ML_DSA_44, ML_DSA_65, FN_DSA. # localwitness_seed_pq_scheme = "ML_DSA_65" -# ML-DSA witness signing seed (FIPS 204), 32-byte hex. Used only after -# ALLOW_ML_DSA = 1 and the witness Permission is upgraded to an ML-DSA scheme. +# Post-quantum witness signing seed, hex-encoded. Length depends on +# localwitness_seed_pq_scheme: 32 bytes (64 hex chars) for ML_DSA_44 / ML_DSA_65 +# (FIPS 204), 48 bytes (96 hex chars) for FN_DSA (Falcon-512). Used only after +# the matching ALLOW_ML_DSA / ALLOW_FN_DSA proposal is active and the witness +# Permission is upgraded to the corresponding PQ scheme. # MUST be produced by a CSPRNG; the value below is an example, never use in prod. # localwitness_seed_pq = [ # "0101010101010101010101010101010101010101010101010101010101010101" 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 index cf863b8dfa5..816a5643c31 100644 --- a/framework/src/test/java/org/tron/common/crypto/pqc/FNDSATest.java +++ b/framework/src/test/java/org/tron/common/crypto/pqc/FNDSATest.java @@ -19,7 +19,7 @@ import org.junit.Test; import org.tron.common.crypto.pqc.MLDSA44; import org.tron.common.crypto.pqc.MLDSA65; -import org.tron.common.crypto.pqc.PqSignatureRegistry; +import org.tron.common.crypto.pqc.PQSignatureRegistry; import org.tron.protos.Protocol.SignatureScheme; public class FNDSATest { @@ -263,24 +263,24 @@ public void computeAddressIs21Bytes() { public void registryDispatchMatchesDirectCalls() { byte[] msg = "registry-dispatch".getBytes(); byte[] sigDirect = FNDSA.sign(sk.getEncoded(), msg); - assertTrue(PqSignatureRegistry.verify( + assertTrue(PQSignatureRegistry.verify( SignatureScheme.FN_DSA, pk.getH(), msg, sigDirect)); - byte[] sigViaRegistry = PqSignatureRegistry.sign( + byte[] sigViaRegistry = PQSignatureRegistry.sign( SignatureScheme.FN_DSA, sk.getEncoded(), msg); assertTrue(FNDSA.verify(pk.getH(), msg, sigViaRegistry)); assertEquals(FNDSA.PUBLIC_KEY_LENGTH, - PqSignatureRegistry.getPublicKeyLength(SignatureScheme.FN_DSA)); + PQSignatureRegistry.getPublicKeyLength(SignatureScheme.FN_DSA)); assertEquals(FNDSA.SIGNATURE_LENGTH, - PqSignatureRegistry.getSignatureLength(SignatureScheme.FN_DSA)); + PQSignatureRegistry.getSignatureLength(SignatureScheme.FN_DSA)); } @Test public void registryIsValidSignatureLengthRespectsUpperBound() { - assertTrue(PqSignatureRegistry.isValidSignatureLength(SignatureScheme.FN_DSA, 1)); - assertTrue(PqSignatureRegistry.isValidSignatureLength( + assertTrue(PQSignatureRegistry.isValidSignatureLength(SignatureScheme.FN_DSA, 1)); + assertTrue(PQSignatureRegistry.isValidSignatureLength( SignatureScheme.FN_DSA, FNDSA.SIGNATURE_LENGTH)); - assertFalse(PqSignatureRegistry.isValidSignatureLength(SignatureScheme.FN_DSA, 0)); - assertFalse(PqSignatureRegistry.isValidSignatureLength( + assertFalse(PQSignatureRegistry.isValidSignatureLength(SignatureScheme.FN_DSA, 0)); + assertFalse(PQSignatureRegistry.isValidSignatureLength( SignatureScheme.FN_DSA, FNDSA.SIGNATURE_LENGTH + 1)); } @@ -327,18 +327,18 @@ public void mlDsa65ValidateSignatureRemainsStrictEquality() { @Test public void registryIsValidSignatureLengthForFixedSchemesIsStrictEquality() { - assertTrue(PqSignatureRegistry.isValidSignatureLength( + assertTrue(PQSignatureRegistry.isValidSignatureLength( SignatureScheme.ML_DSA_44, MLDSA44.SIGNATURE_LENGTH)); - assertFalse(PqSignatureRegistry.isValidSignatureLength( + assertFalse(PQSignatureRegistry.isValidSignatureLength( SignatureScheme.ML_DSA_44, MLDSA44.SIGNATURE_LENGTH - 1)); - assertFalse(PqSignatureRegistry.isValidSignatureLength( + assertFalse(PQSignatureRegistry.isValidSignatureLength( SignatureScheme.ML_DSA_44, MLDSA44.SIGNATURE_LENGTH + 1)); - assertTrue(PqSignatureRegistry.isValidSignatureLength( + assertTrue(PQSignatureRegistry.isValidSignatureLength( SignatureScheme.ML_DSA_65, MLDSA65.SIGNATURE_LENGTH)); - assertFalse(PqSignatureRegistry.isValidSignatureLength( + assertFalse(PQSignatureRegistry.isValidSignatureLength( SignatureScheme.ML_DSA_65, MLDSA65.SIGNATURE_LENGTH - 1)); - assertFalse(PqSignatureRegistry.isValidSignatureLength( + assertFalse(PQSignatureRegistry.isValidSignatureLength( SignatureScheme.ML_DSA_65, MLDSA65.SIGNATURE_LENGTH + 1)); } } diff --git a/framework/src/test/java/org/tron/common/crypto/pqc/PqAuthDigestTest.java b/framework/src/test/java/org/tron/common/crypto/pqc/PQAuthDigestTest.java similarity index 79% rename from framework/src/test/java/org/tron/common/crypto/pqc/PqAuthDigestTest.java rename to framework/src/test/java/org/tron/common/crypto/pqc/PQAuthDigestTest.java index 3ee5f6d008e..1ebd20bebef 100644 --- a/framework/src/test/java/org/tron/common/crypto/pqc/PqAuthDigestTest.java +++ b/framework/src/test/java/org/tron/common/crypto/pqc/PQAuthDigestTest.java @@ -11,7 +11,7 @@ import java.security.MessageDigest; import org.junit.Test; -public class PqAuthDigestTest { +public class PQAuthDigestTest { private static byte[] be4(int v) { return new byte[] { @@ -35,7 +35,7 @@ public void txDigestEqualsExpectedSha256() throws Exception { md.update(be4(keyId)); byte[] expected = md.digest(); - byte[] actual = PqAuthDigest.tx(txid, permissionId, keyId); + byte[] actual = PQAuthDigest.tx(txid, permissionId, keyId); assertArrayEquals(expected, actual); assertEquals(32, actual.length); } @@ -54,7 +54,7 @@ public void blockDigestEqualsExpectedSha256() throws Exception { md.update(be4(keyId)); byte[] expected = md.digest(); - byte[] actual = PqAuthDigest.block(hdrHash, keyId); + byte[] actual = PQAuthDigest.block(hdrHash, keyId); assertArrayEquals(expected, actual); assertEquals(32, actual.length); } @@ -62,8 +62,8 @@ public void blockDigestEqualsExpectedSha256() throws Exception { @Test public void txAndBlockDigestsDifferForSameContext() { byte[] shared = new byte[32]; - byte[] txDigest = PqAuthDigest.tx(shared, 0, 0); - byte[] blockDigest = PqAuthDigest.block(shared, 0); + byte[] txDigest = PQAuthDigest.tx(shared, 0, 0); + byte[] blockDigest = PQAuthDigest.block(shared, 0); assertFalse("tx and block digests must not collide for shared inputs", java.util.Arrays.equals(txDigest, blockDigest)); } @@ -72,36 +72,36 @@ public void txAndBlockDigestsDifferForSameContext() { public void differentKeyIdsProduceDifferentTxDigest() { byte[] txid = new byte[32]; assertNotEquals( - new String(PqAuthDigest.tx(txid, 0, 0)), - new String(PqAuthDigest.tx(txid, 0, 1))); + new String(PQAuthDigest.tx(txid, 0, 0)), + new String(PQAuthDigest.tx(txid, 0, 1))); } @Test public void differentPermissionIdsProduceDifferentDigest() { byte[] txid = new byte[32]; - byte[] d0 = PqAuthDigest.tx(txid, 0, 0); - byte[] d1 = PqAuthDigest.tx(txid, 1, 0); + byte[] d0 = PQAuthDigest.tx(txid, 0, 0); + byte[] d1 = PQAuthDigest.tx(txid, 1, 0); assertFalse(java.util.Arrays.equals(d0, d1)); } @Test public void differentKeyIdsProduceDifferentBlockDigest() { byte[] hdr = new byte[32]; - byte[] d0 = PqAuthDigest.block(hdr, 0); - byte[] d1 = PqAuthDigest.block(hdr, 1); + byte[] d0 = PQAuthDigest.block(hdr, 0); + byte[] d1 = PQAuthDigest.block(hdr, 1); assertFalse(java.util.Arrays.equals(d0, d1)); } @Test public void domainPrefixesAreExact() { - assertEquals("TRON_TX_AUTH_V1", PqAuthDigest.TX_DOMAIN); - assertEquals("TRON_BLOCK_AUTH_V1", PqAuthDigest.BLOCK_DOMAIN); + assertEquals("TRON_TX_AUTH_V1", PQAuthDigest.TX_DOMAIN); + assertEquals("TRON_BLOCK_AUTH_V1", PQAuthDigest.BLOCK_DOMAIN); } @Test public void nullTxidRejected() { try { - PqAuthDigest.tx(null, 0, 0); + PQAuthDigest.tx(null, 0, 0); fail("null txid should be rejected"); } catch (IllegalArgumentException expected) { assertTrue(expected.getMessage().contains("txid")); @@ -111,7 +111,7 @@ public void nullTxidRejected() { @Test public void nullBlockHeaderHashRejected() { try { - PqAuthDigest.block(null, 0); + PQAuthDigest.block(null, 0); fail("null blockHeaderRawHash should be rejected"); } catch (IllegalArgumentException expected) { assertTrue(expected.getMessage().contains("blockHeaderRawHash")); diff --git a/framework/src/test/java/org/tron/common/crypto/pqc/PqSignatureRegistryTest.java b/framework/src/test/java/org/tron/common/crypto/pqc/PQSignatureRegistryTest.java similarity index 69% rename from framework/src/test/java/org/tron/common/crypto/pqc/PqSignatureRegistryTest.java rename to framework/src/test/java/org/tron/common/crypto/pqc/PQSignatureRegistryTest.java index c9b9917994d..2f0f24b9a49 100644 --- a/framework/src/test/java/org/tron/common/crypto/pqc/PqSignatureRegistryTest.java +++ b/framework/src/test/java/org/tron/common/crypto/pqc/PQSignatureRegistryTest.java @@ -8,24 +8,24 @@ import org.junit.Test; import org.tron.protos.Protocol.SignatureScheme; -public class PqSignatureRegistryTest { +public class PQSignatureRegistryTest { @Test public void mlDsa44Registered() { - assertTrue(PqSignatureRegistry.contains(SignatureScheme.ML_DSA_44)); + assertTrue(PQSignatureRegistry.contains(SignatureScheme.ML_DSA_44)); assertEquals(MLDSA44.PUBLIC_KEY_LENGTH, - PqSignatureRegistry.getPublicKeyLength(SignatureScheme.ML_DSA_44)); + PQSignatureRegistry.getPublicKeyLength(SignatureScheme.ML_DSA_44)); assertEquals(MLDSA44.SIGNATURE_LENGTH, - PqSignatureRegistry.getSignatureLength(SignatureScheme.ML_DSA_44)); + PQSignatureRegistry.getSignatureLength(SignatureScheme.ML_DSA_44)); } @Test public void mlDsa65Registered() { - assertTrue(PqSignatureRegistry.contains(SignatureScheme.ML_DSA_65)); + assertTrue(PQSignatureRegistry.contains(SignatureScheme.ML_DSA_65)); assertEquals(MLDSA65.PUBLIC_KEY_LENGTH, - PqSignatureRegistry.getPublicKeyLength(SignatureScheme.ML_DSA_65)); + PQSignatureRegistry.getPublicKeyLength(SignatureScheme.ML_DSA_65)); assertEquals(MLDSA65.SIGNATURE_LENGTH, - PqSignatureRegistry.getSignatureLength(SignatureScheme.ML_DSA_65)); + PQSignatureRegistry.getSignatureLength(SignatureScheme.ML_DSA_65)); } @Test @@ -33,7 +33,7 @@ public void mlDsa44VerifyRoundTrip() { MLDSA44 keypair = new MLDSA44(); byte[] msg = "registry-44".getBytes(); byte[] sig = keypair.sign(msg); - assertTrue(PqSignatureRegistry.verify( + assertTrue(PQSignatureRegistry.verify( SignatureScheme.ML_DSA_44, keypair.getPublicKey(), msg, sig)); } @@ -42,15 +42,15 @@ public void mlDsa65VerifyRoundTrip() { MLDSA65 keypair = new MLDSA65(); byte[] msg = "registry-65".getBytes(); byte[] sig = keypair.sign(msg); - assertTrue(PqSignatureRegistry.verify( + assertTrue(PQSignatureRegistry.verify( SignatureScheme.ML_DSA_65, keypair.getPublicKey(), msg, sig)); } @Test public void ecdsaNotRegistered() { - assertFalse(PqSignatureRegistry.contains(SignatureScheme.ECDSA_SECP256K1)); + assertFalse(PQSignatureRegistry.contains(SignatureScheme.ECDSA_SECP256K1)); try { - PqSignatureRegistry.getPublicKeyLength(SignatureScheme.ECDSA_SECP256K1); + PQSignatureRegistry.getPublicKeyLength(SignatureScheme.ECDSA_SECP256K1); fail("expected IllegalArgumentException for ECDSA_SECP256K1"); } catch (IllegalArgumentException e) { assertTrue(e.getMessage().contains("ECDSA_SECP256K1")); @@ -59,9 +59,9 @@ public void ecdsaNotRegistered() { @Test public void sm2NotRegistered() { - assertFalse(PqSignatureRegistry.contains(SignatureScheme.SM2_SM3)); + assertFalse(PQSignatureRegistry.contains(SignatureScheme.SM2_SM3)); try { - PqSignatureRegistry.verify( + PQSignatureRegistry.verify( SignatureScheme.SM2_SM3, new byte[0], new byte[0], new byte[0]); fail("expected IllegalArgumentException for SM2_SM3"); } catch (IllegalArgumentException e) { @@ -71,9 +71,9 @@ public void sm2NotRegistered() { @Test public void unknownSchemeRejected() { - assertFalse(PqSignatureRegistry.contains(SignatureScheme.UNKNOWN_SIG_SCHEME)); + assertFalse(PQSignatureRegistry.contains(SignatureScheme.UNKNOWN_SIG_SCHEME)); try { - PqSignatureRegistry.getSignatureLength(SignatureScheme.UNKNOWN_SIG_SCHEME); + PQSignatureRegistry.getSignatureLength(SignatureScheme.UNKNOWN_SIG_SCHEME); fail("expected IllegalArgumentException for UNKNOWN_SIG_SCHEME"); } catch (IllegalArgumentException e) { assertTrue(e.getMessage().contains("UNKNOWN_SIG_SCHEME")); diff --git a/framework/src/test/java/org/tron/common/crypto/pqc/program/PqcClient.java b/framework/src/test/java/org/tron/common/crypto/pqc/program/PQClient.java similarity index 90% rename from framework/src/test/java/org/tron/common/crypto/pqc/program/PqcClient.java rename to framework/src/test/java/org/tron/common/crypto/pqc/program/PQClient.java index c4442adb9c3..49646913bd6 100644 --- a/framework/src/test/java/org/tron/common/crypto/pqc/program/PqcClient.java +++ b/framework/src/test/java/org/tron/common/crypto/pqc/program/PQClient.java @@ -13,32 +13,32 @@ import org.tron.api.WalletGrpc; import org.tron.api.WalletGrpc.WalletBlockingStub; import org.tron.common.crypto.pqc.MLDSA44; -import org.tron.common.crypto.pqc.PqAuthDigest; +import org.tron.common.crypto.pqc.PQAuthDigest; import org.tron.common.utils.ByteArray; -import org.tron.protos.Protocol.PqAuthWitness; import org.tron.protos.Protocol.Block; +import org.tron.protos.Protocol.PQAuthWitness; 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 PqcWitnessNode} and broadcasts an ML-DSA-44 + * Demo client that connects to {@link PQWitnessNode} and broadcasts an ML-DSA-44 * signed transfer transaction. * - * The keypair is derived from the same fixed seed used by PqcWitnessNode, so no + * 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.PqcWitnessNode + * ./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.PqcClient + * ./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 PqcClient { +public class PQClient { private static final String HOST = System.getProperty("pqc.host", "localhost"); @@ -56,7 +56,7 @@ public static void main(String[] args) throws Exception { .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 PqcWitnessNode ───── + // ── 1. Derive user keypair from same fixed seed as PQWitnessNode ───── byte[] userSeed = new byte[32]; Arrays.fill(userSeed, (byte) 0x02); MLDSA44 userKp = new MLDSA44(userSeed); @@ -64,7 +64,7 @@ public static void main(String[] args) throws Exception { byte[] userPub = userKp.getPublicKey(); byte[] userPriv = userKp.getPrivateKey(); byte[] signerAddr = MLDSA44.computeAddress(userPub); - byte[] ownerAddr = PqcWitnessNode.USER_ADDR; + byte[] ownerAddr = PQWitnessNode.USER_ADDR; System.out.println("=== PQC Client ==="); System.out.println("Connecting to " + HOST + ":" + PORT); @@ -109,11 +109,11 @@ public static void main(String[] args) throws Exception { // ── 5. Sign with ML-DSA-44 pq_witness ────────────────────────── byte[] txId = sha256(rawData.toByteArray()); - byte[] digest = PqAuthDigest.tx(txId, 0, 0); + byte[] digest = PQAuthDigest.tx(txId, 0, 0); byte[] sig = MLDSA44.sign(userPriv, digest); Transaction signedTx = tx.toBuilder() - .addPqWitness(PqAuthWitness.newBuilder() + .addPqWitness(PQAuthWitness.newBuilder() .setSignature(ByteString.copyFrom(sig))) .build(); 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 similarity index 84% rename from framework/src/test/java/org/tron/common/crypto/pqc/program/PqFullNode.java rename to framework/src/test/java/org/tron/common/crypto/pqc/program/PQFullNode.java index 798d1c16654..55f31205987 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 @@ -16,37 +16,37 @@ import org.tron.core.db.Manager; /** - * Demo fullnode that dials {@link PqcWitnessNode} via P2P and syncs PQ-signed blocks. + * 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 * ML-DSA-44 witness permission + demo user account with an ML-DSA-44 owner permission), - * installed via {@link PqcWitnessNode#installPqGenesisState}. Once the witness produces + * installed via {@link PQWitnessNode#installPQGenesisState}. Once the witness produces * a block it is broadcast over P2P; this node validates {@code BlockHeader.pq_witness} * 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.PqcWitnessNode + * ./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 + * ./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: PqcWitnessNode.P2P_PORT) + * -Dpqc.witness.p2p.port=18888 (default: PQWitnessNode.P2P_PORT) */ -public class PqFullNode { +public class PQFullNode { - /** gRPC port (different from PqcWitnessNode so both can run on one host). */ + /** gRPC port (different from PQWitnessNode so both can run on one host). */ static final int GRPC_PORT = 50052; - /** Full-node HTTP port (different from PqcWitnessNode). */ + /** Full-node HTTP port (different from PQWitnessNode). */ static final int HTTP_PORT = 8091; - /** P2P listen port (different from PqcWitnessNode). */ + /** 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(PqcWitnessNode.P2P_PORT))); + 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 @@ -55,9 +55,9 @@ public static void main(String[] args) throws Exception { .getLogger(ch.qos.logback.classic.Logger.ROOT_LOGGER_NAME)) .setLevel(ch.qos.logback.classic.Level.INFO); - // ── 1. Derive the same deterministic keys used by PqcWitnessNode ────── - MLDSA44 witnessKp = new MLDSA44(PqcWitnessNode.WITNESS_SEED); - MLDSA44 userKp = new MLDSA44(PqcWitnessNode.USER_SEED); + // ── 1. Derive the same deterministic keys used by PQWitnessNode ────── + MLDSA44 witnessKp = new MLDSA44(PQWitnessNode.WITNESS_SEED); + MLDSA44 userKp = new MLDSA44(PQWitnessNode.USER_SEED); byte[] witnessPub = witnessKp.getPublicKey(); byte[] userPub = userKp.getPublicKey(); @@ -100,7 +100,7 @@ public static void main(String[] args) throws Exception { // ── 4. Install matching PQ genesis pre-state ────────────────────────── // Without this the incoming pq_witness would fail to validate because // this node wouldn't know the witness's ML-DSA-44 public key. - PqcWitnessNode.installPqGenesisState(db, chain, witnessPub, userPub); + PQWitnessNode.installPQGenesisState(db, chain, witnessPub, userPub); // ── 5. Start P2P + gRPC (no ConsensusService.start — we don't produce) ─ app.startup(); diff --git a/framework/src/test/java/org/tron/common/crypto/pqc/program/PqcWitnessNode.java b/framework/src/test/java/org/tron/common/crypto/pqc/program/PQWitnessNode.java similarity index 90% rename from framework/src/test/java/org/tron/common/crypto/pqc/program/PqcWitnessNode.java rename to framework/src/test/java/org/tron/common/crypto/pqc/program/PQWitnessNode.java index c9fb23ca0c1..42af5bd94bc 100644 --- a/framework/src/test/java/org/tron/common/crypto/pqc/program/PqcWitnessNode.java +++ b/framework/src/test/java/org/tron/common/crypto/pqc/program/PQWitnessNode.java @@ -21,9 +21,9 @@ import org.tron.protos.Protocol.Account; import org.tron.protos.Protocol.AccountType; import org.tron.protos.Protocol.Key; +import org.tron.protos.Protocol.PQPublicKey; import org.tron.protos.Protocol.Permission; import org.tron.protos.Protocol.Permission.PermissionType; -import org.tron.protos.Protocol.PqPublicKey; import org.tron.protos.Protocol.SignatureScheme; /** @@ -31,22 +31,22 @@ * * Starts an in-process TRON node configured with a PQC witness keypair and * a user account that holds an ML-DSA-44 owner permission — ready to receive - * transactions from {@link PqcClient}. + * transactions from {@link PQClient}. * - * Keypairs are derived from fixed seeds so PqcClient can derive matching keys + * 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.PqcWitnessNode + * ./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.PqcClient + * ./gradlew :framework:run -PmainClass=org.tron.common.crypto.pqc.program.PQClient */ -public class PqcWitnessNode { +public class PQWitnessNode { - /** Fixed seed for the ML-DSA-44 witness keypair (shared with PqcClient for derivation). */ + /** Fixed seed for the ML-DSA-44 witness keypair (shared with PQClient for derivation). */ static final byte[] WITNESS_SEED = filledSeed(0x01); - /** Fixed seed for the ML-DSA-44 user keypair (shared with PqcClient for derivation). */ + /** Fixed seed for the ML-DSA-44 user keypair (shared with PQClient for derivation). */ static final byte[] USER_SEED = filledSeed(0x02); /** gRPC port the node listens on. */ @@ -55,7 +55,7 @@ public class PqcWitnessNode { /** 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). */ + /** 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. */ @@ -107,8 +107,8 @@ public static void main(String[] args) throws Exception { 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); + // ── 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(); @@ -117,7 +117,7 @@ public static void main(String[] args) throws Exception { app.startup(); System.out.println("\nNode is running. Send Ctrl-C to stop."); - System.out.println("Run PqcClient or PqFullNode in another terminal.\n"); + System.out.println("Run PQClient or PQFullNode in another terminal.\n"); Runtime.getRuntime().addShutdownHook(new Thread(() -> { System.out.println("Shutting down..."); @@ -130,10 +130,10 @@ public static void main(String[] args) throws Exception { /** * Apply the PQ-specific pre-state that must exist on every node participating - * in the demo network. Both PqcWitnessNode and PqFullNode call this so their + * 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, + static void installPQGenesisState(Manager db, ChainBaseManager chain, byte[] witnessPub, byte[] userPub) { byte[] witnessAddr = MLDSA44.computeAddress(witnessPub); ByteString witnessAddrBs = ByteString.copyFrom(witnessAddr); @@ -150,7 +150,7 @@ static void installPqGenesisState(Manager db, ChainBaseManager chain, .setId(1).setPermissionName("witness").setThreshold(1) .addKeys(Key.newBuilder() .setAddress(witnessAddrBs).setWeight(1) - .setPqKey(PqPublicKey.newBuilder() + .setPqKey(PQPublicKey.newBuilder() .setScheme(SignatureScheme.ML_DSA_44) .setPublicKey(ByteString.copyFrom(witnessPub)) .build())) @@ -171,7 +171,7 @@ static void installPqGenesisState(Manager db, ChainBaseManager chain, .setType(PermissionType.Owner).setPermissionName("owner").setThreshold(1) .addKeys(Key.newBuilder() .setAddress(signerAddrBs).setWeight(1) - .setPqKey(PqPublicKey.newBuilder() + .setPqKey(PQPublicKey.newBuilder() .setScheme(SignatureScheme.ML_DSA_44) .setPublicKey(ByteString.copyFrom(userPub)) .build())) 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..3ca748868f5 --- /dev/null +++ b/framework/src/test/java/org/tron/common/utils/LocalWitnessesTest.java @@ -0,0 +1,68 @@ +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.Collections; +import org.junit.Test; +import org.tron.core.exception.TronError; +import org.tron.protos.Protocol.SignatureScheme; + +public class LocalWitnessesTest { + + // 32-byte hex seed (ML-DSA-44 / ML-DSA-65). + private static final String SEED_32 = + "0101010101010101010101010101010101010101010101010101010101010101"; + // 48-byte hex seed (FN-DSA / Falcon-512). + private static final String SEED_48 = SEED_32 + "02020202020202020202020202020202"; + + @Test + public void mlDsa65DefaultAccepts32ByteSeed() { + LocalWitnesses lw = new LocalWitnesses(); + lw.setPqSeeds(Collections.singletonList(SEED_32)); + assertEquals(SignatureScheme.ML_DSA_65, lw.getPqScheme()); + assertEquals(1, lw.getPqSeeds().size()); + } + + @Test + public void fnDsaAccepts48ByteSeedWhenSchemeSetFirst() { + LocalWitnesses lw = new LocalWitnesses(); + lw.setPqScheme(SignatureScheme.FN_DSA); + lw.setPqSeeds(Collections.singletonList(SEED_48)); + assertEquals(SignatureScheme.FN_DSA, lw.getPqScheme()); + assertEquals(1, lw.getPqSeeds().size()); + } + + @Test + public void fnDsaRejects32ByteSeed() { + LocalWitnesses lw = new LocalWitnesses(); + lw.setPqScheme(SignatureScheme.FN_DSA); + TronError err = assertThrows(TronError.class, + () -> lw.setPqSeeds(Collections.singletonList(SEED_32))); + assertEquals(TronError.ErrCode.WITNESS_INIT, err.getErrCode()); + assertTrue(err.getMessage().contains("PQ seed")); + assertTrue(err.getMessage().contains("96")); + } + + @Test + public void mlDsa44Rejects48ByteSeed() { + LocalWitnesses lw = new LocalWitnesses(); + lw.setPqScheme(SignatureScheme.ML_DSA_44); + TronError err = assertThrows(TronError.class, + () -> lw.setPqSeeds(Collections.singletonList(SEED_48))); + assertEquals(TronError.ErrCode.WITNESS_INIT, err.getErrCode()); + assertTrue(err.getMessage().contains("PQ seed")); + assertTrue(err.getMessage().contains("64")); + } + + @Test + public void nonHexSeedRejected() { + LocalWitnesses lw = new LocalWitnesses(); + String badSeed = "zz" + SEED_32.substring(2); + TronError err = assertThrows(TronError.class, + () -> lw.setPqSeeds(Collections.singletonList(badSeed))); + assertEquals(TronError.ErrCode.WITNESS_INIT, err.getErrCode()); + assertTrue(err.getMessage().contains("hex")); + } +} diff --git a/framework/src/test/java/org/tron/core/BandwidthProcessorTest.java b/framework/src/test/java/org/tron/core/BandwidthProcessorTest.java index 4fe1f911130..32c00003e82 100755 --- a/framework/src/test/java/org/tron/core/BandwidthProcessorTest.java +++ b/framework/src/test/java/org/tron/core/BandwidthProcessorTest.java @@ -883,7 +883,7 @@ public void testCalculateGlobalNetLimit() { } @Test - public void pqPqAuthWitnessBytesSubtractedInCreateAccountCap() throws Exception { + public void pqPQAuthWitnessBytesSubtractedInCreateAccountCap() throws Exception { chainBaseManager.getDynamicPropertiesStore().saveLatestBlockHeaderTimestamp(1526647838000L); chainBaseManager.getDynamicPropertiesStore().saveTotalNetWeight(10_000_000L); @@ -906,7 +906,7 @@ public void pqPqAuthWitnessBytesSubtractedInCreateAccountCap() throws Exception .build(); byte[] fakeSig = new byte[3309]; - Protocol.PqAuthWitness pqWitness = Protocol.PqAuthWitness.newBuilder() + Protocol.PQAuthWitness pqWitness = Protocol.PQAuthWitness.newBuilder() .setSignature(ByteString.copyFrom(fakeSig)) .build(); @@ -940,7 +940,7 @@ public void pqPqAuthWitnessBytesSubtractedInCreateAccountCap() throws Exception } @Test - public void pqPqAuthWitnessCountedInBandwidthUsage() throws Exception { + public void pqPQAuthWitnessCountedInBandwidthUsage() throws Exception { chainBaseManager.getDynamicPropertiesStore().saveLatestBlockHeaderTimestamp(1526647838000L); chainBaseManager.getDynamicPropertiesStore().saveTotalNetWeight(10_000_000L); @@ -969,7 +969,7 @@ public void pqPqAuthWitnessCountedInBandwidthUsage() throws Exception { .build(); byte[] fakeSig = new byte[3309]; - Protocol.PqAuthWitness pqWitness = Protocol.PqAuthWitness.newBuilder() + Protocol.PQAuthWitness pqWitness = Protocol.PQAuthWitness.newBuilder() .setSignature(ByteString.copyFrom(fakeSig)) .build(); 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 d96e944245f..299df65bf00 100644 --- a/framework/src/test/java/org/tron/core/actuator/AccountPermissionUpdateActuatorTest.java +++ b/framework/src/test/java/org/tron/core/actuator/AccountPermissionUpdateActuatorTest.java @@ -22,9 +22,9 @@ import org.tron.core.exception.ContractValidateException; import org.tron.protos.Protocol.AccountType; import org.tron.protos.Protocol.Key; +import org.tron.protos.Protocol.PQPublicKey; import org.tron.protos.Protocol.Permission; import org.tron.protos.Protocol.Permission.PermissionType; -import org.tron.protos.Protocol.PqPublicKey; import org.tron.protos.Protocol.SignatureScheme; import org.tron.protos.Protocol.Transaction.Contract.ContractType; import org.tron.protos.Protocol.Transaction.Result.code; @@ -1042,7 +1042,7 @@ private Key mlDsaKey(String addr, SignatureScheme scheme, int pkLen, int seed) { return Key.newBuilder() .setAddress(ByteString.copyFrom(ByteArray.fromHexString(addr))) .setWeight(KEY_WEIGHT) - .setPqKey(PqPublicKey.newBuilder() + .setPqKey(PQPublicKey.newBuilder() .setScheme(scheme) .setPublicKey(ByteString.copyFrom(fixedBytes(pkLen, seed))) .build()) @@ -1112,7 +1112,7 @@ public void legacyKeyWithNonEmptyPublicKeyRejected() { Key badLegacy = Key.newBuilder() .setAddress(ByteString.copyFrom(ByteArray.fromHexString(KEY_ADDRESS))) .setWeight(KEY_WEIGHT) - .setPqKey(PqPublicKey.newBuilder() + .setPqKey(PQPublicKey.newBuilder() .setPublicKey(ByteString.copyFrom(new byte[] {1, 2, 3})) .build()) .build(); @@ -1222,7 +1222,7 @@ public void duplicatePublicKeyRejected() { Key k1 = Key.newBuilder() .setAddress(ByteString.copyFrom(ByteArray.fromHexString(KEY_ADDRESS))) .setWeight(KEY_WEIGHT) - .setPqKey(PqPublicKey.newBuilder() + .setPqKey(PQPublicKey.newBuilder() .setScheme(SignatureScheme.ML_DSA_44) .setPublicKey(ByteString.copyFrom(sharedPk)) .build()) @@ -1230,7 +1230,7 @@ public void duplicatePublicKeyRejected() { Key k2 = Key.newBuilder() .setAddress(ByteString.copyFrom(ByteArray.fromHexString(KEY_ADDRESS1))) .setWeight(KEY_WEIGHT) - .setPqKey(PqPublicKey.newBuilder() + .setPqKey(PQPublicKey.newBuilder() .setScheme(SignatureScheme.ML_DSA_44) .setPublicKey(ByteString.copyFrom(sharedPk)) .build()) diff --git a/framework/src/test/java/org/tron/core/capsule/BlockCapsulePqTest.java b/framework/src/test/java/org/tron/core/capsule/BlockCapsulePQTest.java similarity index 83% rename from framework/src/test/java/org/tron/core/capsule/BlockCapsulePqTest.java rename to framework/src/test/java/org/tron/core/capsule/BlockCapsulePQTest.java index 09ff5170198..39cd040063c 100644 --- a/framework/src/test/java/org/tron/core/capsule/BlockCapsulePqTest.java +++ b/framework/src/test/java/org/tron/core/capsule/BlockCapsulePQTest.java @@ -9,21 +9,21 @@ import org.tron.common.TestConstants; import org.tron.common.crypto.ECKey; import org.tron.common.crypto.pqc.MLDSA65; -import org.tron.common.crypto.pqc.PqAuthDigest; +import org.tron.common.crypto.pqc.PQAuthDigest; import org.tron.common.utils.ByteArray; 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.PqAuthWitness; import org.tron.protos.Protocol.Key; +import org.tron.protos.Protocol.PQAuthWitness; +import org.tron.protos.Protocol.PQPublicKey; import org.tron.protos.Protocol.Permission; import org.tron.protos.Protocol.Permission.PermissionType; -import org.tron.protos.Protocol.PqPublicKey; import org.tron.protos.Protocol.SignatureScheme; -public class BlockCapsulePqTest extends BaseTest { +public class BlockCapsulePQTest extends BaseTest { private ECKey witnessKey; private byte[] witnessAddress; @@ -46,7 +46,7 @@ private AccountCapsule buildWitnessAccount(SignatureScheme scheme) { .setAddress(ByteString.copyFrom(witnessAddress)) .setWeight(1); if (scheme == SignatureScheme.ML_DSA_65) { - kb.setPqKey(PqPublicKey.newBuilder() + kb.setPqKey(PQPublicKey.newBuilder() .setScheme(scheme) .setPublicKey(ByteString.copyFrom(pqKeypair.getPublicKey())) .build()); @@ -87,12 +87,12 @@ private BlockCapsule buildUnsignedBlock(byte[] parentHash) { ByteString.copyFrom(witnessAddress)); } - private byte[] signPq(byte[] message) { + private byte[] signPQ(byte[] message) { return MLDSA65.sign(pqKeypair.getPrivateKey(), message); } @Test - public void legacyValidateWithoutPqAuthWitnessAcceptedBeforeActivation() throws Exception { + public void legacyValidateWithoutPQAuthWitnessAcceptedBeforeActivation() throws Exception { dbManager.getDynamicPropertiesStore().saveAllowMultiSign(1L); dbManager.getDynamicPropertiesStore().saveAllowMlDsa(0L); AccountCapsule witness = buildWitnessAccount(SignatureScheme.UNKNOWN_SIG_SCHEME); @@ -113,16 +113,16 @@ public void pqWitnessBeforeActivationRejected() throws Exception { byte[] parentHash = new byte[32]; BlockCapsule block = buildUnsignedBlock(parentHash); - byte[] digest = PqAuthDigest.block(block.getRawHashBytes(), 0); - block.setPqWitness(PqAuthWitness.newBuilder() - .setSignature(ByteString.copyFrom(signPq(digest))) + byte[] digest = PQAuthDigest.block(block.getRawHashBytes(), 0); + block.setPqWitness(PQAuthWitness.newBuilder() + .setSignature(ByteString.copyFrom(signPQ(digest))) .build()); block.validateSignature( dbManager.getDynamicPropertiesStore(), dbManager.getAccountStore()); } @Test(expected = ValidateSignatureException.class) - public void bothLegacyAndPqAuthWitnessRejected() throws Exception { + public void bothLegacyAndPQAuthWitnessRejected() throws Exception { dbManager.getDynamicPropertiesStore().saveAllowMultiSign(1L); dbManager.getDynamicPropertiesStore().saveAllowMlDsa(1L); AccountCapsule witness = buildWitnessAccount(SignatureScheme.ML_DSA_65); @@ -130,9 +130,9 @@ public void bothLegacyAndPqAuthWitnessRejected() throws Exception { byte[] parentHash = new byte[32]; BlockCapsule block = buildSignedBlock(parentHash); - byte[] digest = PqAuthDigest.block(block.getRawHashBytes(), 0); - block.setPqWitness(PqAuthWitness.newBuilder() - .setSignature(ByteString.copyFrom(signPq(digest))) + byte[] digest = PQAuthDigest.block(block.getRawHashBytes(), 0); + block.setPqWitness(PQAuthWitness.newBuilder() + .setSignature(ByteString.copyFrom(signPQ(digest))) .build()); block.validateSignature( dbManager.getDynamicPropertiesStore(), dbManager.getAccountStore()); @@ -152,7 +152,7 @@ public void mlDsaSchemeWithLegacyOnlyRejected() throws Exception { } @Test(expected = ValidateSignatureException.class) - public void legacySchemeWithPqAuthWitnessOnlyRejected() throws Exception { + public void legacySchemeWithPQAuthWitnessOnlyRejected() throws Exception { dbManager.getDynamicPropertiesStore().saveAllowMultiSign(1L); dbManager.getDynamicPropertiesStore().saveAllowMlDsa(1L); AccountCapsule witness = buildWitnessAccount(SignatureScheme.UNKNOWN_SIG_SCHEME); @@ -160,9 +160,9 @@ public void legacySchemeWithPqAuthWitnessOnlyRejected() throws Exception { byte[] parentHash = new byte[32]; BlockCapsule block = buildUnsignedBlock(parentHash); - byte[] digest = PqAuthDigest.block(block.getRawHashBytes(), 0); - block.setPqWitness(PqAuthWitness.newBuilder() - .setSignature(ByteString.copyFrom(signPq(digest))) + byte[] digest = PQAuthDigest.block(block.getRawHashBytes(), 0); + block.setPqWitness(PQAuthWitness.newBuilder() + .setSignature(ByteString.copyFrom(signPQ(digest))) .build()); block.validateSignature( dbManager.getDynamicPropertiesStore(), dbManager.getAccountStore()); @@ -190,16 +190,16 @@ public void pqOnlyAccepted() throws Exception { byte[] parentHash = new byte[32]; BlockCapsule block = buildUnsignedBlock(parentHash); - byte[] digest = PqAuthDigest.block(block.getRawHashBytes(), 0); - block.setPqWitness(PqAuthWitness.newBuilder() - .setSignature(ByteString.copyFrom(signPq(digest))) + byte[] digest = PQAuthDigest.block(block.getRawHashBytes(), 0); + block.setPqWitness(PQAuthWitness.newBuilder() + .setSignature(ByteString.copyFrom(signPQ(digest))) .build()); Assert.assertTrue(block.validateSignature( dbManager.getDynamicPropertiesStore(), dbManager.getAccountStore())); } @Test - public void tamperedPqAuthWitnessFails() throws Exception { + public void tamperedPQAuthWitnessFails() throws Exception { dbManager.getDynamicPropertiesStore().saveAllowMultiSign(1L); dbManager.getDynamicPropertiesStore().saveAllowMlDsa(1L); AccountCapsule witness = buildWitnessAccount(SignatureScheme.ML_DSA_65); @@ -207,10 +207,10 @@ public void tamperedPqAuthWitnessFails() throws Exception { byte[] parentHash = new byte[32]; BlockCapsule block = buildUnsignedBlock(parentHash); - byte[] digest = PqAuthDigest.block(block.getRawHashBytes(), 0); - byte[] pqSig = signPq(digest); + byte[] digest = PQAuthDigest.block(block.getRawHashBytes(), 0); + byte[] pqSig = signPQ(digest); pqSig[pqSig.length - 1] ^= 0x01; - block.setPqWitness(PqAuthWitness.newBuilder() + block.setPqWitness(PQAuthWitness.newBuilder() .setSignature(ByteString.copyFrom(pqSig)) .build()); Assert.assertFalse(block.validateSignature( @@ -226,10 +226,10 @@ public void signerNotInWitnessPermissionRejected() throws Exception { byte[] parentHash = new byte[32]; BlockCapsule block = buildUnsignedBlock(parentHash); - byte[] digest = PqAuthDigest.block(block.getRawHashBytes(), 1); - block.setPqWitness(PqAuthWitness.newBuilder() + byte[] digest = PQAuthDigest.block(block.getRawHashBytes(), 1); + block.setPqWitness(PQAuthWitness.newBuilder() .setKeyId(1) - .setSignature(ByteString.copyFrom(signPq(digest))) + .setSignature(ByteString.copyFrom(signPQ(digest))) .build()); 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 e4bb8cbcaae..ee15dc059d2 100644 --- a/framework/src/test/java/org/tron/core/capsule/TransactionCapsuleTest.java +++ b/framework/src/test/java/org/tron/core/capsule/TransactionCapsuleTest.java @@ -17,7 +17,7 @@ import org.tron.common.crypto.pqc.FNDSA; import org.tron.common.crypto.pqc.MLDSA44; import org.tron.common.crypto.pqc.MLDSA65; -import org.tron.common.crypto.pqc.PqAuthDigest; +import org.tron.common.crypto.pqc.PQAuthDigest; import org.tron.common.utils.ByteArray; import org.tron.common.utils.Sha256Hash; import org.tron.common.utils.StringUtil; @@ -25,11 +25,11 @@ import org.tron.core.config.args.Args; import org.tron.core.exception.ValidateSignatureException; import org.tron.protos.Protocol.AccountType; -import org.tron.protos.Protocol.PqAuthWitness; import org.tron.protos.Protocol.Key; +import org.tron.protos.Protocol.PQAuthWitness; +import org.tron.protos.Protocol.PQPublicKey; import org.tron.protos.Protocol.Permission; import org.tron.protos.Protocol.Permission.PermissionType; -import org.tron.protos.Protocol.PqPublicKey; import org.tron.protos.Protocol.SignatureScheme; import org.tron.protos.Protocol.Transaction; import org.tron.protos.Protocol.Transaction.Contract.ContractType; @@ -113,14 +113,14 @@ private Transaction buildTransferTx(String ownerHex, int permissionId) { return Transaction.newBuilder().setRawData(rawData).build(); } - private void putAccountWithPqPermission( + private void putAccountWithPQPermission( String ownerHex, byte[] pqPublicKey, SignatureScheme scheme) { byte[] addr = ByteArray.fromHexString(ownerHex); byte[] signerAddr = ByteArray.fromHexString(PQ_SIGNER_HEX); Key pqKey = Key.newBuilder() .setAddress(ByteString.copyFrom(signerAddr)) .setWeight(1L) - .setPqKey(PqPublicKey.newBuilder() + .setPqKey(PQPublicKey.newBuilder() .setScheme(scheme) .setPublicKey(ByteString.copyFrom(pqPublicKey)) .build()) @@ -142,7 +142,7 @@ public void pqWitnessBeforeActivationRejected() { dbManager.getDynamicPropertiesStore().saveAllowMlDsa(0L); dbManager.getDynamicPropertiesStore().saveAllowFnDsa(0L); Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0).toBuilder() - .addPqWitness(PqAuthWitness.newBuilder() + .addPqWitness(PQAuthWitness.newBuilder() .setSignature(ByteString.copyFrom(new byte[2420])) .build()) .build(); @@ -157,11 +157,11 @@ public void pqWitnessBeforeActivationRejected() { } @Test - public void signatureAndPqAuthWitnessAreMutuallyExclusive() { + public void signatureAndPQAuthWitnessAreMutuallyExclusive() { dbManager.getDynamicPropertiesStore().saveAllowMlDsa(1L); Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0).toBuilder() .addSignature(ByteString.copyFrom(new byte[65])) - .addPqWitness(PqAuthWitness.newBuilder() + .addPqWitness(PQAuthWitness.newBuilder() .setSignature(ByteString.copyFrom(new byte[2420])) .build()) .build(); @@ -176,18 +176,18 @@ public void signatureAndPqAuthWitnessAreMutuallyExclusive() { } @Test - public void validPqAuthWitnessAccepted() throws Exception { + public void validPQAuthWitnessAccepted() throws Exception { dbManager.getDynamicPropertiesStore().saveAllowMlDsa(1L); MLDSA44 kp = new MLDSA44(); - putAccountWithPqPermission(PQ_OWNER_HEX, kp.getPublicKey(), SignatureScheme.ML_DSA_44); + putAccountWithPQPermission(PQ_OWNER_HEX, kp.getPublicKey(), SignatureScheme.ML_DSA_44); Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); byte[] txid = Sha256Hash.of(true, tx.getRawData().toByteArray()).getBytes(); - byte[] digest = PqAuthDigest.tx(txid, 0, 0); + byte[] digest = PQAuthDigest.tx(txid, 0, 0); byte[] sig = sign(kp, digest); Transaction signed = tx.toBuilder() - .addPqWitness(PqAuthWitness.newBuilder() + .addPqWitness(PQAuthWitness.newBuilder() .setSignature(ByteString.copyFrom(sig)) .build()) .build(); @@ -200,13 +200,13 @@ public void validPqAuthWitnessAccepted() throws Exception { public void duplicateSignerRejected() throws Exception { dbManager.getDynamicPropertiesStore().saveAllowMlDsa(1L); MLDSA44 kp = new MLDSA44(); - putAccountWithPqPermission(PQ_OWNER_HEX, kp.getPublicKey(), SignatureScheme.ML_DSA_44); + putAccountWithPQPermission(PQ_OWNER_HEX, kp.getPublicKey(), SignatureScheme.ML_DSA_44); Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); byte[] txid = Sha256Hash.of(true, tx.getRawData().toByteArray()).getBytes(); - byte[] digest = PqAuthDigest.tx(txid, 0, 0); + byte[] digest = PQAuthDigest.tx(txid, 0, 0); byte[] sig = sign(kp, digest); - PqAuthWitness aw = PqAuthWitness.newBuilder() + PQAuthWitness aw = PQAuthWitness.newBuilder() .setSignature(ByteString.copyFrom(sig)) .build(); Transaction signed = tx.toBuilder().addPqWitness(aw).addPqWitness(aw).build(); @@ -222,19 +222,19 @@ public void duplicateSignerRejected() throws Exception { } @Test - public void tamperedPqAuthWitnessRejected() throws Exception { + public void tamperedPQAuthWitnessRejected() throws Exception { dbManager.getDynamicPropertiesStore().saveAllowMlDsa(1L); MLDSA44 kp = new MLDSA44(); - putAccountWithPqPermission(PQ_OWNER_HEX, kp.getPublicKey(), SignatureScheme.ML_DSA_44); + putAccountWithPQPermission(PQ_OWNER_HEX, kp.getPublicKey(), SignatureScheme.ML_DSA_44); Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); byte[] txid = Sha256Hash.of(true, tx.getRawData().toByteArray()).getBytes(); - byte[] digest = PqAuthDigest.tx(txid, 0, 0); + byte[] digest = PQAuthDigest.tx(txid, 0, 0); byte[] sig = sign(kp, digest); sig[0] ^= 0x01; Transaction signed = tx.toBuilder() - .addPqWitness(PqAuthWitness.newBuilder() + .addPqWitness(PQAuthWitness.newBuilder() .setSignature(ByteString.copyFrom(sig)) .build()) .build(); @@ -252,15 +252,15 @@ public void tamperedPqAuthWitnessRejected() throws Exception { public void signerNotInPermissionRejected() throws Exception { dbManager.getDynamicPropertiesStore().saveAllowMlDsa(1L); MLDSA44 kp = new MLDSA44(); - putAccountWithPqPermission(PQ_OWNER_HEX, kp.getPublicKey(), SignatureScheme.ML_DSA_44); + putAccountWithPQPermission(PQ_OWNER_HEX, kp.getPublicKey(), SignatureScheme.ML_DSA_44); Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); byte[] txid = Sha256Hash.of(true, tx.getRawData().toByteArray()).getBytes(); - byte[] digest = PqAuthDigest.tx(txid, 0, 1); + byte[] digest = PQAuthDigest.tx(txid, 0, 1); byte[] sig = sign(kp, digest); Transaction signed = tx.toBuilder() - .addPqWitness(PqAuthWitness.newBuilder() + .addPqWitness(PQAuthWitness.newBuilder() .setKeyId(1) .setSignature(ByteString.copyFrom(sig)) .build()) @@ -323,9 +323,9 @@ private long[][] measureSizes(Transaction baseTx) { // ML-DSA-44: 2420-byte signature in pq_witness MLDSA44 kp44 = new MLDSA44(); byte[] txid44 = Sha256Hash.of(true, baseTx.getRawData().toByteArray()).getBytes(); - byte[] sig44 = MLDSA44.sign(kp44.getPrivateKey(), PqAuthDigest.tx(txid44, 0, 0)); + byte[] sig44 = MLDSA44.sign(kp44.getPrivateKey(), PQAuthDigest.tx(txid44, 0, 0)); Transaction tx44 = baseTx.toBuilder() - .addPqWitness(PqAuthWitness.newBuilder() + .addPqWitness(PQAuthWitness.newBuilder() .setSignature(ByteString.copyFrom(sig44)) .build()) .build(); @@ -336,9 +336,9 @@ private long[][] measureSizes(Transaction baseTx) { // ML-DSA-65: 3309-byte signature in pq_witness MLDSA65 kp65 = new MLDSA65(); byte[] txid65 = Sha256Hash.of(true, baseTx.getRawData().toByteArray()).getBytes(); - byte[] sig65 = MLDSA65.sign(kp65.getPrivateKey(), PqAuthDigest.tx(txid65, 0, 0)); + byte[] sig65 = MLDSA65.sign(kp65.getPrivateKey(), PQAuthDigest.tx(txid65, 0, 0)); Transaction tx65 = baseTx.toBuilder() - .addPqWitness(PqAuthWitness.newBuilder() + .addPqWitness(PQAuthWitness.newBuilder() .setSignature(ByteString.copyFrom(sig65)) .build()) .build(); @@ -379,18 +379,18 @@ public void transactionSizeComparisonByScheme() { } @Test - public void mlDsa65PqAuthWitnessAlsoAccepted() throws Exception { + public void mlDsa65PQAuthWitnessAlsoAccepted() throws Exception { dbManager.getDynamicPropertiesStore().saveAllowMlDsa(1L); MLDSA65 kp = new MLDSA65(); - putAccountWithPqPermission(PQ_OWNER_HEX, kp.getPublicKey(), SignatureScheme.ML_DSA_65); + putAccountWithPQPermission(PQ_OWNER_HEX, kp.getPublicKey(), SignatureScheme.ML_DSA_65); Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); byte[] txid = Sha256Hash.of(true, tx.getRawData().toByteArray()).getBytes(); - byte[] digest = PqAuthDigest.tx(txid, 0, 0); + byte[] digest = PQAuthDigest.tx(txid, 0, 0); byte[] sig = MLDSA65.sign(kp.getPrivateKey(), digest); Transaction signed = tx.toBuilder() - .addPqWitness(PqAuthWitness.newBuilder() + .addPqWitness(PQAuthWitness.newBuilder() .setSignature(ByteString.copyFrom(sig)) .build()) .build(); @@ -400,20 +400,20 @@ public void mlDsa65PqAuthWitnessAlsoAccepted() throws Exception { } @Test - public void fnDsaPqAuthWitnessAccepted() throws Exception { + public void fnDsaPQAuthWitnessAccepted() throws Exception { dbManager.getDynamicPropertiesStore().saveAllowFnDsa(1L); FNDSA kp = new FNDSA(); - putAccountWithPqPermission(PQ_OWNER_HEX, kp.getPublicKey(), SignatureScheme.FN_DSA); + putAccountWithPQPermission(PQ_OWNER_HEX, kp.getPublicKey(), SignatureScheme.FN_DSA); Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); byte[] txid = Sha256Hash.of(true, tx.getRawData().toByteArray()).getBytes(); - byte[] digest = PqAuthDigest.tx(txid, 0, 0); + byte[] digest = PQAuthDigest.tx(txid, 0, 0); byte[] sig = FNDSA.sign(kp.getPrivateKey(), digest); Assert.assertTrue("FN-DSA signature must be within protocol bound", sig.length > 0 && sig.length <= FNDSA.SIGNATURE_LENGTH); Transaction signed = tx.toBuilder() - .addPqWitness(PqAuthWitness.newBuilder() + .addPqWitness(PQAuthWitness.newBuilder() .setSignature(ByteString.copyFrom(sig)) .build()) .build(); @@ -423,19 +423,19 @@ public void fnDsaPqAuthWitnessAccepted() throws Exception { } @Test - public void fnDsaTamperedPqAuthWitnessRejected() throws Exception { + public void fnDsaTamperedPQAuthWitnessRejected() throws Exception { dbManager.getDynamicPropertiesStore().saveAllowFnDsa(1L); FNDSA kp = new FNDSA(); - putAccountWithPqPermission(PQ_OWNER_HEX, kp.getPublicKey(), SignatureScheme.FN_DSA); + putAccountWithPQPermission(PQ_OWNER_HEX, kp.getPublicKey(), SignatureScheme.FN_DSA); Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); byte[] txid = Sha256Hash.of(true, tx.getRawData().toByteArray()).getBytes(); - byte[] digest = PqAuthDigest.tx(txid, 0, 0); + byte[] digest = PQAuthDigest.tx(txid, 0, 0); byte[] sig = FNDSA.sign(kp.getPrivateKey(), digest); sig[0] ^= 0x01; Transaction signed = tx.toBuilder() - .addPqWitness(PqAuthWitness.newBuilder() + .addPqWitness(PQAuthWitness.newBuilder() .setSignature(ByteString.copyFrom(sig)) .build()) .build(); @@ -450,19 +450,19 @@ public void fnDsaTamperedPqAuthWitnessRejected() throws Exception { } @Test - public void fnDsaPqAuthWitnessRejectedWhenNotActivated() throws Exception { + public void fnDsaPQAuthWitnessRejectedWhenNotActivated() throws Exception { dbManager.getDynamicPropertiesStore().saveAllowMlDsa(0L); dbManager.getDynamicPropertiesStore().saveAllowFnDsa(0L); FNDSA kp = new FNDSA(); - putAccountWithPqPermission(PQ_OWNER_HEX, kp.getPublicKey(), SignatureScheme.FN_DSA); + putAccountWithPQPermission(PQ_OWNER_HEX, kp.getPublicKey(), SignatureScheme.FN_DSA); Transaction tx = buildTransferTx(PQ_OWNER_HEX, 0); byte[] txid = Sha256Hash.of(true, tx.getRawData().toByteArray()).getBytes(); - byte[] digest = PqAuthDigest.tx(txid, 0, 0); + byte[] digest = PQAuthDigest.tx(txid, 0, 0); byte[] sig = FNDSA.sign(kp.getPrivateKey(), digest); Transaction signed = tx.toBuilder() - .addPqWitness(PqAuthWitness.newBuilder() + .addPqWitness(PQAuthWitness.newBuilder() .setSignature(ByteString.copyFrom(sig)) .build()) .build(); 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 a1ac825abc1..73c7b8923c2 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,7 +15,7 @@ import org.tron.core.config.args.Args; import org.tron.core.utils.TransactionUtil; import org.tron.protos.Protocol; -import org.tron.protos.Protocol.PqAuthWitness; +import org.tron.protos.Protocol.PQAuthWitness; import org.tron.protos.Protocol.Transaction; public class UtilTest extends BaseTest { @@ -169,12 +169,12 @@ public void testPackTransaction() { } @Test - public void roundtripPqAuthWitnessJson() throws Exception { + public void roundtripPQAuthWitnessJson() throws Exception { byte[] sig = new byte[3309]; for (int i = 0; i < sig.length; i++) { sig[i] = (byte) (i & 0xff); } - PqAuthWitness pqWitness = PqAuthWitness.newBuilder() + PQAuthWitness pqWitness = PQAuthWitness.newBuilder() .setKeyId(1) .setSignature(ByteString.copyFrom(sig)) .build(); diff --git a/protocol/src/main/protos/core/Tron.proto b/protocol/src/main/protos/core/Tron.proto index 85241032d14..4a49577f83a 100644 --- a/protocol/src/main/protos/core/Tron.proto +++ b/protocol/src/main/protos/core/Tron.proto @@ -17,7 +17,7 @@ enum AccountType { Contract = 2; } -// Signature scheme identifier used by Permission.Key and PqWitness. +// Signature scheme identifier used by Permission.Key and PQWitness. // UNKNOWN_SIG_SCHEME (0) denotes legacy keys authenticated via the existing // Transaction.signature / BlockHeader.witness_signature paths (ECDSA secp256k1 // on mainnet, SM2/SM3 where applicable). Post-quantum schemes use dedicated @@ -258,7 +258,7 @@ message Account { // Post-quantum public key: algorithm identifier paired with raw key bytes. // Used inside Key.pq_key; absent on legacy (ECDSA/SM2) keys. -message PqPublicKey { +message PQPublicKey { SignatureScheme scheme = 1; // Raw key bytes. ML-DSA-44=1312B, ML-DSA-65=1952B, FN-DSA=896B. bytes public_key = 2; @@ -268,15 +268,14 @@ message Key { bytes address = 1; // empty for PQ-only keys int64 weight = 2; // Post-quantum key. Absent for legacy keys. - PqPublicKey pq_key = 3; - reserved 4; // was public_key; merged into pq_key + PQPublicKey pq_key = 3; } // Per-signer post-quantum authentication witness for a transaction or block. // key_id is the 0-based index of the signing key in the permission's key list. // key_id = 0 is the proto3 default and is omitted on the wire — single-key // accounts pay no overhead for this field. -message PqAuthWitness { +message PQAuthWitness { uint32 key_id = 1; bytes signature = 2; } @@ -489,7 +488,7 @@ message Transaction { // a transaction against a PQ Permission MUST use pq_witness and empty // signature; a transaction against a legacy Permission MUST use signature // and empty pq_witness. - repeated PqAuthWitness pq_witness = 6; + repeated PQAuthWitness pq_witness = 6; } message TransactionInfo { @@ -560,7 +559,7 @@ message BlockHeader { // Witness Permission with scheme = ML_DSA_65, pq_witness SHALL be // present in addition to witness_signature (Dual-Sign). Otherwise this // field SHALL be empty. - PqAuthWitness pq_witness = 3; + PQAuthWitness pq_witness = 3; } // block From 0626d30d2292d08e7e292d4cb01238b2c9e5fe01 Mon Sep 17 00:00:00 2001 From: federico Date: Tue, 28 Apr 2026 15:45:31 +0800 Subject: [PATCH 12/12] feat(actuator): support PQ-native account creation --- .../core/actuator/CreateAccountActuator.java | 23 +++ .../org/tron/core/capsule/AccountCapsule.java | 61 ++++++++ .../crypto/pqc/PQSignatureRegistry.java | 16 +++ .../actuator/CreateAccountActuatorTest.java | 134 ++++++++++++++++++ .../core/contract/account_contract.proto | 4 + 5 files changed, 238 insertions(+) diff --git a/actuator/src/main/java/org/tron/core/actuator/CreateAccountActuator.java b/actuator/src/main/java/org/tron/core/actuator/CreateAccountActuator.java index 352f394d6cb..f34930e7b7f 100755 --- a/actuator/src/main/java/org/tron/core/actuator/CreateAccountActuator.java +++ b/actuator/src/main/java/org/tron/core/actuator/CreateAccountActuator.java @@ -4,8 +4,10 @@ import com.google.protobuf.ByteString; import com.google.protobuf.InvalidProtocolBufferException; +import java.util.Arrays; import java.util.Objects; import lombok.extern.slf4j.Slf4j; +import org.tron.common.crypto.pqc.PQSignatureRegistry; import org.tron.common.utils.DecodeUtil; import org.tron.common.utils.StringUtil; import org.tron.core.capsule.AccountCapsule; @@ -15,6 +17,8 @@ import org.tron.core.exception.ContractValidateException; import org.tron.core.store.AccountStore; import org.tron.core.store.DynamicPropertiesStore; +import org.tron.protos.Protocol.PQPublicKey; +import org.tron.protos.Protocol.SignatureScheme; import org.tron.protos.Protocol.Transaction.Contract.ContractType; import org.tron.protos.Protocol.Transaction.Result.code; import org.tron.protos.contract.AccountContract.AccountCreateContract; @@ -120,6 +124,25 @@ public boolean validate() throws ContractValidateException { throw new ContractValidateException("Account has existed"); } + if (contract.hasPqKey()) { + PQPublicKey pq = contract.getPqKey(); + SignatureScheme scheme = pq.getScheme(); + DynamicPropertiesStore dyn = chainBaseManager.getDynamicPropertiesStore(); + if (!dyn.isPqSchemeAllowed(scheme)) { + throw new ContractValidateException("PQ scheme not activated: " + scheme); + } + byte[] pubKey = pq.getPublicKey().toByteArray(); + if (pubKey.length != PQSignatureRegistry.getPublicKeyLength(scheme)) { + throw new ContractValidateException( + "Invalid PQ public key length for scheme " + scheme); + } + byte[] derived = PQSignatureRegistry.computeAddress(scheme, pubKey); + if (!Arrays.equals(derived, accountAddress)) { + throw new ContractValidateException( + "account_address does not match the address derived from pq_key"); + } + } + return true; } diff --git a/chainbase/src/main/java/org/tron/core/capsule/AccountCapsule.java b/chainbase/src/main/java/org/tron/core/capsule/AccountCapsule.java index 1af7b55c8b2..d35ac2abb90 100644 --- a/chainbase/src/main/java/org/tron/core/capsule/AccountCapsule.java +++ b/chainbase/src/main/java/org/tron/core/capsule/AccountCapsule.java @@ -46,6 +46,7 @@ import org.tron.protos.Protocol.Account.UnFreezeV2; import org.tron.protos.Protocol.AccountType; import org.tron.protos.Protocol.Key; +import org.tron.protos.Protocol.PQPublicKey; import org.tron.protos.Protocol.Permission; import org.tron.protos.Protocol.Permission.PermissionType; import org.tron.protos.Protocol.Vote; @@ -98,6 +99,22 @@ public AccountCapsule(final AccountCreateContract contract) { */ public AccountCapsule(final AccountCreateContract contract, long createTime, boolean withDefaultPermission, DynamicPropertiesStore dynamicPropertiesStore) { + if (contract.hasPqKey()) { + Permission owner = createDefaultPqOwnerPermission(contract.getPqKey()); + Permission active = createDefaultPqActivePermission(contract.getPqKey(), + dynamicPropertiesStore); + + this.account = Account.newBuilder() + .setType(contract.getType()) + .setAddress(contract.getAccountAddress()) + .setTypeValue(contract.getTypeValue()) + .setCreateTime(createTime) + .setOwnerPermission(owner) + .addActivePermission(active) + .build(); + return; + } + if (withDefaultPermission) { Permission owner = createDefaultOwnerPermission(contract.getAccountAddress()); Permission active = createDefaultActivePermission(contract.getAccountAddress(), @@ -225,6 +242,50 @@ public static Permission createDefaultActivePermission(ByteString address, return active.build(); } + /** + * Default Owner permission bound to a PQ public key. The {@code address} + * field is left empty — the PQ key is authoritative and resolved by + * {@code key_id} during witness verification. + */ + public static Permission createDefaultPqOwnerPermission(PQPublicKey pqKey) { + Key key = Key.newBuilder() + .setAddress(ByteString.EMPTY) + .setWeight(1) + .setPqKey(pqKey) + .build(); + + return Permission.newBuilder() + .setType(PermissionType.Owner) + .setId(0) + .setPermissionName("owner") + .setThreshold(1) + .setParentId(0) + .addKeys(key) + .build(); + } + + /** + * Default Active permission bound to a PQ public key. + */ + public static Permission createDefaultPqActivePermission(PQPublicKey pqKey, + DynamicPropertiesStore dynamicPropertiesStore) { + Key key = Key.newBuilder() + .setAddress(ByteString.EMPTY) + .setWeight(1) + .setPqKey(pqKey) + .build(); + + return Permission.newBuilder() + .setType(PermissionType.Active) + .setId(2) + .setPermissionName("active") + .setThreshold(1) + .setParentId(0) + .setOperations(getActiveDefaultOperations(dynamicPropertiesStore)) + .addKeys(key) + .build(); + } + public static Permission createDefaultWitnessPermission(ByteString address) { Key.Builder key = Key.newBuilder(); key.setAddress(address); diff --git a/crypto/src/main/java/org/tron/common/crypto/pqc/PQSignatureRegistry.java b/crypto/src/main/java/org/tron/common/crypto/pqc/PQSignatureRegistry.java index aa1d1b2cf76..d9913fdc701 100644 --- a/crypto/src/main/java/org/tron/common/crypto/pqc/PQSignatureRegistry.java +++ b/crypto/src/main/java/org/tron/common/crypto/pqc/PQSignatureRegistry.java @@ -3,6 +3,7 @@ import java.util.Collections; import java.util.EnumMap; import java.util.Map; +import org.tron.common.crypto.Hash; import org.tron.protos.Protocol.SignatureScheme; /** @@ -144,6 +145,21 @@ public static PQSignature fromSeed(SignatureScheme scheme, byte[] seed) { return require(scheme).ops.fromSeed(seed); } + /** + * Derive the 21-byte TRON address from a PQ public key. Uses + * {@code Hash.sha3omit12(publicKey)} so the mapping matches the existing + * {@link PQSignature#getAddress()} contract. + */ + public static byte[] computeAddress(SignatureScheme 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 Hash.sha3omit12(publicKey); + } + private static SchemeInfo require(SignatureScheme scheme) { SchemeInfo info = SCHEMES.get(scheme); if (info == null) { 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..2bda708d62b 100755 --- a/framework/src/test/java/org/tron/core/actuator/CreateAccountActuatorTest.java +++ b/framework/src/test/java/org/tron/core/actuator/CreateAccountActuatorTest.java @@ -4,12 +4,17 @@ import com.google.protobuf.Any; import com.google.protobuf.ByteString; +import java.util.Arrays; import lombok.extern.slf4j.Slf4j; 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.pqc.FNDSA; +import org.tron.common.crypto.pqc.MLDSA44; +import org.tron.common.crypto.pqc.MLDSA65; +import org.tron.common.crypto.pqc.PQSignatureRegistry; import org.tron.common.utils.ByteArray; import org.tron.common.utils.StringUtil; import org.tron.core.Wallet; @@ -19,6 +24,8 @@ import org.tron.core.exception.ContractExeException; import org.tron.core.exception.ContractValidateException; import org.tron.protos.Protocol.AccountType; +import org.tron.protos.Protocol.PQPublicKey; +import org.tron.protos.Protocol.SignatureScheme; import org.tron.protos.Protocol.Transaction.Result.code; import org.tron.protos.contract.AccountContract.AccountCreateContract; import org.tron.protos.contract.AssetIssueContractOuterClass; @@ -220,6 +227,133 @@ public void commonErrorCheck() { } + private static byte[] filledSeed(int value, int length) { + byte[] seed = new byte[length]; + Arrays.fill(seed, (byte) value); + return seed; + } + + private Any pqContract(String ownerAddress, byte[] accountAddress, + SignatureScheme scheme, byte[] pqPublicKey) { + return Any.pack( + AccountCreateContract.newBuilder() + .setOwnerAddress(ByteString.copyFrom(ByteArray.fromHexString(ownerAddress))) + .setAccountAddress(ByteString.copyFrom(accountAddress)) + .setPqKey(PQPublicKey.newBuilder() + .setScheme(scheme) + .setPublicKey(ByteString.copyFrom(pqPublicKey)) + .build()) + .build()); + } + + private void runPqHappyPath(SignatureScheme scheme, byte[] pqPublicKey) { + dbManager.getDynamicPropertiesStore().saveAllowMultiSign(1L); + if (scheme == SignatureScheme.FN_DSA) { + dbManager.getDynamicPropertiesStore().saveAllowFnDsa(1L); + } else { + dbManager.getDynamicPropertiesStore().saveAllowMlDsa(1L); + } + byte[] derivedAddress = PQSignatureRegistry.computeAddress(scheme, pqPublicKey); + dbManager.getAccountStore().delete(derivedAddress); + + CreateAccountActuator actuator = new CreateAccountActuator(); + actuator.setChainBaseManager(dbManager.getChainBaseManager()) + .setAny(pqContract(OWNER_ADDRESS_SECOND, derivedAddress, scheme, pqPublicKey)); + TransactionResultCapsule ret = new TransactionResultCapsule(); + try { + actuator.validate(); + actuator.execute(ret); + Assert.assertEquals(code.SUCESS, ret.getInstance().getRet()); + + AccountCapsule created = dbManager.getAccountStore().get(derivedAddress); + Assert.assertNotNull(created); + // Owner permission bound to PQ key, address field empty. + Assert.assertEquals(1, created.getInstance().getOwnerPermission().getKeysCount()); + Assert.assertEquals(ByteString.EMPTY, + created.getInstance().getOwnerPermission().getKeys(0).getAddress()); + Assert.assertEquals(scheme, + created.getInstance().getOwnerPermission().getKeys(0).getPqKey().getScheme()); + Assert.assertEquals(ByteString.copyFrom(pqPublicKey), + created.getInstance().getOwnerPermission().getKeys(0).getPqKey().getPublicKey()); + // Active permission bound to same PQ key. + Assert.assertEquals(1, created.getInstance().getActivePermissionCount()); + Assert.assertEquals(scheme, + created.getInstance().getActivePermission(0).getKeys(0).getPqKey().getScheme()); + } catch (ContractValidateException | ContractExeException e) { + logger.info(e.getMessage()); + Assert.fail(e.getMessage()); + } + } + + @Test + public void createPqAccount_mlDsa44_success() { + MLDSA44 kp = new MLDSA44(filledSeed(0x11, MLDSA44.SEED_LENGTH)); + runPqHappyPath(SignatureScheme.ML_DSA_44, kp.getPublicKey()); + } + + @Test + public void createPqAccount_mlDsa65_success() { + MLDSA65 kp = new MLDSA65(filledSeed(0x12, MLDSA65.SEED_LENGTH)); + runPqHappyPath(SignatureScheme.ML_DSA_65, kp.getPublicKey()); + } + + @Test + public void createPqAccount_fnDsa_success() { + FNDSA kp = new FNDSA(filledSeed(0x13, FNDSA.SEED_LENGTH)); + runPqHappyPath(SignatureScheme.FN_DSA, kp.getPublicKey()); + } + + @Test + public void createPqAccount_addressMismatch() { + dbManager.getDynamicPropertiesStore().saveAllowMlDsa(1L); + MLDSA44 kp = new MLDSA44(filledSeed(0x21, MLDSA44.SEED_LENGTH)); + byte[] wrongAddress = ByteArray.fromHexString(OWNER_ADDRESS_FIRST); + + CreateAccountActuator actuator = new CreateAccountActuator(); + actuator.setChainBaseManager(dbManager.getChainBaseManager()) + .setAny(pqContract(OWNER_ADDRESS_SECOND, wrongAddress, + SignatureScheme.ML_DSA_44, kp.getPublicKey())); + TransactionResultCapsule ret = new TransactionResultCapsule(); + processAndCheckInvalid(actuator, ret, + "account_address does not match the address derived from pq_key", + "account_address does not match the address derived from pq_key"); + } + + @Test + public void createPqAccount_wrongPubKeyLength() { + dbManager.getDynamicPropertiesStore().saveAllowMlDsa(1L); + byte[] truncated = new byte[MLDSA44.PUBLIC_KEY_LENGTH - 1]; + byte[] derivedAddress = ByteArray.fromHexString(OWNER_ADDRESS_FIRST); + + CreateAccountActuator actuator = new CreateAccountActuator(); + actuator.setChainBaseManager(dbManager.getChainBaseManager()) + .setAny(pqContract(OWNER_ADDRESS_SECOND, derivedAddress, + SignatureScheme.ML_DSA_44, truncated)); + TransactionResultCapsule ret = new TransactionResultCapsule(); + processAndCheckInvalid(actuator, ret, + "Invalid PQ public key length for scheme ML_DSA_44", + "Invalid PQ public key length for scheme ML_DSA_44"); + } + + @Test + public void createPqAccount_schemeNotActivated() { + dbManager.getDynamicPropertiesStore().saveAllowMlDsa(0L); + dbManager.getDynamicPropertiesStore().saveAllowFnDsa(0L); + MLDSA44 kp = new MLDSA44(filledSeed(0x31, MLDSA44.SEED_LENGTH)); + byte[] derivedAddress = PQSignatureRegistry.computeAddress( + SignatureScheme.ML_DSA_44, kp.getPublicKey()); + dbManager.getAccountStore().delete(derivedAddress); + + CreateAccountActuator actuator = new CreateAccountActuator(); + actuator.setChainBaseManager(dbManager.getChainBaseManager()) + .setAny(pqContract(OWNER_ADDRESS_SECOND, derivedAddress, + SignatureScheme.ML_DSA_44, kp.getPublicKey())); + TransactionResultCapsule ret = new TransactionResultCapsule(); + processAndCheckInvalid(actuator, ret, + "PQ scheme not activated: ML_DSA_44", + "PQ scheme not activated: ML_DSA_44"); + } + private void processAndCheckInvalid(CreateAccountActuator actuator, TransactionResultCapsule ret, String failMsg, String expectedMsg) { diff --git a/protocol/src/main/protos/core/contract/account_contract.proto b/protocol/src/main/protos/core/contract/account_contract.proto index d3180048f43..08ea06b8c4e 100644 --- a/protocol/src/main/protos/core/contract/account_contract.proto +++ b/protocol/src/main/protos/core/contract/account_contract.proto @@ -27,6 +27,10 @@ message AccountCreateContract { bytes owner_address = 1; bytes account_address = 2; AccountType type = 3; + // Optional PQ public key. If set, account_address must equal + // sha3omit12(pq_key.public_key) and the new account's default Owner/Active + // permissions are bound to this PQ key (no ECDSA participation). + PQPublicKey pq_key = 4; } // Update account name. Account name is not unique now.