diff --git a/actuator/src/main/java/org/tron/core/vm/PQPrecompiledContracts.java b/actuator/src/main/java/org/tron/core/vm/PQPrecompiledContracts.java
new file mode 100644
index 0000000000..eeb86b1105
--- /dev/null
+++ b/actuator/src/main/java/org/tron/core/vm/PQPrecompiledContracts.java
@@ -0,0 +1,844 @@
+package org.tron.core.vm;
+
+import static java.util.Arrays.copyOfRange;
+import static org.tron.common.math.StrictMathWrapper.multiplyExact;
+import static org.tron.common.runtime.vm.DataWord.WORD_SIZE;
+import static org.tron.common.utils.ByteUtil.EMPTY_BYTE_ARRAY;
+import java.util.ArrayList;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.tuple.Pair;
+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.es.ExecutorServiceManager;
+import org.tron.common.math.StrictMathWrapper;
+import org.tron.common.parameter.CommonParameter;
+import org.tron.common.runtime.vm.DataWord;
+import org.tron.common.utils.ByteArray;
+import org.tron.common.utils.ByteUtil;
+import org.tron.common.utils.Sha256Hash;
+import org.tron.core.capsule.AccountCapsule;
+import org.tron.core.capsule.TransactionCapsule;
+import org.tron.core.vm.config.VMConfig;
+import org.tron.core.vm.program.Program;
+import org.tron.core.vm.program.Program.OutOfTimeException;
+import org.tron.protos.Protocol.PQScheme;
+import org.tron.protos.Protocol.Permission;
+
+@Slf4j(topic = "VM")
+public class PQPrecompiledContracts {
+
+ /**
+ * Best-effort cancellation of all submitted batch-verify tasks. Tasks that
+ * have not yet started execution are removed from the worker queue; tasks
+ * already running receive an interrupt but BouncyCastle's PQ verify routines
+ * do not poll the interrupt flag and will run to completion.
+ */
+ static void cancelAll(List extends Future>> futures) {
+ for (Future> f : futures) {
+ f.cancel(true);
+ }
+ }
+
+ /**
+ * Returns the logical Falcon-512 signature length packed at the start of a
+ * fixed slot {@code data[from..to)}: the offset of the last non-zero byte
+ * (exclusive). Canonical Falcon encodings always end in a non-zero byte
+ * ({@code compressed_s2}'s unary terminator), so anything beyond is zero
+ * padding. Returns 0 if the slot is all zero. Shared by 0x02000016, 0x02000017, and 0x0200001a
+ * because every precompile slot for Falcon sigs is the same 666-byte slot.
+ */
+ static int recoverFalconSigLen(byte[] data, int from, int to) {
+ for (int i = to - 1; i >= from; i--) {
+ if (data[i] != 0) {
+ return i - from + 1;
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Reconstructs the BC-native Falcon-512 signature from an EIP-8052 headerless
+ * slot. The slot {@code data[from..to)} holds {@code salt ‖ s2_compressed}
+ * (no leading {@code 0x39}) zero-padded to
+ * {@code SIGNATURE_MAX_LENGTH - SIGNATURE_HEADER_LENGTH};
+ * the logical body ends at the last non-zero byte. Returns
+ * {@code 0x39 ‖ body} so BC's {@code FalconSigner} (which requires the header)
+ * can verify it, or {@code null} if the recovered body length is out of range.
+ * Shared by 0x02000016, 0x02000017, and 0x0200001a.
+ */
+ static byte[] falconSlotToHeaderedSig(byte[] data, int from, int to) {
+ int bodyLen = recoverFalconSigLen(data, from, to);
+ if (bodyLen < FNDSA512.SIGNATURE_MIN_LENGTH - FNDSA512.SIGNATURE_HEADER_LENGTH
+ || bodyLen > FNDSA512.SIGNATURE_MAX_LENGTH - FNDSA512.SIGNATURE_HEADER_LENGTH) {
+ return null;
+ }
+ byte[] sig = new byte[bodyLen + FNDSA512.SIGNATURE_HEADER_LENGTH];
+ sig[0] = FNDSA512.SIGNATURE_HEADER;
+ System.arraycopy(data, from, sig, FNDSA512.SIGNATURE_HEADER_LENGTH, bodyLen);
+ return sig;
+ }
+
+ /**
+ * Structural pre-check for ABI head: word-aligned length and room for the
+ * fixed head. The PQ precompiles cannot reuse the ABI encoding check from {@code PrecompiledContracts}
+ * because their {@code bytes[]} entries (PQ signatures, 1..752 bytes) are
+ * variable-length, so the trailing divisibility check does not apply.
+ */
+ static boolean isValidAbiHead(byte[] data, int headWords) {
+ return data != null
+ && data.length % WORD_SIZE == 0
+ && data.length >= multiplyExact(headWords, WORD_SIZE);
+ }
+
+ /**
+ * Verifies that the array offset stored at {@code words[offsetWordIndex]} is
+ * word-aligned, falls inside the dynamic data region (≥ head), and points to
+ * a length word that still fits inside {@code words}. Sister check to
+ * {@code PrecompiledContracts.isValidAbiEncoding} for ABIs whose items are not uniform width.
+ */
+ static boolean isValidArrayOffset(DataWord[] words, int offsetWordIndex, int headWords) {
+ long offsetBytes = words[offsetWordIndex].longValueSafe();
+ if (offsetBytes < (long) headWords * WORD_SIZE || offsetBytes % WORD_SIZE != 0) {
+ return false;
+ }
+ long lengthWordIdx = offsetBytes / WORD_SIZE;
+ return lengthWordIdx < words.length;
+ }
+
+ static byte[][] extractBytesArrayChecked(DataWord[] words, int offset, byte[] data) {
+ if (offset > words.length - 1) {
+ return new byte[0][];
+ }
+ int len = words[offset].intValueSafe();
+ if ((long) offset + len + 1 > words.length) {
+ return new byte[0][];
+ }
+ byte[][] bytesArray = new byte[len][];
+ for (int i = 0; i < len; i++) {
+ int bytesOffsetBytes = words[offset + i + 1].intValueSafe();
+ if (bytesOffsetBytes % WORD_SIZE != 0) {
+ return new byte[0][];
+ }
+ int bytesOffset = bytesOffsetBytes / WORD_SIZE;
+ if ((long) offset + bytesOffset + 1 > words.length - 1) {
+ return new byte[0][];
+ }
+ int bytesLen = words[offset + bytesOffset + 1].intValueSafe();
+ long fromL = ((long) bytesOffset + offset + 2) * WORD_SIZE;
+ long toL = fromL + bytesLen;
+ if (fromL > data.length || toL > data.length) {
+ return new byte[0][];
+ }
+ bytesArray[i] = PrecompiledContracts.extractBytes(data, (int) fromL, bytesLen);
+ }
+ return bytesArray;
+ }
+
+ /**
+ * Verifies a FN-DSA / Falcon-512 signature (FIPS-206 draft). EIP-8052 / TRON extension.
+ *
+ *
Input layout (fixed-length, EIP-8052):
+ *
+ * [msg 32B | sig 666B (zero-padded) | pk 896B] total = 1594B
+ *
+ * The 666-byte sig slot holds the EIP-8052 headerless encoding
+ * {@code salt(40B) ‖ s2_compressed}: unlike BouncyCastle's native form there is
+ * no leading {@code 0x39} header byte. The headerless body is logically
+ * variable (≤ 665B after the salt); encoders write it into the prefix of the slot
+ * and zero-pad the tail to length 666. The {@code compressed_s2} encoding always
+ * ends in a non-zero byte (its unary terminator bit), so the logical body length
+ * is recovered by scanning the slot backwards for the first non-zero byte. Before
+ * verifying, the precompile re-inserts the {@code 0x39} header that BC's
+ * {@code FalconSigner} requires (it rejects any first byte ≠ {@code 0x30 + logn}).
+ * Total input length must equal exactly 1594 (no trailing bytes; matches 0x100
+ * P256Verify / EIP-7951 strictness).
+ *
+ * Returns a 32-byte word: 1 on valid signature, 0 otherwise. Malformed
+ * input (wrong total length, sig slot all zero, recovered length out of
+ * range, BC verification failure) returns 0 without error.
+ */
+ public static class VerifyFnDsa512 extends PrecompiledContracts.PrecompiledContract {
+
+ private static final int MSG_LEN = 32;
+ private static final int SIG_SLOT_LEN =
+ FNDSA512.SIGNATURE_MAX_LENGTH - FNDSA512.SIGNATURE_HEADER_LENGTH;
+ private static final int PK_LEN = FNDSA512.PUBLIC_KEY_LENGTH;
+ private static final int INPUT_LEN = MSG_LEN + SIG_SLOT_LEN + PK_LEN;
+ private static final long ENERGY = 170;
+
+ @Override
+ public long getEnergyForData(byte[] data) {
+ return ENERGY;
+ }
+
+ @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);
+ int sigStart = MSG_LEN;
+ int sigEnd = MSG_LEN + SIG_SLOT_LEN;
+ // The slot carries the EIP-8052 headerless body (salt ‖ s2); reconstruct
+ // the BC-headered form (re-inserts 0x39) BC's FalconSigner requires.
+ byte[] sig = falconSlotToHeaderedSig(data, sigStart, sigEnd);
+ if (sig == null) {
+ return Pair.of(true, DataWord.ZERO().getData());
+ }
+ byte[] pk = copyOfRange(data, sigEnd, INPUT_LEN);
+ boolean ok = FNDSA512.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());
+ }
+ }
+
+ }
+
+
+ /**
+ * 0x02000017 BatchValidateFnDsa512 — independent per-element Falcon-512 verify.
+ *
+ * Returns a 256-bit bitmap (matching 0x09) where bit {@code i} is set iff
+ * {@code derive(pk_i) == expectedAddr_i} AND {@code FNDSA512.verify(pk_i, hash, sig_i)}.
+ *
+ *
ABI:
+ *
+ * batchValidateFnDsa512(
+ * bytes32 hash, // word[0]
+ * bytes[] signatures, // word[1] = offset; each 666 B EIP-8052 headerless
+ * // slot (salt‖s2, no 0x39), zero-padded;
+ * // body ends at last non-zero byte
+ * bytes[] publicKeys, // word[2] = offset; each 896 B
+ * bytes32[] expectedAddresses // word[3] = offset; 21-byte addr in low 21 bytes
+ * ) returns (bytes32)
+ *
+ *
+ * Falcon sigs are pinned to the 666-byte slot from {@code VerifyFnDsa512} (0x02000016)
+ * for cross-precompile consistency; {@link #falconSlotToHeaderedSig} recovers the
+ * headerless body and re-inserts the {@code 0x39} header before BC verification.
+ *
+ *
Reuses the {@code BatchValidateSign.workers} pool when not in a constant
+ * call and enforces {@code getCPUTimeLeftInNanoSecond()} timeout. {@code MAX_SIZE = 16}.
+ * Energy is {@code cnt × 220}.
+ */
+ public static class BatchValidateFnDsa512 extends PrecompiledContracts.PrecompiledContract {
+
+ private static final ExecutorService workers;
+ private static final String workersName = "pq-batch-validate-fndsa512";
+
+ static {
+ workers = ExecutorServiceManager.newFixedThreadPool(workersName,
+ Runtime.getRuntime().availableProcessors() / 2 + 1);
+ }
+
+ private static final int ENERGY_PER_SIGN = 220;
+ private static final int MAX_SIZE = 16;
+ private static final int PK_LEN = FNDSA512.PUBLIC_KEY_LENGTH;
+ private static final int SIG_SLOT_LEN =
+ FNDSA512.SIGNATURE_MAX_LENGTH - FNDSA512.SIGNATURE_HEADER_LENGTH;
+ // hash, sigArrayOffset, pkArrayOffset, addrArrayOffset.
+ private static final int ABI_HEAD_WORDS = 4;
+
+ @Override
+ public long getEnergyForData(byte[] data) {
+ try {
+ DataWord[] words = DataWord.parseArray(data);
+ int cnt = words[words[1].intValueSafe() / WORD_SIZE].intValueSafe();
+ return (long) cnt * ENERGY_PER_SIGN;
+ } catch (Throwable t) {
+ return (long) MAX_SIZE * ENERGY_PER_SIGN;
+ }
+ }
+
+ @Override
+ public Pair execute(byte[] data) {
+ try {
+ return doExecute(data);
+ } catch (Throwable t) {
+ if (t instanceof OutOfTimeException) {
+ throw (OutOfTimeException) t;
+ }
+ if (t instanceof InterruptedException) {
+ Thread.currentThread().interrupt();
+ }
+ return Pair.of(true, new byte[WORD_SIZE]);
+ }
+ }
+
+ private Pair doExecute(byte[] data)
+ throws InterruptedException, ExecutionException {
+ if (!isValidAbiHead(data, ABI_HEAD_WORDS)) {
+ return Pair.of(false, EMPTY_BYTE_ARRAY);
+ }
+ DataWord[] words = DataWord.parseArray(data);
+ if (!isValidArrayOffset(words, 1, ABI_HEAD_WORDS)
+ || !isValidArrayOffset(words, 2, ABI_HEAD_WORDS)
+ || !isValidArrayOffset(words, 3, ABI_HEAD_WORDS)) {
+ return Pair.of(false, EMPTY_BYTE_ARRAY);
+ }
+ byte[] hash = words[0].getData();
+
+ int sigArrayWord = words[1].intValueSafe() / WORD_SIZE;
+ int pkArrayWord = words[2].intValueSafe() / WORD_SIZE;
+ int addrArrayWord = words[3].intValueSafe() / WORD_SIZE;
+
+ int sigArraySize = words[sigArrayWord].intValueSafe();
+ int pkArraySize = words[pkArrayWord].intValueSafe();
+ int addrArraySize = words[addrArrayWord].intValueSafe();
+
+ if (sigArraySize > MAX_SIZE || pkArraySize > MAX_SIZE
+ || addrArraySize > MAX_SIZE
+ || sigArraySize != pkArraySize || sigArraySize != addrArraySize) {
+ return Pair.of(true, DATA_FALSE);
+ }
+
+ byte[][] signatures = extractBytesArrayChecked(words, sigArrayWord, data);
+ byte[][] publicKeys = extractBytesArrayChecked(words, pkArrayWord, data);
+ byte[][] addresses = PrecompiledContracts.extractBytes32Array(words, addrArrayWord);
+
+ int cnt = signatures.length;
+ if (cnt == 0 || publicKeys.length != cnt || addresses.length != cnt) {
+ return Pair.of(true, DATA_FALSE);
+ }
+
+ byte[] res = new byte[WORD_SIZE];
+ if (isConstantCall()) {
+ for (int i = 0; i < cnt; i++) {
+ if (verifyOne(signatures[i], publicKeys[i], hash, addresses[i])) {
+ res[i] = 1;
+ }
+ }
+ } else {
+ CountDownLatch countDownLatch = new CountDownLatch(cnt);
+ List> futures = new ArrayList<>(cnt);
+
+ for (int i = 0; i < cnt; i++) {
+ Future future =
+ workers.submit(
+ new PqVerifyTask(countDownLatch, hash, signatures[i],
+ publicKeys[i], addresses[i], i));
+ futures.add(future);
+ }
+
+ boolean withNoTimeout = countDownLatch
+ .await(getCPUTimeLeftInNanoSecond(), TimeUnit.NANOSECONDS);
+
+ if (!withNoTimeout) {
+ cancelAll(futures);
+ logger.info("BatchValidateFnDsa512 timeout");
+ throw Program.Exception.notEnoughTime("call BatchValidateFnDsa512 precompile method");
+ }
+
+ for (Future future : futures) {
+ PqVerifyResult r = future.get();
+ if (r.success) {
+ res[r.nonce] = 1;
+ }
+ }
+ }
+ return Pair.of(true, res);
+ }
+
+ private static boolean verifyOne(byte[] sig, byte[] pk, byte[] hash,
+ byte[] expectedAddr) {
+ if (pk == null || pk.length != PK_LEN || sig == null || sig.length != SIG_SLOT_LEN) {
+ return false;
+ }
+ // The slot is the EIP-8052 headerless body; rebuild the BC-headered sig.
+ byte[] canonicalSig = falconSlotToHeaderedSig(sig, 0, sig.length);
+ if (canonicalSig == null) {
+ return false;
+ }
+ try {
+ byte[] derived = PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, pk);
+ if (!DataWord.equalAddressByteArray(derived, expectedAddr)) {
+ return false;
+ }
+ return FNDSA512.verify(pk, hash, canonicalSig);
+ } catch (Throwable t) {
+ return false;
+ }
+ }
+
+ @AllArgsConstructor
+ private static class PqVerifyTask implements Callable {
+
+ private CountDownLatch countDownLatch;
+ private byte[] hash;
+ private byte[] signature;
+ private byte[] publicKey;
+ private byte[] expectedAddr;
+ private int nonce;
+
+ @Override
+ public PqVerifyResult call() {
+ try {
+ return new PqVerifyResult(
+ verifyOne(signature, publicKey, hash, expectedAddr), nonce);
+ } finally {
+ countDownLatch.countDown();
+ }
+ }
+ }
+
+ @AllArgsConstructor
+ private static class PqVerifyResult {
+
+ private boolean success;
+ private int nonce;
+ }
+ }
+
+ /**
+ * Verifies an ML-DSA-44 signature (FIPS 204 / CRYSTALS-Dilithium-2).
+ *
+ * Input layout: {@code [msg 32B | sig 2420B | pk 1312B]} — total 3764 B,
+ * strict equality. Returns a 32-byte word (1 on valid, 0 otherwise);
+ * malformed input returns 0 without error.
+ *
+ *
Diverges from EIP-8051 on pk only. {@code msg} and {@code sig}
+ * match EIP-8051; {@code pk} uses the standard FIPS-204 §4 encoding
+ * {@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.
+ */
+ public static class VerifyMlDsa44 extends PrecompiledContracts.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;
+ private static final long ENERGY = 420;
+
+ @Override
+ public long getEnergyForData(byte[] data) {
+ return ENERGY;
+ }
+
+ @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());
+ }
+ }
+ }
+
+ /**
+ * 0x02000019 BatchValidateMlDsa44 — independent per-element ML-DSA-44 verify.
+ * Returns a 256-bit bitmap where bit {@code i} is set iff
+ * {@code derive(pk_i) == expectedAddr_i} AND {@code MLDSA44.verify(pk_i, hash, sig_i)}.
+ * Same ABI shape as 0x02000017, with sigs 2420 B and pks 1312 B.
+ * {@code MAX_SIZE = 16}; energy is {@code cnt × 470}.
+ */
+ public static class BatchValidateMlDsa44 extends PrecompiledContracts.PrecompiledContract {
+
+ private static final ExecutorService workers;
+ private static final String workersName = "pq-batch-validate-mldsa44";
+
+ static {
+ workers = ExecutorServiceManager.newFixedThreadPool(workersName,
+ Runtime.getRuntime().availableProcessors() / 2 + 1);
+ }
+
+ private static final int ENERGY_PER_SIGN = 470;
+ private static final int MAX_SIZE = 16;
+ private static final int PK_LEN = MLDSA44.PUBLIC_KEY_LENGTH;
+ private static final int SIG_LEN = MLDSA44.SIGNATURE_LENGTH;
+ // hash, sigArrayOffset, pkArrayOffset, addrArrayOffset.
+ private static final int ABI_HEAD_WORDS = 4;
+
+ @Override
+ public long getEnergyForData(byte[] data) {
+ try {
+ DataWord[] words = DataWord.parseArray(data);
+ int cnt = words[words[1].intValueSafe() / WORD_SIZE].intValueSafe();
+ return (long) cnt * ENERGY_PER_SIGN;
+ } catch (Throwable t) {
+ return (long) MAX_SIZE * ENERGY_PER_SIGN;
+ }
+ }
+
+ @Override
+ public Pair execute(byte[] data) {
+ try {
+ return doExecute(data);
+ } catch (Throwable t) {
+ if (t instanceof OutOfTimeException) {
+ throw (OutOfTimeException) t;
+ }
+ if (t instanceof InterruptedException) {
+ Thread.currentThread().interrupt();
+ }
+ return Pair.of(true, new byte[WORD_SIZE]);
+ }
+ }
+
+ private Pair doExecute(byte[] data)
+ throws InterruptedException, ExecutionException {
+ if (!isValidAbiHead(data, ABI_HEAD_WORDS)) {
+ return Pair.of(false, EMPTY_BYTE_ARRAY);
+ }
+ DataWord[] words = DataWord.parseArray(data);
+ if (!isValidArrayOffset(words, 1, ABI_HEAD_WORDS)
+ || !isValidArrayOffset(words, 2, ABI_HEAD_WORDS)
+ || !isValidArrayOffset(words, 3, ABI_HEAD_WORDS)) {
+ return Pair.of(false, EMPTY_BYTE_ARRAY);
+ }
+ byte[] hash = words[0].getData();
+
+ int sigArrayWord = words[1].intValueSafe() / WORD_SIZE;
+ int pkArrayWord = words[2].intValueSafe() / WORD_SIZE;
+ int addrArrayWord = words[3].intValueSafe() / WORD_SIZE;
+
+ int sigArraySize = words[sigArrayWord].intValueSafe();
+ int pkArraySize = words[pkArrayWord].intValueSafe();
+ int addrArraySize = words[addrArrayWord].intValueSafe();
+
+ if (sigArraySize > MAX_SIZE || pkArraySize > MAX_SIZE || addrArraySize > MAX_SIZE
+ || sigArraySize != pkArraySize || sigArraySize != addrArraySize) {
+ return Pair.of(true, DATA_FALSE);
+ }
+
+ byte[][] signatures = extractBytesArrayChecked(words, sigArrayWord, data);
+ byte[][] publicKeys = extractBytesArrayChecked(words, pkArrayWord, data);
+ byte[][] addresses = PrecompiledContracts.extractBytes32Array(words, addrArrayWord);
+
+ int cnt = signatures.length;
+ if (cnt == 0 || publicKeys.length != cnt || addresses.length != cnt) {
+ return Pair.of(true, DATA_FALSE);
+ }
+
+ byte[] res = new byte[WORD_SIZE];
+ if (isConstantCall()) {
+ for (int i = 0; i < cnt; i++) {
+ if (verifyOne(signatures[i], publicKeys[i], hash, addresses[i])) {
+ res[i] = 1;
+ }
+ }
+ } else {
+ CountDownLatch countDownLatch = new CountDownLatch(cnt);
+ List> futures = new ArrayList<>(cnt);
+
+ for (int i = 0; i < cnt; i++) {
+ Future future =
+ workers.submit(new PqVerifyTask(countDownLatch, hash, signatures[i],
+ publicKeys[i], addresses[i], i));
+ futures.add(future);
+ }
+
+ boolean withNoTimeout = countDownLatch
+ .await(getCPUTimeLeftInNanoSecond(), TimeUnit.NANOSECONDS);
+
+ if (!withNoTimeout) {
+ cancelAll(futures);
+ logger.info("BatchValidateMlDsa44 timeout");
+ throw Program.Exception.notEnoughTime("call BatchValidateMlDsa44 precompile method");
+ }
+
+ for (Future future : futures) {
+ PqVerifyResult r = future.get();
+ if (r.success) {
+ res[r.nonce] = 1;
+ }
+ }
+ }
+ return Pair.of(true, res);
+ }
+
+ private static boolean verifyOne(byte[] sig, byte[] pk, byte[] hash, byte[] expectedAddr) {
+ if (pk == null || pk.length != PK_LEN || sig == null || sig.length != SIG_LEN) {
+ return false;
+ }
+ try {
+ byte[] derived = PQSchemeRegistry.computeAddress(PQScheme.ML_DSA_44, pk);
+ if (!DataWord.equalAddressByteArray(derived, expectedAddr)) {
+ return false;
+ }
+ return MLDSA44.verify(pk, hash, sig);
+ } catch (Throwable t) {
+ return false;
+ }
+ }
+
+ @AllArgsConstructor
+ private static class PqVerifyTask implements Callable {
+
+ private CountDownLatch countDownLatch;
+ private byte[] hash;
+ private byte[] signature;
+ private byte[] publicKey;
+ private byte[] expectedAddr;
+ private int nonce;
+
+ @Override
+ public PqVerifyResult call() {
+ try {
+ return new PqVerifyResult(
+ verifyOne(signature, publicKey, hash, expectedAddr), nonce);
+ } finally {
+ countDownLatch.countDown();
+ }
+ }
+ }
+
+ @AllArgsConstructor
+ private static class PqVerifyResult {
+
+ private boolean success;
+ private int nonce;
+ }
+ }
+
+
+ /**
+ * 0x0200001a ValidateMultiPQSig — algorithm-agnostic Permission multi-sign. Accepts
+ * ECDSA plus any registered post-quantum scheme (FN-DSA-512, ML-DSA-44, ...)
+ * against {@link Permission}{@code .keys[]} in a single call, dispatched per
+ * entry by an explicit {@code uint8[]} scheme tag array (PQScheme number).
+ *
+ * ABI:
+ *
+ * validateMultiPqSign(
+ * address account, // word[0]
+ * uint256 permissionId, // word[1]
+ * bytes32 data, // word[2]
+ * bytes[] ecdsaSigs, // word[3] = offset; 65 B each
+ * uint8[] pqSchemes, // word[4] = offset; FN_DSA_512=1, ML_DSA_44=2
+ * bytes[] pqSigs, // word[5] = offset; per-scheme fixed slot
+ * bytes[] pqPks // word[6] = offset; per-scheme exact length
+ * ) returns (bytes32) // 1 on (totalWeight >= threshold), 0 otherwise
+ *
+ *
+ * Falcon sigs follow the EIP-8052 666-byte headerless slot convention
+ * (matches 0x02000016/0x02000017): the slot holds {@code salt ‖ s2_compressed} with no
+ * leading {@code 0x39}, zero-padded, the body ending at the last non-zero byte
+ * (Falcon's {@code compressed_s2} always ends with a non-zero terminator);
+ * {@link #falconSlotToHeaderedSig} re-inserts the header before verification.
+ * Dilithium sigs are exactly 2420 B and Dilithium pks 1312 B.
+ *
+ *
{@code MAX_SIZE = 5} across ECDSA + PQ entries combined. Energy is
+ * {@code ecdsaCnt × 1500 + sum_i pqEnergy(scheme_i)} with FN-DSA-512 = 220
+ * and ML-DSA-44 = 470. Unknown tags are charged at worst case so an attacker
+ * cannot underpay by encoding a tag the dispatcher will then reject.
+ *
+ *
Per-entry runtime gate: a Falcon entry returns {@code DATA_FALSE} when
+ * {@code allowFnDsa512()} is false even though 0x0200001a itself is registered as
+ * long as one PQ proposal is active. Same for ML-DSA-44.
+ */
+ public static class ValidateMultiPQSig extends PrecompiledContracts.PrecompiledContract {
+
+ private static final int ECDSA_ENERGY_PER_SIGN = 1500;
+ private static final int FN_DSA_512_ENERGY = 220;
+ private static final int ML_DSA_44_ENERGY = 470;
+ private static final int WORST_PQ_ENERGY = ML_DSA_44_ENERGY;
+ private static final int WORST_ENERGY_PER_SIGN =
+ StrictMathWrapper.max(ECDSA_ENERGY_PER_SIGN, WORST_PQ_ENERGY);
+ private static final int MAX_SIZE = 5;
+ // address, permissionId, data, ecdsaOff, schemeOff, pqSigOff, pqPkOff.
+ private static final int ABI_HEAD_WORDS = 7;
+
+ private static final Map PQ_ENERGY;
+
+ static {
+ EnumMap m = new EnumMap<>(PQScheme.class);
+ m.put(PQScheme.FN_DSA_512, FN_DSA_512_ENERGY);
+ m.put(PQScheme.ML_DSA_44, ML_DSA_44_ENERGY);
+ PQ_ENERGY = m;
+ }
+
+ @Override
+ public long getEnergyForData(byte[] data) {
+ try {
+ DataWord[] words = DataWord.parseArray(data);
+ int ecdsaCnt = words[words[3].intValueSafe() / WORD_SIZE].intValueSafe();
+ int schemeOff = words[4].intValueSafe() / WORD_SIZE;
+ int pqCnt = words[schemeOff].intValueSafe();
+ long energy = (long) ecdsaCnt * ECDSA_ENERGY_PER_SIGN;
+ for (int i = 0; i < pqCnt; i++) {
+ int tag = words[schemeOff + 1 + i].intValueSafe();
+ PQScheme s = PQScheme.forNumber(tag);
+ Integer cost = s == null ? null : PQ_ENERGY.get(s);
+ // Unknown / unregistered tag → charge worst case so a caller can't
+ // encode a junk tag to underpay before execute() rejects it.
+ energy += cost == null ? WORST_PQ_ENERGY : cost;
+ }
+ return energy;
+ } catch (Throwable t) {
+ return (long) MAX_SIZE * WORST_ENERGY_PER_SIGN;
+ }
+ }
+
+ @Override
+ public Pair execute(byte[] rawData) {
+ if (!isValidAbiHead(rawData, ABI_HEAD_WORDS)) {
+ return Pair.of(false, EMPTY_BYTE_ARRAY);
+ }
+ try {
+ DataWord[] words = DataWord.parseArray(rawData);
+ if (!isValidArrayOffset(words, 3, ABI_HEAD_WORDS)
+ || !isValidArrayOffset(words, 4, ABI_HEAD_WORDS)
+ || !isValidArrayOffset(words, 5, ABI_HEAD_WORDS)
+ || !isValidArrayOffset(words, 6, ABI_HEAD_WORDS)) {
+ return Pair.of(false, EMPTY_BYTE_ARRAY);
+ }
+ byte[] address = words[0].toTronAddress();
+ int permissionId = words[1].intValueSafe();
+ byte[] data = words[2].getData();
+
+ byte[] combine = ByteUtil.merge(address, ByteArray.fromInt(permissionId),
+ data);
+ byte[] hash = Sha256Hash.hash(CommonParameter
+ .getInstance().isECKeyCryptoEngine(), combine);
+
+ int ecdsaArrayWord = words[3].intValueSafe() / WORD_SIZE;
+ int schemeArrayWord = words[4].intValueSafe() / WORD_SIZE;
+ int pqSigArrayWord = words[5].intValueSafe() / WORD_SIZE;
+ int pqPkArrayWord = words[6].intValueSafe() / WORD_SIZE;
+
+ int ecdsaCnt = words[ecdsaArrayWord].intValueSafe();
+ int schemeCnt = words[schemeArrayWord].intValueSafe();
+ int pqSigCnt = words[pqSigArrayWord].intValueSafe();
+ int pqPkCnt = words[pqPkArrayWord].intValueSafe();
+
+ // Per-variable bounds first to defeat int overflow in the sum below
+ // (e.g. Integer.MAX_VALUE + 1 wraps to Integer.MIN_VALUE and slips past
+ // a naive `> MAX_SIZE` check).
+ if (ecdsaCnt < 0 || schemeCnt < 0
+ || ecdsaCnt > MAX_SIZE || schemeCnt > MAX_SIZE
+ || schemeCnt != pqSigCnt || schemeCnt != pqPkCnt
+ || ecdsaCnt + schemeCnt == 0
+ || ecdsaCnt + schemeCnt > MAX_SIZE) {
+ return Pair.of(true, DATA_FALSE);
+ }
+
+ byte[][] ecdsaSigs = PrecompiledContracts.extractSigArray(words,
+ ecdsaArrayWord, rawData);
+ byte[][] pqSigs = extractBytesArrayChecked(words, pqSigArrayWord, rawData);
+ byte[][] pqPks = extractBytesArrayChecked(words, pqPkArrayWord, rawData);
+ if (pqSigs.length != schemeCnt || pqPks.length != schemeCnt) {
+ return Pair.of(true, DATA_FALSE);
+ }
+ int[] schemes = new int[schemeCnt];
+ for (int i = 0; i < schemeCnt; i++) {
+ schemes[i] = words[schemeArrayWord + 1 + i].intValueSafe();
+ }
+
+ AccountCapsule account = this.getDeposit().getAccount(address);
+ if (account == null) {
+ return Pair.of(true, DATA_FALSE);
+ }
+ Permission permission = account.getPermissionById(permissionId);
+ if (permission == null) {
+ return Pair.of(true, DATA_FALSE);
+ }
+
+ long totalWeight = 0L;
+ List seenAddrs = new ArrayList<>();
+
+ for (byte[] sign : ecdsaSigs) {
+ byte[] recoveredAddr = PrecompiledContracts.recoverAddrBySign(sign, hash);
+ if (ByteArray.matrixContains(seenAddrs, recoveredAddr)) {
+ continue;
+ }
+ long weight = TransactionCapsule.getWeight(permission, recoveredAddr);
+ if (weight == 0) {
+ return Pair.of(true, DATA_FALSE);
+ }
+ totalWeight += weight;
+ seenAddrs.add(recoveredAddr);
+ }
+
+ for (int i = 0; i < schemes.length; i++) {
+ PQScheme scheme = PQScheme.forNumber(schemes[i]);
+ if (scheme == null || scheme == PQScheme.UNKNOWN_PQ_SCHEME
+ || !PQSchemeRegistry.contains(scheme)) {
+ return Pair.of(true, DATA_FALSE);
+ }
+ // Per-entry runtime gate: the scheme's proposal must be active even
+ // though 0x0200001a was registered under (allowFnDsa512 || allowMlDsa44).
+ if (scheme == PQScheme.FN_DSA_512 && !VMConfig.allowFnDsa512()) {
+ return Pair.of(true, DATA_FALSE);
+ }
+ if (scheme == PQScheme.ML_DSA_44 && !VMConfig.allowMlDsa44()) {
+ return Pair.of(true, DATA_FALSE);
+ }
+ byte[] sig = pqSigs[i];
+ byte[] pk = pqPks[i];
+ int expectedPkLen = PQSchemeRegistry.getPublicKeyLength(scheme);
+ int expectedSigSlot = scheme == PQScheme.FN_DSA_512
+ ? FNDSA512.SIGNATURE_MAX_LENGTH - FNDSA512.SIGNATURE_HEADER_LENGTH
+ : PQSchemeRegistry.getSignatureLength(scheme);
+ if (pk == null || pk.length != expectedPkLen
+ || sig == null || sig.length != expectedSigSlot) {
+ // Slot lengths are exact here (Falcon = 666, Dilithium = 2420) —
+ // a Falcon sig mislabelled as Dilithium fails this check.
+ return Pair.of(true, DATA_FALSE);
+ }
+ if (scheme == PQScheme.FN_DSA_512) {
+ // The Falcon slot is the EIP-8052 headerless body; rebuild the
+ // BC-headered sig (re-inserts 0x39) before verification.
+ sig = falconSlotToHeaderedSig(sig, 0, sig.length);
+ if (sig == null) {
+ return Pair.of(true, DATA_FALSE);
+ }
+ }
+ byte[] derivedAddr;
+ try {
+ derivedAddr = PQSchemeRegistry.computeAddress(scheme, pk);
+ } catch (Throwable t) {
+ return Pair.of(true, DATA_FALSE);
+ }
+ // Both Falcon and Dilithium signing are randomized → the same key
+ // can produce many valid sigs for one message, so dedup keys on the
+ // derived address only (the sig blob is not a stable identity).
+ if (ByteArray.matrixContains(seenAddrs, derivedAddr)) {
+ continue;
+ }
+ long weight = TransactionCapsule.getWeight(permission, derivedAddr);
+ if (weight == 0) {
+ return Pair.of(true, DATA_FALSE);
+ }
+ if (!PQSchemeRegistry.verify(scheme, pk, hash, sig)) {
+ return Pair.of(true, DATA_FALSE);
+ }
+ totalWeight += weight;
+ seenAddrs.add(derivedAddr);
+ }
+
+ if (totalWeight >= permission.getThreshold()) {
+ return Pair.of(true, dataOne());
+ }
+ } catch (Throwable t) {
+ if (t instanceof OutOfTimeException) {
+ throw t;
+ }
+ }
+ return Pair.of(true, DATA_FALSE);
+ }
+ }
+}
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 6730d32163..57fdd0086f 100644
--- a/actuator/src/main/java/org/tron/core/vm/PrecompiledContracts.java
+++ b/actuator/src/main/java/org/tron/core/vm/PrecompiledContracts.java
@@ -29,10 +29,8 @@
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.EnumMap;
import java.util.HashSet;
import java.util.List;
-import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
@@ -58,9 +56,6 @@
import org.tron.common.crypto.Rsv;
import org.tron.common.crypto.SignUtils;
import org.tron.common.crypto.SignatureInterface;
-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.crypto.zksnark.BN128;
import org.tron.common.crypto.zksnark.BN128Fp;
import org.tron.common.crypto.zksnark.BN128G1;
@@ -91,7 +86,6 @@
import org.tron.core.vm.utils.MUtil;
import org.tron.core.vm.utils.VoteRewardUtil;
import org.tron.protos.Protocol;
-import org.tron.protos.Protocol.PQScheme;
import org.tron.protos.Protocol.Permission;
@Slf4j(topic = "VM")
@@ -127,12 +121,17 @@ public class PrecompiledContracts {
private static final KZGPointEvaluation kzgPointEvaluation = new KZGPointEvaluation();
private static final P256Verify p256Verify = new P256Verify();
- private static final VerifyFnDsa512 verifyFnDsa512 = new VerifyFnDsa512();
- private static final BatchValidateFnDsa512 batchValidateFnDsa512 = new BatchValidateFnDsa512();
+ private static final PQPrecompiledContracts.VerifyFnDsa512 verifyFnDsa512 =
+ new PQPrecompiledContracts.VerifyFnDsa512();
+ private static final PQPrecompiledContracts.BatchValidateFnDsa512 batchValidateFnDsa512 =
+ new PQPrecompiledContracts.BatchValidateFnDsa512();
- private static final VerifyMlDsa44 verifyMlDsa44 = new VerifyMlDsa44();
- private static final BatchValidateMlDsa44 batchValidateMlDsa44 = new BatchValidateMlDsa44();
- private static final ValidateMultiPQSig validateMultiPqSig = new ValidateMultiPQSig();
+ private static final PQPrecompiledContracts.VerifyMlDsa44 verifyMlDsa44 =
+ new PQPrecompiledContracts.VerifyMlDsa44();
+ private static final PQPrecompiledContracts.BatchValidateMlDsa44 batchValidateMlDsa44 =
+ new PQPrecompiledContracts.BatchValidateMlDsa44();
+ private static final PQPrecompiledContracts.ValidateMultiPQSig validateMultiPqSig =
+ new PQPrecompiledContracts.ValidateMultiPQSig();
// FreezeV2 PrecompileContracts
private static final GetChainParameter getChainParameter = new GetChainParameter();
@@ -235,34 +234,32 @@ public class PrecompiledContracts {
private static final DataWord kzgPointEvaluationAddr = new DataWord(
"000000000000000000000000000000000000000000000000000000000002000a");
- // EIP-8052 0x16: FN-DSA / Falcon-512 verify (FIPS-206 draft). Input layout:
+ // EIP-8052 0x02000016: FN-DSA / Falcon-512 verify (FIPS-206 draft). Input layout:
// [msg 32B | sig 666B (headerless salt‖s2 slot, zero-padded; body ends at last
// non-zero byte) | pk 896B]. Total 1594 B. The slot holds the EIP-8052 headerless
// signature (no 0x39 byte); the precompile re-inserts the header before verifying.
private static final DataWord verifyFnDsa512Addr = new DataWord(
- "0000000000000000000000000000000000000000000000000000000000000016");
+ "0000000000000000000000000000000000000000000000000000000002000016");
- // 0x17: batch independent Falcon-512 verify — bitmap of (sig, pk, addr)
- // matches; mixed-algorithm contracts call 0x0A and 0x18 separately and OR
- // the bitmaps client-side.
+ // 0x02000017: batch independent Falcon-512 verify — bitmap of (sig, pk, addr) matches.
private static final DataWord batchValidateFnDsa512Addr = new DataWord(
- "0000000000000000000000000000000000000000000000000000000000000017");
+ "0000000000000000000000000000000000000000000000000000000002000017");
- // 0x18: ML-DSA-44 single verify (FIPS 204 / Dilithium-2).
+ // 0x02000018: ML-DSA-44 single verify (FIPS 204 / Dilithium-2).
private static final DataWord verifyMlDsa44Addr = new DataWord(
- "0000000000000000000000000000000000000000000000000000000000000018");
+ "0000000000000000000000000000000000000000000000000000000002000018");
- // 0x19: batch independent ML-DSA-44 verify — bitmap output, same shape as 0x18.
+ // 0x02000019: batch independent ML-DSA-44 verify — bitmap output, same shape as 0x02000017.
private static final DataWord batchValidateMlDsa44Addr = new DataWord(
- "0000000000000000000000000000000000000000000000000000000000000019");
+ "0000000000000000000000000000000000000000000000000000000002000019");
- // 0x1a: algorithm-agnostic Permission multi-sign — accepts ECDSA and any
+ // 0x0200001a: algorithm-agnostic Permission multi-sign — accepts ECDSA and any
// registered PQ scheme (Falcon-512, ML-DSA-44, ...) against the same
// Permission.keys[] in one call, dispatched by an explicit per-entry scheme
- // tag. Replaces the earlier Falcon-only 0x17 and Dilithium-only draft, which
+ // tag. Replaces the earlier Falcon-only 0x02000017 and Dilithium-only draft, which
// were never activated.
private static final DataWord validateMultiPqSigAddr = new DataWord(
- "000000000000000000000000000000000000000000000000000000000000001a");
+ "000000000000000000000000000000000000000000000000000000000200001a");
public static PrecompiledContract getOptimizedContractForConstant(PrecompiledContract contract) {
try {
@@ -352,7 +349,7 @@ public static PrecompiledContract getContractForAddress(DataWord address) {
return p256Verify;
}
- // 0x1a ValidateMultiPQSig is algorithm-agnostic and dispatches per entry,
+ // 0x0200001a ValidateMultiPQSig is algorithm-agnostic and dispatches per entry,
// so it is available whenever ANY registered PQ scheme is active. Per-entry
// runtime checks inside the precompile still reject scheme tags whose
// proposal hasn't passed.
@@ -455,7 +452,7 @@ private static byte[] encodeMultiRes(byte[]... words) {
return res;
}
- private static byte[] recoverAddrBySign(byte[] sign, byte[] hash) {
+ static byte[] recoverAddrBySign(byte[] sign, byte[] hash) {
byte[] out = null;
if (ArrayUtils.isEmpty(sign) || sign.length < 65) {
return new byte[0];
@@ -474,7 +471,7 @@ private static byte[] recoverAddrBySign(byte[] sign, byte[] hash) {
return out;
}
- private static byte[][] extractBytes32Array(DataWord[] words, int offset) {
+ static byte[][] extractBytes32Array(DataWord[] words, int offset) {
int len = words[offset].intValueSafe();
byte[][] bytes32Array = new byte[len][];
for (int i = 0; i < len; i++) {
@@ -498,36 +495,7 @@ private static byte[][] extractBytesArray(DataWord[] words, int offset, byte[] d
return bytesArray;
}
- private static byte[][] extractBytesArrayChecked(DataWord[] words, int offset, byte[] data) {
- if (offset > words.length - 1) {
- return new byte[0][];
- }
- int len = words[offset].intValueSafe();
- if ((long) offset + len + 1 > words.length) {
- return new byte[0][];
- }
- byte[][] bytesArray = new byte[len][];
- for (int i = 0; i < len; i++) {
- int bytesOffsetBytes = words[offset + i + 1].intValueSafe();
- if (bytesOffsetBytes % WORD_SIZE != 0) {
- return new byte[0][];
- }
- int bytesOffset = bytesOffsetBytes / WORD_SIZE;
- if ((long) offset + bytesOffset + 1 > words.length - 1) {
- return new byte[0][];
- }
- int bytesLen = words[offset + bytesOffset + 1].intValueSafe();
- long fromL = ((long) bytesOffset + offset + 2) * WORD_SIZE;
- long toL = fromL + bytesLen;
- if (fromL > data.length || toL > data.length) {
- return new byte[0][];
- }
- bytesArray[i] = extractBytes(data, (int) fromL, bytesLen);
- }
- return bytesArray;
- }
-
- private static byte[][] extractSigArray(DataWord[] words, int offset, byte[] data) {
+ static byte[][] extractSigArray(DataWord[] words, int offset, byte[] data) {
if (offset > words.length - 1) {
return new byte[0][];
}
@@ -541,7 +509,7 @@ private static byte[][] extractSigArray(DataWord[] words, int offset, byte[] dat
return bytesArray;
}
- private static byte[] extractBytes(byte[] data, int offset, int len) {
+ static byte[] extractBytes(byte[] data, int offset, int len) {
return Arrays.copyOfRange(data, offset, offset + len);
}
@@ -553,84 +521,6 @@ private static boolean isValidAbiEncoding(byte[] data, int headerWords, int item
return tail > 0 && tail % multiplyExact(itemWords, WORD_SIZE) == 0;
}
- /**
- * Structural pre-check for ABI head: word-aligned length and room for the
- * fixed head. The PQ precompiles cannot reuse {@link #isValidAbiEncoding}
- * because their {@code bytes[]} entries (PQ signatures, 1..752 bytes) are
- * variable-length, so the trailing divisibility check does not apply.
- */
- private static boolean isValidAbiHead(byte[] data, int headWords) {
- return data != null
- && data.length % WORD_SIZE == 0
- && data.length >= multiplyExact(headWords, WORD_SIZE);
- }
-
- /**
- * Verifies that the array offset stored at {@code words[offsetWordIndex]} is
- * word-aligned, falls inside the dynamic data region (≥ head), and points to
- * a length word that still fits inside {@code words}. Sister check to
- * {@link #isValidAbiEncoding} for ABIs whose items are not uniform width.
- */
- private static boolean isValidArrayOffset(DataWord[] words, int offsetWordIndex, int headWords) {
- long offsetBytes = words[offsetWordIndex].longValueSafe();
- if (offsetBytes < (long) headWords * WORD_SIZE || offsetBytes % WORD_SIZE != 0) {
- return false;
- }
- long lengthWordIdx = offsetBytes / WORD_SIZE;
- return lengthWordIdx < words.length;
- }
-
- /**
- * Best-effort cancellation of all submitted batch-verify tasks. Tasks that
- * have not yet started execution are removed from the worker queue; tasks
- * already running receive an interrupt but BouncyCastle's PQ verify routines
- * do not poll the interrupt flag and will run to completion.
- */
- private static void cancelAll(List extends Future>> futures) {
- for (Future> f : futures) {
- f.cancel(true);
- }
- }
-
- /**
- * Returns the logical Falcon-512 signature length packed at the start of a
- * fixed slot {@code data[from..to)}: the offset of the last non-zero byte
- * (exclusive). Canonical Falcon encodings always end in a non-zero byte
- * ({@code compressed_s2}'s unary terminator), so anything beyond is zero
- * padding. Returns 0 if the slot is all zero. Shared by 0x16, 0x18, and 0x1a
- * because every precompile slot for Falcon sigs is the same 666-byte slot.
- */
- static int recoverFalconSigLen(byte[] data, int from, int to) {
- for (int i = to - 1; i >= from; i--) {
- if (data[i] != 0) {
- return i - from + 1;
- }
- }
- return 0;
- }
-
- /**
- * Reconstructs the BC-native Falcon-512 signature from an EIP-8052 headerless
- * slot. The slot {@code data[from..to)} holds {@code salt ‖ s2_compressed}
- * (no leading {@code 0x39}) zero-padded to
- * {@code SIGNATURE_MAX_LENGTH - SIGNATURE_HEADER_LENGTH};
- * the logical body ends at the last non-zero byte. Returns
- * {@code 0x39 ‖ body} so BC's {@code FalconSigner} (which requires the header)
- * can verify it, or {@code null} if the recovered body length is out of range.
- * Shared by 0x16, 0x18, and 0x1a.
- */
- static byte[] falconSlotToHeaderedSig(byte[] data, int from, int to) {
- int bodyLen = recoverFalconSigLen(data, from, to);
- if (bodyLen < FNDSA512.SIGNATURE_MIN_LENGTH - FNDSA512.SIGNATURE_HEADER_LENGTH
- || bodyLen > FNDSA512.SIGNATURE_MAX_LENGTH - FNDSA512.SIGNATURE_HEADER_LENGTH) {
- return null;
- }
- byte[] sig = new byte[bodyLen + FNDSA512.SIGNATURE_HEADER_LENGTH];
- sig[0] = FNDSA512.SIGNATURE_HEADER;
- System.arraycopy(data, from, sig, FNDSA512.SIGNATURE_HEADER_LENGTH, bodyLen);
- return sig;
- }
-
public abstract static class PrecompiledContract {
protected static final byte[] DATA_FALSE = new byte[WORD_SIZE];
@@ -2567,7 +2457,6 @@ public Pair execute(byte[] data) {
}
}
-
public static class P256Verify extends PrecompiledContract {
private static final X9ECParameters CURVE = SECNamedCurves.getByName("secp256r1");
@@ -2622,677 +2511,4 @@ public Pair execute(byte[] data) {
}
}
}
-
- /**
- * Verifies a FN-DSA / Falcon-512 signature (FIPS-206 draft). EIP-8052 / TRON extension.
- *
- * Input layout (fixed-length, EIP-8052):
- *
- * [msg 32B | sig 666B (zero-padded) | pk 896B] total = 1594B
- *
- * The 666-byte sig slot holds the EIP-8052 headerless encoding
- * {@code salt(40B) ‖ s2_compressed}: unlike BouncyCastle's native form there is
- * no leading {@code 0x39} header byte. The headerless body is logically
- * variable (≤ 665B after the salt); encoders write it into the prefix of the slot
- * and zero-pad the tail to length 666. The {@code compressed_s2} encoding always
- * ends in a non-zero byte (its unary terminator bit), so the logical body length
- * is recovered by scanning the slot backwards for the first non-zero byte. Before
- * verifying, the precompile re-inserts the {@code 0x39} header that BC's
- * {@code FalconSigner} requires (it rejects any first byte ≠ {@code 0x30 + logn}).
- * Total input length must equal exactly 1594 (no trailing bytes; matches 0x100
- * P256Verify / EIP-7951 strictness).
- *
- * Returns a 32-byte word: 1 on valid signature, 0 otherwise. Malformed
- * input (wrong total length, sig slot all zero, recovered length out of
- * range, BC verification failure) returns 0 without error.
- */
- public static class VerifyFnDsa512 extends PrecompiledContract {
-
- private static final int MSG_LEN = 32;
- private static final int SIG_SLOT_LEN =
- FNDSA512.SIGNATURE_MAX_LENGTH - FNDSA512.SIGNATURE_HEADER_LENGTH;
- private static final int PK_LEN = FNDSA512.PUBLIC_KEY_LENGTH;
- private static final int INPUT_LEN = MSG_LEN + SIG_SLOT_LEN + PK_LEN;
- private static final long ENERGY = 170;
-
- @Override
- public long getEnergyForData(byte[] data) {
- return ENERGY;
- }
-
- @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);
- int sigStart = MSG_LEN;
- int sigEnd = MSG_LEN + SIG_SLOT_LEN;
- // The slot carries the EIP-8052 headerless body (salt ‖ s2); reconstruct
- // the BC-headered form (re-inserts 0x39) BC's FalconSigner requires.
- byte[] sig = falconSlotToHeaderedSig(data, sigStart, sigEnd);
- if (sig == null) {
- return Pair.of(true, DataWord.ZERO().getData());
- }
- byte[] pk = copyOfRange(data, sigEnd, INPUT_LEN);
- boolean ok = FNDSA512.verify(pk, msg, sig);
- return Pair.of(true, ok ? DataWord.ONE().getData() : DataWord.ZERO().getData());
- } catch (Throwable t) {
- return Pair.of(true, DataWord.ZERO().getData());
- }
- }
-
- }
-
-
- /**
- * 0x17 BatchValidateFnDsa512 — independent per-element Falcon-512 verify.
- *
- * Returns a 256-bit bitmap (matching 0x09) where bit {@code i} is set iff
- * {@code derive(pk_i) == expectedAddr_i} AND {@code FNDSA512.verify(pk_i, hash, sig_i)}.
- *
- *
ABI:
- *
- * batchValidateFnDsa512(
- * bytes32 hash, // word[0]
- * bytes[] signatures, // word[1] = offset; each 666 B EIP-8052 headerless
- * // slot (salt‖s2, no 0x39), zero-padded;
- * // body ends at last non-zero byte
- * bytes[] publicKeys, // word[2] = offset; each 896 B
- * bytes32[] expectedAddresses // word[3] = offset; 21-byte addr in low 21 bytes
- * ) returns (bytes32)
- *
- *
- * Falcon sigs are pinned to the 666-byte slot from {@code VerifyFnDsa512} (0x16)
- * for cross-precompile consistency; {@link #falconSlotToHeaderedSig} recovers the
- * headerless body and re-inserts the {@code 0x39} header before BC verification.
- *
- *
Reuses the {@code BatchValidateSign.workers} pool when not in a constant
- * call and enforces {@code getCPUTimeLeftInNanoSecond()} timeout. {@code MAX_SIZE = 16}.
- * Energy is {@code cnt × 220}.
- */
- public static class BatchValidateFnDsa512 extends PrecompiledContract {
-
- private static final int ENERGY_PER_SIGN = 220;
- private static final int MAX_SIZE = 16;
- private static final int PK_LEN = FNDSA512.PUBLIC_KEY_LENGTH;
- private static final int SIG_SLOT_LEN =
- FNDSA512.SIGNATURE_MAX_LENGTH - FNDSA512.SIGNATURE_HEADER_LENGTH;
- // hash, sigArrayOffset, pkArrayOffset, addrArrayOffset.
- private static final int ABI_HEAD_WORDS = 4;
-
- @Override
- public long getEnergyForData(byte[] data) {
- try {
- DataWord[] words = DataWord.parseArray(data);
- int cnt = words[words[1].intValueSafe() / WORD_SIZE].intValueSafe();
- return (long) cnt * ENERGY_PER_SIGN;
- } catch (Throwable t) {
- return (long) MAX_SIZE * ENERGY_PER_SIGN;
- }
- }
-
- @Override
- public Pair execute(byte[] data) {
- try {
- return doExecute(data);
- } catch (Throwable t) {
- if (t instanceof OutOfTimeException) {
- throw (OutOfTimeException) t;
- }
- if (t instanceof InterruptedException) {
- Thread.currentThread().interrupt();
- }
- return Pair.of(true, new byte[WORD_SIZE]);
- }
- }
-
- private Pair doExecute(byte[] data)
- throws InterruptedException, ExecutionException {
- if (!isValidAbiHead(data, ABI_HEAD_WORDS)) {
- return Pair.of(false, EMPTY_BYTE_ARRAY);
- }
- DataWord[] words = DataWord.parseArray(data);
- if (!isValidArrayOffset(words, 1, ABI_HEAD_WORDS)
- || !isValidArrayOffset(words, 2, ABI_HEAD_WORDS)
- || !isValidArrayOffset(words, 3, ABI_HEAD_WORDS)) {
- return Pair.of(false, EMPTY_BYTE_ARRAY);
- }
- byte[] hash = words[0].getData();
-
- int sigArrayWord = words[1].intValueSafe() / WORD_SIZE;
- int pkArrayWord = words[2].intValueSafe() / WORD_SIZE;
- int addrArrayWord = words[3].intValueSafe() / WORD_SIZE;
-
- int sigArraySize = words[sigArrayWord].intValueSafe();
- int pkArraySize = words[pkArrayWord].intValueSafe();
- int addrArraySize = words[addrArrayWord].intValueSafe();
-
- if (sigArraySize > MAX_SIZE || pkArraySize > MAX_SIZE
- || addrArraySize > MAX_SIZE
- || sigArraySize != pkArraySize || sigArraySize != addrArraySize) {
- return Pair.of(true, DATA_FALSE);
- }
-
- byte[][] signatures = extractBytesArrayChecked(words, sigArrayWord, data);
- byte[][] publicKeys = extractBytesArrayChecked(words, pkArrayWord, data);
- byte[][] addresses = extractBytes32Array(words, addrArrayWord);
-
- int cnt = signatures.length;
- if (cnt == 0 || publicKeys.length != cnt || addresses.length != cnt) {
- return Pair.of(true, DATA_FALSE);
- }
-
- byte[] res = new byte[WORD_SIZE];
- if (isConstantCall()) {
- for (int i = 0; i < cnt; i++) {
- if (verifyOne(signatures[i], publicKeys[i], hash, addresses[i])) {
- res[i] = 1;
- }
- }
- } else {
- CountDownLatch countDownLatch = new CountDownLatch(cnt);
- List> futures = new ArrayList<>(cnt);
-
- for (int i = 0; i < cnt; i++) {
- Future future = BatchValidateSign.workers.submit(
- new PqVerifyTask(countDownLatch, hash, signatures[i],
- publicKeys[i], addresses[i], i));
- futures.add(future);
- }
-
- boolean withNoTimeout = countDownLatch
- .await(getCPUTimeLeftInNanoSecond(), TimeUnit.NANOSECONDS);
-
- if (!withNoTimeout) {
- cancelAll(futures);
- logger.info("BatchValidateFnDsa512 timeout");
- throw Program.Exception.notEnoughTime("call BatchValidateFnDsa512 precompile method");
- }
-
- for (Future future : futures) {
- PqVerifyResult r = future.get();
- if (r.success) {
- res[r.nonce] = 1;
- }
- }
- }
- return Pair.of(true, res);
- }
-
- private static boolean verifyOne(byte[] sig, byte[] pk, byte[] hash, byte[] expectedAddr) {
- if (pk == null || pk.length != PK_LEN || sig == null || sig.length != SIG_SLOT_LEN) {
- return false;
- }
- // The slot is the EIP-8052 headerless body; rebuild the BC-headered sig.
- byte[] canonicalSig = falconSlotToHeaderedSig(sig, 0, sig.length);
- if (canonicalSig == null) {
- return false;
- }
- try {
- byte[] derived = PQSchemeRegistry.computeAddress(PQScheme.FN_DSA_512, pk);
- if (!DataWord.equalAddressByteArray(derived, expectedAddr)) {
- return false;
- }
- return FNDSA512.verify(pk, hash, canonicalSig);
- } catch (Throwable t) {
- return false;
- }
- }
-
- @AllArgsConstructor
- private static class PqVerifyTask implements Callable {
-
- private CountDownLatch countDownLatch;
- private byte[] hash;
- private byte[] signature;
- private byte[] publicKey;
- private byte[] expectedAddr;
- private int nonce;
-
- @Override
- public PqVerifyResult call() {
- try {
- return new PqVerifyResult(verifyOne(signature, publicKey, hash, expectedAddr), nonce);
- } finally {
- countDownLatch.countDown();
- }
- }
- }
-
- @AllArgsConstructor
- private static class PqVerifyResult {
-
- private boolean success;
- private int nonce;
- }
- }
-
- /**
- * Verifies an ML-DSA-44 signature (FIPS 204 / CRYSTALS-Dilithium-2).
- *
- * Input layout: {@code [msg 32B | sig 2420B | pk 1312B]} — total 3764 B,
- * strict equality. Returns a 32-byte word (1 on valid, 0 otherwise);
- * malformed input returns 0 without error.
- *
- *
Diverges from EIP-8051 on pk only. {@code msg} and {@code sig}
- * match EIP-8051; {@code pk} uses the standard FIPS-204 §4 encoding
- * {@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. The EIP-8051 expanded-pk
- * variant is implemented separately at 0x12 — 0x19 stays as-is.
- */
- 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;
- private static final long ENERGY = 420;
-
- @Override
- public long getEnergyForData(byte[] data) {
- return ENERGY;
- }
-
- @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());
- }
- }
- }
-
- /**
- * 0x1a ValidateMultiPQSig — algorithm-agnostic Permission multi-sign. Accepts
- * ECDSA plus any registered post-quantum scheme (FN-DSA-512, ML-DSA-44, ...)
- * against {@link Permission}{@code .keys[]} in a single call, dispatched per
- * entry by an explicit {@code uint8[]} scheme tag array (PQScheme number).
- *
- * ABI:
- *
- * validateMultiPqSign(
- * address account, // word[0]
- * uint256 permissionId, // word[1]
- * bytes32 data, // word[2]
- * bytes[] ecdsaSigs, // word[3] = offset; 65 B each
- * uint8[] pqSchemes, // word[4] = offset; FN_DSA_512=1, ML_DSA_44=2
- * bytes[] pqSigs, // word[5] = offset; per-scheme fixed slot
- * bytes[] pqPks // word[6] = offset; per-scheme exact length
- * ) returns (bytes32) // 1 on (totalWeight >= threshold), 0 otherwise
- *
- *
- * Falcon sigs follow the EIP-8052 666-byte headerless slot convention
- * (matches 0x16/0x18): the slot holds {@code salt ‖ s2_compressed} with no
- * leading {@code 0x39}, zero-padded, the body ending at the last non-zero byte
- * (Falcon's {@code compressed_s2} always ends with a non-zero terminator);
- * {@link #falconSlotToHeaderedSig} re-inserts the header before verification.
- * Dilithium sigs are exactly 2420 B and Dilithium pks 1312 B.
- *
- *
{@code MAX_SIZE = 5} across ECDSA + PQ entries combined. Energy is
- * {@code ecdsaCnt × 1500 + sum_i pqEnergy(scheme_i)} with FN-DSA-512 = 220
- * and ML-DSA-44 = 470. Unknown tags are charged at worst case so an attacker
- * cannot underpay by encoding a tag the dispatcher will then reject.
- *
- *
Per-entry runtime gate: a Falcon entry returns {@code DATA_FALSE} when
- * {@code allowFnDsa512()} is false even though 0x1a itself is registered as
- * long as one PQ proposal is active. Same for ML-DSA-44.
- */
- public static class ValidateMultiPQSig extends PrecompiledContract {
-
- private static final int ECDSA_ENERGY_PER_SIGN = 1500;
- private static final int FN_DSA_512_ENERGY = 220;
- private static final int ML_DSA_44_ENERGY = 470;
- private static final int WORST_PQ_ENERGY = ML_DSA_44_ENERGY;
- private static final int MAX_SIZE = 5;
- // address, permissionId, data, ecdsaOff, schemeOff, pqSigOff, pqPkOff.
- private static final int ABI_HEAD_WORDS = 7;
-
- private static final Map PQ_ENERGY;
-
- static {
- EnumMap m = new EnumMap<>(PQScheme.class);
- m.put(PQScheme.FN_DSA_512, FN_DSA_512_ENERGY);
- m.put(PQScheme.ML_DSA_44, ML_DSA_44_ENERGY);
- PQ_ENERGY = m;
- }
-
- @Override
- public long getEnergyForData(byte[] data) {
- try {
- DataWord[] words = DataWord.parseArray(data);
- int ecdsaCnt = words[words[3].intValueSafe() / WORD_SIZE].intValueSafe();
- int schemeOff = words[4].intValueSafe() / WORD_SIZE;
- int pqCnt = words[schemeOff].intValueSafe();
- long energy = (long) ecdsaCnt * ECDSA_ENERGY_PER_SIGN;
- for (int i = 0; i < pqCnt; i++) {
- int tag = words[schemeOff + 1 + i].intValueSafe();
- PQScheme s = PQScheme.forNumber(tag);
- Integer cost = s == null ? null : PQ_ENERGY.get(s);
- // Unknown / unregistered tag → charge worst case so a caller can't
- // encode a junk tag to underpay before execute() rejects it.
- energy += cost == null ? WORST_PQ_ENERGY : cost;
- }
- return energy;
- } catch (Throwable t) {
- return (long) MAX_SIZE * WORST_PQ_ENERGY;
- }
- }
-
- @Override
- public Pair execute(byte[] rawData) {
- if (!isValidAbiHead(rawData, ABI_HEAD_WORDS)) {
- return Pair.of(false, EMPTY_BYTE_ARRAY);
- }
- try {
- DataWord[] words = DataWord.parseArray(rawData);
- if (!isValidArrayOffset(words, 3, ABI_HEAD_WORDS)
- || !isValidArrayOffset(words, 4, ABI_HEAD_WORDS)
- || !isValidArrayOffset(words, 5, ABI_HEAD_WORDS)
- || !isValidArrayOffset(words, 6, ABI_HEAD_WORDS)) {
- return Pair.of(false, EMPTY_BYTE_ARRAY);
- }
- byte[] address = words[0].toTronAddress();
- int permissionId = words[1].intValueSafe();
- byte[] data = words[2].getData();
-
- byte[] combine = ByteUtil.merge(address, ByteArray.fromInt(permissionId), data);
- byte[] hash = Sha256Hash.hash(CommonParameter
- .getInstance().isECKeyCryptoEngine(), combine);
-
- int ecdsaArrayWord = words[3].intValueSafe() / WORD_SIZE;
- int schemeArrayWord = words[4].intValueSafe() / WORD_SIZE;
- int pqSigArrayWord = words[5].intValueSafe() / WORD_SIZE;
- int pqPkArrayWord = words[6].intValueSafe() / WORD_SIZE;
-
- int ecdsaCnt = words[ecdsaArrayWord].intValueSafe();
- int schemeCnt = words[schemeArrayWord].intValueSafe();
- int pqSigCnt = words[pqSigArrayWord].intValueSafe();
- int pqPkCnt = words[pqPkArrayWord].intValueSafe();
-
- // Per-variable bounds first to defeat int overflow in the sum below
- // (e.g. Integer.MAX_VALUE + 1 wraps to Integer.MIN_VALUE and slips past
- // a naive `> MAX_SIZE` check).
- if (ecdsaCnt < 0 || schemeCnt < 0
- || ecdsaCnt > MAX_SIZE || schemeCnt > MAX_SIZE
- || schemeCnt != pqSigCnt || schemeCnt != pqPkCnt
- || ecdsaCnt + schemeCnt == 0
- || ecdsaCnt + schemeCnt > MAX_SIZE) {
- return Pair.of(true, DATA_FALSE);
- }
-
- byte[][] ecdsaSigs = extractSigArray(words, ecdsaArrayWord, rawData);
- byte[][] pqSigs = extractBytesArrayChecked(words, pqSigArrayWord, rawData);
- byte[][] pqPks = extractBytesArrayChecked(words, pqPkArrayWord, rawData);
- if (pqSigs.length != schemeCnt || pqPks.length != schemeCnt) {
- return Pair.of(true, DATA_FALSE);
- }
- int[] schemes = new int[schemeCnt];
- for (int i = 0; i < schemeCnt; i++) {
- schemes[i] = words[schemeArrayWord + 1 + i].intValueSafe();
- }
-
- AccountCapsule account = this.getDeposit().getAccount(address);
- if (account == null) {
- return Pair.of(true, DATA_FALSE);
- }
- Permission permission = account.getPermissionById(permissionId);
- if (permission == null) {
- return Pair.of(true, DATA_FALSE);
- }
-
- long totalWeight = 0L;
- List seenAddrs = new ArrayList<>();
-
- for (byte[] sign : ecdsaSigs) {
- byte[] recoveredAddr = recoverAddrBySign(sign, hash);
- if (ByteArray.matrixContains(seenAddrs, recoveredAddr)) {
- continue;
- }
- long weight = TransactionCapsule.getWeight(permission, recoveredAddr);
- if (weight == 0) {
- return Pair.of(true, DATA_FALSE);
- }
- totalWeight += weight;
- seenAddrs.add(recoveredAddr);
- }
-
- for (int i = 0; i < schemes.length; i++) {
- PQScheme scheme = PQScheme.forNumber(schemes[i]);
- if (scheme == null || scheme == PQScheme.UNKNOWN_PQ_SCHEME
- || !PQSchemeRegistry.contains(scheme)) {
- return Pair.of(true, DATA_FALSE);
- }
- // Per-entry runtime gate: the scheme's proposal must be active even
- // though 0x1a was registered under (allowFnDsa512 || allowMlDsa44).
- if (scheme == PQScheme.FN_DSA_512 && !VMConfig.allowFnDsa512()) {
- return Pair.of(true, DATA_FALSE);
- }
- if (scheme == PQScheme.ML_DSA_44 && !VMConfig.allowMlDsa44()) {
- return Pair.of(true, DATA_FALSE);
- }
- byte[] sig = pqSigs[i];
- byte[] pk = pqPks[i];
- int expectedPkLen = PQSchemeRegistry.getPublicKeyLength(scheme);
- int expectedSigSlot = scheme == PQScheme.FN_DSA_512
- ? FNDSA512.SIGNATURE_MAX_LENGTH - FNDSA512.SIGNATURE_HEADER_LENGTH
- : PQSchemeRegistry.getSignatureLength(scheme);
- if (pk == null || pk.length != expectedPkLen
- || sig == null || sig.length != expectedSigSlot) {
- // Slot lengths are exact here (Falcon = 666, Dilithium = 2420) —
- // a Falcon sig mislabelled as Dilithium fails this check.
- return Pair.of(true, DATA_FALSE);
- }
- if (scheme == PQScheme.FN_DSA_512) {
- // The Falcon slot is the EIP-8052 headerless body; rebuild the
- // BC-headered sig (re-inserts 0x39) before verification.
- sig = falconSlotToHeaderedSig(sig, 0, sig.length);
- if (sig == null) {
- return Pair.of(true, DATA_FALSE);
- }
- }
- byte[] derivedAddr;
- try {
- derivedAddr = PQSchemeRegistry.computeAddress(scheme, pk);
- } catch (Throwable t) {
- return Pair.of(true, DATA_FALSE);
- }
- // Both Falcon and Dilithium signing are randomized → the same key
- // can produce many valid sigs for one message, so dedup keys on the
- // derived address only (the sig blob is not a stable identity).
- if (ByteArray.matrixContains(seenAddrs, derivedAddr)) {
- continue;
- }
- long weight = TransactionCapsule.getWeight(permission, derivedAddr);
- if (weight == 0) {
- return Pair.of(true, DATA_FALSE);
- }
- if (!PQSchemeRegistry.verify(scheme, pk, hash, sig)) {
- return Pair.of(true, DATA_FALSE);
- }
- totalWeight += weight;
- seenAddrs.add(derivedAddr);
- }
-
- if (totalWeight >= permission.getThreshold()) {
- return Pair.of(true, dataOne());
- }
- } catch (Throwable t) {
- if (t instanceof OutOfTimeException) {
- throw t;
- }
- }
- return Pair.of(true, DATA_FALSE);
- }
- }
-
- /**
- * 0x19 BatchValidateMlDsa44 — independent per-element ML-DSA-44 verify.
- * Returns a 256-bit bitmap where bit {@code i} is set iff
- * {@code derive(pk_i) == expectedAddr_i} AND {@code MLDSA44.verify(pk_i, hash, sig_i)}.
- * Same ABI shape as 0x17, with sigs 2420 B and pks 1312 B.
- * {@code MAX_SIZE = 16}; energy is {@code cnt × 470}.
- */
- public static class BatchValidateMlDsa44 extends PrecompiledContract {
-
- private static final int ENERGY_PER_SIGN = 470;
- private static final int MAX_SIZE = 16;
- private static final int PK_LEN = MLDSA44.PUBLIC_KEY_LENGTH;
- private static final int SIG_LEN = MLDSA44.SIGNATURE_LENGTH;
- // hash, sigArrayOffset, pkArrayOffset, addrArrayOffset.
- private static final int ABI_HEAD_WORDS = 4;
-
- @Override
- public long getEnergyForData(byte[] data) {
- try {
- DataWord[] words = DataWord.parseArray(data);
- int cnt = words[words[1].intValueSafe() / WORD_SIZE].intValueSafe();
- return (long) cnt * ENERGY_PER_SIGN;
- } catch (Throwable t) {
- return (long) MAX_SIZE * ENERGY_PER_SIGN;
- }
- }
-
- @Override
- public Pair execute(byte[] data) {
- try {
- return doExecute(data);
- } catch (Throwable t) {
- if (t instanceof OutOfTimeException) {
- throw (OutOfTimeException) t;
- }
- if (t instanceof InterruptedException) {
- Thread.currentThread().interrupt();
- }
- return Pair.of(true, new byte[WORD_SIZE]);
- }
- }
-
- private Pair doExecute(byte[] data)
- throws InterruptedException, ExecutionException {
- if (!isValidAbiHead(data, ABI_HEAD_WORDS)) {
- return Pair.of(false, EMPTY_BYTE_ARRAY);
- }
- DataWord[] words = DataWord.parseArray(data);
- if (!isValidArrayOffset(words, 1, ABI_HEAD_WORDS)
- || !isValidArrayOffset(words, 2, ABI_HEAD_WORDS)
- || !isValidArrayOffset(words, 3, ABI_HEAD_WORDS)) {
- return Pair.of(false, EMPTY_BYTE_ARRAY);
- }
- byte[] hash = words[0].getData();
-
- int sigArrayWord = words[1].intValueSafe() / WORD_SIZE;
- int pkArrayWord = words[2].intValueSafe() / WORD_SIZE;
- int addrArrayWord = words[3].intValueSafe() / WORD_SIZE;
-
- int sigArraySize = words[sigArrayWord].intValueSafe();
- int pkArraySize = words[pkArrayWord].intValueSafe();
- int addrArraySize = words[addrArrayWord].intValueSafe();
-
- if (sigArraySize > MAX_SIZE || pkArraySize > MAX_SIZE
- || addrArraySize > MAX_SIZE
- || sigArraySize != pkArraySize || sigArraySize != addrArraySize) {
- return Pair.of(true, DATA_FALSE);
- }
-
- byte[][] signatures = extractBytesArrayChecked(words, sigArrayWord, data);
- byte[][] publicKeys = extractBytesArrayChecked(words, pkArrayWord, data);
- byte[][] addresses = extractBytes32Array(words, addrArrayWord);
-
- int cnt = signatures.length;
- if (cnt == 0 || publicKeys.length != cnt || addresses.length != cnt) {
- return Pair.of(true, DATA_FALSE);
- }
-
- byte[] res = new byte[WORD_SIZE];
- if (isConstantCall()) {
- for (int i = 0; i < cnt; i++) {
- if (verifyOne(signatures[i], publicKeys[i], hash, addresses[i])) {
- res[i] = 1;
- }
- }
- } else {
- CountDownLatch countDownLatch = new CountDownLatch(cnt);
- List> futures = new ArrayList<>(cnt);
-
- for (int i = 0; i < cnt; i++) {
- Future future = BatchValidateSign.workers.submit(
- new PqVerifyTask(countDownLatch, hash, signatures[i],
- publicKeys[i], addresses[i], i));
- futures.add(future);
- }
-
- boolean withNoTimeout = countDownLatch
- .await(getCPUTimeLeftInNanoSecond(), TimeUnit.NANOSECONDS);
-
- if (!withNoTimeout) {
- cancelAll(futures);
- logger.info("BatchValidateMlDsa44 timeout");
- throw Program.Exception.notEnoughTime("call BatchValidateMlDsa44 precompile method");
- }
-
- for (Future future : futures) {
- PqVerifyResult r = future.get();
- if (r.success) {
- res[r.nonce] = 1;
- }
- }
- }
- return Pair.of(true, res);
- }
-
- private static boolean verifyOne(byte[] sig, byte[] pk, byte[] hash, byte[] expectedAddr) {
- if (pk == null || pk.length != PK_LEN || sig == null || sig.length != SIG_LEN) {
- return false;
- }
- try {
- byte[] derived = PQSchemeRegistry.computeAddress(PQScheme.ML_DSA_44, pk);
- if (!DataWord.equalAddressByteArray(derived, expectedAddr)) {
- return false;
- }
- return MLDSA44.verify(pk, hash, sig);
- } catch (Throwable t) {
- return false;
- }
- }
-
- @AllArgsConstructor
- private static class PqVerifyTask implements Callable {
-
- private CountDownLatch countDownLatch;
- private byte[] hash;
- private byte[] signature;
- private byte[] publicKey;
- private byte[] expectedAddr;
- private int nonce;
-
- @Override
- public PqVerifyResult call() {
- try {
- return new PqVerifyResult(verifyOne(signature, publicKey, hash, expectedAddr), nonce);
- } finally {
- countDownLatch.countDown();
- }
- }
- }
-
- @AllArgsConstructor
- private static class PqVerifyResult {
-
- private boolean success;
- private int nonce;
- }
- }
-
}
diff --git a/framework/src/test/java/org/tron/common/runtime/vm/BatchValidateFnDsa512Test.java b/framework/src/test/java/org/tron/common/runtime/vm/BatchValidateFnDsa512Test.java
index b8d4cc60d3..17bde42933 100644
--- a/framework/src/test/java/org/tron/common/runtime/vm/BatchValidateFnDsa512Test.java
+++ b/framework/src/test/java/org/tron/common/runtime/vm/BatchValidateFnDsa512Test.java
@@ -14,13 +14,13 @@
import org.tron.common.crypto.pqc.PQSchemeRegistry;
import org.tron.common.utils.client.utils.AbiUtil;
import org.tron.core.vm.PrecompiledContracts;
-import org.tron.core.vm.PrecompiledContracts.BatchValidateFnDsa512;
+import org.tron.core.vm.PQPrecompiledContracts.BatchValidateFnDsa512;
import org.tron.core.vm.PrecompiledContracts.PrecompiledContract;
import org.tron.core.vm.config.VMConfig;
import org.tron.protos.Protocol.PQScheme;
/**
- * Unit tests for the 0x17 batch independent Falcon-512 verify precompile.
+ * Unit tests for the 0x02000017 batch independent Falcon-512 verify precompile.
* Returns a 256-bit bitmap where bit i is set iff
* {@code derive(pk_i) == expectedAddr_i && FNDSA512.verify(pk_i, hash, sig_i)}.
* Stateless — no chain DB.
@@ -28,8 +28,8 @@
@Slf4j
public class BatchValidateFnDsa512Test {
- private static final DataWord ADDR_0X17 = new DataWord(
- "0000000000000000000000000000000000000000000000000000000000000017");
+ private static final DataWord ADDR_0X02000017 = new DataWord(
+ "0000000000000000000000000000000000000000000000000000000002000017");
private static final String METHOD_SIGN =
"batchvalidatefndsa512(bytes32,bytes[],bytes[],bytes32[])";
@@ -58,12 +58,12 @@ public void disableProposal() {
@Test
public void switchOff_returnsNull() {
VMConfig.initAllowFnDsa512(0L);
- Assert.assertNull(PrecompiledContracts.getContractForAddress(ADDR_0X17));
+ Assert.assertNull(PrecompiledContracts.getContractForAddress(ADDR_0X02000017));
}
@Test
public void switchOn_returnsContract() {
- PrecompiledContract pc = PrecompiledContracts.getContractForAddress(ADDR_0X17);
+ PrecompiledContract pc = PrecompiledContracts.getContractForAddress(ADDR_0X02000017);
Assert.assertNotNull(pc);
Assert.assertTrue(pc instanceof BatchValidateFnDsa512);
}
@@ -430,7 +430,8 @@ public void pointerWordsExceedInput_returnsDataFalse() {
/**
* Pin a Falcon-512 signature into the precompile's fixed 666-byte slot using the
- * EIP-8052 headerless convention enforced by 0x16 / 0x17 / 0x1a: strip BC's leading
+ * EIP-8052 headerless convention enforced by 0x02000016 / 0x02000017 / 0x0200001a:
+ * strip BC's leading
* 0x39 header so the slot holds {@code salt ‖ s2}; the tail is zero-padded.
*/
private static byte[] padSlot(byte[] sig) {
@@ -452,7 +453,7 @@ private Pair run(byte[] hash, List sigs,
contract.setVmShouldEndInUs(System.nanoTime() / 1000 + 5_000_000L);
}
Pair ret = contract.execute(input);
- logger.info("0x17 bitmap: {}", Hex.toHexString(ret.getRight()));
+ logger.info("0x02000017 bitmap: {}", Hex.toHexString(ret.getRight()));
return ret;
}
diff --git a/framework/src/test/java/org/tron/common/runtime/vm/BatchValidateMlDsa44Test.java b/framework/src/test/java/org/tron/common/runtime/vm/BatchValidateMlDsa44Test.java
index 9c2a7d2f27..1984e886cd 100644
--- a/framework/src/test/java/org/tron/common/runtime/vm/BatchValidateMlDsa44Test.java
+++ b/framework/src/test/java/org/tron/common/runtime/vm/BatchValidateMlDsa44Test.java
@@ -14,13 +14,13 @@
import org.tron.common.crypto.pqc.PQSchemeRegistry;
import org.tron.common.utils.client.utils.AbiUtil;
import org.tron.core.vm.PrecompiledContracts;
-import org.tron.core.vm.PrecompiledContracts.BatchValidateMlDsa44;
+import org.tron.core.vm.PQPrecompiledContracts.BatchValidateMlDsa44;
import org.tron.core.vm.PrecompiledContracts.PrecompiledContract;
import org.tron.core.vm.config.VMConfig;
import org.tron.protos.Protocol.PQScheme;
/**
- * Unit tests for the 0x19 batch independent ML-DSA-44 verify precompile.
+ * Unit tests for the 0x02000019 batch independent ML-DSA-44 verify precompile.
* Returns a 256-bit bitmap where bit i is set iff
* {@code derive(pk_i) == expectedAddr_i && MLDSA44.verify(pk_i, hash, sig_i)}.
* Stateless — no chain DB.
@@ -28,8 +28,8 @@
@Slf4j
public class BatchValidateMlDsa44Test {
- private static final DataWord ADDR_0X19 = new DataWord(
- "0000000000000000000000000000000000000000000000000000000000000019");
+ private static final DataWord ADDR_0X02000019 = new DataWord(
+ "0000000000000000000000000000000000000000000000000000000002000019");
private static final String METHOD_SIGN =
"batchvalidatemldsa44(bytes32,bytes[],bytes[],bytes32[])";
@@ -58,12 +58,12 @@ public void disableProposal() {
@Test
public void switchOff_returnsNull() {
VMConfig.initAllowMlDsa44(0L);
- Assert.assertNull(PrecompiledContracts.getContractForAddress(ADDR_0X19));
+ Assert.assertNull(PrecompiledContracts.getContractForAddress(ADDR_0X02000019));
}
@Test
public void switchOn_returnsContract() {
- PrecompiledContract pc = PrecompiledContracts.getContractForAddress(ADDR_0X19);
+ PrecompiledContract pc = PrecompiledContracts.getContractForAddress(ADDR_0X02000019);
Assert.assertNotNull(pc);
Assert.assertTrue(pc instanceof BatchValidateMlDsa44);
}
@@ -311,7 +311,7 @@ private Pair run(byte[] hash, List sigs,
contract.setVmShouldEndInUs(System.nanoTime() / 1000 + 10_000_000L);
}
Pair ret = contract.execute(input);
- logger.info("0x19 bitmap: {}", Hex.toHexString(ret.getRight()));
+ logger.info("0x02000019 bitmap: {}", Hex.toHexString(ret.getRight()));
return ret;
}
diff --git a/framework/src/test/java/org/tron/common/runtime/vm/FnDsaPrecompileTest.java b/framework/src/test/java/org/tron/common/runtime/vm/FnDsaPrecompileTest.java
index f02eb3cce8..d3fd31ccc5 100644
--- a/framework/src/test/java/org/tron/common/runtime/vm/FnDsaPrecompileTest.java
+++ b/framework/src/test/java/org/tron/common/runtime/vm/FnDsaPrecompileTest.java
@@ -11,7 +11,7 @@
import org.tron.core.vm.config.VMConfig;
/**
- * Unit tests for the FN-DSA / Falcon-512 (0x16) verify precompile (EIP-8052 / TRON extension).
+ * Unit tests for the FN-DSA / Falcon-512 (0x02000016) verify precompile (EIP-8052 / TRON ext).
* Input layout (fixed-length): [msg 32B | sig 666B (zero-padded) | pk 896B] = 1594B total.
* The 666-byte sig slot holds the EIP-8052 headerless body (salt ‖ s2): BC's
* leading 0x39 header is stripped on the way in and re-inserted by the precompile.
@@ -20,7 +20,7 @@
public class FnDsaPrecompileTest {
private static final DataWord FNDSA_ADDR = new DataWord(
- "0000000000000000000000000000000000000000000000000000000000000016");
+ "0000000000000000000000000000000000000000000000000000000002000016");
private static final int INPUT_LEN =
32 + FNDSA512.SIGNATURE_MAX_LENGTH - 1 + FNDSA512.PUBLIC_KEY_LENGTH;
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 c7ad239cd7..ac8f86ac3c 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
@@ -12,12 +12,12 @@
/**
* Unit tests for the ML-DSA-44 verify precompile (FIPS 204 / Dilithium-2).
- * Address 0x18: standard 1312-byte FIPS public key layout.
+ * Address 0x02000018: standard 1312-byte FIPS public key layout.
*/
public class MlDsa44PrecompileTest {
private static final DataWord MLDSA_DRAFT_ADDR = new DataWord(
- "0000000000000000000000000000000000000000000000000000000000000018");
+ "0000000000000000000000000000000000000000000000000000000002000018");
private static final byte[] MESSAGE_HASH = new byte[32];
diff --git a/framework/src/test/java/org/tron/common/runtime/vm/ValidateMultiPQSigTest.java b/framework/src/test/java/org/tron/common/runtime/vm/ValidateMultiPQSigTest.java
index 6a295c30e6..898b669446 100644
--- a/framework/src/test/java/org/tron/common/runtime/vm/ValidateMultiPQSigTest.java
+++ b/framework/src/test/java/org/tron/common/runtime/vm/ValidateMultiPQSigTest.java
@@ -29,7 +29,7 @@
import org.tron.core.store.StoreFactory;
import org.tron.core.vm.PrecompiledContracts;
import org.tron.core.vm.PrecompiledContracts.PrecompiledContract;
-import org.tron.core.vm.PrecompiledContracts.ValidateMultiPQSig;
+import org.tron.core.vm.PQPrecompiledContracts.ValidateMultiPQSig;
import org.tron.core.vm.config.VMConfig;
import org.tron.core.vm.repository.Repository;
import org.tron.core.vm.repository.RepositoryImpl;
@@ -37,7 +37,7 @@
import org.tron.protos.Protocol.PQScheme;
/**
- * Unit tests for the unified 0x1a algorithm-agnostic Permission multi-sign
+ * Unit tests for the unified 0x0200001a algorithm-agnostic Permission multi-sign
* precompile. Replaces the per-scheme {@code ValidateMultiFnDsa512Test} and
* {@code ValidateMultiMlDsa44Test}: a single call may now mix ECDSA, FN-DSA-512
* and ML-DSA-44 entries against the same {@code Permission.keys[]}, dispatched
@@ -46,8 +46,8 @@
@Slf4j
public class ValidateMultiPQSigTest extends BaseTest {
- private static final DataWord ADDR_0X1A = new DataWord(
- "000000000000000000000000000000000000000000000000000000000000001a");
+ private static final DataWord ADDR_0X0200001A = new DataWord(
+ "000000000000000000000000000000000000000000000000000000000200001a");
private static final String METHOD_SIGN =
"validatemultipqsign(address,uint256,bytes32,bytes[],uint8[],bytes[],bytes[])";
@@ -85,13 +85,13 @@ public void after() {
public void bothSwitchesOff_returnsNull() {
VMConfig.initAllowFnDsa512(0L);
VMConfig.initAllowMlDsa44(0L);
- Assert.assertNull(PrecompiledContracts.getContractForAddress(ADDR_0X1A));
+ Assert.assertNull(PrecompiledContracts.getContractForAddress(ADDR_0X0200001A));
}
@Test
public void onlyFalconSwitchOn_returnsContract() {
VMConfig.initAllowMlDsa44(0L);
- PrecompiledContract pc = PrecompiledContracts.getContractForAddress(ADDR_0X1A);
+ PrecompiledContract pc = PrecompiledContracts.getContractForAddress(ADDR_0X0200001A);
Assert.assertNotNull(pc);
Assert.assertTrue(pc instanceof ValidateMultiPQSig);
}
@@ -99,14 +99,14 @@ public void onlyFalconSwitchOn_returnsContract() {
@Test
public void onlyDilithiumSwitchOn_returnsContract() {
VMConfig.initAllowFnDsa512(0L);
- PrecompiledContract pc = PrecompiledContracts.getContractForAddress(ADDR_0X1A);
+ PrecompiledContract pc = PrecompiledContracts.getContractForAddress(ADDR_0X0200001A);
Assert.assertNotNull(pc);
Assert.assertTrue(pc instanceof ValidateMultiPQSig);
}
@Test
public void bothSwitchesOn_returnsContract() {
- PrecompiledContract pc = PrecompiledContracts.getContractForAddress(ADDR_0X1A);
+ PrecompiledContract pc = PrecompiledContracts.getContractForAddress(ADDR_0X0200001A);
Assert.assertNotNull(pc);
Assert.assertTrue(pc instanceof ValidateMultiPQSig);
}
@@ -367,7 +367,7 @@ public void mismatchedSchemeAndPqSigArrayLengths_returnsZero() {
@Test
public void falconEntryWhileFalconDisabled_returnsZero() {
- // 0x1a stays registered because ML-DSA is still active, but a Falcon entry
+ // 0x0200001a stays registered because ML-DSA is still active, but a Falcon entry
// must be rejected per-entry when its proposal isn't passed.
VMConfig.initAllowFnDsa512(0L);
FNDSA512 falcon = new FNDSA512();
@@ -809,7 +809,7 @@ private Pair runContract(byte[] ownerAddr, int permissionId, by
Repository deposit = RepositoryImpl.createRoot(StoreFactory.getInstance());
contract.setRepository(deposit);
Pair ret = contract.execute(input);
- logger.info("0x1a result: {}", Hex.toHexString(ret.getRight()));
+ logger.info("0x0200001a result: {}", Hex.toHexString(ret.getRight()));
return ret;
}
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 5983026bda..5105942ed6 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
@@ -816,9 +816,8 @@ public void validateAllowFnDsa512() {
long maintenanceTimeInterval = forkUtils.getManager().getDynamicPropertiesStore()
.getMaintenanceTimeInterval();
long hardForkTime =
- ((ForkBlockVersionEnum.VERSION_4_8_2_PQ1.getHardForkTime() - 1) / maintenanceTimeInterval +
- 1)
- * maintenanceTimeInterval;
+ ((ForkBlockVersionEnum.VERSION_4_8_2_PQ1.getHardForkTime() - 1) / maintenanceTimeInterval
+ + 1) * maintenanceTimeInterval;
forkUtils.getManager().getDynamicPropertiesStore()
.saveLatestBlockHeaderTimestamp(hardForkTime - 1);
@@ -879,9 +878,8 @@ public void validateAllowMlDsa44() {
long maintenanceTimeInterval = forkUtils.getManager().getDynamicPropertiesStore()
.getMaintenanceTimeInterval();
long hardForkTime =
- ((ForkBlockVersionEnum.VERSION_4_8_2_PQ1.getHardForkTime() - 1) / maintenanceTimeInterval +
- 1)
- * maintenanceTimeInterval;
+ ((ForkBlockVersionEnum.VERSION_4_8_2_PQ1.getHardForkTime() - 1) / maintenanceTimeInterval
+ + 1) * maintenanceTimeInterval;
forkUtils.getManager().getDynamicPropertiesStore()
.saveLatestBlockHeaderTimestamp(hardForkTime - 1);