Skip to content

Commit 7067a00

Browse files
committed
fix(crypto): address PQ review feedback
1 parent 4b45b4d commit 7067a00

29 files changed

Lines changed: 775 additions & 281 deletions

File tree

actuator/src/main/java/org/tron/core/vm/PrecompiledContracts.java

Lines changed: 97 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
import org.tron.common.crypto.Rsv;
5454
import org.tron.common.crypto.SignUtils;
5555
import org.tron.common.crypto.SignatureInterface;
56-
import org.tron.common.crypto.pqc.FNDSA;
56+
import org.tron.common.crypto.pqc.FNDSA512;
5757
import org.tron.common.crypto.pqc.PQSchemeRegistry;
5858
import org.tron.common.crypto.zksnark.BN128;
5959
import org.tron.common.crypto.zksnark.BN128Fp;
@@ -120,10 +120,10 @@ public class PrecompiledContracts {
120120
private static final Blake2F blake2F = new Blake2F();
121121
private static final P256Verify p256Verify = new P256Verify();
122122

123-
private static final VerifyFnDsa verifyFnDsa = new VerifyFnDsa();
124-
private static final ValidateMultiSignPQ validateMultiSignPQ =
125-
new ValidateMultiSignPQ();
126-
private static final BatchValidateSignPQ batchValidateSignPQ = new BatchValidateSignPQ();
123+
private static final VerifyFnDsa512 verifyFnDsa512 = new VerifyFnDsa512();
124+
private static final ValidateMultiFnDsa512 validateMultiFnDsa512 =
125+
new ValidateMultiFnDsa512();
126+
private static final BatchValidateFnDsa512 batchValidateFnDsa512 = new BatchValidateFnDsa512();
127127

128128
// FreezeV2 PrecompileContracts
129129
private static final GetChainParameter getChainParameter = new GetChainParameter();
@@ -223,19 +223,19 @@ public class PrecompiledContracts {
223223
// EIP-8052 0x16: FN-DSA / Falcon-512 verify (FIPS-206 draft). Input layout:
224224
// [msg 32B | sig_len 2B (big-endian) | sig sig_len B (1..752) | pk 896B].
225225
// Variable-length signature is prefixed with a 2-byte length field.
226-
private static final DataWord verifyFnDsaAddr = new DataWord(
226+
private static final DataWord verifyFnDsa512Addr = new DataWord(
227227
"0000000000000000000000000000000000000000000000000000000000000016");
228228

229229
// 0x17: algorithm-agnostic Permission multi-sign — accepts both ECDSA and
230230
// Falcon-512 signatures against the same Permission.keys[] in one call,
231231
// matching transaction-side §2.3.5 mixed-weight semantics.
232-
private static final DataWord validateMultiSignPQAddr = new DataWord(
232+
private static final DataWord validateMultiFnDsa512Addr = new DataWord(
233233
"0000000000000000000000000000000000000000000000000000000000000017");
234234

235235
// 0x18: batch independent Falcon-512 verify — bitmap of (sig, pk, addr)
236236
// matches; mixed-algorithm contracts call 0x0A and 0x18 separately and OR
237237
// the bitmaps client-side.
238-
private static final DataWord batchValidateSignPqAddr = new DataWord(
238+
private static final DataWord batchValidateFnDsa512Addr = new DataWord(
239239
"0000000000000000000000000000000000000000000000000000000000000018");
240240

241241
public static PrecompiledContract getOptimizedContractForConstant(PrecompiledContract contract) {
@@ -323,16 +323,18 @@ public static PrecompiledContract getContractForAddress(DataWord address) {
323323
return p256Verify;
324324
}
325325

326-
if (VMConfig.allowFnDsa512() && address.equals(verifyFnDsaAddr)) {
327-
return verifyFnDsa;
328-
}
329-
330-
if (VMConfig.allowFnDsa512() && address.equals(validateMultiSignPQAddr)) {
331-
return validateMultiSignPQ;
332-
}
333-
334-
if (VMConfig.allowFnDsa512() && address.equals(batchValidateSignPqAddr)) {
335-
return batchValidateSignPQ;
326+
// FN-DSA-512 is the first PQ signature scheme supported by TRON, so its proposal flag
327+
// gates every PQ-related precompile (single verify, multisig verify, batch verify).
328+
if (VMConfig.allowFnDsa512()) {
329+
if (address.equals(verifyFnDsa512Addr)) {
330+
return verifyFnDsa512;
331+
}
332+
if (address.equals(validateMultiFnDsa512Addr)) {
333+
return validateMultiFnDsa512;
334+
}
335+
if (address.equals(batchValidateFnDsa512Addr)) {
336+
return batchValidateFnDsa512;
337+
}
336338
}
337339

338340
if (VMConfig.allowTvmFreezeV2()) {
@@ -475,6 +477,35 @@ private static boolean isValidAbiEncoding(byte[] data, int headerWords, int item
475477
return tail > 0 && tail % multiplyExact(itemWords, WORD_SIZE) == 0;
476478
}
477479

480+
/**
481+
* Structural pre-check for ABI head: word-aligned length and room for the
482+
* fixed head. The PQ precompiles cannot reuse {@link #isValidAbiEncoding}
483+
* because their {@code bytes[]} entries (PQ signatures, 1..752 bytes) are
484+
* variable-length, so the trailing divisibility check does not apply.
485+
*/
486+
private static boolean isValidAbiHead(byte[] data, int headWords) {
487+
return data != null
488+
&& data.length % WORD_SIZE == 0
489+
&& data.length >= multiplyExact(headWords, WORD_SIZE);
490+
}
491+
492+
/**
493+
* Verifies that the array offset stored at {@code words[offsetWordIndex]} is
494+
* word-aligned, falls inside the dynamic data region (≥ head), and points to
495+
* a length word that still fits inside {@code words}. Sister check to
496+
* {@link #isValidAbiEncoding} for ABIs whose items are not uniform width.
497+
*/
498+
private static boolean isValidArrayOffset(DataWord[] words, int offsetWordIndex,
499+
int headWords) {
500+
long offsetBytes = words[offsetWordIndex].longValueSafe();
501+
if (offsetBytes < (long) headWords * WORD_SIZE
502+
|| offsetBytes % WORD_SIZE != 0) {
503+
return false;
504+
}
505+
long lengthWordIdx = offsetBytes / WORD_SIZE;
506+
return lengthWordIdx < words.length;
507+
}
508+
478509
public abstract static class PrecompiledContract {
479510

480511
protected static final byte[] DATA_FALSE = new byte[WORD_SIZE];
@@ -2425,22 +2456,23 @@ public Pair<Boolean, byte[]> execute(byte[] data) {
24252456
* <pre>
24262457
* [msg 32B | sig_len 2B (big-endian, 1..752) | sig sig_len B | pk 896B]
24272458
* </pre>
2428-
* Minimum input: 32 + 2 + 1 + 896 = 931 bytes.
2459+
* Total length must equal exactly {@code 32 + 2 + sig_len + 896} (no trailing
2460+
* bytes; matches 0x100 P256Verify / EIP-7951 strictness).
24292461
*
24302462
* <p>Returns a 32-byte word: 1 on valid signature, 0 otherwise.
24312463
* Malformed input (wrong lengths, out-of-range sig_len) returns 0 without error.
24322464
*/
2433-
public static class VerifyFnDsa extends PrecompiledContract {
2465+
public static class VerifyFnDsa512 extends PrecompiledContract {
24342466

24352467
private static final int MSG_LEN = 32;
24362468
private static final int SIG_LEN_FIELD = 2;
2437-
private static final int PK_LEN = FNDSA.PUBLIC_KEY_LENGTH;
2438-
private static final int MAX_SIG_LEN = FNDSA.SIGNATURE_LENGTH;
2469+
private static final int PK_LEN = FNDSA512.PUBLIC_KEY_LENGTH;
2470+
private static final int MAX_SIG_LEN = FNDSA512.SIGNATURE_LENGTH;
24392471
private static final int MIN_INPUT_LEN = MSG_LEN + SIG_LEN_FIELD + 1 + PK_LEN;
24402472

24412473
@Override
24422474
public long getEnergyForData(byte[] data) {
2443-
return 2500;
2475+
return 4000;
24442476
}
24452477

24462478
@Override
@@ -2455,14 +2487,17 @@ public Pair<Boolean, byte[]> execute(byte[] data) {
24552487
return Pair.of(true, DataWord.ZERO().getData());
24562488
}
24572489
int pkOffset = MSG_LEN + SIG_LEN_FIELD + sigLen;
2458-
if (data.length < pkOffset + PK_LEN) {
2490+
// Strict equality (cf. 0x100 P256Verify): one logical input ↔ one encoding,
2491+
// leaves room for future EIP-8052 trailing fields.
2492+
if (data.length != pkOffset + PK_LEN) {
24592493
return Pair.of(true, DataWord.ZERO().getData());
24602494
}
24612495
byte[] sig = copyOfRange(data, MSG_LEN + SIG_LEN_FIELD, pkOffset);
24622496
byte[] pk = copyOfRange(data, pkOffset, pkOffset + PK_LEN);
2463-
boolean ok = FNDSA.verify(pk, msg, sig);
2497+
boolean ok = FNDSA512.verify(pk, msg, sig);
24642498
return Pair.of(true, ok ? DataWord.ONE().getData() : DataWord.ZERO().getData());
24652499
} catch (Throwable t) {
2500+
logger.info("VerifyFnDsa512 error:{}", t.getMessage());
24662501
return Pair.of(true, DataWord.ZERO().getData());
24672502
}
24682503
}
@@ -2489,15 +2524,17 @@ public Pair<Boolean, byte[]> execute(byte[] data) {
24892524
* </pre>
24902525
*
24912526
* <p>{@code MAX_SIZE = 5} applies to the total signature count
2492-
* ({@code ecdsaCnt + pqCnt}). Energy is split: {@code ecdsaCnt × 1500 + pqCnt × 15000}.
2527+
* ({@code ecdsaCnt + pqCnt}). Energy is split: {@code ecdsaCnt × 1500 + pqCnt × 2000}.
24932528
*/
2494-
public static class ValidateMultiSignPQ extends PrecompiledContract {
2529+
public static class ValidateMultiFnDsa512 extends PrecompiledContract {
24952530

24962531
private static final int ECDSA_ENERGY_PER_SIGN = 1500;
2497-
private static final int PQ_ENERGY_PER_SIGN = 15000;
2532+
private static final int PQ_ENERGY_PER_SIGN = 2000;
24982533
private static final int MAX_SIZE = 5;
2499-
private static final int PK_LEN = FNDSA.PUBLIC_KEY_LENGTH;
2500-
private static final int MAX_SIG_LEN = FNDSA.SIGNATURE_LENGTH;
2534+
private static final int PK_LEN = FNDSA512.PUBLIC_KEY_LENGTH;
2535+
private static final int MAX_SIG_LEN = FNDSA512.SIGNATURE_LENGTH;
2536+
// address, permissionId, data, ecdsaOffset, pqSigOffset, pqPkOffset.
2537+
private static final int ABI_HEAD_WORDS = 6;
25012538

25022539
@Override
25032540
public long getEnergyForData(byte[] data) {
@@ -2514,8 +2551,16 @@ public long getEnergyForData(byte[] data) {
25142551

25152552
@Override
25162553
public Pair<Boolean, byte[]> execute(byte[] rawData) {
2554+
if (!isValidAbiHead(rawData, ABI_HEAD_WORDS)) {
2555+
return Pair.of(false, EMPTY_BYTE_ARRAY);
2556+
}
25172557
try {
25182558
DataWord[] words = DataWord.parseArray(rawData);
2559+
if (!isValidArrayOffset(words, 3, ABI_HEAD_WORDS)
2560+
|| !isValidArrayOffset(words, 4, ABI_HEAD_WORDS)
2561+
|| !isValidArrayOffset(words, 5, ABI_HEAD_WORDS)) {
2562+
return Pair.of(false, EMPTY_BYTE_ARRAY);
2563+
}
25192564
byte[] address = words[0].toTronAddress();
25202565
int permissionId = words[1].intValueSafe();
25212566
byte[] data = words[2].getData();
@@ -2596,7 +2641,7 @@ public Pair<Boolean, byte[]> execute(byte[] rawData) {
25962641
if (weight == 0) {
25972642
return Pair.of(true, DATA_FALSE);
25982643
}
2599-
if (!FNDSA.verify(pk, hash, sig)) {
2644+
if (!FNDSA512.verify(pk, hash, sig)) {
26002645
return Pair.of(true, DATA_FALSE);
26012646
}
26022647
totalWeight += weight;
@@ -2617,13 +2662,13 @@ public Pair<Boolean, byte[]> execute(byte[] rawData) {
26172662
}
26182663

26192664
/**
2620-
* 0x18 BatchValidateSignPQ — independent per-element Falcon-512 verify.
2665+
* 0x18 BatchValidateFnDsa512 — independent per-element Falcon-512 verify.
26212666
* <p>Returns a 256-bit bitmap (matching 0x0A) where bit {@code i} is set iff
2622-
* {@code derive(pk_i) == expectedAddr_i} AND {@code FNDSA.verify(pk_i, hash, sig_i)}.
2667+
* {@code derive(pk_i) == expectedAddr_i} AND {@code FNDSA512.verify(pk_i, hash, sig_i)}.
26232668
*
26242669
* <p>ABI:
26252670
* <pre>
2626-
* batchValidateSignPQ(
2671+
* batchValidateFnDsa512(
26272672
* bytes32 hash, // word[0]
26282673
* bytes[] signatures, // word[1] = offset; each 1..752 B
26292674
* bytes[] publicKeys, // word[2] = offset; each 896 B
@@ -2633,14 +2678,16 @@ public Pair<Boolean, byte[]> execute(byte[] rawData) {
26332678
*
26342679
* <p>Reuses the {@code BatchValidateSign.workers} pool when not in a constant
26352680
* call and enforces {@code getCPUTimeLeftInNanoSecond()} timeout. {@code MAX_SIZE = 16}.
2636-
* Energy is {@code cnt × 15000}.
2681+
* Energy is {@code cnt × 2000}.
26372682
*/
2638-
public static class BatchValidateSignPQ extends PrecompiledContract {
2683+
public static class BatchValidateFnDsa512 extends PrecompiledContract {
26392684

2640-
private static final int ENERGY_PER_SIGN = 15000;
2685+
private static final int ENERGY_PER_SIGN = 2000;
26412686
private static final int MAX_SIZE = 16;
2642-
private static final int PK_LEN = FNDSA.PUBLIC_KEY_LENGTH;
2643-
private static final int MAX_SIG_LEN = FNDSA.SIGNATURE_LENGTH;
2687+
private static final int PK_LEN = FNDSA512.PUBLIC_KEY_LENGTH;
2688+
private static final int MAX_SIG_LEN = FNDSA512.SIGNATURE_LENGTH;
2689+
// hash, sigArrayOffset, pkArrayOffset, addrArrayOffset.
2690+
private static final int ABI_HEAD_WORDS = 4;
26442691

26452692
@Override
26462693
public long getEnergyForData(byte[] data) {
@@ -2670,7 +2717,15 @@ public Pair<Boolean, byte[]> execute(byte[] data) {
26702717

26712718
private Pair<Boolean, byte[]> doExecute(byte[] data)
26722719
throws InterruptedException, ExecutionException {
2720+
if (!isValidAbiHead(data, ABI_HEAD_WORDS)) {
2721+
return Pair.of(false, EMPTY_BYTE_ARRAY);
2722+
}
26732723
DataWord[] words = DataWord.parseArray(data);
2724+
if (!isValidArrayOffset(words, 1, ABI_HEAD_WORDS)
2725+
|| !isValidArrayOffset(words, 2, ABI_HEAD_WORDS)
2726+
|| !isValidArrayOffset(words, 3, ABI_HEAD_WORDS)) {
2727+
return Pair.of(false, EMPTY_BYTE_ARRAY);
2728+
}
26742729
byte[] hash = words[0].getData();
26752730

26762731
int sigArrayWord = words[1].intValueSafe() / WORD_SIZE;
@@ -2718,8 +2773,8 @@ private Pair<Boolean, byte[]> doExecute(byte[] data)
27182773
.await(getCPUTimeLeftInNanoSecond(), TimeUnit.NANOSECONDS);
27192774

27202775
if (!withNoTimeout) {
2721-
logger.info("BatchValidateSignPQ timeout");
2722-
throw Program.Exception.notEnoughTime("call BatchValidateSignPQ precompile method");
2776+
logger.info("BatchValidateFnDsa512 timeout");
2777+
throw Program.Exception.notEnoughTime("call BatchValidateFnDsa512 precompile method");
27232778
}
27242779

27252780
for (Future<PqVerifyResult> future : futures) {
@@ -2743,7 +2798,7 @@ private static boolean verifyOne(byte[] sig, byte[] pk, byte[] hash,
27432798
if (!DataWord.equalAddressByteArray(derived, expectedAddr)) {
27442799
return false;
27452800
}
2746-
return FNDSA.verify(pk, hash, sig);
2801+
return FNDSA512.verify(pk, hash, sig);
27472802
} catch (Throwable t) {
27482803
return false;
27492804
}

chainbase/src/main/java/org/tron/core/capsule/BlockCapsule.java

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -201,10 +201,7 @@ public boolean validateSignature(DynamicPropertiesStore dynamicPropertiesStore,
201201
AccountStore accountStore) throws ValidateSignatureException {
202202
BlockHeader header = block.getBlockHeader();
203203
boolean hasLegacy = !header.getWitnessSignature().isEmpty();
204-
PQAuthSig pqAuthSig = header.getPqAuthSig();
205-
boolean hasPq = pqAuthSig != null
206-
&& pqAuthSig.getSignature() != null
207-
&& !pqAuthSig.getSignature().isEmpty();
204+
boolean hasPq = header.hasPqAuthSig();
208205

209206
if (hasLegacy && hasPq) {
210207
throw new ValidateSignatureException(
@@ -217,7 +214,7 @@ public boolean validateSignature(DynamicPropertiesStore dynamicPropertiesStore,
217214
byte[] witnessAccountAddress = header.getRawData().getWitnessAddress().toByteArray();
218215
if (hasPq) {
219216
return validatePQSignature(dynamicPropertiesStore, accountStore,
220-
witnessAccountAddress, pqAuthSig);
217+
witnessAccountAddress, header.getPqAuthSig());
221218
}
222219
return validateLegacySignature(dynamicPropertiesStore, accountStore, witnessAccountAddress);
223220
}

chainbase/src/main/java/org/tron/core/capsule/TransactionCapsule.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -794,7 +794,8 @@ public boolean validateSignature(AccountStore accountStore,
794794
if (!ArrayUtils.isEmpty(owner)) { //transfer from transparent address
795795
validatePubSignature(accountStore, dynamicPropertiesStore);
796796
} else { //transfer from shielded address
797-
if (this.transaction.getSignatureCount() > 0) {
797+
if (this.transaction.getSignatureCount() > 0
798+
|| this.transaction.getPqAuthSigCount() > 0) {
798799
throw new ValidateSignatureException("there should be no signatures signed by "
799800
+ "transparent address when transfer from shielded address");
800801
}

consensus/src/main/java/org/tron/consensus/base/Param.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,14 @@ public static Param getInstance() {
5454
return param;
5555
}
5656

57+
/** Signing key family carried by a {@link Miner}. */
58+
public enum MinerType {
59+
/** Legacy ECDSA / SM2 witness; signs blocks via {@code BlockCapsule.sign}. */
60+
ECDSA,
61+
/** Post-quantum witness; signs blocks via {@code signWitnessAuth}. */
62+
PQ
63+
}
64+
5765
public class Miner {
5866

5967
@Getter
@@ -76,6 +84,15 @@ public class Miner {
7684
@Setter
7785
private PQScheme pqScheme;
7886

87+
/**
88+
* Explicit signing-family marker so the block producer doesn't have to infer
89+
* key type from {@code privateKey == null}. Defaults to {@link MinerType#ECDSA};
90+
* PQ-only miners must call {@link #setType(MinerType)} with {@link MinerType#PQ}.
91+
*/
92+
@Getter
93+
@Setter
94+
private MinerType type = MinerType.ECDSA;
95+
7996
public Miner(byte[] privateKey, ByteString privateKeyAddress, ByteString witnessAddress) {
8097
this.privateKey = privateKey;
8198
this.privateKeyAddress = privateKeyAddress;

0 commit comments

Comments
 (0)