From b5e0056bf53d000f6645a8e510506e5839179290 Mon Sep 17 00:00:00 2001 From: federico Date: Tue, 23 Jun 2026 17:05:06 +0800 Subject: [PATCH 1/3] fix(vm): relocate pqc precompiles to avoid address conflicts --- .../tron/core/vm/PrecompiledContracts.java | 52 +++++++++---------- .../runtime/vm/BatchValidateFnDsa512Test.java | 14 ++--- .../runtime/vm/BatchValidateMlDsa44Test.java | 12 ++--- .../runtime/vm/FnDsaPrecompileTest.java | 4 +- .../runtime/vm/MlDsa44PrecompileTest.java | 4 +- .../runtime/vm/ValidateMultiPQSigTest.java | 18 +++---- 6 files changed, 52 insertions(+), 52 deletions(-) diff --git a/actuator/src/main/java/org/tron/core/vm/PrecompiledContracts.java b/actuator/src/main/java/org/tron/core/vm/PrecompiledContracts.java index 6730d32163..17ce650466 100644 --- a/actuator/src/main/java/org/tron/core/vm/PrecompiledContracts.java +++ b/actuator/src/main/java/org/tron/core/vm/PrecompiledContracts.java @@ -235,34 +235,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 +350,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. @@ -597,7 +595,7 @@ private static void cancelAll(List> futures) { * 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 + * padding. Returns 0 if the slot is all zero. Shared by 0x02000016, 0x02000018, and 0x0200001a * because every precompile slot for Falcon sigs is the same 666-byte slot. */ static int recoverFalconSigLen(byte[] data, int from, int to) { @@ -617,7 +615,7 @@ static int recoverFalconSigLen(byte[] data, int from, int to) { * 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. + * Shared by 0x02000016, 0x02000018, and 0x0200001a. */ static byte[] falconSlotToHeaderedSig(byte[] data, int from, int to) { int bodyLen = recoverFalconSigLen(data, from, to); @@ -2687,7 +2685,7 @@ public Pair execute(byte[] data) { /** - * 0x17 BatchValidateFnDsa512 — independent per-element Falcon-512 verify. + * 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)}. @@ -2704,7 +2702,7 @@ public Pair execute(byte[] data) { * ) returns (bytes32) * * - *

Falcon sigs are pinned to the 666-byte slot from {@code VerifyFnDsa512} (0x16) + *

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. * @@ -2882,7 +2880,7 @@ private static class PqVerifyResult { * (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. + * variant is implemented separately at 0x12 — 0x02000019 stays as-is. */ public static class VerifyMlDsa44 extends PrecompiledContract { @@ -2915,7 +2913,7 @@ public Pair execute(byte[] data) { } /** - * 0x1a ValidateMultiPQSig — algorithm-agnostic Permission multi-sign. Accepts + * 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). @@ -2934,7 +2932,7 @@ public Pair execute(byte[] data) { * * *

Falcon sigs follow the EIP-8052 666-byte headerless slot convention - * (matches 0x16/0x18): the slot holds {@code salt ‖ s2_compressed} with no + * (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. @@ -2946,7 +2944,7 @@ public Pair execute(byte[] data) { * 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 + * {@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 PrecompiledContract { @@ -2955,6 +2953,8 @@ public static class ValidateMultiPQSig extends PrecompiledContract { 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; @@ -2986,7 +2986,7 @@ public long getEnergyForData(byte[] data) { } return energy; } catch (Throwable t) { - return (long) MAX_SIZE * WORST_PQ_ENERGY; + return (long) MAX_SIZE * WORST_ENERGY_PER_SIGN; } } @@ -3075,7 +3075,7 @@ public Pair execute(byte[] rawData) { 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). + // though 0x0200001a was registered under (allowFnDsa512 || allowMlDsa44). if (scheme == PQScheme.FN_DSA_512 && !VMConfig.allowFnDsa512()) { return Pair.of(true, DATA_FALSE); } @@ -3138,10 +3138,10 @@ public Pair execute(byte[] rawData) { } /** - * 0x19 BatchValidateMlDsa44 — independent per-element ML-DSA-44 verify. + * 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 0x17, with sigs 2420 B and pks 1312 B. + * 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 PrecompiledContract { 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..27e50e97e8 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 @@ -20,7 +20,7 @@ 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,7 @@ 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 +452,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..f7763c4418 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 @@ -20,7 +20,7 @@ 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..ea8acce7b2 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 extension). * 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..13b9f73d02 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 @@ -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; } From a5fda58d3aa4f4363060236137258aa8b31324c5 Mon Sep 17 00:00:00 2001 From: federico Date: Tue, 23 Jun 2026 17:52:25 +0800 Subject: [PATCH 2/3] style(vm): fix checkstyle line-length and operator-wrap violations in tests --- .../java/org/tron/core/vm/PrecompiledContracts.java | 4 ++-- .../common/runtime/vm/BatchValidateFnDsa512Test.java | 3 ++- .../tron/common/runtime/vm/FnDsaPrecompileTest.java | 2 +- .../org/tron/core/actuator/utils/ProposalUtilTest.java | 10 ++++------ 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/actuator/src/main/java/org/tron/core/vm/PrecompiledContracts.java b/actuator/src/main/java/org/tron/core/vm/PrecompiledContracts.java index 17ce650466..e3663453a9 100644 --- a/actuator/src/main/java/org/tron/core/vm/PrecompiledContracts.java +++ b/actuator/src/main/java/org/tron/core/vm/PrecompiledContracts.java @@ -595,7 +595,7 @@ private static void cancelAll(List> futures) { * 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, 0x02000018, and 0x0200001a + * 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) { @@ -615,7 +615,7 @@ static int recoverFalconSigLen(byte[] data, int from, int to) { * 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, 0x02000018, and 0x0200001a. + * Shared by 0x02000016, 0x02000017, and 0x0200001a. */ static byte[] falconSlotToHeaderedSig(byte[] data, int from, int to) { int bodyLen = recoverFalconSigLen(data, from, to); 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 27e50e97e8..70677e29af 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 @@ -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 0x02000016 / 0x02000017 / 0x0200001a: 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) { 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 ea8acce7b2..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 (0x02000016) 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. 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); From e77911fd2bddec3446450eca8378ef6c4327a0f2 Mon Sep 17 00:00:00 2001 From: federico Date: Wed, 24 Jun 2026 14:16:49 +0800 Subject: [PATCH 3/3] refactor(vm): extract PQC precompile contracts --- .../tron/core/vm/PQPrecompiledContracts.java | 844 ++++++++++++++++++ .../tron/core/vm/PrecompiledContracts.java | 812 +---------------- .../runtime/vm/BatchValidateFnDsa512Test.java | 2 +- .../runtime/vm/BatchValidateMlDsa44Test.java | 2 +- .../runtime/vm/ValidateMultiPQSigTest.java | 2 +- 5 files changed, 861 insertions(+), 801 deletions(-) create mode 100644 actuator/src/main/java/org/tron/core/vm/PQPrecompiledContracts.java 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> 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 e3663453a9..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(); @@ -453,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]; @@ -472,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++) { @@ -496,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][]; } @@ -539,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); } @@ -551,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> 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; - } - public abstract static class PrecompiledContract { protected static final byte[] DATA_FALSE = new byte[WORD_SIZE]; @@ -2565,7 +2457,6 @@ public Pair execute(byte[] data) { } } - public static class P256Verify extends PrecompiledContract { private static final X9ECParameters CURVE = SECNamedCurves.getByName("secp256r1"); @@ -2620,679 +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()); - } - } - - } - - - /** - * 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 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 — 0x02000019 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()); - } - } - } - - /** - * 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 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 = 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 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); - } - } - - /** - * 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 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 70677e29af..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,7 +14,7 @@ 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; 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 f7763c4418..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,7 +14,7 @@ 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; 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 13b9f73d02..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;