From ae3df1b39a05b513e941bbaa1f9bdc675bbd981c Mon Sep 17 00:00:00 2001 From: federico Date: Mon, 25 May 2026 01:56:58 +0800 Subject: [PATCH 1/8] refactor(framework): wrap pq miner identity and add 0x12 verifier --- .../org/tron/core/actuator/VMActuator.java | 8 +- .../tron/core/vm/PrecompiledContracts.java | 59 ++++++- .../org/tron/common/utils/LocalWitnesses.java | 8 +- .../org/tron/core/capsule/BlockCapsule.java | 22 +-- .../tron/core/capsule/TransactionCapsule.java | 7 +- .../src/main/java/org/tron/core/Constant.java | 8 - .../java/org/tron/consensus/base/Param.java | 94 ++++++++-- .../org/tron/consensus/dpos/DposService.java | 3 +- .../consensus/pbft/PbftMessageHandle.java | 2 +- .../signers/mldsa/MLDSA44Eip8051Verifier.java | 162 ++++++++++++++++++ .../org/tron/common/crypto/pqc/MLDSA44.java | 4 +- .../tron/core/consensus/ConsensusService.java | 13 +- .../main/java/org/tron/core/db/Manager.java | 33 ++-- .../runtime/vm/MlDsa44PrecompileTest.java | 84 ++++++--- .../core/capsule/TransactionCapsuleTest.java | 33 +++- 15 files changed, 430 insertions(+), 110 deletions(-) create mode 100644 crypto/src/main/java/org/bouncycastle/crypto/signers/mldsa/MLDSA44Eip8051Verifier.java diff --git a/actuator/src/main/java/org/tron/core/actuator/VMActuator.java b/actuator/src/main/java/org/tron/core/actuator/VMActuator.java index 0a9045a1586..ef454af5e98 100644 --- a/actuator/src/main/java/org/tron/core/actuator/VMActuator.java +++ b/actuator/src/main/java/org/tron/core/actuator/VMActuator.java @@ -178,8 +178,9 @@ public void execute(Object object) throws ContractExeException { ProgramResult result = context.getProgramResult(); try { if (program != null) { - if (null != blockCap && blockCap.generatedByMyself && blockCap.hasWitnessSignature(context.getStoreFactory().getChainBaseManager() - .getDynamicPropertiesStore()) + if (null != blockCap && blockCap.generatedByMyself && blockCap.hasWitnessSignature( + context.getStoreFactory().getChainBaseManager() + .getDynamicPropertiesStore()) && null != TransactionUtil.getContractRet(trx) && contractResult.OUT_OF_TIME == TransactionUtil.getContractRet(trx)) { result = program.getResult(); @@ -402,7 +403,8 @@ private void create() long thisTxCPULimitInUs = calculateCpuLimitInUs(isConstantCall, rootRepository.getDynamicPropertiesStore().getMaxCpuTimeOfOneTx(), - getCpuLimitInUsRatio(rootRepository.getDynamicPropertiesStore()), CommonParameter.getInstance().getConstantCallTimeoutMs()); + getCpuLimitInUsRatio(rootRepository.getDynamicPropertiesStore()), + CommonParameter.getInstance().getConstantCallTimeoutMs()); long vmStartInUs = System.nanoTime() / VMConstant.ONE_THOUSAND; long vmShouldEndInUs = vmStartInUs + thisTxCPULimitInUs; ProgramInvoke programInvoke = ProgramInvokeFactory 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 b5bbe2ab4cd..75a14298654 100644 --- a/actuator/src/main/java/org/tron/core/vm/PrecompiledContracts.java +++ b/actuator/src/main/java/org/tron/core/vm/PrecompiledContracts.java @@ -49,6 +49,7 @@ import org.bouncycastle.crypto.params.ECDomainParameters; import org.bouncycastle.crypto.params.ECPublicKeyParameters; import org.bouncycastle.crypto.signers.ECDSASigner; +import org.bouncycastle.crypto.signers.mldsa.MLDSA44Eip8051Verifier; import org.bouncycastle.math.ec.ECPoint; import org.tron.common.crypto.Blake2bfMessageDigest; import org.tron.common.crypto.Hash; @@ -126,6 +127,7 @@ public class PrecompiledContracts { private static final VerifyFnDsa512 verifyFnDsa512 = new VerifyFnDsa512(); private static final BatchValidateFnDsa512 batchValidateFnDsa512 = new BatchValidateFnDsa512(); + private static final VerifyMlDsa44Eip8051 verifyMlDsa44Eip8051 = new VerifyMlDsa44Eip8051(); private static final VerifyMlDsa44 verifyMlDsa44 = new VerifyMlDsa44(); private static final BatchValidateMlDsa44 batchValidateMlDsa44 = new BatchValidateMlDsa44(); private static final ValidateMultiPQSig validateMultiPqSig = new ValidateMultiPQSig(); @@ -242,10 +244,13 @@ public class PrecompiledContracts { private static final DataWord batchValidateFnDsa512Addr = new DataWord( "0000000000000000000000000000000000000000000000000000000000000018"); - // 0x19: ML-DSA-44 single verify (FIPS 204 / CRYSTALS-Dilithium-2). TRON-style - // layout uses the standard 1312-byte public key encoding rho‖t1, not the - // EIP-8051 20512-byte expanded form — the standard encoding lets us call - // BC's stock MLDSASigner directly without re-implementing FIPS 204 §6.5. + // 0x12: EIP-8051 VERIFY_MLDSA. Uses the EIP expanded public key layout + // [A_hat 16384B | tr 32B | t1_ntt 4096B], not the 1312B FIPS public key. + private static final DataWord verifyMlDsa44Eip8051Addr = new DataWord( + "0000000000000000000000000000000000000000000000000000000000000012"); + + // 0x19: existing TRON draft address for ML-DSA-44 single verify. Kept for + // compatibility with contracts/tests already targeting this PR branch. private static final DataWord verifyMlDsa44Addr = new DataWord( "0000000000000000000000000000000000000000000000000000000000000019"); @@ -370,6 +375,9 @@ public static PrecompiledContract getContractForAddress(DataWord address) { // ML-DSA-44 (FIPS 204 / Dilithium-2): single verify and batch verify are // gated by their own proposal flag. if (VMConfig.allowMlDsa44()) { + if (address.equals(verifyMlDsa44Eip8051Addr)) { + return verifyMlDsa44Eip8051; + } if (address.equals(verifyMlDsa44Addr)) { return verifyMlDsa44; } @@ -524,7 +532,7 @@ private static byte[] extractBytes(byte[] data, int offset, int len) { if (offset < 0 || len < 0 || offset > data.length) { return EMPTY_BYTE_ARRAY; } - int safe = Math.min(len, data.length - offset); + int safe = StrictMathWrapper.min(len, data.length - offset); return Arrays.copyOfRange(data, offset, offset + safe); } @@ -600,7 +608,8 @@ static int recoverFalconSigLen(byte[] data, int from, int to) { * breaks caller expectations and must be avoided. * *

Single-verify convention (e.g. {@code VerifyFnDsa512} 0x16, - * {@code VerifyMlDsa44} 0x19): {@code execute} always returns + * {@code VerifyMlDsa44Eip8051} 0x12, {@code VerifyMlDsa44} 0x19): + * {@code execute} always returns * {@code Pair.of(true, X)} where {@code X} is a 32-byte word — {@code dataOne()} * on cryptographic success, {@code DATA_FALSE} on any malformed input or * verification failure. The caller never observes an ABI/structural error; @@ -2814,8 +2823,8 @@ private static class PqVerifyResult { * {@code rho ‖ t1} (1312 B) instead of EIP-8051's 20512 B expanded form * (precomputed {@code A_hat = ExpandA(rho)}). BC 1.84's {@code MLDSASigner} * only accepts the standard form; we pay the per-call {@code ExpandA} - * cost so 1312 B Dilithium-2 keys work unchanged. An expanded-pk variant, - * if added later, will get a new precompile slot — 0x19 stays as-is. + * cost so 1312 B Dilithium-2 keys work unchanged. The EIP-8051 expanded-pk + * variant is implemented separately at 0x12 — 0x19 stays as-is. */ public static class VerifyMlDsa44 extends PrecompiledContract { @@ -2846,6 +2855,40 @@ public Pair execute(byte[] data) { } } + /** + * 0x12 EIP-8051 VERIFY_MLDSA for ML-DSA-44 expanded public keys. + * + *

Input layout: {@code [msg 32B | sig 2420B | expandedPk 20512B]}, where + * {@code expandedPk = A_hat(16384B) || tr(32B) || t1_ntt(4096B)}. Field + * elements inside {@code A_hat} and {@code t1_ntt} are 32-bit big-endian + * values and must be canonical ({@code < q}). + */ + public static class VerifyMlDsa44Eip8051 extends PrecompiledContract { + + @Override + public long getEnergyForData(byte[] data) { + return 4500; + } + + @Override + public Pair execute(byte[] data) { + if (data == null || data.length != MLDSA44Eip8051Verifier.INPUT_LENGTH) { + return Pair.of(true, DataWord.ZERO().getData()); + } + try { + int msgLen = MLDSA44Eip8051Verifier.MESSAGE_LENGTH; + int sigLen = MLDSA44Eip8051Verifier.SIGNATURE_LENGTH; + byte[] msg = copyOfRange(data, 0, msgLen); + byte[] sig = copyOfRange(data, msgLen, msgLen + sigLen); + byte[] pk = copyOfRange(data, msgLen + sigLen, data.length); + boolean ok = MLDSA44Eip8051Verifier.verify(msg, sig, pk); + return Pair.of(true, ok ? DataWord.ONE().getData() : DataWord.ZERO().getData()); + } catch (Throwable t) { + return Pair.of(true, DataWord.ZERO().getData()); + } + } + } + /** * 0x1a ValidateMultiPQSig — algorithm-agnostic Permission multi-sign. Accepts * ECDSA plus any registered post-quantum scheme (FN-DSA-512, ML-DSA-44, ...) 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 c234d5d4595..cd7de299ee2 100644 --- a/chainbase/src/main/java/org/tron/common/utils/LocalWitnesses.java +++ b/chainbase/src/main/java/org/tron/common/utils/LocalWitnesses.java @@ -37,6 +37,10 @@ public class LocalWitnesses { @Getter private List privateKeys = Lists.newArrayList(); + @Setter + @Getter + private byte[] witnessAccountAddress; + /** * Pre-derived PQ keypairs (scheme + private + public, hex), one per witness. * Each keypair declares its own PQ scheme so a single node can host SRs @@ -52,10 +56,6 @@ public class LocalWitnesses { @Getter private List pqKeypairs = Lists.newArrayList(); - @Setter - @Getter - private byte[] witnessAccountAddress; - /** * PQ-side counterpart to {@link #witnessAccountAddress}. Distinct from the * ECDSA address so a node can host two different SRs (one ECDSA + one PQ). 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 484ab159219..13b49ef973f 100755 --- a/chainbase/src/main/java/org/tron/core/capsule/BlockCapsule.java +++ b/chainbase/src/main/java/org/tron/core/capsule/BlockCapsule.java @@ -220,22 +220,18 @@ public boolean validateSignature(DynamicPropertiesStore dynamicPropertiesStore, boolean hasLegacy = !header.getWitnessSignature().isEmpty(); boolean hasPq = header.hasPqAuthSig(); - if (!dynamicPropertiesStore.isAnyPqSchemeAllowed()) { - if (hasPq) { - throw new ValidateSignatureException( - "pq_auth_sig not allowed: no post-quantum scheme is activated"); - } - return validateLegacySignature(header, witnessPermissionAddress); - } - - if (hasLegacy && hasPq) { + if (hasLegacy == hasPq) { throw new ValidateSignatureException( - "witness_signature and pq_auth_sig are mutually exclusive"); - } - if (!hasLegacy && !hasPq) { - throw new ValidateSignatureException("missing witness signature"); + hasLegacy + ? "witness_signature and pq_auth_sig are mutually exclusive" + : "missing witness signature"); } + if (hasPq) { + if (!dynamicPropertiesStore.isAnyPqSchemeAllowed()) { + throw new ValidateSignatureException( + "pq_auth_sig not allowed: no post-quantum scheme is activated"); + } return validatePQSignature(dynamicPropertiesStore, witnessPermissionAddress, header.getPqAuthSig()); } diff --git a/chainbase/src/main/java/org/tron/core/capsule/TransactionCapsule.java b/chainbase/src/main/java/org/tron/core/capsule/TransactionCapsule.java index ecf14e10dee..5987073cb7d 100755 --- a/chainbase/src/main/java/org/tron/core/capsule/TransactionCapsule.java +++ b/chainbase/src/main/java/org/tron/core/capsule/TransactionCapsule.java @@ -503,7 +503,8 @@ public static boolean validateSignature(Transaction transaction, if (dynamicPropertiesStore.isAnyPqSchemeAllowed() && transaction.getPqAuthSigCount() > 0) { try { weight = StrictMathWrapper.addExact(weight, - validatePQSignatureGetWeight(transaction, permission, dynamicPropertiesStore, approveList)); + validatePQSignatureGetWeight(transaction, permission, dynamicPropertiesStore, + approveList)); } catch (ArithmeticException e) { throw new PermissionException("weight overflow"); } @@ -637,7 +638,7 @@ public void addSign(byte[] privateKey, AccountStore accountStore) .signHash(getTransactionId().getBytes()))); this.transaction = this.transaction.toBuilder().addSignature(sig).build(); } - + private static void checkPermission(int permissionId, Permission permission, Transaction.Contract contract) throws PermissionException { if (permissionId != 0) { if (permission.getType() != PermissionType.Active) { @@ -810,7 +811,7 @@ public boolean validateSignature(AccountStore accountStore, } } isVerified = true; - } + } return true; } diff --git a/common/src/main/java/org/tron/core/Constant.java b/common/src/main/java/org/tron/core/Constant.java index 370a442fc06..1437d319346 100644 --- a/common/src/main/java/org/tron/core/Constant.java +++ b/common/src/main/java/org/tron/core/Constant.java @@ -60,14 +60,6 @@ public class Constant { // Crypto engine public static final String ECKey_ENGINE = "ECKey"; - // Post-quantum (FIPS 206 draft) FN-DSA / Falcon-512 signature constants. - // Falcon signatures are variable-length; SIGNATURE_MAX_LENGTH is the protocol-level - // upper bound, not an exact length. - public static final int FN_DSA_PUBLIC_KEY_LENGTH = 896; - public static final int FN_DSA_SIGNATURE_MAX_LENGTH = 752; - public static final String PQ_TX_AUTH_DOMAIN = "TRON_TX_AUTH_V1"; - public static final String PQ_BLOCK_AUTH_DOMAIN = "TRON_BLOCK_AUTH_V1"; - // Network public static final String LOCAL_HOST = "127.0.0.1"; diff --git a/consensus/src/main/java/org/tron/consensus/base/Param.java b/consensus/src/main/java/org/tron/consensus/base/Param.java index 4c7d075f061..b08648c47ba 100644 --- a/consensus/src/main/java/org/tron/consensus/base/Param.java +++ b/consensus/src/main/java/org/tron/consensus/base/Param.java @@ -68,40 +68,98 @@ public class Miner { @Setter private ByteString witnessAddress; - private byte[] pqPrivateKey; - - private byte[] pqPublicKey; - /** - * Post-quantum signature scheme for this miner. When unset (null), the - * miner signs blocks with the legacy ECDSA path using {@link #privateKey}; - * when set (e.g. {@code FN_DSA_512}), the PQ path is used with - * {@link #pqPrivateKey} / {@link #pqPublicKey}. + * Post-quantum identity for this miner — non-null iff the miner signs + * blocks via the PQ path. ECDSA fields above are left null when this is + * set so the two miner kinds never share a slot. */ @Getter - @Setter - private PQScheme pqScheme; + private final PQMiner pq; public Miner(byte[] privateKey, ByteString privateKeyAddress, ByteString witnessAddress) { this.privateKey = privateKey; this.privateKeyAddress = privateKeyAddress; this.witnessAddress = witnessAddress; + this.pq = null; + } + + /** + * PQ-miner constructor. {@code privateKeyAddress} carries the PQ-derived + * address (the key-slot identity), {@code witnessAddress} carries the + * on-chain witness identity (often the same, but may differ in multi-sig + * setups). The ECDSA fields {@link #privateKey} / {@link #privateKeyAddress} + * / {@link #witnessAddress} are left null on purpose so ECDSA-only code + * paths cannot accidentally consume a PQ identity. + */ + public Miner(PQScheme scheme, byte[] privateKey, byte[] publicKey, + ByteString privateKeyAddress, ByteString witnessAddress) { + this.pq = new PQMiner(scheme, privateKey, publicKey, privateKeyAddress, witnessAddress); } - public byte[] getPQPrivateKey() { - return pqPrivateKey == null ? null : pqPrivateKey.clone(); + /** True iff this miner signs via the PQ path (i.e. has a {@link PQMiner}). */ + public boolean isPq() { + return pq != null; } - public void setPQPrivateKey(byte[] pqPrivateKey) { - this.pqPrivateKey = pqPrivateKey == null ? null : pqPrivateKey.clone(); + /** + * Returns the on-chain witness address regardless of signing scheme — PQ + * miners route to {@link PQMiner#getWitnessAddress()}, ECDSA miners to + * {@link #witnessAddress}. Use this from scheme-agnostic call sites + * (block-producer map keys, witness-set filters, generic logging). + */ + public ByteString getEffectiveWitnessAddress() { + return pq != null ? pq.getWitnessAddress() : witnessAddress; } - public byte[] getPQPublicKey() { - return pqPublicKey == null ? null : pqPublicKey.clone(); + /** + * Returns the signing-key-derived address regardless of signing scheme — + * PQ miners route to {@link PQMiner#getPrivateKeyAddress()}, ECDSA miners to + * {@link #privateKeyAddress}. Use this from scheme-agnostic call sites + * (e.g. multi-sign permission checks). + */ + public ByteString getEffectivePrivateKeyAddress() { + return pq != null ? pq.getPrivateKeyAddress() : privateKeyAddress; } - public void setPQPublicKey(byte[] pqPublicKey) { - this.pqPublicKey = pqPublicKey == null ? null : pqPublicKey.clone(); + /** + * Post-quantum identity bundle: scheme + key material + derived addresses. + * Immutable; key bytes are defensively copied on the way in and out so the + * stored material can't be mutated by callers. + */ + public class PQMiner { + + @Getter + private final PQScheme scheme; + + private final byte[] privateKey; + + private final byte[] publicKey; + + /** Address derived from the PQ public key (key-slot identity). */ + @Getter + private final ByteString privateKeyAddress; + + /** On-chain witness identity — may differ from {@link #privateKeyAddress} + * in multi-sig setups, otherwise equal to it. */ + @Getter + private final ByteString witnessAddress; + + public PQMiner(PQScheme scheme, byte[] privateKey, byte[] publicKey, + ByteString privateKeyAddress, ByteString witnessAddress) { + this.scheme = scheme; + this.privateKey = privateKey == null ? null : privateKey.clone(); + this.publicKey = publicKey == null ? null : publicKey.clone(); + this.privateKeyAddress = privateKeyAddress; + this.witnessAddress = witnessAddress; + } + + public byte[] getPrivateKey() { + return privateKey == null ? null : privateKey.clone(); + } + + public byte[] getPublicKey() { + return publicKey == null ? null : publicKey.clone(); + } } } diff --git a/consensus/src/main/java/org/tron/consensus/dpos/DposService.java b/consensus/src/main/java/org/tron/consensus/dpos/DposService.java index 397c9d0835c..56f029b6dd6 100644 --- a/consensus/src/main/java/org/tron/consensus/dpos/DposService.java +++ b/consensus/src/main/java/org/tron/consensus/dpos/DposService.java @@ -77,7 +77,8 @@ public void start(Param param) { this.blockHandle = param.getBlockHandle(); this.genesisBlock = param.getGenesisBlock(); this.genesisBlockTime = Long.parseLong(param.getGenesisBlock().getTimestamp()); - param.getMiners().forEach(miner -> miners.put(miner.getWitnessAddress(), miner)); + param.getMiners().forEach(miner -> + miners.put(miner.getEffectiveWitnessAddress(), miner)); dposTask.setDposService(this); dposSlot.setDposService(this); diff --git a/consensus/src/main/java/org/tron/consensus/pbft/PbftMessageHandle.java b/consensus/src/main/java/org/tron/consensus/pbft/PbftMessageHandle.java index 523ffac4d61..dfed063352b 100644 --- a/consensus/src/main/java/org/tron/consensus/pbft/PbftMessageHandle.java +++ b/consensus/src/main/java/org/tron/consensus/pbft/PbftMessageHandle.java @@ -99,7 +99,7 @@ public List getSrMinerList(long epoch) { compareList = maintenanceManager.getBeforeWitness(); } return Param.getInstance().getMiners().stream() - .filter(miner -> compareList.contains(miner.getWitnessAddress())) + .filter(miner -> compareList.contains(miner.getEffectiveWitnessAddress())) .collect(Collectors.toList()); } diff --git a/crypto/src/main/java/org/bouncycastle/crypto/signers/mldsa/MLDSA44Eip8051Verifier.java b/crypto/src/main/java/org/bouncycastle/crypto/signers/mldsa/MLDSA44Eip8051Verifier.java new file mode 100644 index 00000000000..1adbba9e806 --- /dev/null +++ b/crypto/src/main/java/org/bouncycastle/crypto/signers/mldsa/MLDSA44Eip8051Verifier.java @@ -0,0 +1,162 @@ +package org.bouncycastle.crypto.signers.mldsa; + +import org.bouncycastle.crypto.digests.SHAKEDigest; +import org.bouncycastle.crypto.params.MLDSAParameters; +import org.bouncycastle.util.Arrays; + +/** + * EIP-8051 VERIFY_MLDSA verifier for ML-DSA-44 expanded public keys. + * + *

This class intentionally lives in Bouncy Castle's ML-DSA internal package so it can reuse + * the package-private polynomial primitives. Bouncy Castle 1.84 exposes only the standard + * FIPS-204 public key verifier ({@code rho || t1}); EIP-8051 instead supplies + * {@code A_hat || tr || t1_ntt}. + */ +public final class MLDSA44Eip8051Verifier { + + public static final int MESSAGE_LENGTH = 32; + public static final int SIGNATURE_LENGTH = 2420; + public static final int EXPANDED_PUBLIC_KEY_LENGTH = 20512; + public static final int INPUT_LENGTH = + MESSAGE_LENGTH + SIGNATURE_LENGTH + EXPANDED_PUBLIC_KEY_LENGTH; + + private static final int K = 4; + private static final int L = 4; + private static final int FIELD_ELEMENT_BYTES = 4; + private static final int MATRIX_BYTES = + K * L * MLDSAEngine.DilithiumN * FIELD_ELEMENT_BYTES; + private static final int TR_BYTES = 32; + private static final int TWO_POWER_D = 1 << MLDSAEngine.DilithiumD; + + private MLDSA44Eip8051Verifier() { + } + + public static boolean verify(byte[] message, byte[] signature, byte[] expandedPublicKey) { + if (message == null || message.length != MESSAGE_LENGTH + || signature == null || signature.length != SIGNATURE_LENGTH + || expandedPublicKey == null + || expandedPublicKey.length != EXPANDED_PUBLIC_KEY_LENGTH) { + return false; + } + + try { + MLDSAEngine engine = MLDSAEngine.getInstance(MLDSAParameters.ml_dsa_44, null); + PolyVecL[] aHat = decodeMatrix(expandedPublicKey, 0, engine); + byte[] tr = Arrays.copyOfRange(expandedPublicKey, MATRIX_BYTES, MATRIX_BYTES + TR_BYTES); + PolyVecK t1Ntt = decodePolyVecK( + expandedPublicKey, MATRIX_BYTES + TR_BYTES, engine); + + return verifyInternal(message, signature, aHat, tr, t1Ntt, engine); + } catch (RuntimeException e) { + return false; + } + } + + private static boolean verifyInternal(byte[] message, byte[] signature, PolyVecL[] aHat, + byte[] tr, PolyVecK t1Ntt, MLDSAEngine engine) { + PolyVecK h = new PolyVecK(engine); + PolyVecL z = new PolyVecL(engine); + if (!Packing.unpackSignature(z, h, signature, engine)) { + return false; + } + if (z.checkNorm(engine.getDilithiumGamma1() - engine.getDilithiumBeta())) { + return false; + } + + byte[] buf = new byte[StrictMath.max( + MLDSAEngine.CrhBytes + K * engine.getDilithiumPolyW1PackedBytes(), + engine.getDilithiumCTilde())]; + SHAKEDigest shake = new SHAKEDigest(256); + shake.update(tr, 0, TR_BYTES); + shake.update(message, 0, MESSAGE_LENGTH); + shake.doFinal(buf, 0, MLDSAEngine.CrhBytes); + + Poly c = new Poly(engine); + c.challenge(signature, 0, engine.getDilithiumCTilde()); + + z.polyVecNtt(); + PolyVecK w1 = pointwiseMontgomery(aHat, z, engine); + + c.polyNtt(); + PolyVecK ct1 = new PolyVecK(engine); + ct1.pointwisePolyMontgomery(c, t1Ntt); + multiplyByTwoPowerD(ct1); + + w1.subtract(ct1); + w1.reduce(); + w1.invNttToMont(); + w1.conditionalAddQ(); + w1.useHint(w1, h); + w1.packW1(engine, buf, MLDSAEngine.CrhBytes); + + shake = new SHAKEDigest(256); + shake.update(buf, 0, MLDSAEngine.CrhBytes + K * engine.getDilithiumPolyW1PackedBytes()); + shake.doFinal(buf, 0, engine.getDilithiumCTilde()); + return Arrays.constantTimeAreEqual(engine.getDilithiumCTilde(), signature, 0, buf, 0); + } + + private static PolyVecK pointwiseMontgomery(PolyVecL[] aHat, PolyVecL z, + MLDSAEngine engine) { + PolyVecK out = new PolyVecK(engine); + Poly tmp = new Poly(engine); + for (int i = 0; i < K; i++) { + out.getVectorIndex(i).pointwiseMontgomery(aHat[i].getVectorIndex(0), z.getVectorIndex(0)); + for (int j = 1; j < L; j++) { + tmp.pointwiseMontgomery(aHat[i].getVectorIndex(j), z.getVectorIndex(j)); + out.getVectorIndex(i).addPoly(tmp); + } + } + return out; + } + + private static PolyVecL[] decodeMatrix(byte[] in, int offset, MLDSAEngine engine) { + PolyVecL[] matrix = new PolyVecL[K]; + int pos = offset; + for (int i = 0; i < K; i++) { + matrix[i] = new PolyVecL(engine); + for (int j = 0; j < L; j++) { + decodePoly(in, pos, matrix[i].getVectorIndex(j)); + pos += MLDSAEngine.DilithiumN * FIELD_ELEMENT_BYTES; + } + } + return matrix; + } + + private static PolyVecK decodePolyVecK(byte[] in, int offset, MLDSAEngine engine) { + PolyVecK out = new PolyVecK(engine); + int pos = offset; + for (int i = 0; i < K; i++) { + decodePoly(in, pos, out.getVectorIndex(i)); + pos += MLDSAEngine.DilithiumN * FIELD_ELEMENT_BYTES; + } + return out; + } + + private static void decodePoly(byte[] in, int offset, Poly out) { + int pos = offset; + for (int i = 0; i < MLDSAEngine.DilithiumN; i++) { + int coeff = ((in[pos] & 0xff) << 24) + | ((in[pos + 1] & 0xff) << 16) + | ((in[pos + 2] & 0xff) << 8) + | (in[pos + 3] & 0xff); + if (coeff >= MLDSAEngine.DilithiumQ) { + throw new IllegalArgumentException("invalid ML-DSA field element"); + } + out.setCoeffIndex(i, coeff); + pos += FIELD_ELEMENT_BYTES; + } + } + + private static void multiplyByTwoPowerD(PolyVecK v) { + for (int i = 0; i < K; i++) { + Poly poly = v.getVectorIndex(i); + for (int j = 0; j < MLDSAEngine.DilithiumN; j++) { + long coeff = poly.getCoeffIndex(j) % (long) MLDSAEngine.DilithiumQ; + if (coeff < 0) { + coeff += MLDSAEngine.DilithiumQ; + } + poly.setCoeffIndex(j, (int) ((coeff * TWO_POWER_D) % MLDSAEngine.DilithiumQ)); + } + } + } +} 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 bce303352f3..e21bdd9eb1e 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 @@ -29,8 +29,8 @@ * derived {@code t1} stays in memory after instantiation). * *

Pure ML-DSA only (no SHA2-512 pre-hash variant). The "pure" mode signs - * the raw message under SHAKE-256 per FIPS 204 §5.2, matching the verify - * side of the EVM precompile at address 0x19 (EIP-8051). + * the raw message under SHAKE-256 per FIPS 204 §5.2, matching the standard + * 1312-byte public key verify path used by the 0x19 precompile. */ public final class MLDSA44 implements PQSignature { 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 1087d1d4757..0356c6e3cbf 100644 --- a/framework/src/main/java/org/tron/core/consensus/ConsensusService.java +++ b/framework/src/main/java/org/tron/core/consensus/ConsensusService.java @@ -93,7 +93,8 @@ public void start() { Miner miner = buildPQMiner(param, kp, null); miners.add(miner); logger.info("Add {} witness (from configured keypair): {}, size: {}", - kp.getScheme(), Hex.toHexString(miner.getWitnessAddress().toByteArray()), + kp.getScheme(), + Hex.toHexString(miner.getPq().getWitnessAddress().toByteArray()), miners.size()); } } else if (pqKeypairs.size() == 1) { @@ -101,7 +102,8 @@ public void start() { Args.getLocalWitnesses().getPqWitnessAccountAddress()); miners.add(miner); logger.info("Add {} witness (from configured keypair): {}", - miner.getPqScheme(), Hex.toHexString(miner.getWitnessAddress().toByteArray())); + miner.getPq().getScheme(), + Hex.toHexString(miner.getPq().getWitnessAddress().toByteArray())); } param.setMiners(miners); @@ -132,12 +134,9 @@ private Miner buildPQMiner(Param param, PqKeypair pqKeypair, byte[] witnessAddre if (witnessStore.get(witnessAddress) == null) { logger.warn("Witness {} is not in witnessStore.", Hex.toHexString(witnessAddress)); } - Miner miner = param.new Miner(null, + return param.new Miner(scheme, + keypair.getPrivateKey(), keypair.getPublicKey(), ByteString.copyFrom(pqAddress), ByteString.copyFrom(witnessAddress)); - miner.setPQPrivateKey(keypair.getPrivateKey()); - miner.setPQPublicKey(keypair.getPublicKey()); - miner.setPqScheme(scheme); - return miner; } private static void requireSupportedPqScheme(PQScheme scheme) { 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 be9fe097bc9..8db9777f1bb 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -1617,7 +1617,8 @@ public TransactionInfo processTransaction(final TransactionCapsule trxCap, Block * Generate a block. */ public BlockCapsule generateBlock(Miner miner, long blockTime, long timeout) { - String address = StringUtil.encode58Check(miner.getWitnessAddress().toByteArray()); + ByteString witnessAddress = miner.getEffectiveWitnessAddress(); + String address = StringUtil.encode58Check(witnessAddress.toByteArray()); final Histogram.Timer timer = Metrics.histogramStartTimer( MetricKeys.Histogram.BLOCK_GENERATE_LATENCY, address); Metrics.histogramObserve(MetricKeys.Histogram.MINER_DELAY, @@ -1627,7 +1628,7 @@ public BlockCapsule generateBlock(Miner miner, long blockTime, long timeout) { BlockCapsule blockCapsule = new BlockCapsule(chainBaseManager.getHeadBlockNum() + 1, chainBaseManager.getHeadBlockId(), - blockTime, miner.getWitnessAddress()); + blockTime, witnessAddress); blockCapsule.generatedByMyself = true; session.reset(); session.setValue(revokingStore.buildSession()); @@ -1636,9 +1637,9 @@ public BlockCapsule generateBlock(Miner miner, long blockTime, long timeout) { accountStateCallBack.preExecute(blockCapsule); if (getDynamicPropertiesStore().getAllowMultiSign() == 1) { - byte[] privateKeyAddress = miner.getPrivateKeyAddress().toByteArray(); + byte[] privateKeyAddress = miner.getEffectivePrivateKeyAddress().toByteArray(); AccountCapsule witnessAccount = getAccountStore() - .get(miner.getWitnessAddress().toByteArray()); + .get(witnessAddress.toByteArray()); if (!Arrays.equals(privateKeyAddress, witnessAccount.getWitnessPermissionAddress())) { logger.warn("Witness permission is wrong."); return null; @@ -1747,7 +1748,7 @@ public BlockCapsule generateBlock(Miner miner, long blockTime, long timeout) { session.reset(); blockCapsule.setMerkleRoot(); - if (miner.getPqScheme() != null) { + if (miner.isPq()) { // PQ-only miner: never fall back to ECDSA signing — miner.getPrivateKey() is // null on this path, and a silent fallback would NPE inside blockCapsule.sign. // Fail fast with a clear cause; DposTask's Throwable handler logs it and the @@ -1755,10 +1756,11 @@ public BlockCapsule generateBlock(Miner miner, long blockTime, long timeout) { // Gate on this miner's specific scheme, not on the broader "any PQ scheme // allowed" flag — a Falcon-configured miner must not produce while only // ML-DSA is active (and vice versa). - if (!getDynamicPropertiesStore().isPqSchemeAllowed(miner.getPqScheme())) { + Param.Miner.PQMiner pq = miner.getPq(); + if (!getDynamicPropertiesStore().isPqSchemeAllowed(pq.getScheme())) { throw new IllegalStateException( - "PQ miner " + Hex.toHexString(miner.getWitnessAddress().toByteArray()) - + " has scheme " + miner.getPqScheme() + "PQ miner " + Hex.toHexString(pq.getWitnessAddress().toByteArray()) + + " has scheme " + pq.getScheme() + " configured but that scheme is not allowed by dynamic properties"); } signBlockCapsuleWithPQ(blockCapsule, miner); @@ -1781,24 +1783,25 @@ public BlockCapsule generateBlock(Miner miner, long blockTime, long timeout) { } private void signBlockCapsuleWithPQ(BlockCapsule blockCapsule, Miner miner) { - PQScheme scheme = miner.getPqScheme(); + Param.Miner.PQMiner pq = miner.getPq(); + PQScheme scheme = pq.getScheme(); if (scheme == null || !PQSchemeRegistry.contains(scheme)) { throw new IllegalStateException( - "PQ miner " + Hex.toHexString(miner.getWitnessAddress().toByteArray()) - + " has scheme " + miner.getPqScheme() + "PQ miner " + Hex.toHexString(pq.getWitnessAddress().toByteArray()) + + " has scheme " + scheme + " which is not registered in PQSchemeRegistry"); } if (!chainBaseManager.getDynamicPropertiesStore().isPqSchemeAllowed(scheme)) { throw new IllegalStateException( - "PQ miner " + Hex.toHexString(miner.getWitnessAddress().toByteArray()) + "PQ miner " + Hex.toHexString(pq.getWitnessAddress().toByteArray()) + " has scheme " + scheme + " but it is not allowed by dynamic properties"); } - byte[] pqPrivateKey = miner.getPQPrivateKey(); - byte[] pqPublicKey = miner.getPQPublicKey(); + byte[] pqPrivateKey = pq.getPrivateKey(); + byte[] pqPublicKey = pq.getPublicKey(); if (pqPrivateKey == null || pqPublicKey == null) { throw new IllegalStateException( - "miner " + Hex.toHexString(miner.getWitnessAddress().toByteArray()) + "miner " + Hex.toHexString(pq.getWitnessAddress().toByteArray()) + " has scheme " + scheme + " set but local PQ key material is missing"); } diff --git a/framework/src/test/java/org/tron/common/runtime/vm/MlDsa44PrecompileTest.java b/framework/src/test/java/org/tron/common/runtime/vm/MlDsa44PrecompileTest.java index 4728bea42d6..79bf982df68 100644 --- a/framework/src/test/java/org/tron/common/runtime/vm/MlDsa44PrecompileTest.java +++ b/framework/src/test/java/org/tron/common/runtime/vm/MlDsa44PrecompileTest.java @@ -11,14 +11,20 @@ import org.tron.core.vm.config.VMConfig; /** - * Unit tests for the ML-DSA-44 (0x19) verify precompile (FIPS 204 / Dilithium-2). - * Input layout: [msg 32B | sig 2420B | pk 1312B]. Stateless — no chain DB. + * Unit tests for the ML-DSA-44 verify precompile (FIPS 204 / Dilithium-2). + * Address 0x12 follows EIP-8051 with expanded public keys; 0x19 remains the + * existing TRON draft path with standard 1312-byte public keys. */ public class MlDsa44PrecompileTest { - private static final DataWord MLDSA_ADDR = new DataWord( + private static final DataWord MLDSA_EIP8051_ADDR = new DataWord( + "0000000000000000000000000000000000000000000000000000000000000012"); + + private static final DataWord MLDSA_DRAFT_ADDR = new DataWord( "0000000000000000000000000000000000000000000000000000000000000019"); + private static final int EIP8051_INPUT_LENGTH = 32 + MLDSA44.SIGNATURE_LENGTH + 20512; + private static final byte[] MESSAGE_HASH = new byte[32]; static { @@ -40,21 +46,26 @@ public void disableProposal() { @Test public void switchOff_returnsNull() { VMConfig.initAllowMlDsa44(0L); - Assert.assertNull(PrecompiledContracts.getContractForAddress(MLDSA_ADDR)); + Assert.assertNull(PrecompiledContracts.getContractForAddress(MLDSA_EIP8051_ADDR)); + } + + @Test + public void switchOn_returnsEip8051Contract() { + Assert.assertNotNull(PrecompiledContracts.getContractForAddress(MLDSA_EIP8051_ADDR)); } @Test - public void switchOn_returnsContract() { - Assert.assertNotNull(PrecompiledContracts.getContractForAddress(MLDSA_ADDR)); + public void draftAddress19StillReturnsContract() { + Assert.assertNotNull(PrecompiledContracts.getContractForAddress(MLDSA_DRAFT_ADDR)); } @Test - public void validSignature_returnsOne() { + public void draftAddress19ValidSignature_returnsOne() { MLDSA44 key = new MLDSA44(); byte[] sig = key.sign(MESSAGE_HASH); byte[] input = buildInput(MESSAGE_HASH, sig, key.getPublicKey()); - PrecompiledContract pc = PrecompiledContracts.getContractForAddress(MLDSA_ADDR); + PrecompiledContract pc = PrecompiledContracts.getContractForAddress(MLDSA_DRAFT_ADDR); Pair result = pc.execute(input); Assert.assertTrue(result.getLeft()); @@ -63,7 +74,7 @@ public void validSignature_returnsOne() { } @Test - public void tamperedMessage_returnsZero() { + public void draftAddress19TamperedMessage_returnsZero() { MLDSA44 key = new MLDSA44(); byte[] sig = key.sign(MESSAGE_HASH); byte[] tampered = MESSAGE_HASH.clone(); @@ -71,71 +82,71 @@ public void tamperedMessage_returnsZero() { byte[] input = buildInput(tampered, sig, key.getPublicKey()); Pair result = - PrecompiledContracts.getContractForAddress(MLDSA_ADDR).execute(input); + PrecompiledContracts.getContractForAddress(MLDSA_DRAFT_ADDR).execute(input); Assert.assertTrue(result.getLeft()); Assert.assertArrayEquals(DataWord.ZERO().getData(), result.getRight()); } @Test - public void tamperedSignature_returnsZero() { + public void draftAddress19TamperedSignature_returnsZero() { MLDSA44 key = new MLDSA44(); byte[] sig = key.sign(MESSAGE_HASH); sig[0] ^= 0x01; byte[] input = buildInput(MESSAGE_HASH, sig, key.getPublicKey()); Pair result = - PrecompiledContracts.getContractForAddress(MLDSA_ADDR).execute(input); + PrecompiledContracts.getContractForAddress(MLDSA_DRAFT_ADDR).execute(input); Assert.assertTrue(result.getLeft()); Assert.assertArrayEquals(DataWord.ZERO().getData(), result.getRight()); } @Test - public void wrongPublicKey_returnsZero() { + public void draftAddress19WrongPublicKey_returnsZero() { MLDSA44 signer = new MLDSA44(); MLDSA44 other = new MLDSA44(); byte[] sig = signer.sign(MESSAGE_HASH); byte[] input = buildInput(MESSAGE_HASH, sig, other.getPublicKey()); Pair result = - PrecompiledContracts.getContractForAddress(MLDSA_ADDR).execute(input); + PrecompiledContracts.getContractForAddress(MLDSA_DRAFT_ADDR).execute(input); Assert.assertTrue(result.getLeft()); Assert.assertArrayEquals(DataWord.ZERO().getData(), result.getRight()); } @Test - public void nullInput_returnsZero() { + public void draftAddress19NullInput_returnsZero() { Pair result = - PrecompiledContracts.getContractForAddress(MLDSA_ADDR).execute(null); + PrecompiledContracts.getContractForAddress(MLDSA_DRAFT_ADDR).execute(null); Assert.assertTrue(result.getLeft()); Assert.assertArrayEquals(DataWord.ZERO().getData(), result.getRight()); } @Test - public void shortInput_returnsZero() { + public void draftAddress19ShortInput_returnsZero() { Pair result = - PrecompiledContracts.getContractForAddress(MLDSA_ADDR).execute(new byte[100]); + PrecompiledContracts.getContractForAddress(MLDSA_DRAFT_ADDR).execute(new byte[100]); Assert.assertTrue(result.getLeft()); Assert.assertArrayEquals(DataWord.ZERO().getData(), result.getRight()); } @Test - public void wrongLengthInput_returnsZero() { + public void draftAddress19WrongLengthInput_returnsZero() { // ML-DSA-44 input is fixed-length 3764B; any other length must be rejected. int expected = 32 + MLDSA44.SIGNATURE_LENGTH + MLDSA44.PUBLIC_KEY_LENGTH; byte[] oneByteShort = new byte[expected - 1]; Pair r1 = - PrecompiledContracts.getContractForAddress(MLDSA_ADDR).execute(oneByteShort); + PrecompiledContracts.getContractForAddress(MLDSA_DRAFT_ADDR).execute(oneByteShort); Assert.assertTrue(r1.getLeft()); Assert.assertArrayEquals(DataWord.ZERO().getData(), r1.getRight()); } @Test - public void trailingBytes_returnsZero() { + public void draftAddress19TrailingBytes_returnsZero() { // Strict equality: even one extra trailing byte must be rejected. MLDSA44 key = new MLDSA44(); byte[] sig = key.sign(MESSAGE_HASH); @@ -144,7 +155,36 @@ public void trailingBytes_returnsZero() { System.arraycopy(valid, 0, padded, 0, valid.length); Pair result = - PrecompiledContracts.getContractForAddress(MLDSA_ADDR).execute(padded); + PrecompiledContracts.getContractForAddress(MLDSA_DRAFT_ADDR).execute(padded); + + Assert.assertTrue(result.getLeft()); + Assert.assertArrayEquals(DataWord.ZERO().getData(), result.getRight()); + } + + @Test + public void eip8051Address12RejectsStandardPublicKeyLayout() { + MLDSA44 key = new MLDSA44(); + byte[] sig = key.sign(MESSAGE_HASH); + byte[] standardInput = buildInput(MESSAGE_HASH, sig, key.getPublicKey()); + + Pair result = + PrecompiledContracts.getContractForAddress(MLDSA_EIP8051_ADDR).execute(standardInput); + + Assert.assertTrue(result.getLeft()); + Assert.assertArrayEquals(DataWord.ZERO().getData(), result.getRight()); + } + + @Test + public void eip8051Address12RejectsNonCanonicalFieldElement() { + byte[] input = new byte[EIP8051_INPUT_LENGTH]; + int pkOffset = 32 + MLDSA44.SIGNATURE_LENGTH; + input[pkOffset] = 0x00; + input[pkOffset + 1] = 0x7f; + input[pkOffset + 2] = (byte) 0xe0; + input[pkOffset + 3] = 0x01; + + Pair result = + PrecompiledContracts.getContractForAddress(MLDSA_EIP8051_ADDR).execute(input); Assert.assertTrue(result.getLeft()); Assert.assertArrayEquals(DataWord.ZERO().getData(), result.getRight()); 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 7c1c7356383..ea019835181 100644 --- a/framework/src/test/java/org/tron/core/capsule/TransactionCapsuleTest.java +++ b/framework/src/test/java/org/tron/core/capsule/TransactionCapsuleTest.java @@ -23,6 +23,7 @@ import org.tron.common.TestConstants; import org.tron.common.crypto.ECKey; import org.tron.common.crypto.pqc.FNDSA512; +import org.tron.common.crypto.pqc.MLDSA44; import org.tron.common.crypto.pqc.PQSchemeRegistry; import org.tron.common.utils.ByteArray; import org.tron.common.utils.Sha256Hash; @@ -365,7 +366,7 @@ private Transaction buildTrc20TransferTx(String ownerHex, int permissionId) { /** * Returns [serializedSize, packSize, maxTxPerBlock] rows ordered by signature size: - * ECKey, FN-DSA-512. + * ECKey, FN-DSA-512, ML-DSA-44. */ private long[][] measureSizes(Transaction baseTx) { final long blockLimit = 2_000_000L; @@ -377,10 +378,11 @@ private long[][] measureSizes(Transaction baseTx) { long ecSerial = ecCap.getInstance().toByteArray().length; long ecPack = ecCap.computeTrxSizeForBlockMessage(); + byte[] txid = txId(baseTx); + // FN-DSA-512: variable-length signature (<= 752 bytes) + 897-byte public key FNDSA512 kpFn = new FNDSA512(); - byte[] txidFn = txId(baseTx); - byte[] sigFn = FNDSA512.sign(kpFn.getPrivateKey(), txidFn); + byte[] sigFn = FNDSA512.sign(kpFn.getPrivateKey(), txid); Transaction txFn = baseTx.toBuilder() .addPqAuthSig(PQAuthSig.newBuilder() .setScheme(PQScheme.FN_DSA_512) @@ -392,9 +394,24 @@ private long[][] measureSizes(Transaction baseTx) { long dFnSerial = txFn.toByteArray().length; long dFnPack = capFn.computeTrxSizeForBlockMessage(); + // ML-DSA-44: fixed 2420-byte signature + 1312-byte public key + MLDSA44 kpMl = new MLDSA44(); + byte[] sigMl = MLDSA44.sign(kpMl.getPrivateKey(), txid); + Transaction txMl = baseTx.toBuilder() + .addPqAuthSig(PQAuthSig.newBuilder() + .setScheme(PQScheme.ML_DSA_44) + .setPublicKey(ByteString.copyFrom(kpMl.getPublicKey())) + .setSignature(ByteString.copyFrom(sigMl)) + .build()) + .build(); + TransactionCapsule capMl = new TransactionCapsule(txMl); + long dMlSerial = txMl.toByteArray().length; + long dMlPack = capMl.computeTrxSizeForBlockMessage(); + return new long[][]{ {ecSerial, ecPack, blockLimit / ecPack}, {dFnSerial, dFnPack, blockLimit / dFnPack}, + {dMlSerial, dMlPack, blockLimit / dMlPack}, }; } @@ -403,7 +420,7 @@ public void transactionSizeComparisonByScheme() { long[][] trx = measureSizes(buildTransferTx(PQ_OWNER_HEX, 0)); long[][] trc20 = measureSizes(buildTrc20TransferTx(PQ_OWNER_HEX, 0)); - String[] labels = {"ECKey (ECDSA)", "FN-DSA-512"}; + String[] labels = {"ECKey (ECDSA)", "FN-DSA-512", "ML-DSA-44"}; System.out.println("=== TRX transfer ==="); for (int i = 0; i < labels.length; i++) { System.out.printf(" %s: serial=%d B pack=%d B maxTx/block=%d%n", @@ -415,11 +432,17 @@ public void transactionSizeComparisonByScheme() { labels[i], trc20[i][0], trc20[i][1], trc20[i][2]); } - // FN-DSA-512 envelope is larger than ECKey, so it fits fewer txs per block. + // Both PQ envelopes are larger than ECKey, so they fit fewer txs per block. + // ML-DSA-44 (2420 B sig + 1312 B pk) is the heaviest, FN-DSA-512 sits between. Assert.assertTrue(trx[1][0] > trx[0][0]); Assert.assertTrue(trc20[1][0] > trc20[0][0]); Assert.assertTrue(trx[1][2] < trx[0][2]); Assert.assertTrue(trc20[1][2] < trc20[0][2]); + + Assert.assertTrue(trx[2][0] > trx[1][0]); + Assert.assertTrue(trc20[2][0] > trc20[1][0]); + Assert.assertTrue(trx[2][2] < trx[1][2]); + Assert.assertTrue(trc20[2][2] < trc20[1][2]); } @Test From b8f20c3796c34b61017bfd28d72e185161aa59ff Mon Sep 17 00:00:00 2001 From: GrapeS Date: Mon, 25 May 2026 17:36:38 +0800 Subject: [PATCH 2/8] feat(metrics): add tx_fetch_latency histogram MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors block_fetch_latency for transactions: measures the end-to-end round-trip from sending GET_DATA to receiving the full TXS message, using the timestamp already stored in PeerConnection.advInvRequest at fetch-dispatch time. Records nothing for transactions pushed via gossip (no prior GET_DATA), which is intentional — this metric only captures the active-fetch path. Overhead is ~500 ns per tx (Item allocation + ConcurrentHashMap.remove + currentTimeMillis + histogram observe), negligible even at >1k TPS. Useful for PQ migration baseline / stress-test comparison: shows how much extra time the bigger Falcon-512 payloads add to in-flight transaction propagation, independent of local processing cost. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../org/tron/common/prometheus/MetricKeys.java | 1 + .../common/prometheus/MetricsHistogram.java | 2 ++ .../messagehandler/TransactionsMsgHandler.java | 17 ++++++++++++++++- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/common/src/main/java/org/tron/common/prometheus/MetricKeys.java b/common/src/main/java/org/tron/common/prometheus/MetricKeys.java index 95a38c4b479..47eca4dd903 100644 --- a/common/src/main/java/org/tron/common/prometheus/MetricKeys.java +++ b/common/src/main/java/org/tron/common/prometheus/MetricKeys.java @@ -67,6 +67,7 @@ public static class Histogram { public static final String BLOCK_FETCH_LATENCY = "tron:block_fetch_latency_seconds"; public static final String BLOCK_RECEIVE_DELAY = "tron:block_receive_delay_seconds"; public static final String BLOCK_TRANSACTION_COUNT = "tron:block_transaction_count"; + public static final String TX_FETCH_LATENCY = "tron:tx_fetch_latency_seconds"; private Histogram() { throw new IllegalStateException("Histogram"); diff --git a/common/src/main/java/org/tron/common/prometheus/MetricsHistogram.java b/common/src/main/java/org/tron/common/prometheus/MetricsHistogram.java index fa42a59aeaa..d8adf7e18c2 100644 --- a/common/src/main/java/org/tron/common/prometheus/MetricsHistogram.java +++ b/common/src/main/java/org/tron/common/prometheus/MetricsHistogram.java @@ -48,6 +48,8 @@ public class MetricsHistogram { init(MetricKeys.Histogram.BLOCK_FETCH_LATENCY, "fetch block latency."); init(MetricKeys.Histogram.BLOCK_RECEIVE_DELAY, "receive block delay time, receiveTime - blockTime."); + init(MetricKeys.Histogram.TX_FETCH_LATENCY, + "fetch transaction latency: GET_DATA send to full TXS received round-trip."); init(MetricKeys.Histogram.BLOCK_TRANSACTION_COUNT, "Distribution of transaction counts per block.", diff --git a/framework/src/main/java/org/tron/core/net/messagehandler/TransactionsMsgHandler.java b/framework/src/main/java/org/tron/core/net/messagehandler/TransactionsMsgHandler.java index e153e21f331..bd1d591e34c 100644 --- a/framework/src/main/java/org/tron/core/net/messagehandler/TransactionsMsgHandler.java +++ b/framework/src/main/java/org/tron/core/net/messagehandler/TransactionsMsgHandler.java @@ -14,6 +14,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.tron.common.es.ExecutorServiceManager; +import org.tron.common.prometheus.MetricKeys; +import org.tron.common.prometheus.Metrics; import org.tron.common.utils.Sha256Hash; import org.tron.core.ChainBaseManager; import org.tron.core.config.args.Args; @@ -169,10 +171,23 @@ private void handleTransaction(PeerConnection peer, TransactionMessage trx) { return; } - if (advService.getMessage(new Item(trx.getMessageId(), InventoryType.TRX)) != null) { + Item item = new Item(trx.getMessageId(), InventoryType.TRX); + + if (advService.getMessage(item) != null) { return; } + // Measure end-to-end fetch latency: from GET_DATA send (recorded in + // advInvRequest when consumerInvToFetch picks this peer) to full TXS + // received here. Returns null if this tx wasn't actively fetched (e.g. + // pushed via gossip without a prior GET_DATA), in which case no sample + // is observed. + Long requestTime = peer.getAdvInvRequest().remove(item); + if (requestTime != null) { + Metrics.histogramObserve(MetricKeys.Histogram.TX_FETCH_LATENCY, + (System.currentTimeMillis() - requestTime) / Metrics.MILLISECONDS_PER_SECOND); + } + try { trx.getTransactionCapsule().checkExpiration(chainBaseManager.getNextBlockSlotTime()); tronNetDelegate.pushTransaction(trx.getTransactionCapsule()); From 773c9128ad3e7a086bbd9a29a939f24a199bfe9a Mon Sep 17 00:00:00 2001 From: GrapeS Date: Mon, 25 May 2026 17:37:17 +0800 Subject: [PATCH 3/8] fix merge --- .../src/main/java/org/tron/core/actuator/VMActuator.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/actuator/src/main/java/org/tron/core/actuator/VMActuator.java b/actuator/src/main/java/org/tron/core/actuator/VMActuator.java index 0a9045a1586..3c92d1c0c33 100644 --- a/actuator/src/main/java/org/tron/core/actuator/VMActuator.java +++ b/actuator/src/main/java/org/tron/core/actuator/VMActuator.java @@ -178,8 +178,7 @@ public void execute(Object object) throws ContractExeException { ProgramResult result = context.getProgramResult(); try { if (program != null) { - if (null != blockCap && blockCap.generatedByMyself && blockCap.hasWitnessSignature(context.getStoreFactory().getChainBaseManager() - .getDynamicPropertiesStore()) + if (null != blockCap && blockCap.generatedByMyself && blockCap.hasWitnessSignature() && null != TransactionUtil.getContractRet(trx) && contractResult.OUT_OF_TIME == TransactionUtil.getContractRet(trx)) { result = program.getResult(); @@ -675,7 +674,7 @@ private double getCpuLimitInUsRatio(DynamicPropertiesStore dynamicPropertiesStor if (ExecutorType.ET_NORMAL_TYPE == executorType) { // self witness generates block if (blockCap != null && blockCap.generatedByMyself - && !blockCap.hasWitnessSignature(dynamicPropertiesStore)) { + && !blockCap.hasWitnessSignature()) { cpuLimitRatio = 1.0; } else { // self witness or other witness or fullnode verifies block From e12b47f22fe4abaaca5e524d1e6fe4443917b19b Mon Sep 17 00:00:00 2001 From: GrapeS Date: Mon, 25 May 2026 17:41:59 +0800 Subject: [PATCH 4/8] fix merge --- .../src/main/java/org/tron/core/actuator/VMActuator.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/actuator/src/main/java/org/tron/core/actuator/VMActuator.java b/actuator/src/main/java/org/tron/core/actuator/VMActuator.java index 3c92d1c0c33..1b0e8a6637f 100644 --- a/actuator/src/main/java/org/tron/core/actuator/VMActuator.java +++ b/actuator/src/main/java/org/tron/core/actuator/VMActuator.java @@ -37,7 +37,6 @@ import org.tron.core.db.TransactionContext; import org.tron.core.exception.ContractExeException; import org.tron.core.exception.ContractValidateException; -import org.tron.core.store.DynamicPropertiesStore; import org.tron.core.utils.TransactionUtil; import org.tron.core.vm.EnergyCost; import org.tron.core.vm.LogInfoTriggerParser; @@ -401,7 +400,7 @@ private void create() long thisTxCPULimitInUs = calculateCpuLimitInUs(isConstantCall, rootRepository.getDynamicPropertiesStore().getMaxCpuTimeOfOneTx(), - getCpuLimitInUsRatio(rootRepository.getDynamicPropertiesStore()), CommonParameter.getInstance().getConstantCallTimeoutMs()); + getCpuLimitInUsRatio(), CommonParameter.getInstance().getConstantCallTimeoutMs()); long vmStartInUs = System.nanoTime() / VMConstant.ONE_THOUSAND; long vmShouldEndInUs = vmStartInUs + thisTxCPULimitInUs; ProgramInvoke programInvoke = ProgramInvokeFactory @@ -515,7 +514,7 @@ private void call() long thisTxCPULimitInUs = calculateCpuLimitInUs(isConstantCall, rootRepository.getDynamicPropertiesStore().getMaxCpuTimeOfOneTx(), - getCpuLimitInUsRatio(rootRepository.getDynamicPropertiesStore()), CommonParameter.getInstance().getConstantCallTimeoutMs()); + getCpuLimitInUsRatio(), CommonParameter.getInstance().getConstantCallTimeoutMs()); long vmStartInUs = System.nanoTime() / VMConstant.ONE_THOUSAND; long vmShouldEndInUs = vmStartInUs + thisTxCPULimitInUs; ProgramInvoke programInvoke = ProgramInvokeFactory @@ -667,7 +666,7 @@ public void checkTokenValueAndId(long tokenValue, long tokenId) throws ContractV } - private double getCpuLimitInUsRatio(DynamicPropertiesStore dynamicPropertiesStore) { + private double getCpuLimitInUsRatio() { double cpuLimitRatio; From 650d3d8abce62d89b41e4e2c29b27da62f0d6af4 Mon Sep 17 00:00:00 2001 From: GrapeS Date: Mon, 25 May 2026 17:45:29 +0800 Subject: [PATCH 5/8] fix merge --- actuator/src/main/java/org/tron/core/actuator/VMActuator.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/actuator/src/main/java/org/tron/core/actuator/VMActuator.java b/actuator/src/main/java/org/tron/core/actuator/VMActuator.java index 1b0e8a6637f..f2eb59850d1 100644 --- a/actuator/src/main/java/org/tron/core/actuator/VMActuator.java +++ b/actuator/src/main/java/org/tron/core/actuator/VMActuator.java @@ -400,7 +400,8 @@ private void create() long thisTxCPULimitInUs = calculateCpuLimitInUs(isConstantCall, rootRepository.getDynamicPropertiesStore().getMaxCpuTimeOfOneTx(), - getCpuLimitInUsRatio(), CommonParameter.getInstance().getConstantCallTimeoutMs()); + getCpuLimitInUsRatio(), + CommonParameter.getInstance().getConstantCallTimeoutMs()); long vmStartInUs = System.nanoTime() / VMConstant.ONE_THOUSAND; long vmShouldEndInUs = vmStartInUs + thisTxCPULimitInUs; ProgramInvoke programInvoke = ProgramInvokeFactory From 72ef8ca40d42a5367cb34033d066aec685b121ce Mon Sep 17 00:00:00 2001 From: GrapeS Date: Tue, 26 May 2026 10:23:53 +0800 Subject: [PATCH 6/8] fix(metrics): observe tx_fetch_latency in processMessage, not handleTransaction The original placement in handleTransaction() was dead code: processMessage() drains advInvRequest at L92-95 (before enqueueing tx work onto the smartContract/queue handlers), so by the time the worker thread reaches handleTransaction() the timestamp is already gone and remove(item) always returns null. Move the histogram observe into the same draining loop in processMessage(), using a single currentTimeMillis() reference captured just before the loop. This is both correct (we observe at the only remove() call that ever sees a non-null value) and slightly cheaper (one currentTimeMillis() per TRXS message instead of per tx). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../TransactionsMsgHandler.java | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/framework/src/main/java/org/tron/core/net/messagehandler/TransactionsMsgHandler.java b/framework/src/main/java/org/tron/core/net/messagehandler/TransactionsMsgHandler.java index bd1d591e34c..198d12690d4 100644 --- a/framework/src/main/java/org/tron/core/net/messagehandler/TransactionsMsgHandler.java +++ b/framework/src/main/java/org/tron/core/net/messagehandler/TransactionsMsgHandler.java @@ -89,9 +89,17 @@ public void processMessage(PeerConnection peer, TronMessage msg) throws P2pExcep } TransactionsMessage transactionsMessage = (TransactionsMessage) msg; check(peer, transactionsMessage); + long now = System.currentTimeMillis(); for (Transaction trx : transactionsMessage.getTransactions().getTransactionsList()) { Item item = new Item(new TransactionMessage(trx).getMessageId(), InventoryType.TRX); - peer.getAdvInvRequest().remove(item); + // Observe end-to-end fetch latency (GET_DATA send → full TXS received) + // before consuming the timestamp. Null means this tx wasn't actively + // fetched (e.g. pushed via gossip), in which case no sample is recorded. + Long requestTime = peer.getAdvInvRequest().remove(item); + if (requestTime != null) { + Metrics.histogramObserve(MetricKeys.Histogram.TX_FETCH_LATENCY, + (now - requestTime) / Metrics.MILLISECONDS_PER_SECOND); + } } int smartContractQueueSize = 0; int trxHandlePoolQueueSize = 0; @@ -177,17 +185,6 @@ private void handleTransaction(PeerConnection peer, TransactionMessage trx) { return; } - // Measure end-to-end fetch latency: from GET_DATA send (recorded in - // advInvRequest when consumerInvToFetch picks this peer) to full TXS - // received here. Returns null if this tx wasn't actively fetched (e.g. - // pushed via gossip without a prior GET_DATA), in which case no sample - // is observed. - Long requestTime = peer.getAdvInvRequest().remove(item); - if (requestTime != null) { - Metrics.histogramObserve(MetricKeys.Histogram.TX_FETCH_LATENCY, - (System.currentTimeMillis() - requestTime) / Metrics.MILLISECONDS_PER_SECOND); - } - try { trx.getTransactionCapsule().checkExpiration(chainBaseManager.getNextBlockSlotTime()); tronNetDelegate.pushTransaction(trx.getTransactionCapsule()); From c546dd536f00d704248364f26146e8b2411a1cfd Mon Sep 17 00:00:00 2001 From: GrapeS Date: Tue, 26 May 2026 10:29:16 +0800 Subject: [PATCH 7/8] revert some --- .../tron/core/net/messagehandler/TransactionsMsgHandler.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/framework/src/main/java/org/tron/core/net/messagehandler/TransactionsMsgHandler.java b/framework/src/main/java/org/tron/core/net/messagehandler/TransactionsMsgHandler.java index 198d12690d4..7189150e9ee 100644 --- a/framework/src/main/java/org/tron/core/net/messagehandler/TransactionsMsgHandler.java +++ b/framework/src/main/java/org/tron/core/net/messagehandler/TransactionsMsgHandler.java @@ -179,9 +179,7 @@ private void handleTransaction(PeerConnection peer, TransactionMessage trx) { return; } - Item item = new Item(trx.getMessageId(), InventoryType.TRX); - - if (advService.getMessage(item) != null) { + if (advService.getMessage(new Item(trx.getMessageId(), InventoryType.TRX)) != null) { return; } From 9cb41b946783f863a7d3bbe48f5410e448492c80 Mon Sep 17 00:00:00 2001 From: GrapeS Date: Tue, 26 May 2026 10:33:10 +0800 Subject: [PATCH 8/8] add comment --- .../main/java/org/tron/common/prometheus/MetricKeys.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/common/src/main/java/org/tron/common/prometheus/MetricKeys.java b/common/src/main/java/org/tron/common/prometheus/MetricKeys.java index 47eca4dd903..d473dac2ccb 100644 --- a/common/src/main/java/org/tron/common/prometheus/MetricKeys.java +++ b/common/src/main/java/org/tron/common/prometheus/MetricKeys.java @@ -67,6 +67,14 @@ public static class Histogram { public static final String BLOCK_FETCH_LATENCY = "tron:block_fetch_latency_seconds"; public static final String BLOCK_RECEIVE_DELAY = "tron:block_receive_delay_seconds"; public static final String BLOCK_TRANSACTION_COUNT = "tron:block_transaction_count"; + /** + * Transaction fetch round-trip latency in seconds: from sending + * {@code GET_DATA (FETCH_INV_DATA)} to receiving the full {@code TXS} + * message. + *

Transactions pushed via gossip without a prior {@code GET_DATA} + * (i.e. not actively fetched by this node) are not sampled; + *

Companion to {@link #BLOCK_FETCH_LATENCY} for the TX path. + */ public static final String TX_FETCH_LATENCY = "tron:tx_fetch_latency_seconds"; private Histogram() {