diff --git a/common/src/main/java/org/tron/common/parameter/CommonParameter.java b/common/src/main/java/org/tron/common/parameter/CommonParameter.java index eeb92fdbd60..49c865958e2 100644 --- a/common/src/main/java/org/tron/common/parameter/CommonParameter.java +++ b/common/src/main/java/org/tron/common/parameter/CommonParameter.java @@ -439,6 +439,9 @@ public class CommonParameter { @Getter @Setter public String cryptoEngine = Constant.ECKey_ENGINE; + @Getter + @Setter + public boolean useNativeSecp256k1 = false; @Getter @Setter diff --git a/common/src/main/java/org/tron/core/config/args/MiscConfig.java b/common/src/main/java/org/tron/core/config/args/MiscConfig.java index 0c6d3631ba8..6991743525d 100644 --- a/common/src/main/java/org/tron/core/config/args/MiscConfig.java +++ b/common/src/main/java/org/tron/core/config/args/MiscConfig.java @@ -24,6 +24,7 @@ public class MiscConfig { private long trxExpirationTimeInMilliseconds = Constant.TRANSACTION_DEFAULT_EXPIRATION_TIME; private long blockNumForEnergyLimit = 4727890L; private String cryptoEngine = Constant.ECKey_ENGINE; + private boolean useNativeSecp256k1 = false; private List seedNodeIpList = new ArrayList<>(); public static MiscConfig fromConfig(Config config) { @@ -51,6 +52,8 @@ public static MiscConfig fromConfig(Config config) { // crypto mc.cryptoEngine = config.hasPath("crypto.engine") ? config.getString("crypto.engine") : Constant.ECKey_ENGINE; + mc.useNativeSecp256k1 = config.hasPath("crypto.useNativeSecp256k1") + && config.getBoolean("crypto.useNativeSecp256k1"); // seed node mc.seedNodeIpList = config.hasPath("seed.node.ip.list") diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf index d8c483d932a..5ffb54a7d74 100644 --- a/common/src/main/resources/reference.conf +++ b/common/src/main/resources/reference.conf @@ -166,6 +166,8 @@ node.backup { # Algorithm for generating public key from private key. Do not modify to avoid forks. crypto { engine = "eckey" # Signature engine. + # Use JNA-backed libsecp256k1 for signature verification when engine = "eckey". + useNativeSecp256k1 = false } # Energy limit block number (config key has typo "enery" preserved for backward compatibility) diff --git a/common/src/test/java/org/tron/core/config/args/MiscConfigTest.java b/common/src/test/java/org/tron/core/config/args/MiscConfigTest.java index 89a2d6e7b3c..bdde5599ee0 100644 --- a/common/src/test/java/org/tron/core/config/args/MiscConfigTest.java +++ b/common/src/test/java/org/tron/core/config/args/MiscConfigTest.java @@ -30,6 +30,7 @@ public void testDefaults() { mc.getTrxExpirationTimeInMilliseconds()); // reference.conf has crypto.engine = "eckey" (lowercase) assertEquals("eckey", mc.getCryptoEngine()); + assertFalse(mc.isUseNativeSecp256k1()); // reference.conf has seed.node.ip.list with actual IPs assertFalse(mc.getSeedNodeIpList().isEmpty()); } @@ -40,13 +41,14 @@ public void testFromConfig() { "storage { needToUpdateAsset = false," + " balance { history { lookup = true } } }\n" + "trx { reference { block = head } }\n" - + "crypto { engine = sm2 }\n" + + "crypto { engine = sm2, useNativeSecp256k1 = true }\n" + "seed.node { ip.list = [\"1.2.3.4:18888\"] }"); MiscConfig mc = MiscConfig.fromConfig(config); assertFalse(mc.isNeedToUpdateAsset()); assertTrue(mc.isHistoryBalanceLookup()); assertEquals("head", mc.getTrxReferenceBlock()); assertEquals("sm2", mc.getCryptoEngine()); + assertTrue(mc.isUseNativeSecp256k1()); assertEquals(1, mc.getSeedNodeIpList().size()); } } diff --git a/consensus/src/main/java/org/tron/consensus/pbft/message/PbftBaseMessage.java b/consensus/src/main/java/org/tron/consensus/pbft/message/PbftBaseMessage.java index 4eb61f3e22e..22e4bc59504 100644 --- a/consensus/src/main/java/org/tron/consensus/pbft/message/PbftBaseMessage.java +++ b/consensus/src/main/java/org/tron/consensus/pbft/message/PbftBaseMessage.java @@ -5,7 +5,7 @@ import java.security.SignatureException; import java.util.stream.Collectors; import org.bouncycastle.util.encoders.Hex; -import org.tron.common.crypto.ECKey; +import org.tron.common.crypto.SignUtils; import org.tron.common.overlay.message.Message; import org.tron.common.utils.ByteUtil; import org.tron.common.utils.Sha256Hash; @@ -96,8 +96,8 @@ public DataType getDataType() { public void analyzeSignature() throws SignatureException { byte[] hash = Sha256Hash.hash(true, getPbftMessage().getRawData().toByteArray()); - publicKey = ECKey.signatureToAddress(hash, TransactionCapsule - .getBase64FromByteString(getPbftMessage().getSignature())); + publicKey = SignUtils.signatureToAddress(hash, TransactionCapsule + .getBase64FromByteString(getPbftMessage().getSignature()), true); } @Override diff --git a/crypto/build.gradle b/crypto/build.gradle index 82814af49e6..b7f98dd4f38 100644 --- a/crypto/build.gradle +++ b/crypto/build.gradle @@ -12,6 +12,8 @@ repositories { dependencies { api project(":common") + implementation 'net.java.dev.jna:jna:5.12.1' + implementation 'io.github.federico2014:secp256k1:1.3.12' } jacocoTestReport { diff --git a/crypto/src/main/java/org/tron/common/crypto/NativeSecp256k1.java b/crypto/src/main/java/org/tron/common/crypto/NativeSecp256k1.java new file mode 100644 index 00000000000..339e9bb1835 --- /dev/null +++ b/crypto/src/main/java/org/tron/common/crypto/NativeSecp256k1.java @@ -0,0 +1,365 @@ +package org.tron.common.crypto; + +import static org.hyperledger.besu.nativelib.secp256k1.LibSecp256k1.SECP256K1_EC_UNCOMPRESSED; + +import com.sun.jna.ptr.IntByReference; +import com.sun.jna.ptr.LongByReference; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.security.SecureRandom; +import java.security.SignatureException; +import java.util.Arrays; +import lombok.extern.slf4j.Slf4j; +import org.bouncycastle.util.encoders.Base64; +import org.hyperledger.besu.nativelib.secp256k1.LibSecp256k1; +import org.tron.common.utils.ByteArray; +import org.tron.common.utils.ByteUtil; + +/** + * JNA-backed secp256k1 key, signing and public-key recovery implementation. + * + *

The wire format, canonical S value and recovery-id handling intentionally match {@link + * ECKey}. Enabling native verification does not automatically switch key generation or signing; + * callers must invoke {@link #sign(byte[], byte[])} explicitly. + */ +@Slf4j(topic = "crypto") +public final class NativeSecp256k1 extends ECKey { + + private static final int HASH_LENGTH = 32; + private static final int MAX_PRIVATE_KEY_LENGTH = 32; + private static final int COMPACT_SIGNATURE_LENGTH = 64; + private static final int BASE64_SIGNATURE_LENGTH = 65; + private static final int UNCOMPRESSED_PUBLIC_KEY_LENGTH = 65; + private static final boolean AVAILABLE = loadNativeLibrary(); + private final LibSecp256k1.secp256k1_pubkey publicKey = new LibSecp256k1.secp256k1_pubkey(); + + /** + * Generates a new secp256k1 key pair using the default source of randomness. + */ + public NativeSecp256k1() throws SignatureException { + super(); + initializePublicKey(); + } + + /** + * Generates a new secp256k1 key pair using the supplied source of randomness. + * + * @param secureRandom source of randomness used to generate the private key + */ + public NativeSecp256k1(SecureRandom secureRandom) throws SignatureException { + super(requireSecureRandom(secureRandom)); + initializePublicKey(); + } + + /** + * Creates a secp256k1 key pair from private-key bytes. + * + * @param privateKey unsigned private-key bytes in the range {@code [1, n - 1]} + */ + public NativeSecp256k1(byte[] privateKey) throws SignatureException { + super(validatePrivateKey(privateKey), true); + initializePublicKey(); + } + + public static boolean isAvailable() { + return AVAILABLE; + } + + @Override + public byte[] getPubKey() { + ByteBuffer serialized = ByteBuffer.allocate(UNCOMPRESSED_PUBLIC_KEY_LENGTH); + LongByReference serializedLength = new LongByReference(UNCOMPRESSED_PUBLIC_KEY_LENGTH); + if (LibSecp256k1.secp256k1_ec_pubkey_serialize( + LibSecp256k1.CONTEXT, + serialized, + serializedLength, + publicKey, + SECP256K1_EC_UNCOMPRESSED) == 0 + || serializedLength.getValue() != UNCOMPRESSED_PUBLIC_KEY_LENGTH) { + throw new IllegalStateException("Could not serialize native secp256k1 public key"); + } + return serialized.array(); + } + + @Override + public ECDSASignature sign(byte[] messageHash) { + byte[] privateKey = getPrivateKey(); + try { + return sign(messageHash, privateKey); + } catch (SignatureException e) { + throw new IllegalStateException("Could not create native secp256k1 signature", e); + } finally { + if (privateKey != null) { + Arrays.fill(privateKey, (byte) 0); + } + } + } + + private static boolean loadNativeLibrary() { + try { + boolean available = LibSecp256k1.CONTEXT != null; + if (!available) { + logger.warn("Native secp256k1 context is unavailable"); + } + return available; + } catch (LinkageError | RuntimeException e) { + logger.warn("Unable to load native secp256k1 library", e); + return false; + } + } + + private void initializePublicKey() throws SignatureException { + ensureAvailable(); + byte[] privateKey = getPrivateKey(); + try { + if (privateKey == null || LibSecp256k1.secp256k1_ec_pubkey_create( + LibSecp256k1.CONTEXT, publicKey, privateKey) == 0) { + throw new SignatureException("Could not create native secp256k1 public key"); + } + } finally { + if (privateKey != null) { + Arrays.fill(privateKey, (byte) 0); + } + } + } + + /** + * Creates a recoverable ECDSA signature for a 32-byte message hash. + * + * @param messageHash 32-byte message hash + * @param privateKey unsigned secp256k1 private key using 1 to 32 bytes, or a 33-byte Java + * {@link BigInteger} encoding with a leading zero sign byte + * @return canonical recoverable signature compatible with {@link ECKey} + */ + public static ECDSASignature sign(byte[] messageHash, byte[] privateKey) + throws SignatureException { + ensureAvailable(); + validateMessageHash(messageHash); + byte[] nativePrivateKey = validateAndNormalizePrivateKey(privateKey); + try { + LibSecp256k1.secp256k1_ecdsa_recoverable_signature nativeSignature = + new LibSecp256k1.secp256k1_ecdsa_recoverable_signature(); + if (LibSecp256k1.secp256k1_ecdsa_sign_recoverable( + LibSecp256k1.CONTEXT, + nativeSignature, + messageHash, + nativePrivateKey, + null, + null) == 0) { + throw new SignatureException("Could not create native secp256k1 signature"); + } + + ByteBuffer compactSignature = ByteBuffer.allocate(COMPACT_SIGNATURE_LENGTH); + IntByReference recoveryIdReference = new IntByReference(); + LibSecp256k1.secp256k1_ecdsa_recoverable_signature_serialize_compact( + LibSecp256k1.CONTEXT, + compactSignature, + recoveryIdReference, + nativeSignature); + + int recoveryId = recoveryIdReference.getValue(); + if (recoveryId < 0 || recoveryId > 3) { + throw new SignatureException("Native signature recovery ID is out of range: " + + recoveryId); + } + byte[] signatureBytes = compactSignature.array(); + ECDSASignature signature = ECDSASignature.fromComponents( + Arrays.copyOfRange(signatureBytes, 0, HASH_LENGTH), + Arrays.copyOfRange(signatureBytes, HASH_LENGTH, COMPACT_SIGNATURE_LENGTH), + (byte) (recoveryId + 27)); + + if (signature.s.compareTo(ECKey.HALF_CURVE_ORDER) > 0) { + signature = ECDSASignature.fromComponents( + ByteUtil.bigIntegerToBytes(signature.r, HASH_LENGTH), + ByteUtil.bigIntegerToBytes(ECKey.CURVE.getN().subtract(signature.s), HASH_LENGTH), + (byte) ((recoveryId ^ 1) + 27)); + } + return signature; + } finally { + Arrays.fill(nativePrivateKey, (byte) 0); + } + } + + /** + * Recovers the TRON address from a base64 signature encoded as {@code v || r || s}. + */ + public static byte[] signatureToAddress(byte[] messageHash, String signatureBase64) + throws SignatureException { + return Hash.computeAddress(signatureToKeyBytes(messageHash, signatureBase64)); + } + + /** + * Recovers the TRON address from the supplied ECDSA signature components. + */ + public static byte[] signatureToAddress(byte[] messageHash, ECDSASignature signature) + throws SignatureException { + return Hash.computeAddress(signatureToKeyBytes(messageHash, signature)); + } + + public static byte[] signatureToKeyBytes(byte[] messageHash, String signatureBase64) + throws SignatureException { + byte[] encoded; + try { + encoded = Base64.decode(signatureBase64); + } catch (RuntimeException e) { + throw new SignatureException("Could not decode base64", e); + } + // ECKey historically accepts trailing bytes, so preserve that consensus behaviour. + if (encoded.length < BASE64_SIGNATURE_LENGTH) { + throw new SignatureException("Signature truncated, expected 65 bytes and got " + + encoded.length); + } + + ECDSASignature signature = ECDSASignature.fromComponents( + Arrays.copyOfRange(encoded, 1, 33), + Arrays.copyOfRange(encoded, 33, 65), + encoded[0]); + return signatureToKeyBytes(messageHash, signature); + } + + public static byte[] signatureToKeyBytes(byte[] messageHash, ECDSASignature signature) + throws SignatureException { + ensureAvailable(); + validateMessageHash(messageHash); + if (signature == null) { + throw new SignatureException("Signature must not be null"); + } + if (signature.r == null || signature.s == null + || signature.r.signum() < 0 || signature.s.signum() < 0 + || signature.r.bitLength() > HASH_LENGTH * Byte.SIZE + || signature.s.bitLength() > HASH_LENGTH * Byte.SIZE) { + throw new SignatureException("Signature components must be unsigned 32-byte integers"); + } + + int recoveryId = getRecoveryId(signature.v); + + if (!hasNativeCompatibleScalars(signature)) { + // ECKey historically accepts some non-canonical scalar combinations. Route those directly + // to the legacy implementation instead of paying for a native operation that cannot match. + return ECKey.signatureToKeyBytes(messageHash, signature); + } + + byte[] compactSignature = ByteUtil.merge( + ByteUtil.bigIntegerToBytes(signature.r, HASH_LENGTH), + ByteUtil.bigIntegerToBytes(signature.s, HASH_LENGTH)); + if (compactSignature.length != COMPACT_SIGNATURE_LENGTH) { + throw new SignatureException("Compact signature must be 64 bytes"); + } + + LibSecp256k1.secp256k1_ecdsa_recoverable_signature nativeSignature = + new LibSecp256k1.secp256k1_ecdsa_recoverable_signature(); + if (LibSecp256k1.secp256k1_ecdsa_recoverable_signature_parse_compact( + LibSecp256k1.CONTEXT, nativeSignature, compactSignature, (byte) recoveryId) == 0) { + // Native acceleration must preserve the legacy ECKey recovery result. + return ECKey.signatureToKeyBytes(messageHash, signature); + } + + LibSecp256k1.secp256k1_pubkey publicKey = new LibSecp256k1.secp256k1_pubkey(); + if (LibSecp256k1.secp256k1_ecdsa_recover( + LibSecp256k1.CONTEXT, publicKey, nativeSignature, messageHash) == 0) { + // ECKey may also encode the point at infinity for legacy inputs. Preserve that consensus + // behaviour only when native recovery cannot produce a public key. + return ECKey.signatureToKeyBytes(messageHash, signature); + } + + ByteBuffer serialized = ByteBuffer.allocate(UNCOMPRESSED_PUBLIC_KEY_LENGTH); + LongByReference serializedLength = + new LongByReference(UNCOMPRESSED_PUBLIC_KEY_LENGTH); + if (LibSecp256k1.secp256k1_ec_pubkey_serialize( + LibSecp256k1.CONTEXT, + serialized, + serializedLength, + publicKey, + SECP256K1_EC_UNCOMPRESSED) == 0 + || serializedLength.getValue() != UNCOMPRESSED_PUBLIC_KEY_LENGTH) { + throw new SignatureException("Could not serialize recovered public key"); + } + return serialized.array(); + } + + private static int getRecoveryId(byte value) throws SignatureException { + int header = value; + if (header < 27 || header > 34) { + throw new SignatureException("Header byte out of range: " + header); + } + if (header >= 31) { + header -= 4; + } + return header - 27; + } + + private static boolean hasNativeCompatibleScalars(ECDSASignature signature) { + BigInteger curveOrder = ECKey.CURVE.getN(); + return signature.r.signum() > 0 + && signature.s.signum() > 0 + && signature.r.compareTo(curveOrder) < 0 + && signature.s.compareTo(curveOrder) < 0; + } + + private static void ensureAvailable() throws SignatureException { + if (!AVAILABLE) { + throw new SignatureException("Native secp256k1 library is unavailable"); + } + } + + private static void validateMessageHash(byte[] messageHash) { + if (messageHash == null || messageHash.length != HASH_LENGTH) { + throw new IllegalArgumentException("messageHash argument must be 32 bytes"); + } + } + + private static SecureRandom requireSecureRandom(SecureRandom secureRandom) { + if (secureRandom == null) { + throw new IllegalArgumentException("secureRandom argument must not be null"); + } + return secureRandom; + } + + /** + * Validates a variable-length private-key encoding and left-pads it to 32 bytes. + * + *

The native libsecp256k1 signing API accepts a pointer to a 32-byte, big-endian secret key + * without a separate length argument. A shorter Java array must therefore never be passed to + * the native function directly. The returned temporary array is owned by the caller and must be + * cleared after use. + */ + private static byte[] validateAndNormalizePrivateKey(byte[] privateKey) { + byte[] validatedPrivateKey = validatePrivateKey(privateKey); + try { + byte[] normalizedPrivateKey = new byte[MAX_PRIVATE_KEY_LENGTH]; + System.arraycopy(validatedPrivateKey, 0, normalizedPrivateKey, + normalizedPrivateKey.length - validatedPrivateKey.length, + validatedPrivateKey.length); + return normalizedPrivateKey; + } finally { + if (validatedPrivateKey != privateKey) { + Arrays.fill(validatedPrivateKey, (byte) 0); + } + } + } + + /** + * Validates and normalises a private-key byte array for native secp256k1 operations. + * Accepts unsigned encodings up to 32 bytes and Java {@link BigInteger} encodings with + * one leading zero sign byte ({@link BigInteger#toByteArray()} prepends {@code 0x00} when + * the scalar's high bit is set). The leading byte is stripped before the range check. + */ + private static byte[] validatePrivateKey(byte[] privateKey) { + if (ByteArray.isEmpty(privateKey)) { + throw new IllegalArgumentException("privateKey argument must contain 1 to 32 bytes"); + } + // Strip BigInteger.toByteArray() sign-padding when present + if (privateKey.length == MAX_PRIVATE_KEY_LENGTH + 1 + && privateKey[0] == 0 + && (privateKey[1] & 0x80) != 0) { + privateKey = Arrays.copyOfRange(privateKey, 1, privateKey.length); + } else if (privateKey.length > MAX_PRIVATE_KEY_LENGTH) { + throw new IllegalArgumentException("privateKey argument must contain 1 to 32 bytes"); + } + BigInteger privateKeyValue = new BigInteger(1, privateKey); + if (privateKeyValue.signum() <= 0 || privateKeyValue.compareTo(ECKey.CURVE.getN()) >= 0) { + throw new IllegalArgumentException("privateKey argument is outside the secp256k1 range"); + } + return privateKey; + } +} diff --git a/crypto/src/main/java/org/tron/common/crypto/SignUtils.java b/crypto/src/main/java/org/tron/common/crypto/SignUtils.java index e0e20fb2677..a5117dd88fa 100644 --- a/crypto/src/main/java/org/tron/common/crypto/SignUtils.java +++ b/crypto/src/main/java/org/tron/common/crypto/SignUtils.java @@ -5,12 +5,36 @@ import java.security.SecureRandom; import java.security.SignatureException; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; import org.tron.common.crypto.ECKey.ECDSASignature; import org.tron.common.crypto.sm2.SM2; import org.tron.common.crypto.sm2.SM2.SM2Signature; +@Slf4j(topic = "crypto") public class SignUtils { + @Getter + private static volatile boolean useNativeSecp256k1; + + public static void setUseNativeSecp256k1(boolean enabled) { + if (!enabled) { + useNativeSecp256k1 = false; + return; + } + setUseNativeSecp256k1(true, NativeSecp256k1.isAvailable()); + } + + static void setUseNativeSecp256k1(boolean enabled, boolean nativeAvailable) { + if (enabled && !nativeAvailable) { + logger.warn("crypto.useNativeSecp256k1=true, but native secp256k1 is unavailable; " + + "falling back to ECKey"); + useNativeSecp256k1 = false; + return; + } + useNativeSecp256k1 = enabled; + } + /** * Strict signature-length check for admission entry-points (RPC broadcast, * P2P transaction ingress, peer hello handshake). Accepts only sizes in @@ -46,6 +70,9 @@ public static byte[] signatureToAddress( throws SignatureException { try { if (isECKeyCryptoEngine) { + if (useNativeSecp256k1) { + return NativeSecp256k1.signatureToAddress(messageHash, signatureBase64); + } return ECKey.signatureToAddress(messageHash, signatureBase64); } return SM2.signatureToAddress(messageHash, signatureBase64); @@ -66,6 +93,10 @@ public static byte[] signatureToAddress( byte[] messageHash, SignatureInterface signatureInterface, boolean isECKeyCryptoEngine) throws SignatureException { if (isECKeyCryptoEngine) { + if (useNativeSecp256k1) { + return NativeSecp256k1.signatureToAddress( + messageHash, (ECDSASignature) signatureInterface); + } return ECKey.signatureToAddress(messageHash, (ECDSASignature) signatureInterface); } return SM2.signatureToAddress(messageHash, (SM2Signature) signatureInterface); diff --git a/framework/src/main/java/org/tron/core/config/args/Args.java b/framework/src/main/java/org/tron/core/config/args/Args.java index 0bca242606e..d3566f3c8b7 100644 --- a/framework/src/main/java/org/tron/core/config/args/Args.java +++ b/framework/src/main/java/org/tron/core/config/args/Args.java @@ -43,6 +43,7 @@ import org.tron.common.args.GenesisBlock; import org.tron.common.args.Witness; import org.tron.common.cron.CronExpression; +import org.tron.common.crypto.SignUtils; import org.tron.common.logsfilter.EventPluginConfig; import org.tron.common.logsfilter.FilterQuery; import org.tron.common.logsfilter.TriggerConfig; @@ -295,10 +296,26 @@ private static void applyGenesisConfig(GenesisConfig gc, Config config) { } /** - * Bridge MiscConfig bean values to CommonParameter fields. + * Bridge crypto config values and initialize the runtime signature verification implementation. */ - private static void applyMiscConfig(MiscConfig mc) { + private static void applyCryptoConfig(MiscConfig mc) { PARAMETER.cryptoEngine = mc.getCryptoEngine(); + boolean nativeRequested = mc.isUseNativeSecp256k1(); + SignUtils.setUseNativeSecp256k1(PARAMETER.isECKeyCryptoEngine() && nativeRequested); + PARAMETER.useNativeSecp256k1 = SignUtils.isUseNativeSecp256k1(); + + String implementation = PARAMETER.isECKeyCryptoEngine() + ? (PARAMETER.useNativeSecp256k1 ? "NativeSecp256k1" : "ECKey") + : "SM2"; + logger.info("Crypto signature verification: engine={}, nativeRequested={}, " + + "nativeActive={}, implementation={}", + PARAMETER.cryptoEngine, nativeRequested, PARAMETER.useNativeSecp256k1, implementation); + } + + /** + * Bridge non-crypto MiscConfig bean values to CommonParameter fields. + */ + private static void applyMiscConfig(MiscConfig mc) { PARAMETER.needToUpdateAsset = mc.isNeedToUpdateAsset(); PARAMETER.historyBalanceLookup = mc.isHistoryBalanceLookup(); PARAMETER.trxReferenceBlock = mc.getTrxReferenceBlock(); @@ -730,6 +747,7 @@ public static void applyConfigParams( // Misc config: storage, trx, energy — small domains, read via beans miscConfig = MiscConfig.fromConfig(config); + applyCryptoConfig(miscConfig); applyMiscConfig(miscConfig); // vm, committee already handled above @@ -934,6 +952,7 @@ private static void initLocalWitnesses(Config config, CLIParameter cmd) { @VisibleForTesting public static void clearParam() { CommonParameter.reset(); + SignUtils.setUseNativeSecp256k1(false); configFilePath = ""; localWitnesses = null; nodeConfig = null; @@ -1315,4 +1334,3 @@ private static Map getOptionGroup() { return optionGroupMap; } } - diff --git a/framework/src/main/java/org/tron/core/net/messagehandler/PbftDataSyncHandler.java b/framework/src/main/java/org/tron/core/net/messagehandler/PbftDataSyncHandler.java index d66fa6d41f7..a8cbca026af 100644 --- a/framework/src/main/java/org/tron/core/net/messagehandler/PbftDataSyncHandler.java +++ b/framework/src/main/java/org/tron/core/net/messagehandler/PbftDataSyncHandler.java @@ -18,7 +18,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import org.tron.common.crypto.ECKey; +import org.tron.common.crypto.SignUtils; import org.tron.common.es.ExecutorServiceManager; import org.tron.common.utils.ByteArray; import org.tron.common.utils.Sha256Hash; @@ -173,8 +173,8 @@ private class ValidPbftSignTask implements Callable { @Override public Boolean call() throws Exception { try { - byte[] srAddress = ECKey.signatureToAddress(dataHash, - TransactionCapsule.getBase64FromByteString(sign)); + byte[] srAddress = SignUtils.signatureToAddress(dataHash, + TransactionCapsule.getBase64FromByteString(sign), true); if (!srSet.contains(ByteString.copyFrom(srAddress))) { logger.error("valid sr signature fail,error sr address:{}", ByteArray.toHexString(srAddress)); diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf index 1176dd46311..b14bb605ff5 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -66,6 +66,9 @@ node.backup { # Specify the algorithm for generating a public key from private key. To avoid forks, please do not modify it crypto { engine = "eckey" + # Use JNA-backed libsecp256k1 for signature verification when engine = "eckey". + # Keep false to use the original pure-Java implementation. + useNativeSecp256k1 = false } node.metrics = { diff --git a/framework/src/test/java/org/tron/common/ParameterTest.java b/framework/src/test/java/org/tron/common/ParameterTest.java index 0b66c96462c..eabd3a0fc93 100644 --- a/framework/src/test/java/org/tron/common/ParameterTest.java +++ b/framework/src/test/java/org/tron/common/ParameterTest.java @@ -218,6 +218,8 @@ public void testCommonParameter() { assertNull(parameter.getEventFilter()); parameter.setCryptoEngine(ECKey_ENGINE); assertEquals(ECKey_ENGINE, parameter.getCryptoEngine()); + parameter.setUseNativeSecp256k1(true); + assertTrue(parameter.isUseNativeSecp256k1()); parameter.setFullNodeHttpEnable(false); assertFalse(parameter.isFullNodeHttpEnable()); parameter.setSolidityNodeHttpEnable(false); diff --git a/framework/src/test/java/org/tron/common/crypto/ECKeyTest.java b/framework/src/test/java/org/tron/common/crypto/ECKeyTest.java index 273672e8342..c277663473b 100644 --- a/framework/src/test/java/org/tron/common/crypto/ECKeyTest.java +++ b/framework/src/test/java/org/tron/common/crypto/ECKeyTest.java @@ -154,7 +154,8 @@ public void testGetAddressFromPrivateKey() { @Test public void testToString() { ECKey key = ECKey.fromPrivate(BigInteger.TEN); // An example private key. - assertEquals("pub:04a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7893aba42" + assertEquals( + "pub:04a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7893aba42" + "5419bc27a3b6c7e693a24c696f794c2ed877a1593cbee53b037368d7", key.toString()); } diff --git a/framework/src/test/java/org/tron/common/crypto/NativeSecp256k1BenchmarkTest.java b/framework/src/test/java/org/tron/common/crypto/NativeSecp256k1BenchmarkTest.java new file mode 100644 index 00000000000..a36e8e0a28e --- /dev/null +++ b/framework/src/test/java/org/tron/common/crypto/NativeSecp256k1BenchmarkTest.java @@ -0,0 +1,133 @@ +package org.tron.common.crypto; + +import static org.junit.Assert.assertArrayEquals; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.util.Locale; +import lombok.extern.slf4j.Slf4j; +import org.junit.Assume; +import org.junit.Test; +import org.tron.common.crypto.ECKey.ECDSASignature; + +/** + * Manual microbenchmark for secp256k1 signing and signature address recovery. + * + *

Enable with {@code NATIVE_SECP256K1_BENCHMARK=true}. Optional environment variables + * {@code NATIVE_SECP256K1_BENCHMARK_WARMUP} and {@code + * NATIVE_SECP256K1_BENCHMARK_ITERATIONS} control the sample size. This is intended for quick + * local comparisons; use JMH when statistically rigorous results are required. + */ +@Slf4j(topic = "benchmark") +public class NativeSecp256k1BenchmarkTest { + + private static final String ENABLE_ENV = "NATIVE_SECP256K1_BENCHMARK"; + private static final String WARMUP_ENV = "NATIVE_SECP256K1_BENCHMARK_WARMUP"; + private static final String ITERATIONS_ENV = "NATIVE_SECP256K1_BENCHMARK_ITERATIONS"; + private static final int DEFAULT_WARMUP = 2_000; + private static final int DEFAULT_ITERATIONS = 10_000; + private static final int MAX_ITERATIONS = 1_000_000; + private static volatile int blackhole; + + @Test + public void benchmarkSigningAndSignatureRecovery() throws Exception { + Assume.assumeTrue("Set " + ENABLE_ENV + "=true to run this benchmark", + Boolean.parseBoolean(System.getenv(ENABLE_ENV))); + Assume.assumeTrue("Native secp256k1 library is unavailable", + NativeSecp256k1.isAvailable()); + + int warmup = readPositiveInt(WARMUP_ENV, DEFAULT_WARMUP); + int iterations = readPositiveInt(ITERATIONS_ENV, DEFAULT_ITERATIONS); + ECKey key = ECKey.fromPrivate(BigInteger.TEN); + byte[] privateKey = key.getPrivateKey(); + byte[] messageHash = Hash.sha3( + "native secp256k1 benchmark".getBytes(StandardCharsets.UTF_8)); + + ECDSASignature javaSignature = key.sign(messageHash); + ECDSASignature nativeSignature = NativeSecp256k1.sign(messageHash, privateKey); + assertArrayEquals(javaSignature.toByteArray(), nativeSignature.toByteArray()); + assertArrayEquals( + ECKey.signatureToAddress(messageHash, nativeSignature), + NativeSecp256k1.signatureToAddress(messageHash, javaSignature)); + + warmUp(warmup, () -> key.sign(messageHash)); + warmUp(warmup, () -> NativeSecp256k1.sign(messageHash, privateKey)); + long javaSignNs = measure(iterations, () -> key.sign(messageHash)); + long nativeSignNs = measure( + iterations, () -> NativeSecp256k1.sign(messageHash, privateKey)); + report("sign", iterations, javaSignNs, nativeSignNs); + + warmUp(warmup, () -> ECKey.signatureToAddress(messageHash, javaSignature)); + warmUp(warmup, + () -> NativeSecp256k1.signatureToAddress(messageHash, javaSignature)); + long javaRecoveryNs = measure( + iterations, () -> ECKey.signatureToAddress(messageHash, javaSignature)); + long nativeRecoveryNs = measure( + iterations, () -> NativeSecp256k1.signatureToAddress(messageHash, javaSignature)); + report("signature-address-recovery", iterations, javaRecoveryNs, nativeRecoveryNs); + } + + private static int readPositiveInt(String environmentVariable, int defaultValue) { + String configured = System.getenv(environmentVariable); + if (configured == null || configured.trim().isEmpty()) { + return defaultValue; + } + int value; + try { + value = Integer.parseInt(configured); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(environmentVariable + " must be an integer", e); + } + if (value <= 0 || value > MAX_ITERATIONS) { + throw new IllegalArgumentException(environmentVariable + " must be between 1 and " + + MAX_ITERATIONS); + } + return value; + } + + private static void warmUp(int iterations, Operation operation) throws Exception { + for (int i = 0; i < iterations; i++) { + consume(operation.run()); + } + } + + private static long measure(int iterations, Operation operation) throws Exception { + long startedAt = System.nanoTime(); + for (int i = 0; i < iterations; i++) { + consume(operation.run()); + } + return (System.nanoTime() - startedAt) / iterations; + } + + private static void consume(Object value) { + if (value instanceof byte[]) { + byte[] bytes = (byte[]) value; + blackhole ^= bytes.length == 0 ? 0 : bytes[0]; + } else if (value instanceof ECDSASignature) { + ECDSASignature signature = (ECDSASignature) value; + blackhole ^= signature.r.intValue() ^ signature.s.intValue() ^ signature.v; + } else { + blackhole ^= value.hashCode(); + } + } + + private static void report( + String operation, int iterations, long javaNsPerOperation, long nativeNsPerOperation) { + long effectiveNative = nativeNsPerOperation == 0 ? 1 : nativeNsPerOperation; + double speedup = (double) javaNsPerOperation / effectiveNative; + logger.info( + "secp256k1 benchmark: operation={}, iterations={}, ECKey={} ns/op, " + + "NativeSecp256k1={} ns/op, speedup={}x", + operation, + iterations, + javaNsPerOperation, + nativeNsPerOperation, + String.format(Locale.ROOT, "%.2f", speedup)); + } + + @FunctionalInterface + private interface Operation { + + Object run() throws Exception; + } +} diff --git a/framework/src/test/java/org/tron/common/crypto/NativeSecp256k1Test.java b/framework/src/test/java/org/tron/common/crypto/NativeSecp256k1Test.java new file mode 100644 index 00000000000..32ac2632a0d --- /dev/null +++ b/framework/src/test/java/org/tron/common/crypto/NativeSecp256k1Test.java @@ -0,0 +1,361 @@ +package org.tron.common.crypto; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mockStatic; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; +import java.security.SignatureException; +import java.util.Arrays; +import org.bouncycastle.util.encoders.Base64; +import org.junit.After; +import org.junit.Assume; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.slf4j.LoggerFactory; +import org.tron.common.crypto.ECKey.ECDSASignature; + +public class NativeSecp256k1Test { + + private static final byte[] MESSAGE_HASH = + Hash.sha3("native verification".getBytes(StandardCharsets.UTF_8)); + private static final ECKey KEY = ECKey.fromPrivate(BigInteger.TEN); + + @After + public void resetVerificationMode() { + SignUtils.setUseNativeSecp256k1(false); + } + + @Test + public void testNativeDisabledByDefault() { + assertFalse(SignUtils.isUseNativeSecp256k1()); + } + + @Test + public void testUnavailableNativeFallback() { + Logger logger = (Logger) LoggerFactory.getLogger("crypto"); + ListAppender appender = new ListAppender<>(); + appender.start(); + logger.addAppender(appender); + try { + SignUtils.setUseNativeSecp256k1(true, false); + + assertFalse(SignUtils.isUseNativeSecp256k1()); + assertTrue(appender.list.stream().anyMatch(event -> + event.getLevel() == Level.WARN + && event.getFormattedMessage().contains("crypto.useNativeSecp256k1=true") + && event.getFormattedMessage().contains("falling back to ECKey"))); + } finally { + logger.detachAppender(appender); + appender.stop(); + } + } + + @Test + public void testPublicKeyRecovery() throws SignatureException { + requireNativeLibrary(); + ECDSASignature signature = KEY.sign(MESSAGE_HASH); + + assertArrayEquals( + ECKey.signatureToKeyBytes(MESSAGE_HASH, signature), + NativeSecp256k1.signatureToKeyBytes(MESSAGE_HASH, signature)); + } + + @Test + public void testCompressedHeaderRecovery() + throws SignatureException { + requireNativeLibrary(); + ECDSASignature signature = KEY.sign(MESSAGE_HASH); + ECDSASignature compressedHeaderSignature = ECDSASignature.fromComponents( + signature.r.toByteArray(), signature.s.toByteArray(), (byte) (signature.v + 4)); + + byte[] javaPublicKey = ECKey.signatureToKeyBytes(MESSAGE_HASH, compressedHeaderSignature); + assertEquals(65, javaPublicKey.length); + assertArrayEquals(javaPublicKey, + NativeSecp256k1.signatureToKeyBytes(MESSAGE_HASH, compressedHeaderSignature)); + } + + @Test + public void testShortPrivateKeySigning() throws SignatureException { + requireNativeLibrary(); + byte[] privateKey = {(byte) 0x0a}; + byte[] originalPrivateKey = Arrays.copyOf(privateKey, privateKey.length); + + ECDSASignature nativeSignature = NativeSecp256k1.sign(MESSAGE_HASH, privateKey); + + assertArrayEquals(KEY.sign(MESSAGE_HASH).toByteArray(), nativeSignature.toByteArray()); + assertArrayEquals(originalPrivateKey, privateKey); + } + + @Test + public void testBigIntegerPrivateKeySigning() throws SignatureException { + requireNativeLibrary(); + BigInteger privateKeyValue = ECKey.CURVE.getN().subtract(BigInteger.ONE); + byte[] privateKey = privateKeyValue.toByteArray(); + ECKey key = ECKey.fromPrivate(privateKeyValue); + + assertEquals(33, privateKey.length); + assertArrayEquals(key.sign(MESSAGE_HASH).toByteArray(), + NativeSecp256k1.sign(MESSAGE_HASH, privateKey).toByteArray()); + } + + @Test + public void testInvalidPrivateKeySigning() throws SignatureException { + requireNativeLibrary(); + + try { + NativeSecp256k1.sign(MESSAGE_HASH, new byte[]{0}); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("outside the secp256k1 range")); + } + } + + @Test + public void testNativeKeyOperations() throws SignatureException { + requireNativeLibrary(); + NativeSecp256k1 nativeKey = new NativeSecp256k1(KEY.getPrivateKey()); + + assertArrayEquals(KEY.getPrivateKey(), nativeKey.getPrivateKey()); + assertArrayEquals(KEY.getPubKey(), nativeKey.getPubKey()); + assertArrayEquals(KEY.getAddress(), nativeKey.getAddress()); + assertArrayEquals(KEY.sign(MESSAGE_HASH).toByteArray(), + nativeKey.sign(MESSAGE_HASH).toByteArray()); + } + + @Test + public void testKeyPairConstructors() throws SignatureException { + requireNativeLibrary(); + assertCompatibleKeyPair(new NativeSecp256k1()); + assertCompatibleKeyPair(new NativeSecp256k1(new SecureRandom())); + } + + @Test + public void testInvalidConstructorArguments() throws SignatureException { + requireNativeLibrary(); + assertInvalidPrivateKey(null); + assertInvalidPrivateKey(new byte[0]); + assertInvalidPrivateKey(new byte[32]); + assertInvalidPrivateKey(new byte[33]); + assertInvalidPrivateKey(ECKey.CURVE.getN().toByteArray()); + + try { + new NativeSecp256k1((SecureRandom) null); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("must not be null")); + } + } + + @Test + public void testJavaNativeCrossCompatibility() throws SignatureException { + requireNativeLibrary(); + for (int i = 1; i <= 32; i++) { + ECKey key = ECKey.fromPrivate(BigInteger.valueOf(i)); + byte[] messageHash = Hash.sha3( + ("cross verification " + i).getBytes(StandardCharsets.UTF_8)); + ECDSASignature javaSignature = key.sign(messageHash); + ECDSASignature nativeSignature = + NativeSecp256k1.sign(messageHash, key.getPrivateKey()); + + assertArrayEquals( + key.getAddress(), NativeSecp256k1.signatureToAddress(messageHash, javaSignature)); + assertArrayEquals( + key.getAddress(), ECKey.signatureToAddress(messageHash, nativeSignature)); + assertEquals(javaSignature.toBase64(), nativeSignature.toBase64()); + } + } + + @Test + public void testBase64TrailingBytesRecovery() throws SignatureException { + requireNativeLibrary(); + ECDSASignature signature = KEY.sign(MESSAGE_HASH); + byte[] encoded = Base64.decode(signature.toBase64()); + byte[] padded = Arrays.copyOf(encoded, encoded.length + 3); + String paddedBase64 = new String(Base64.encode(padded), StandardCharsets.US_ASCII); + + assertArrayEquals( + ECKey.signatureToAddress(MESSAGE_HASH, paddedBase64), + NativeSecp256k1.signatureToAddress(MESSAGE_HASH, paddedBase64)); + } + + @Test + public void testHighSRecovery() throws SignatureException { + requireNativeLibrary(); + ECDSASignature signature = KEY.sign(MESSAGE_HASH); + BigInteger highS = ECKey.CURVE.getN().subtract(signature.s); + byte flippedV = signature.v == 27 ? (byte) 28 : (byte) 27; + ECDSASignature highSSignature = ECDSASignature.fromComponents( + signature.r.toByteArray(), highS.toByteArray(), flippedV); + + assertArrayEquals( + ECKey.signatureToAddress(MESSAGE_HASH, highSSignature), + NativeSecp256k1.signatureToAddress(MESSAGE_HASH, highSSignature)); + } + + @Test + public void testScalarBoundaryRecovery() { + requireNativeLibrary(); + BigInteger curveOrder = ECKey.CURVE.getN(); + BigInteger[] boundaryValues = { + BigInteger.ZERO, + BigInteger.ONE, + curveOrder.subtract(BigInteger.ONE), + curveOrder, + curveOrder.add(BigInteger.ONE) + }; + + for (BigInteger r : boundaryValues) { + for (BigInteger s : boundaryValues) { + for (byte header = 27; header <= 34; header++) { + assertSameRecoveryOutcome(r, s, header); + } + } + } + } + + @Test + public void testInfinityRecovery() throws SignatureException { + requireNativeLibrary(); + byte[] unitHash = new byte[32]; + unitHash[unitHash.length - 1] = 1; + BigInteger generatorX = ECKey.CURVE.getG().getXCoord().toBigInteger(); + byte header = ECKey.CURVE.getG().getYCoord().toBigInteger().testBit(0) + ? (byte) 28 : (byte) 27; + ECDSASignature signature = new ECDSASignature(generatorX, BigInteger.ONE); + signature.v = header; + + assertArrayEquals( + ECKey.signatureToKeyBytes(unitHash, signature), + NativeSecp256k1.signatureToKeyBytes(unitHash, signature)); + } + + @Test + public void testConfiguredNativeRouting() + throws SignatureException { + String signature = KEY.signHash(MESSAGE_HASH); + byte[] nativeAddress = {1, 2, 3}; + + try (MockedStatic nativeSecp256k1 = + mockStatic(NativeSecp256k1.class)) { + nativeSecp256k1.when(NativeSecp256k1::isAvailable).thenReturn(true); + nativeSecp256k1.when( + () -> NativeSecp256k1.signatureToAddress(MESSAGE_HASH, signature)) + .thenReturn(nativeAddress); + + SignUtils.setUseNativeSecp256k1(true); + + assertTrue(SignUtils.isUseNativeSecp256k1()); + assertArrayEquals(nativeAddress, + SignUtils.signatureToAddress(MESSAGE_HASH, signature, true)); + nativeSecp256k1.verify( + () -> NativeSecp256k1.signatureToAddress(MESSAGE_HASH, signature)); + } + } + + @Test + public void testConfiguredNativeAddressCompatibility() + throws SignatureException { + requireNativeLibrary(); + String signature = KEY.signHash(MESSAGE_HASH); + + SignUtils.setUseNativeSecp256k1(true); + + assertTrue(SignUtils.isUseNativeSecp256k1()); + assertArrayEquals( + ECKey.signatureToAddress(MESSAGE_HASH, signature), + SignUtils.signatureToAddress(MESSAGE_HASH, signature, true)); + } + + @Test + public void testInvalidRecoveryHeaders() { + requireNativeLibrary(); + ECDSASignature valid = KEY.sign(MESSAGE_HASH); + + for (byte header : new byte[]{26, 35}) { + ECDSASignature invalid = ECDSASignature.fromComponents( + valid.r.toByteArray(), valid.s.toByteArray(), header); + try { + NativeSecp256k1.signatureToAddress(MESSAGE_HASH, invalid); + fail("Expected SignatureException"); + } catch (SignatureException e) { + assertTrue(e.getMessage().contains("Header byte out of range")); + } + } + } + + @Test + public void testOversizedSignatureComponents() { + requireNativeLibrary(); + ECDSASignature valid = KEY.sign(MESSAGE_HASH); + BigInteger oversizedR = BigInteger.ONE.shiftLeft(256).add(valid.r); + ECDSASignature invalid = ECDSASignature.fromComponents( + oversizedR.toByteArray(), valid.s.toByteArray(), valid.v); + + try { + NativeSecp256k1.signatureToAddress(MESSAGE_HASH, invalid); + fail("Expected SignatureException"); + } catch (SignatureException e) { + assertTrue(e.getMessage().contains("unsigned 32-byte integers")); + } + } + + private static void requireNativeLibrary() { + Assume.assumeTrue("Native secp256k1 library is unavailable", + NativeSecp256k1.isAvailable()); + } + + private static void assertSameRecoveryOutcome(BigInteger r, BigInteger s, byte header) { + ECDSASignature signature = new ECDSASignature(r, s); + signature.v = header; + String message = "Recovery mismatch for r=" + r + ", s=" + s + ", v=" + header; + + byte[] javaPublicKey; + try { + javaPublicKey = ECKey.signatureToKeyBytes(MESSAGE_HASH, signature); + } catch (Exception javaFailure) { + try { + NativeSecp256k1.signatureToKeyBytes(MESSAGE_HASH, signature); + fail(message + ": Java rejected but native recovered"); + return; + } catch (Exception nativeFailure) { + assertEquals(message, javaFailure.getClass(), nativeFailure.getClass()); + return; + } + } + + try { + assertArrayEquals(message, javaPublicKey, + NativeSecp256k1.signatureToKeyBytes(MESSAGE_HASH, signature)); + } catch (Exception nativeFailure) { + fail(message + ": Java recovered but native rejected: " + nativeFailure.getMessage()); + } + } + + private static void assertCompatibleKeyPair(NativeSecp256k1 nativeKey) { + ECKey javaKey = ECKey.fromPrivate(nativeKey.getPrivateKey()); + + assertArrayEquals(javaKey.getPubKey(), nativeKey.getPubKey()); + assertArrayEquals(javaKey.getAddress(), nativeKey.getAddress()); + assertArrayEquals(javaKey.sign(MESSAGE_HASH).toByteArray(), + nativeKey.sign(MESSAGE_HASH).toByteArray()); + } + + private static void assertInvalidPrivateKey(byte[] privateKey) throws SignatureException { + try { + new NativeSecp256k1(privateKey); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("privateKey argument")); + } + } +} diff --git a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java index 36b8a3269c1..fba7180952b 100644 --- a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java +++ b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java @@ -15,6 +15,10 @@ package org.tron.core.config.args; +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; import com.google.common.collect.Lists; import com.typesafe.config.Config; import com.typesafe.config.ConfigFactory; @@ -31,14 +35,16 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.slf4j.LoggerFactory; import org.tron.common.TestConstants; import org.tron.common.args.GenesisBlock; +import org.tron.common.crypto.NativeSecp256k1; +import org.tron.common.crypto.SignUtils; import org.tron.common.parameter.CommonParameter; import org.tron.common.utils.ByteArray; import org.tron.common.utils.DecodeUtil; import org.tron.common.utils.LocalWitnesses; import org.tron.common.utils.PublicMethod; -import org.tron.core.exception.ContractValidateException; import org.tron.core.exception.TronError; @Slf4j @@ -537,6 +543,57 @@ public void testRpcMaxMessageSizeExceedsIntMax() { } } + @Test + public void testUseNativeSecp256k1ConfiguresSignatureVerification() { + Map configMap = new HashMap<>(); + configMap.put("storage.db.directory", "database"); + configMap.put("crypto.useNativeSecp256k1", "true"); + Config config = ConfigFactory.parseMap(configMap) + .withFallback(ConfigFactory.defaultReference()); + Logger logger = (Logger) LoggerFactory.getLogger("app"); + ListAppender appender = new ListAppender<>(); + appender.start(); + logger.addAppender(appender); + try { + Args.applyConfigParams(config); + + boolean expectedActive = NativeSecp256k1.isAvailable(); + Assert.assertEquals(expectedActive, Args.getInstance().isUseNativeSecp256k1()); + Assert.assertEquals(expectedActive, SignUtils.isUseNativeSecp256k1()); + Assert.assertTrue(appender.list.stream().anyMatch(event -> + event.getLevel() == Level.INFO + && event.getFormattedMessage().contains("Crypto signature verification:") + && event.getFormattedMessage().contains("engine=eckey") + && event.getFormattedMessage().contains("nativeRequested=true") + && event.getFormattedMessage().contains("nativeActive=" + expectedActive) + && event.getFormattedMessage().contains( + "implementation=" + (expectedActive ? "NativeSecp256k1" : "ECKey")))); + } finally { + Args.clearParam(); + logger.detachAppender(appender); + appender.stop(); + } + Assert.assertFalse(SignUtils.isUseNativeSecp256k1()); + } + + @Test + public void testUseNativeSecp256k1IsIgnoredForSm2() { + Map configMap = new HashMap<>(); + configMap.put("storage.db.directory", "database"); + configMap.put("crypto.engine", "sm2"); + configMap.put("crypto.useNativeSecp256k1", "true"); + Config config = ConfigFactory.parseMap(configMap) + .withFallback(ConfigFactory.defaultReference()); + try { + Args.applyConfigParams(config); + + Assert.assertFalse(Args.getInstance().isUseNativeSecp256k1()); + Assert.assertFalse(SignUtils.isUseNativeSecp256k1()); + } finally { + Args.clearParam(); + } + } + // ===== checkBackupMembers() tests ===== @Test diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 6a3e641d5d6..349ea75922d 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -1110,6 +1110,22 @@ + + + + + + + + + + + + + + + + @@ -1599,6 +1615,14 @@ + + + + + + + +