From a9e32b31c40968f8e0aa19fa6cecd3391b5a522e Mon Sep 17 00:00:00 2001 From: garyschulte Date: Fri, 24 Oct 2025 17:04:19 -0700 Subject: [PATCH 1/4] squash commit of gnark and secp256k1 static libs Signed-off-by: garyschulte --- build.sh | 9 + gnark/build.gradle | 89 ++ .../nativelib/gnark/LibGnarkEIP196Graal.java | 239 ++++++ .../nativelib/gnark/LibGnarkEIP2537Graal.java | 570 ++++++++++++ .../besu/nativelib/gnark/LibGnarkGraal.java | 90 ++ gradle.properties | 2 +- secp256k1/build.gradle | 82 +- secp256k1/secp256k1_jni/Makefile | 27 +- .../secp256k1/LibSecp256k1EcrecoverGraal.java | 183 ++++ .../secp256k1/LibSecp256k1Graal.java | 809 ++++++++++++++++++ 10 files changed, 2097 insertions(+), 3 deletions(-) create mode 100644 gnark/src/main/java/org/hyperledger/besu/nativelib/gnark/LibGnarkEIP196Graal.java create mode 100644 gnark/src/main/java/org/hyperledger/besu/nativelib/gnark/LibGnarkEIP2537Graal.java create mode 100644 gnark/src/main/java/org/hyperledger/besu/nativelib/gnark/LibGnarkGraal.java create mode 100644 secp256k1/src/main/java/org/hyperledger/besu/nativelib/secp256k1/LibSecp256k1EcrecoverGraal.java create mode 100644 secp256k1/src/main/java/org/hyperledger/besu/nativelib/secp256k1/LibSecp256k1Graal.java diff --git a/build.sh b/build.sh index 0d74cb61..b7be136a 100755 --- a/build.sh +++ b/build.sh @@ -302,18 +302,27 @@ EOF if [[ "$OSTYPE" == "msys" ]]; then LIBRARY_EXTENSION=dll + STATIC_EXTENSION=lib elif [[ "$OSTYPE" == "linux-gnu"* ]]; then LIBRARY_EXTENSION=so + STATIC_EXTENSION=a elif [[ "$OSTYPE" == "darwin"* ]]; then LIBRARY_EXTENSION=dylib + STATIC_EXTENSION=a export GOROOT=$(brew --prefix go@1.24)/libexec export PATH=$GOROOT/bin:$PATH fi + # Build shared libraries go build -buildmode=c-shared -o libgnark_jni.$LIBRARY_EXTENSION gnark-jni.go go build -buildmode=c-shared -o libgnark_eip_2537.$LIBRARY_EXTENSION gnark-eip-2537.go go build -buildmode=c-shared -o libgnark_eip_196.$LIBRARY_EXTENSION gnark-eip-196.go + # Build static libraries (c-archive creates .a files) + go build -buildmode=c-archive -o libgnark_jni.$STATIC_EXTENSION gnark-jni.go + go build -buildmode=c-archive -o libgnark_eip_2537.$STATIC_EXTENSION gnark-eip-2537.go + go build -buildmode=c-archive -o libgnark_eip_196.$STATIC_EXTENSION gnark-eip-196.go + mkdir -p "$SCRIPTDIR/gnark/build/${OSARCH}/lib" cp libgnark_jni.* "$SCRIPTDIR/gnark/build/${OSARCH}/lib" cp libgnark_eip_2537.* "$SCRIPTDIR/gnark/build/${OSARCH}/lib" diff --git a/gnark/build.gradle b/gnark/build.gradle index a8315018..ecedf674 100644 --- a/gnark/build.gradle +++ b/gnark/build.gradle @@ -19,9 +19,31 @@ plugins { id 'com.jfrog.artifactory' version '5.2.3' } +// Configure source sets - separate GraalVM classes from main JAR +sourceSets { + graal { + java { + srcDir 'src/main/java' + include '**/*Graal*.java' + } + // Graal source set needs access to main source set classes + compileClasspath += sourceSets.main.output + } + main { + java { + exclude '**/*Graal*.java' + } + } +} + dependencies { implementation 'net.java.dev.jna:jna:5.12.1' implementation project(':common') + + // GraalVM SDK only for graal source set + graalImplementation 'org.graalvm.sdk:graal-sdk:24.0.2' + graalImplementation project(':common') + graalImplementation 'net.java.dev.jna:jna:5.12.1' testImplementation 'com.google.guava:guava:31.1-jre' testImplementation 'net.java.dev.jna:jna:5.12.1' testImplementation 'io.consensys.tuweni:tuweni-bytes:2.7.1' @@ -30,6 +52,7 @@ dependencies { testImplementation 'org.mockito:mockito-core:4.4.0' } +// Dynamic library copy tasks (for JNA) task macArmLibCopy(type: Copy) { from 'build/darwin-aarch64/lib/libgnark_jni.dylib' from 'build/darwin-aarch64/lib/libgnark_eip_2537.dylib' @@ -70,6 +93,63 @@ task linuxRiscv64LibCopy(type: Copy) { } processResources.dependsOn linuxRiscv64LibCopy +// Static library copy tasks (for GraalVM native-image) +task macArmStaticLibCopy(type: Copy) { + from 'build/darwin-aarch64/lib/libgnark_jni.a' + from 'build/darwin-aarch64/lib/libgnark_eip_2537.a' + from 'build/darwin-aarch64/lib/libgnark_eip_196.a' + from 'build/darwin-aarch64/lib/libgnark_jni.h' + from 'build/darwin-aarch64/lib/libgnark_eip_2537.h' + from 'build/darwin-aarch64/lib/libgnark_eip_196.h' + into 'build/resources-static/lib/aarch64' +} + +task macStaticLibCopy(type: Copy) { + from 'build/darwin-x86-64/lib/libgnark_jni.a' + from 'build/darwin-x86-64/lib/libgnark_eip_2537.a' + from 'build/darwin-x86-64/lib/libgnark_eip_196.a' + from 'build/darwin-x86-64/lib/libgnark_jni.h' + from 'build/darwin-x86-64/lib/libgnark_eip_2537.h' + from 'build/darwin-x86-64/lib/libgnark_eip_196.h' + into 'build/resources-static/lib/x86-64' +} + +task linuxStaticLibCopy(type: Copy) { + from 'build/linux-gnu-x86_64/lib/libgnark_jni.a' + from 'build/linux-gnu-x86_64/lib/libgnark_eip_2537.a' + from 'build/linux-gnu-x86_64/lib/libgnark_eip_196.a' + from 'build/linux-gnu-x86_64/lib/libgnark_jni.h' + from 'build/linux-gnu-x86_64/lib/libgnark_eip_2537.h' + from 'build/linux-gnu-x86_64/lib/libgnark_eip_196.h' + into 'build/resources-static/lib/x86-64' +} + +task linuxArm64StaticLibCopy(type: Copy) { + from 'build/linux-gnu-aarch64/lib/libgnark_jni.a' + from 'build/linux-gnu-aarch64/lib/libgnark_eip_2537.a' + from 'build/linux-gnu-aarch64/lib/libgnark_eip_196.a' + from 'build/linux-gnu-aarch64/lib/libgnark_jni.h' + from 'build/linux-gnu-aarch64/lib/libgnark_eip_2537.h' + from 'build/linux-gnu-aarch64/lib/libgnark_eip_196.h' + into 'build/resources-static/lib/aarch64' +} + +task linuxRiscv64StaticLibCopy(type: Copy) { + from 'build/linux-gnu-riscv64/lib/libgnark_jni.a' + from 'build/linux-gnu-riscv64/lib/libgnark_eip_2537.a' + from 'build/linux-gnu-riscv64/lib/libgnark_eip_196.a' + from 'build/linux-gnu-riscv64/lib/libgnark_jni.h' + from 'build/linux-gnu-riscv64/lib/libgnark_eip_2537.h' + from 'build/linux-gnu-riscv64/lib/libgnark_eip_196.h' + into 'build/resources-static/lib/riscv64' +} + +// Task to prepare static resources +task prepareStaticResources { + dependsOn macArmStaticLibCopy, macStaticLibCopy, linuxStaticLibCopy, + linuxArm64StaticLibCopy, linuxRiscv64StaticLibCopy +} + jar { archiveBaseName = 'besu-native-gnark' includeEmptyDirs = false @@ -96,6 +176,14 @@ task javadocJar(type: Jar, dependsOn: javadoc) { from javadoc.destinationDir } +task staticJar(type: Jar, dependsOn: [prepareStaticResources, graalClasses]) { + archiveBaseName = 'besu-native-gnark' + archiveClassifier = 'static' + from 'build/resources-static' + from sourceSets.graal.output + includeEmptyDirs = false +} + publishing { publications { @@ -107,6 +195,7 @@ publishing { from components.java artifact sourcesJar artifact javadocJar + artifact staticJar pom { name = "Besu Native - ${project.name}" diff --git a/gnark/src/main/java/org/hyperledger/besu/nativelib/gnark/LibGnarkEIP196Graal.java b/gnark/src/main/java/org/hyperledger/besu/nativelib/gnark/LibGnarkEIP196Graal.java new file mode 100644 index 00000000..ae41c832 --- /dev/null +++ b/gnark/src/main/java/org/hyperledger/besu/nativelib/gnark/LibGnarkEIP196Graal.java @@ -0,0 +1,239 @@ +/* + * Copyright contributors to Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * + */ +package org.hyperledger.besu.nativelib.gnark; + +import org.graalvm.nativeimage.PinnedObject; +import org.graalvm.nativeimage.c.CContext; +import org.graalvm.nativeimage.c.function.CFunction; +import org.graalvm.nativeimage.c.type.CCharPointer; +import org.graalvm.nativeimage.c.type.CIntPointer; + +import java.util.Collections; +import java.util.List; + +/** + * GraalVM native-image compatible interface to gnark EIP-196 static library. + * Provides operations for Alt-BN128 elliptic curve operations including G1 addition, + * scalar multiplication, and pairing checks. + */ +public class LibGnarkEIP196Graal { + + /** Recommended buffer size for operation results. */ + public static final int EIP196_PREALLOCATE_FOR_RESULT_BYTES = 128; + + /** Recommended buffer size for error messages. */ + public static final int EIP196_PREALLOCATE_FOR_ERROR_BYTES = 256; + + /** Operation code for G1 point addition. */ + public static final byte EIP196_ADD_OPERATION_RAW_VALUE = 1; + + /** Operation code for G1 scalar multiplication. */ + public static final byte EIP196_MUL_OPERATION_RAW_VALUE = 2; + + /** Operation code for pairing check. */ + public static final byte EIP196_PAIR_OPERATION_RAW_VALUE = 3; + + /** Private constructor to prevent instantiation of utility class. */ + private LibGnarkEIP196Graal() {} + + @CContext(LibGnarkEIP196Graal.Directives.class) + public static class Directives implements CContext.Directives { + @Override + public List getHeaderFiles() { + return Collections.singletonList(""); + } + + @Override + public List getLibraries() { + return Collections.singletonList("gnark_eip_196"); + } + + @Override + public List getLibraryPaths() { + // Library paths should be configured via native-image build arguments + return Collections.emptyList(); + } + } + + @CFunction(value = "eip196altbn128G1Add") + public static native int eip196altbn128G1AddNative( + CCharPointer input, + CCharPointer output, + CCharPointer error, + int inputSize, + CIntPointer outputSize, + CIntPointer errorSize); + + @CFunction(value = "eip196altbn128G1Mul") + public static native int eip196altbn128G1MulNative( + CCharPointer input, + CCharPointer output, + CCharPointer error, + int inputSize, + CIntPointer outputSize, + CIntPointer errorSize); + + @CFunction(value = "eip196altbn128Pairing") + public static native int eip196altbn128PairingNative( + CCharPointer input, + CCharPointer output, + CCharPointer error, + int inputSize, + CIntPointer outputSize, + CIntPointer errorSize); + + /** + * Compatibility shim for the pre-existing matter-labs implementation. + * Routes operation codes to the appropriate native function. + * + * @param op operation code (ADD, MUL, or PAIR) + * @param input input data buffer + * @param inputLength length of input data + * @param output output data buffer + * @param outputSize output size reference (updated by native function) + * @param error error message buffer + * @param errorSize error size reference (updated by native function) + * @return result code from native function + */ + public static int eip196_perform_operation( + byte op, + byte[] input, + int inputLength, + byte[] output, + int[] outputSize, + byte[] error, + int[] errorSize) { + + int ret = -1; + switch(op) { + case EIP196_ADD_OPERATION_RAW_VALUE: + ret = eip196altbn128G1Add(input, output, error, inputLength, outputSize, errorSize); + break; + case EIP196_MUL_OPERATION_RAW_VALUE: + ret = eip196altbn128G1Mul(input, output, error, inputLength, outputSize, errorSize); + break; + case EIP196_PAIR_OPERATION_RAW_VALUE: + ret = eip196altbn128Pairing(input, output, error, inputLength, outputSize, errorSize); + break; + default: + throw new RuntimeException("Not Implemented EIP-196 operation " + op); + } + + return ret; + } + + /** + * Java-friendly wrapper for Alt-BN128 G1 point addition. + * + * @param input input data containing two G1 points to add + * @param output output buffer for result + * @param error error message buffer + * @param inputSize size of input data + * @param outputSize output size reference (updated by native function) + * @param errorSize error size reference (updated by native function) + * @return result code from native function + */ + public static int eip196altbn128G1Add( + byte[] input, + byte[] output, + byte[] error, + int inputSize, + int[] outputSize, + int[] errorSize) { + try (PinnedObject pinnedInput = PinnedObject.create(input); + PinnedObject pinnedOutput = PinnedObject.create(output); + PinnedObject pinnedError = PinnedObject.create(error); + PinnedObject pinnedOutputSize = PinnedObject.create(outputSize); + PinnedObject pinnedErrorSize = PinnedObject.create(errorSize)) { + return eip196altbn128G1AddNative( + pinnedInput.addressOfArrayElement(0), + pinnedOutput.addressOfArrayElement(0), + pinnedError.addressOfArrayElement(0), + inputSize, + pinnedOutputSize.addressOfArrayElement(0), + pinnedErrorSize.addressOfArrayElement(0) + ); + } + } + + /** + * Java-friendly wrapper for Alt-BN128 G1 scalar multiplication. + * + * @param input input data containing G1 point and scalar + * @param output output buffer for result + * @param error error message buffer + * @param inputSize size of input data + * @param outputSize output size reference (updated by native function) + * @param errorSize error size reference (updated by native function) + * @return result code from native function + */ + public static int eip196altbn128G1Mul( + byte[] input, + byte[] output, + byte[] error, + int inputSize, + int[] outputSize, + int[] errorSize) { + try (PinnedObject pinnedInput = PinnedObject.create(input); + PinnedObject pinnedOutput = PinnedObject.create(output); + PinnedObject pinnedError = PinnedObject.create(error); + PinnedObject pinnedOutputSize = PinnedObject.create(outputSize); + PinnedObject pinnedErrorSize = PinnedObject.create(errorSize)) { + return eip196altbn128G1MulNative( + pinnedInput.addressOfArrayElement(0), + pinnedOutput.addressOfArrayElement(0), + pinnedError.addressOfArrayElement(0), + inputSize, + pinnedOutputSize.addressOfArrayElement(0), + pinnedErrorSize.addressOfArrayElement(0) + ); + } + } + + /** + * Java-friendly wrapper for Alt-BN128 pairing check operation. + * + * @param input input data containing pairs of G1 and G2 points + * @param output output buffer for result (boolean encoded as bytes) + * @param error error message buffer + * @param inputSize size of input data + * @param outputSize output size reference (updated by native function) + * @param errorSize error size reference (updated by native function) + * @return result code from native function + */ + public static int eip196altbn128Pairing( + byte[] input, + byte[] output, + byte[] error, + int inputSize, + int[] outputSize, + int[] errorSize) { + try (PinnedObject pinnedInput = PinnedObject.create(input); + PinnedObject pinnedOutput = PinnedObject.create(output); + PinnedObject pinnedError = PinnedObject.create(error); + PinnedObject pinnedOutputSize = PinnedObject.create(outputSize); + PinnedObject pinnedErrorSize = PinnedObject.create(errorSize)) { + return eip196altbn128PairingNative( + pinnedInput.addressOfArrayElement(0), + pinnedOutput.addressOfArrayElement(0), + pinnedError.addressOfArrayElement(0), + inputSize, + pinnedOutputSize.addressOfArrayElement(0), + pinnedErrorSize.addressOfArrayElement(0) + ); + } + } +} diff --git a/gnark/src/main/java/org/hyperledger/besu/nativelib/gnark/LibGnarkEIP2537Graal.java b/gnark/src/main/java/org/hyperledger/besu/nativelib/gnark/LibGnarkEIP2537Graal.java new file mode 100644 index 00000000..c5569cd4 --- /dev/null +++ b/gnark/src/main/java/org/hyperledger/besu/nativelib/gnark/LibGnarkEIP2537Graal.java @@ -0,0 +1,570 @@ +/* + * Copyright contributors to Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * + */ +package org.hyperledger.besu.nativelib.gnark; + +import org.graalvm.nativeimage.PinnedObject; +import org.graalvm.nativeimage.c.CContext; +import org.graalvm.nativeimage.c.function.CFunction; +import org.graalvm.nativeimage.c.type.CCharPointer; + +import java.util.Collections; +import java.util.List; + +/** + * GraalVM native-image compatible interface to gnark EIP-2537 static library. + * Provides operations for BLS12-381 elliptic curve operations including G1/G2 addition, + * multi-scalar multiplication, pairing checks, and curve/subgroup validation. + */ +public class LibGnarkEIP2537Graal { + + /** Recommended buffer size for operation results. */ + public static final int EIP2537_PREALLOCATE_FOR_RESULT_BYTES = 256; + + /** Recommended buffer size for error messages. */ + public static final int EIP2537_PREALLOCATE_FOR_ERROR_BYTES = 256; + + /** Operation code for BLS12-381 G1 point addition. */ + public static final byte BLS12_G1ADD_OPERATION_SHIM_VALUE = 1; + + /** Operation code for BLS12-381 G1 multi-scalar multiplication. */ + public static final byte BLS12_G1MULTIEXP_OPERATION_SHIM_VALUE = 2; + + /** Operation code for BLS12-381 G2 point addition. */ + public static final byte BLS12_G2ADD_OPERATION_SHIM_VALUE = 3; + + /** Operation code for BLS12-381 G2 multi-scalar multiplication. */ + public static final byte BLS12_G2MULTIEXP_OPERATION_SHIM_VALUE = 4; + + /** Operation code for BLS12-381 pairing check. */ + public static final byte BLS12_PAIR_OPERATION_SHIM_VALUE = 5; + + /** Operation code for mapping field element to G1. */ + public static final byte BLS12_MAP_FP_TO_G1_OPERATION_SHIM_VALUE = 6; + + /** Operation code for mapping field element to G2. */ + public static final byte BLS12_MAP_FP2_TO_G2_OPERATION_SHIM_VALUE = 7; + + /** Degree of parallelism for multi-scalar multiplication (0 = auto-detect CPU cores). */ + private static int degreeOfMSMParallelism = 0; + + /** Private constructor to prevent instantiation of utility class. */ + private LibGnarkEIP2537Graal() {} + + @CContext(LibGnarkEIP2537Graal.Directives.class) + public static class Directives implements CContext.Directives { + @Override + public List getHeaderFiles() { + return Collections.singletonList(""); + } + + @Override + public List getLibraries() { + return Collections.singletonList("gnark_eip_2537"); + } + + @Override + public List getLibraryPaths() { + // Library paths should be configured via native-image build arguments + return Collections.emptyList(); + } + } + + @CFunction(value = "eip2537blsG1Add") + public static native int eip2537blsG1AddNative( + CCharPointer input, + CCharPointer output, + CCharPointer error, + int inputSize, + int outputLength, + int errorLength); + + @CFunction(value = "eip2537blsG1MultiExp") + public static native int eip2537blsG1MultiExpNative( + CCharPointer input, + CCharPointer output, + CCharPointer error, + int inputSize, + int outputLength, + int errorLength, + int nbTasks); + + @CFunction(value = "eip2537blsG2Add") + public static native int eip2537blsG2AddNative( + CCharPointer input, + CCharPointer output, + CCharPointer error, + int inputSize, + int outputLength, + int errorLength); + + @CFunction(value = "eip2537blsG2MultiExp") + public static native int eip2537blsG2MultiExpNative( + CCharPointer input, + CCharPointer output, + CCharPointer error, + int inputSize, + int outputLength, + int errorLength, + int nbTasks); + + @CFunction(value = "eip2537blsPairing") + public static native int eip2537blsPairingNative( + CCharPointer input, + CCharPointer output, + CCharPointer error, + int inputSize, + int outputLength, + int errorLength); + + @CFunction(value = "eip2537blsMapFpToG1") + public static native int eip2537blsMapFpToG1Native( + CCharPointer input, + CCharPointer output, + CCharPointer error, + int inputSize, + int outputLength, + int errorLength); + + @CFunction(value = "eip2537blsMapFp2ToG2") + public static native int eip2537blsMapFp2ToG2Native( + CCharPointer input, + CCharPointer output, + CCharPointer error, + int inputSize, + int outputLength, + int errorLength); + + @CFunction(value = "eip2537G1IsOnCurve") + public static native boolean eip2537G1IsOnCurveNative( + CCharPointer input, + CCharPointer error, + int inputSize, + int errorLength); + + @CFunction(value = "eip2537G2IsOnCurve") + public static native boolean eip2537G2IsOnCurveNative( + CCharPointer input, + CCharPointer error, + int inputSize, + int errorLength); + + @CFunction(value = "eip2537G1IsInSubGroup") + public static native boolean eip2537G1IsInSubGroupNative( + CCharPointer input, + CCharPointer error, + int inputSize, + int errorLength); + + @CFunction(value = "eip2537G2IsInSubGroup") + public static native boolean eip2537G2IsInSubGroupNative( + CCharPointer input, + CCharPointer error, + int inputSize, + int errorLength); + + /** + * Here as a compatibility shim for the pre-existing matter-labs implementation. + * + * IMPORTANT: The output buffer MUST be zero-initialized before calling this method. + * The native implementation relies on this pre-initialization for proper functioning. + */ + public static int eip2537_perform_operation( + byte op, + byte[] input, + int inputLength, + byte[] output, + int[] outputLength, + byte[] error, + int[] errorLength) { + + int ret = -1; + switch(op) { + case BLS12_G1ADD_OPERATION_SHIM_VALUE: + ret = eip2537blsG1Add(input, output, error, inputLength, + EIP2537_PREALLOCATE_FOR_RESULT_BYTES, + EIP2537_PREALLOCATE_FOR_ERROR_BYTES); + outputLength[0] = 128; + break; + case BLS12_G1MULTIEXP_OPERATION_SHIM_VALUE: + ret = eip2537blsG1MultiExp(input, output, error, inputLength, + EIP2537_PREALLOCATE_FOR_RESULT_BYTES, + EIP2537_PREALLOCATE_FOR_ERROR_BYTES, + degreeOfMSMParallelism); + outputLength[0] = 128; + break; + case BLS12_G2ADD_OPERATION_SHIM_VALUE: + ret = eip2537blsG2Add(input, output, error, inputLength, + EIP2537_PREALLOCATE_FOR_RESULT_BYTES, + EIP2537_PREALLOCATE_FOR_ERROR_BYTES); + outputLength[0] = 256; + break; + case BLS12_G2MULTIEXP_OPERATION_SHIM_VALUE: + ret = eip2537blsG2MultiExp(input, output, error, inputLength, + EIP2537_PREALLOCATE_FOR_RESULT_BYTES, + EIP2537_PREALLOCATE_FOR_ERROR_BYTES, + degreeOfMSMParallelism); + outputLength[0] = 256; + break; + case BLS12_PAIR_OPERATION_SHIM_VALUE: + ret = eip2537blsPairing(input, output, error, inputLength, + EIP2537_PREALLOCATE_FOR_RESULT_BYTES, + EIP2537_PREALLOCATE_FOR_ERROR_BYTES); + outputLength[0] = 32; + break; + case BLS12_MAP_FP_TO_G1_OPERATION_SHIM_VALUE: + ret = eip2537blsMapFpToG1(input, output, error, inputLength, + EIP2537_PREALLOCATE_FOR_RESULT_BYTES, + EIP2537_PREALLOCATE_FOR_ERROR_BYTES); + outputLength[0] = 128; + break; + case BLS12_MAP_FP2_TO_G2_OPERATION_SHIM_VALUE: + ret = eip2537blsMapFp2ToG2(input, output, error, inputLength, + EIP2537_PREALLOCATE_FOR_RESULT_BYTES, + EIP2537_PREALLOCATE_FOR_ERROR_BYTES); + outputLength[0] = 256; + break; + default: + throw new RuntimeException("Not Implemented EIP-2537 operation " + op); + } + + if (ret != 0) { + errorLength[0] = LibGnarkUtils.findFirstTrailingZeroIndex(error); + outputLength[0] = 0; + } else { + errorLength[0] = 0; + } + return ret; + } + + /** + * Java-friendly wrapper for BLS12-381 G1 point addition. + * + * @param input input data containing two G1 points to add + * @param output output buffer for result + * @param error error message buffer + * @param inputSize size of input data + * @param outputLength size of output buffer + * @param errorLength size of error buffer + * @return result code from native function (0 = success) + */ + public static int eip2537blsG1Add( + byte[] input, + byte[] output, + byte[] error, + int inputSize, + int outputLength, + int errorLength) { + try (PinnedObject pinnedInput = PinnedObject.create(input); + PinnedObject pinnedOutput = PinnedObject.create(output); + PinnedObject pinnedError = PinnedObject.create(error)) { + return eip2537blsG1AddNative( + pinnedInput.addressOfArrayElement(0), + pinnedOutput.addressOfArrayElement(0), + pinnedError.addressOfArrayElement(0), + inputSize, outputLength, errorLength + ); + } + } + + /** + * Java-friendly wrapper for BLS12-381 G1 multi-scalar multiplication. + * + * @param input input data containing G1 points and scalars + * @param output output buffer for result + * @param error error message buffer + * @param inputSize size of input data + * @param outputLength size of output buffer + * @param errorLength size of error buffer + * @param nbTasks number of parallel tasks (0 = auto-detect) + * @return result code from native function (0 = success) + */ + public static int eip2537blsG1MultiExp( + byte[] input, + byte[] output, + byte[] error, + int inputSize, + int outputLength, + int errorLength, + int nbTasks) { + try (PinnedObject pinnedInput = PinnedObject.create(input); + PinnedObject pinnedOutput = PinnedObject.create(output); + PinnedObject pinnedError = PinnedObject.create(error)) { + return eip2537blsG1MultiExpNative( + pinnedInput.addressOfArrayElement(0), + pinnedOutput.addressOfArrayElement(0), + pinnedError.addressOfArrayElement(0), + inputSize, outputLength, errorLength, nbTasks + ); + } + } + + /** + * Java-friendly wrapper for BLS12-381 G2 point addition. + * + * @param input input data containing two G2 points to add + * @param output output buffer for result + * @param error error message buffer + * @param inputSize size of input data + * @param outputLength size of output buffer + * @param errorLength size of error buffer + * @return result code from native function (0 = success) + */ + public static int eip2537blsG2Add( + byte[] input, + byte[] output, + byte[] error, + int inputSize, + int outputLength, + int errorLength) { + try (PinnedObject pinnedInput = PinnedObject.create(input); + PinnedObject pinnedOutput = PinnedObject.create(output); + PinnedObject pinnedError = PinnedObject.create(error)) { + return eip2537blsG2AddNative( + pinnedInput.addressOfArrayElement(0), + pinnedOutput.addressOfArrayElement(0), + pinnedError.addressOfArrayElement(0), + inputSize, outputLength, errorLength + ); + } + } + + /** + * Java-friendly wrapper for BLS12-381 G2 multi-scalar multiplication. + * + * @param input input data containing G2 points and scalars + * @param output output buffer for result + * @param error error message buffer + * @param inputSize size of input data + * @param outputLength size of output buffer + * @param errorLength size of error buffer + * @param nbTasks number of parallel tasks (0 = auto-detect) + * @return result code from native function (0 = success) + */ + public static int eip2537blsG2MultiExp( + byte[] input, + byte[] output, + byte[] error, + int inputSize, + int outputLength, + int errorLength, + int nbTasks) { + try (PinnedObject pinnedInput = PinnedObject.create(input); + PinnedObject pinnedOutput = PinnedObject.create(output); + PinnedObject pinnedError = PinnedObject.create(error)) { + return eip2537blsG2MultiExpNative( + pinnedInput.addressOfArrayElement(0), + pinnedOutput.addressOfArrayElement(0), + pinnedError.addressOfArrayElement(0), + inputSize, outputLength, errorLength, nbTasks + ); + } + } + + /** + * Java-friendly wrapper for BLS12-381 pairing check operation. + * + * @param input input data containing pairs of G1 and G2 points + * @param output output buffer for result (boolean encoded as bytes) + * @param error error message buffer + * @param inputSize size of input data + * @param outputLength size of output buffer + * @param errorLength size of error buffer + * @return result code from native function (0 = success) + */ + public static int eip2537blsPairing( + byte[] input, + byte[] output, + byte[] error, + int inputSize, + int outputLength, + int errorLength) { + try (PinnedObject pinnedInput = PinnedObject.create(input); + PinnedObject pinnedOutput = PinnedObject.create(output); + PinnedObject pinnedError = PinnedObject.create(error)) { + return eip2537blsPairingNative( + pinnedInput.addressOfArrayElement(0), + pinnedOutput.addressOfArrayElement(0), + pinnedError.addressOfArrayElement(0), + inputSize, outputLength, errorLength + ); + } + } + + /** + * Java-friendly wrapper for mapping field element to BLS12-381 G1 point. + * + * @param input input data containing field element + * @param output output buffer for resulting G1 point + * @param error error message buffer + * @param inputSize size of input data + * @param outputLength size of output buffer + * @param errorLength size of error buffer + * @return result code from native function (0 = success) + */ + public static int eip2537blsMapFpToG1( + byte[] input, + byte[] output, + byte[] error, + int inputSize, + int outputLength, + int errorLength) { + try (PinnedObject pinnedInput = PinnedObject.create(input); + PinnedObject pinnedOutput = PinnedObject.create(output); + PinnedObject pinnedError = PinnedObject.create(error)) { + return eip2537blsMapFpToG1Native( + pinnedInput.addressOfArrayElement(0), + pinnedOutput.addressOfArrayElement(0), + pinnedError.addressOfArrayElement(0), + inputSize, outputLength, errorLength + ); + } + } + + /** + * Java-friendly wrapper for mapping field element to BLS12-381 G2 point. + * + * @param input input data containing field element + * @param output output buffer for resulting G2 point + * @param error error message buffer + * @param inputSize size of input data + * @param outputLength size of output buffer + * @param errorLength size of error buffer + * @return result code from native function (0 = success) + */ + public static int eip2537blsMapFp2ToG2( + byte[] input, + byte[] output, + byte[] error, + int inputSize, + int outputLength, + int errorLength) { + try (PinnedObject pinnedInput = PinnedObject.create(input); + PinnedObject pinnedOutput = PinnedObject.create(output); + PinnedObject pinnedError = PinnedObject.create(error)) { + return eip2537blsMapFp2ToG2Native( + pinnedInput.addressOfArrayElement(0), + pinnedOutput.addressOfArrayElement(0), + pinnedError.addressOfArrayElement(0), + inputSize, outputLength, errorLength + ); + } + } + + /** + * Java-friendly wrapper for checking if a point is on the BLS12-381 G1 curve. + * + * @param input input data containing G1 point to validate + * @param error error message buffer + * @param inputSize size of input data + * @param errorLength size of error buffer + * @return true if point is on curve, false otherwise + */ + public static boolean eip2537G1IsOnCurve( + byte[] input, + byte[] error, + int inputSize, + int errorLength) { + try (PinnedObject pinnedInput = PinnedObject.create(input); + PinnedObject pinnedError = PinnedObject.create(error)) { + return eip2537G1IsOnCurveNative( + pinnedInput.addressOfArrayElement(0), + pinnedError.addressOfArrayElement(0), + inputSize, errorLength + ); + } + } + + /** + * Java-friendly wrapper for checking if a point is on the BLS12-381 G2 curve. + * + * @param input input data containing G2 point to validate + * @param error error message buffer + * @param inputSize size of input data + * @param errorLength size of error buffer + * @return true if point is on curve, false otherwise + */ + public static boolean eip2537G2IsOnCurve( + byte[] input, + byte[] error, + int inputSize, + int errorLength) { + try (PinnedObject pinnedInput = PinnedObject.create(input); + PinnedObject pinnedError = PinnedObject.create(error)) { + return eip2537G2IsOnCurveNative( + pinnedInput.addressOfArrayElement(0), + pinnedError.addressOfArrayElement(0), + inputSize, errorLength + ); + } + } + + /** + * Java-friendly wrapper for checking if a G1 point is in the correct subgroup. + * + * @param input input data containing G1 point to validate + * @param error error message buffer + * @param inputSize size of input data + * @param errorLength size of error buffer + * @return true if point is in subgroup, false otherwise + */ + public static boolean eip2537G1IsInSubGroup( + byte[] input, + byte[] error, + int inputSize, + int errorLength) { + try (PinnedObject pinnedInput = PinnedObject.create(input); + PinnedObject pinnedError = PinnedObject.create(error)) { + return eip2537G1IsInSubGroupNative( + pinnedInput.addressOfArrayElement(0), + pinnedError.addressOfArrayElement(0), + inputSize, errorLength + ); + } + } + + /** + * Java-friendly wrapper for checking if a G2 point is in the correct subgroup. + * + * @param input input data containing G2 point to validate + * @param error error message buffer + * @param inputSize size of input data + * @param errorLength size of error buffer + * @return true if point is in subgroup, false otherwise + */ + public static boolean eip2537G2IsInSubGroup( + byte[] input, + byte[] error, + int inputSize, + int errorLength) { + try (PinnedObject pinnedInput = PinnedObject.create(input); + PinnedObject pinnedError = PinnedObject.create(error)) { + return eip2537G2IsInSubGroupNative( + pinnedInput.addressOfArrayElement(0), + pinnedError.addressOfArrayElement(0), + inputSize, errorLength + ); + } + } + + /** + * Sets the degree of parallelism for multi-scalar multiplication operations. + * + * @param nbTasks number of parallel tasks (0 = auto-detect CPU cores) + */ + public static void setDegreeOfMSMParallelism(int nbTasks) { + degreeOfMSMParallelism = nbTasks; + } +} diff --git a/gnark/src/main/java/org/hyperledger/besu/nativelib/gnark/LibGnarkGraal.java b/gnark/src/main/java/org/hyperledger/besu/nativelib/gnark/LibGnarkGraal.java new file mode 100644 index 00000000..4d0c824e --- /dev/null +++ b/gnark/src/main/java/org/hyperledger/besu/nativelib/gnark/LibGnarkGraal.java @@ -0,0 +1,90 @@ +/* + * Copyright contributors to Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * + */ +package org.hyperledger.besu.nativelib.gnark; + +import org.graalvm.nativeimage.PinnedObject; +import org.graalvm.nativeimage.c.CContext; +import org.graalvm.nativeimage.c.function.CFunction; +import org.graalvm.nativeimage.c.type.CCharPointer; +import org.graalvm.word.WordFactory; + +import java.util.Collections; +import java.util.List; + +/** + * GraalVM native-image compatible interface to gnark static library + */ +public class LibGnarkGraal { + + @CContext(LibGnarkGraal.Directives.class) + public static class Directives implements CContext.Directives { + @Override + public List getHeaderFiles() { + return Collections.singletonList(""); + } + + @Override + public List getLibraries() { + return Collections.singletonList("gnark_jni"); + } + + @Override + public List getLibraryPaths() { + // Library paths should be configured via native-image build arguments + return Collections.emptyList(); + } + } + + @CFunction(value = "computeMimcBn254") + public static native int computeMimcBn254Native( + CCharPointer input, + int inputLength, + CCharPointer output); + + @CFunction(value = "computeMimcBls12377") + public static native int computeMimcBls12377Native( + CCharPointer input, + int inputLength, + CCharPointer output); + + /** + * Java-friendly wrapper for computeMimcBn254 + */ + public static int computeMimcBn254(byte[] input, int inputLength, byte[] output) { + try (PinnedObject pinnedInput = PinnedObject.create(input); + PinnedObject pinnedOutput = PinnedObject.create(output)) { + return computeMimcBn254Native( + pinnedInput.addressOfArrayElement(0), + inputLength, + pinnedOutput.addressOfArrayElement(0) + ); + } + } + + /** + * Java-friendly wrapper for computeMimcBls12377 + */ + public static int computeMimcBls12377(byte[] input, int inputLength, byte[] output) { + try (PinnedObject pinnedInput = PinnedObject.create(input); + PinnedObject pinnedOutput = PinnedObject.create(output)) { + return computeMimcBls12377Native( + pinnedInput.addressOfArrayElement(0), + inputLength, + pinnedOutput.addressOfArrayElement(0) + ); + } + } +} diff --git a/gradle.properties b/gradle.properties index 3768476e..5a1c848c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=1.4.1-SNAPSHOT +version=1.4.1-SNASPSHOT diff --git a/secp256k1/build.gradle b/secp256k1/build.gradle index f396f7a7..53ff3b56 100644 --- a/secp256k1/build.gradle +++ b/secp256k1/build.gradle @@ -19,10 +19,29 @@ plugins { id 'com.jfrog.artifactory' version '5.2.3' } +// Configure source sets - separate GraalVM classes from main JAR +sourceSets { + graal { + java { + srcDir 'src/main/java' + include '**/*Graal*.java' + } + } + main { + java { + exclude '**/*Graal*.java' + } + } +} + dependencies { implementation project(':common') - testImplementation 'com.google.guava:guava:31.1-jre' implementation 'net.java.dev.jna:jna:5.12.1' + + // GraalVM SDK only for graal source set + graalImplementation 'org.graalvm.sdk:graal-sdk:24.0.2' + + testImplementation 'com.google.guava:guava:31.1-jre' testImplementation 'net.java.dev.jna:jna:5.12.1' testImplementation 'junit:junit:4.13.2' testImplementation 'org.assertj:assertj-core:3.22.0' @@ -92,6 +111,58 @@ task linuxRiscv64JNILibCopy(type: Copy) { } processResources.dependsOn linuxRiscv64JNILibCopy +// Static library copy tasks (for GraalVM native-image) +task macArmStaticLibCopy(type: Copy) { + from 'secp256k1_jni/build/darwin-aarch64/lib/libsecp256k1.a' + from 'secp256k1_jni/build/darwin-aarch64/lib/libsecp256k1_ecrecover.a' + from 'secp256k1_jni/build/darwin-aarch64/lib/secp256k1_ecrecover.h' + from 'secp256k1_jni/build/darwin-aarch64/lib/secp256k1.h' + from 'secp256k1_jni/build/darwin-aarch64/lib/secp256k1_recovery.h' + into 'build/resources-static/lib/aarch64' +} + +task macStaticLibCopy(type: Copy) { + from 'secp256k1_jni/build/darwin-x86-64/lib/libsecp256k1.a' + from 'secp256k1_jni/build/darwin-x86-64/lib/libsecp256k1_ecrecover.a' + from 'secp256k1_jni/build/darwin-x86-64/lib/secp256k1_ecrecover.h' + from 'secp256k1_jni/build/darwin-x86-64/lib/secp256k1.h' + from 'secp256k1_jni/build/darwin-x86-64/lib/secp256k1_recovery.h' + into 'build/resources-static/lib/x86-64' +} + +task linuxStaticLibCopy(type: Copy) { + from 'secp256k1_jni/build/linux-gnu-x86_64/lib/libsecp256k1.a' + from 'secp256k1_jni/build/linux-gnu-x86_64/lib/libsecp256k1_ecrecover.a' + from 'secp256k1_jni/build/linux-gnu-x86_64/lib/secp256k1_ecrecover.h' + from 'secp256k1_jni/build/linux-gnu-x86_64/lib/secp256k1.h' + from 'secp256k1_jni/build/linux-gnu-x86_64/lib/secp256k1_recovery.h' + into 'build/resources-static/lib/x86-64' +} + +task linuxArm64StaticLibCopy(type: Copy) { + from 'secp256k1_jni/build/linux-gnu-aarch64/lib/libsecp256k1.a' + from 'secp256k1_jni/build/linux-gnu-aarch64/lib/libsecp256k1_ecrecover.a' + from 'secp256k1_jni/build/linux-gnu-aarch64/lib/secp256k1_ecrecover.h' + from 'secp256k1_jni/build/linux-gnu-aarch64/lib/secp256k1.h' + from 'secp256k1_jni/build/linux-gnu-aarch64/lib/secp256k1_recovery.h' + into 'build/resources-static/lib/aarch64' +} + +task linuxRiscv64StaticLibCopy(type: Copy) { + from 'secp256k1_jni/build/linux-gnu-riscv64/lib/libsecp256k1.a' + from 'secp256k1_jni/build/linux-gnu-riscv64/lib/libsecp256k1_ecrecover.a' + from 'secp256k1_jni/build/linux-gnu-riscv64/lib/secp256k1_ecrecover.h' + from 'secp256k1_jni/build/linux-gnu-riscv64/lib/secp256k1.h' + from 'secp256k1_jni/build/linux-gnu-riscv64/lib/secp256k1_recovery.h' + into 'build/resources-static/lib/riscv64' +} + +// Task to prepare static resources +task prepareStaticResources { + dependsOn macArmStaticLibCopy, macStaticLibCopy, linuxStaticLibCopy, + linuxArm64StaticLibCopy, linuxRiscv64StaticLibCopy +} + jar { archiveBaseName = 'besu-native-secp256k1' includeEmptyDirs = false @@ -118,6 +189,14 @@ task javadocJar(type: Jar, dependsOn: javadoc) { from javadoc.destinationDir } +task staticJar(type: Jar, dependsOn: [prepareStaticResources, graalClasses]) { + archiveBaseName = 'besu-native-secp256k1' + archiveClassifier = 'static' + from 'build/resources-static' + from sourceSets.graal.output + includeEmptyDirs = false +} + publishing { publications { mavenJava(MavenPublication) { @@ -128,6 +207,7 @@ publishing { from components.java artifact sourcesJar artifact javadocJar + artifact staticJar pom { name = "Besu Native - ${project.name}" diff --git a/secp256k1/secp256k1_jni/Makefile b/secp256k1/secp256k1_jni/Makefile index cae3eabe..fb89c917 100644 --- a/secp256k1/secp256k1_jni/Makefile +++ b/secp256k1/secp256k1_jni/Makefile @@ -11,7 +11,9 @@ CC = gcc CFLAGS = -Wall -Wextra -O3 -fPIC -std=c99 INCLUDES = -I$(SECP256K1_DIR)/include -I$(SECP256K1_DIR)/src LDFLAGS = -shared +LDFLAGS_STATIC = LIBS = -L$(SECP256K1_DIR)/.libs -lsecp256k1 +LIBS_STATIC = $(SECP256K1_DIR)/.libs/libsecp256k1.a # Platform-specific settings UNAME_S := $(shell uname -s) @@ -20,10 +22,12 @@ UNAME_M := $(shell uname -m) ifeq ($(UNAME_S),Darwin) PLATFORM = darwin SHARED_EXT = dylib + STATIC_EXT = a LDFLAGS += -undefined dynamic_lookup else ifeq ($(UNAME_S),Linux) PLATFORM = linux-gnu SHARED_EXT = so + STATIC_EXT = a LDFLAGS += -Wl,-soname,libsecp256k1_ecrecover.$(SHARED_EXT) else $(error Unsupported platform: $(UNAME_S)) @@ -42,13 +46,15 @@ endif TARGET_DIR = $(BUILD_DIR)/$(PLATFORM)-$(ARCH) TARGET_LIB_DIR = $(TARGET_DIR)/lib TARGET_LIB = $(TARGET_LIB_DIR)/libsecp256k1_ecrecover.$(SHARED_EXT) +TARGET_STATIC_LIB = $(TARGET_LIB_DIR)/libsecp256k1_ecrecover.$(STATIC_EXT) +TARGET_HEADER = $(TARGET_LIB_DIR)/secp256k1_ecrecover.h # Source files SOURCES = secp256k1_ecrecover.c OBJECTS = $(SOURCES:%.c=$(OBJ_DIR)/%.o) # Default target -all: $(TARGET_LIB) +all: $(TARGET_LIB) $(TARGET_STATIC_LIB) $(TARGET_LIB_DIR)/libsecp256k1.a $(TARGET_HEADER) $(TARGET_LIB_DIR)/secp256k1.h $(TARGET_LIB_DIR)/secp256k1_recovery.h # Create directories $(OBJ_DIR) $(TARGET_LIB_DIR): @@ -62,6 +68,25 @@ $(OBJ_DIR)/%.o: %.c | $(OBJ_DIR) $(TARGET_LIB): $(OBJECTS) | $(TARGET_LIB_DIR) $(CC) $(LDFLAGS) -o $@ $(OBJECTS) $(LIBS) +# Create static library (JNI wrapper only, not including libsecp256k1.a) +$(TARGET_STATIC_LIB): $(OBJECTS) | $(TARGET_LIB_DIR) + ar rcs $@ $(OBJECTS) + +# Copy libsecp256k1.a from bitcoin-core-secp256k1 +$(TARGET_LIB_DIR)/libsecp256k1.a: $(SECP256K1_DIR)/.libs/libsecp256k1.a | $(TARGET_LIB_DIR) + cp $(SECP256K1_DIR)/.libs/libsecp256k1.a $@ + +# Copy header files +$(TARGET_HEADER): secp256k1_ecrecover.h | $(TARGET_LIB_DIR) + cp secp256k1_ecrecover.h $@ + +# Copy bitcoin-core-secp256k1 headers +$(TARGET_LIB_DIR)/secp256k1.h: $(SECP256K1_DIR)/include/secp256k1.h | $(TARGET_LIB_DIR) + cp $(SECP256K1_DIR)/include/secp256k1.h $@ + +$(TARGET_LIB_DIR)/secp256k1_recovery.h: $(SECP256K1_DIR)/include/secp256k1_recovery.h | $(TARGET_LIB_DIR) + cp $(SECP256K1_DIR)/include/secp256k1_recovery.h $@ + # Build secp256k1 dependency if needed $(SECP256K1_DIR)/libsecp256k1.la: cd $(SECP256K1_DIR) && ./autogen.sh && ./configure --enable-module-recovery --enable-shared && make diff --git a/secp256k1/src/main/java/org/hyperledger/besu/nativelib/secp256k1/LibSecp256k1EcrecoverGraal.java b/secp256k1/src/main/java/org/hyperledger/besu/nativelib/secp256k1/LibSecp256k1EcrecoverGraal.java new file mode 100644 index 00000000..2c85c83e --- /dev/null +++ b/secp256k1/src/main/java/org/hyperledger/besu/nativelib/secp256k1/LibSecp256k1EcrecoverGraal.java @@ -0,0 +1,183 @@ +/* + * Copyright contributors to Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * + */ +package org.hyperledger.besu.nativelib.secp256k1; + +import org.graalvm.nativeimage.PinnedObject; +import org.graalvm.nativeimage.c.CContext; +import org.graalvm.nativeimage.c.function.CFunction; +import org.graalvm.nativeimage.c.type.CCharPointer; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * GraalVM native-image compatible interface to secp256k1_ecrecover static library. + * Provides ECRECOVER operation for Ethereum precompile (address 0x01). + * + *

This class uses GraalVM's @CFunction annotation to call native C functions + * directly from statically linked libraries, avoiding the overhead of JNA. + * + *

The native libraries required are: + *

    + *
  • libsecp256k1.a - Core Bitcoin secp256k1 library
  • + *
  • libsecp256k1_ecrecover.a - JNI wrapper for ECRECOVER
  • + *
+ */ +public class LibSecp256k1EcrecoverGraal { + + /** Size of message hash in bytes. */ + public static final int MESSAGE_HASH_SIZE = 32; + + /** Size of signature in bytes (r || s). */ + public static final int SIGNATURE_SIZE = 64; + + /** Size of recovered public key in bytes (uncompressed format). */ + public static final int PUBLIC_KEY_SIZE = 65; + + /** Private constructor to prevent instantiation of utility class. */ + private LibSecp256k1EcrecoverGraal() {} + + /** + * CContext directives for configuring GraalVM native-image compilation. + * Specifies header files and libraries required for static linking. + */ + @CContext(LibSecp256k1EcrecoverGraal.Directives.class) + public static class Directives implements CContext.Directives { + @Override + public List getHeaderFiles() { + // Specify the header files needed for compilation + return Arrays.asList( + "", + "", + "" + ); + } + + @Override + public List getLibraries() { + // Specify the static libraries to link against + // Order matters: list dependencies first, then dependents + return Arrays.asList("secp256k1", "secp256k1_ecrecover"); + } + + @Override + public List getLibraryPaths() { + // Library paths should be configured via native-image build arguments + // (-H:CLibraryPath=) + return Collections.emptyList(); + } + } + + /** + * Native ECRECOVER function wrapper. + * + *

This function combines signature parsing, public key recovery, and serialization + * into a single native call for optimal performance. + * + * @param messageHash pointer to 32-byte message hash + * @param signature pointer to 64-byte compact signature (r || s) + * @param recoveryId recovery ID (0 or 1) + * @param outputBuffer pointer to 65-byte output buffer for uncompressed public key + * @return 0 if recovery was successful, 1 otherwise + */ + @CFunction(value = "secp256k1_ecrecover_jni") + public static native int secp256k1EcrecoverNative( + CCharPointer messageHash, + CCharPointer signature, + int recoveryId, + CCharPointer outputBuffer); + + /** + * Java-friendly wrapper for ECRECOVER operation. + * + *

Recovers the public key from an ECDSA signature using the secp256k1 curve. + * This is the core operation for Ethereum transaction signature verification. + * + *

Input validation: + *

    + *
  • messageHash must be exactly 32 bytes
  • + *
  • signature must be exactly 64 bytes (r || s)
  • + *
  • recoveryId must be 0 or 1
  • + *
  • outputBuffer must be exactly 65 bytes
  • + *
+ * + *

Output format: The recovered public key is written to outputBuffer + * in uncompressed format (65 bytes: 0x04 || x || y). + * + * @param messageHash 32-byte Keccak-256 hash of the signed message + * @param signature 64-byte compact signature (r || s, each 32 bytes big-endian) + * @param recoveryId recovery ID (0 or 1) for selecting the correct public key + * @param outputBuffer 65-byte buffer to receive the recovered public key + * @return 0 if recovery was successful, 1 if recovery failed or inputs are invalid + * @throws IllegalArgumentException if input sizes are incorrect + */ + public static int secp256k1Ecrecover( + byte[] messageHash, + byte[] signature, + int recoveryId, + byte[] outputBuffer) { + + // Validate input sizes + if (messageHash.length != MESSAGE_HASH_SIZE) { + throw new IllegalArgumentException( + "Message hash must be " + MESSAGE_HASH_SIZE + " bytes, got " + messageHash.length); + } + if (signature.length != SIGNATURE_SIZE) { + throw new IllegalArgumentException( + "Signature must be " + SIGNATURE_SIZE + " bytes, got " + signature.length); + } + if (outputBuffer.length != PUBLIC_KEY_SIZE) { + throw new IllegalArgumentException( + "Output buffer must be " + PUBLIC_KEY_SIZE + " bytes, got " + outputBuffer.length); + } + if (recoveryId < 0 || recoveryId > 1) { + throw new IllegalArgumentException( + "Recovery ID must be 0 or 1, got " + recoveryId); + } + + // Call native function with pinned byte arrays + try (PinnedObject pinnedHash = PinnedObject.create(messageHash); + PinnedObject pinnedSig = PinnedObject.create(signature); + PinnedObject pinnedOutput = PinnedObject.create(outputBuffer)) { + return secp256k1EcrecoverNative( + pinnedHash.addressOfArrayElement(0), + pinnedSig.addressOfArrayElement(0), + recoveryId, + pinnedOutput.addressOfArrayElement(0) + ); + } + } + + /** + * Convenience method for ECRECOVER that allocates and returns the output buffer. + * + * @param messageHash 32-byte message hash + * @param signature 64-byte compact signature (r || s) + * @param recoveryId recovery ID (0 or 1) + * @return 65-byte recovered public key in uncompressed format, or null if recovery failed + */ + public static byte[] secp256k1EcrecoverWithAlloc( + byte[] messageHash, + byte[] signature, + int recoveryId) { + + byte[] output = new byte[PUBLIC_KEY_SIZE]; + int result = secp256k1Ecrecover(messageHash, signature, recoveryId, output); + + return result == 0 ? output : null; + } +} diff --git a/secp256k1/src/main/java/org/hyperledger/besu/nativelib/secp256k1/LibSecp256k1Graal.java b/secp256k1/src/main/java/org/hyperledger/besu/nativelib/secp256k1/LibSecp256k1Graal.java new file mode 100644 index 00000000..a12502f1 --- /dev/null +++ b/secp256k1/src/main/java/org/hyperledger/besu/nativelib/secp256k1/LibSecp256k1Graal.java @@ -0,0 +1,809 @@ +/* + * Copyright contributors to Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * + */ +package org.hyperledger.besu.nativelib.secp256k1; + +import org.graalvm.nativeimage.PinnedObject; +import org.graalvm.nativeimage.c.CContext; +import org.graalvm.nativeimage.c.function.CFunction; +import org.graalvm.nativeimage.c.type.CCharPointer; +import org.graalvm.nativeimage.c.type.CIntPointer; +import org.graalvm.word.PointerBase; +import org.graalvm.word.WordFactory; + +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * GraalVM native-image compatible interface to secp256k1 static library. + * Provides full secp256k1 API for ECDSA operations including signing, verification, + * key generation, and public key recovery. + * + *

This class uses GraalVM's @CFunction annotation to call native C functions + * directly from statically linked libraries, avoiding the overhead of JNA. + * + *

Unlike JNA, this implementation treats secp256k1 structures as opaque byte arrays, + * avoiding the need for @CStruct definitions which require struct definitions at compile time. + * + *

This class provides a simple byte-array based API. Implementing projects can + * create their own type-safe wrapper classes if desired. + * + *

The native library required is: + *

    + *
  • libsecp256k1.a - Core Bitcoin secp256k1 library with recovery module
  • + *
+ */ +public class LibSecp256k1Graal { + + // Structure sizes (opaque to us, but we need to allocate the right amount) + public static final int SECP256K1_PUBKEY_SIZE = 64; + public static final int SECP256K1_ECDSA_SIGNATURE_SIZE = 64; + public static final int SECP256K1_ECDSA_RECOVERABLE_SIGNATURE_SIZE = 65; + + // Context flags + private static final int SECP256K1_FLAGS_TYPE_CONTEXT = 1; + private static final int SECP256K1_FLAGS_TYPE_COMPRESSION = 1 << 1; + private static final int SECP256K1_FLAGS_BIT_CONTEXT_VERIFY = 1 << 8; + private static final int SECP256K1_FLAGS_BIT_CONTEXT_SIGN = 1 << 9; + private static final int SECP256K1_FLAGS_BIT_CONTEXT_DECLASSIFY = 1 << 10; + private static final int SECP256K1_FLAGS_BIT_COMPRESSION = 1 << 8; + + public static final int SECP256K1_CONTEXT_VERIFY = + SECP256K1_FLAGS_TYPE_CONTEXT | SECP256K1_FLAGS_BIT_CONTEXT_VERIFY; + public static final int SECP256K1_CONTEXT_SIGN = + SECP256K1_FLAGS_TYPE_CONTEXT | SECP256K1_FLAGS_BIT_CONTEXT_SIGN; + public static final int SECP256K1_CONTEXT_DECLASSIFY = + SECP256K1_FLAGS_TYPE_CONTEXT | SECP256K1_FLAGS_BIT_CONTEXT_DECLASSIFY; + public static final int SECP256K1_CONTEXT_NONE = SECP256K1_FLAGS_TYPE_CONTEXT; + + public static final int SECP256K1_EC_COMPRESSED = + SECP256K1_FLAGS_TYPE_COMPRESSION | SECP256K1_FLAGS_BIT_COMPRESSION; + public static final int SECP256K1_EC_UNCOMPRESSED = SECP256K1_FLAGS_TYPE_COMPRESSION; + + /** Lazy-initialized context holder to avoid static initialization issues with GraalVM. */ + private static class ContextHolder { + static final PointerBase INSTANCE = createContext(); + } + + /** + * Get the global secp256k1 context (lazily initialized). + * The context is initialized for both verification and signing operations. + * + * @return the global context + */ + public static PointerBase getContext() { + return ContextHolder.INSTANCE; + } + + /** Private constructor to prevent instantiation of utility class. */ + private LibSecp256k1Graal() {} + + /** + * CContext directives for configuring GraalVM native-image compilation. + * Specifies header files and libraries required for static linking. + */ + @CContext(LibSecp256k1Graal.Directives.class) + public static class Directives implements CContext.Directives { + @Override + public List getHeaderFiles() { + return Arrays.asList( + "", + "" + ); + } + + @Override + public List getLibraries() { + return Collections.singletonList("secp256k1"); + } + + @Override + public List getLibraryPaths() { + // Library paths should be configured via native-image build arguments + return Collections.emptyList(); + } + } + + // ============ Context Management ============ + + /** + * Create a secp256k1 context object. + * + * @param flags which parts of the context to initialize + * @return a newly created context object + */ + @CFunction(value = "secp256k1_context_create") + static native PointerBase secp256k1ContextCreate(int flags); + + /** + * Updates the context randomization to protect against side-channel leakage. + * + * @param ctx pointer to a context object + * @param seed32 pointer to a 32-byte random seed + * @return 1 if randomization successfully updated, 0 if an error occurred + */ + @CFunction(value = "secp256k1_context_randomize") + static native int secp256k1ContextRandomize(PointerBase ctx, CCharPointer seed32); + + /** + * Destroy a secp256k1 context object. + * + * @param ctx pointer to a context object + */ + @CFunction(value = "secp256k1_context_destroy") + static native void secp256k1ContextDestroy(PointerBase ctx); + + // ============ Public Key Operations ============ + + /** + * Parse a variable-length public key into the pubkey object. + * + * @param ctx a secp256k1 context object + * @param pubkey pointer to a 64-byte pubkey buffer (output) + * @param input pointer to a serialized public key + * @param inputlen length of the array pointed to by input + * @return 1 if the public key was fully valid, 0 otherwise + */ + @CFunction(value = "secp256k1_ec_pubkey_parse") + static native int secp256k1EcPubkeyParse( + PointerBase ctx, + CCharPointer pubkey, + CCharPointer input, + long inputlen); + + /** + * Serialize a pubkey object into a serialized byte sequence. + * + * @param ctx a secp256k1 context object + * @param output pointer to output array (33 or 65 bytes) + * @param outputlen pointer to output length (input/output) + * @param pubkey pointer to a 64-byte pubkey buffer + * @param flags SECP256K1_EC_COMPRESSED or SECP256K1_EC_UNCOMPRESSED + * @return 1 always + */ + @CFunction(value = "secp256k1_ec_pubkey_serialize") + static native int secp256k1EcPubkeySerialize( + PointerBase ctx, + CCharPointer output, + CIntPointer outputlen, + CCharPointer pubkey, + int flags); + + /** + * Compute the public key for a secret key. + * + * @param ctx pointer to a context object + * @param pubkey pointer to 64-byte pubkey buffer (output) + * @param seckey pointer to a 32-byte private key + * @return 1 if secret was valid, 0 if secret was invalid + */ + @CFunction(value = "secp256k1_ec_pubkey_create") + static native int secp256k1EcPubkeyCreate( + PointerBase ctx, + CCharPointer pubkey, + CCharPointer seckey); + + // ============ ECDSA Signature Operations ============ + + /** + * Parse an ECDSA signature in compact (64 bytes) format. + * + * @param ctx a secp256k1 context object + * @param sig pointer to a 64-byte signature buffer (output) + * @param input64 pointer to the 64-byte array to parse + * @return 1 when the signature could be parsed, 0 otherwise + */ + @CFunction(value = "secp256k1_ecdsa_signature_parse_compact") + static native int secp256k1EcdsaSignatureParseCompact( + PointerBase ctx, + CCharPointer sig, + CCharPointer input64); + + /** + * Convert a signature to a normalized lower-S form. + * + * @param ctx a secp256k1 context object + * @param sigout pointer to 64-byte output signature buffer (output) + * @param sigin pointer to 64-byte input signature buffer + * @return 1 if sigin was not normalized, 0 if it already was + */ + @CFunction(value = "secp256k1_ecdsa_signature_normalize") + static native int secp256k1EcdsaSignatureNormalize( + PointerBase ctx, + CCharPointer sigout, + CCharPointer sigin); + + /** + * Verify an ECDSA signature. + * + * @param ctx a secp256k1 context object + * @param sig pointer to 64-byte signature buffer + * @param msg32 the 32-byte message hash being verified + * @param pubkey pointer to 64-byte pubkey buffer + * @return 1 if correct signature, 0 if incorrect or unparseable signature + */ + @CFunction(value = "secp256k1_ecdsa_verify") + static native int secp256k1EcdsaVerify( + PointerBase ctx, + CCharPointer sig, + CCharPointer msg32, + CCharPointer pubkey); + + /** + * Create an ECDSA signature. + * + * @param ctx a secp256k1 context object, initialized for signing + * @param sig pointer to 64-byte signature buffer (output) + * @param msg32 the 32-byte message hash being signed + * @param seckey pointer to a 32-byte secret key + * @param noncefp pointer to a nonce generation function (can be null for default) + * @param ndata pointer to arbitrary data for nonce generation function (can be null) + * @return 1 if signature created, 0 if nonce generation failed or secret key invalid + */ + @CFunction(value = "secp256k1_ecdsa_sign") + static native int secp256k1EcdsaSign( + PointerBase ctx, + CCharPointer sig, + CCharPointer msg32, + CCharPointer seckey, + PointerBase noncefp, + PointerBase ndata); + + // ============ Recoverable Signature Operations ============ + + /** + * Create a recoverable ECDSA signature. + * + * @param ctx a secp256k1 context object, initialized for signing + * @param sig pointer to 65-byte recoverable signature buffer (output) + * @param msg32 the 32-byte message hash being signed + * @param seckey pointer to a 32-byte secret key + * @param noncefp pointer to a nonce generation function (can be null for default) + * @param ndata pointer to arbitrary data for nonce generation function (can be null) + * @return 1 if signature created, 0 if nonce generation failed or secret key invalid + */ + @CFunction(value = "secp256k1_ecdsa_sign_recoverable") + static native int secp256k1EcdsaSignRecoverable( + PointerBase ctx, + CCharPointer sig, + CCharPointer msg32, + CCharPointer seckey, + PointerBase noncefp, + PointerBase ndata); + + /** + * Parse a compact ECDSA signature (64 bytes + recovery id). + * + * @param ctx a secp256k1 context object + * @param sig pointer to a 65-byte recoverable signature buffer (output) + * @param input64 pointer to a 64-byte compact signature + * @param recid the recovery id (0, 1, 2 or 3) + * @return 1 when the signature could be parsed, 0 otherwise + */ + @CFunction(value = "secp256k1_ecdsa_recoverable_signature_parse_compact") + static native int secp256k1EcdsaRecoverableSignatureParseCompact( + PointerBase ctx, + CCharPointer sig, + CCharPointer input64, + int recid); + + /** + * Serialize an ECDSA signature in compact format (64 bytes + recovery id). + * + * @param ctx a secp256k1 context object + * @param output64 pointer to a 64-byte array (output) + * @param recid pointer to an integer to hold the recovery id (output) + * @param sig pointer to 65-byte recoverable signature buffer + */ + @CFunction(value = "secp256k1_ecdsa_recoverable_signature_serialize_compact") + static native void secp256k1EcdsaRecoverableSignatureSerializeCompact( + PointerBase ctx, + CCharPointer output64, + CIntPointer recid, + CCharPointer sig); + + /** + * Recover an ECDSA public key from a signature. + * + * @param ctx pointer to a context object + * @param pubkey pointer to 64-byte pubkey buffer (output) + * @param sig pointer to 65-byte recoverable signature buffer + * @param msg32 the 32-byte message hash assumed to be signed + * @return 1 if public key successfully recovered, 0 otherwise + */ + @CFunction(value = "secp256k1_ecdsa_recover") + static native int secp256k1EcdsaRecover( + PointerBase ctx, + CCharPointer pubkey, + CCharPointer sig, + CCharPointer msg32); + + // ============ Helper Methods ============ + + /** + * Create and initialize the global context with randomization. + * + * @return initialized context (non-null, may be zero/invalid on error) + */ + private static PointerBase createContext() { + try { + PointerBase context = secp256k1ContextCreate(SECP256K1_CONTEXT_VERIFY | SECP256K1_CONTEXT_SIGN); + if (context.isNull()) { + throw new RuntimeException("Failed to create secp256k1 context"); + } + + if (Boolean.parseBoolean(System.getProperty("secp256k1.randomize", "true"))) { + byte[] seed = new byte[32]; + SecureRandom.getInstanceStrong().nextBytes(seed); + try (PinnedObject pinnedSeed = PinnedObject.create(seed)) { + if (secp256k1ContextRandomize(context, pinnedSeed.addressOfArrayElement(0)) != 1) { + secp256k1ContextDestroy(context); + throw new RuntimeException("Failed to randomize secp256k1 context"); + } + } + } + return context; + } catch (final Throwable t) { + throw new RuntimeException("Failed to initialize secp256k1 context", t); + } + } + + /** + * Java-friendly wrapper for public key parsing. + * + * @param ctx context object + * @param pubkey 64-byte array to receive parsed public key + * @param input serialized public key (33 or 65 bytes) + * @return 1 if valid, 0 otherwise + */ + public static int ecPubkeyParse(PointerBase ctx, byte[] pubkey, byte[] input) { + if (pubkey.length != SECP256K1_PUBKEY_SIZE) { + throw new IllegalArgumentException("Public key buffer must be " + SECP256K1_PUBKEY_SIZE + " bytes"); + } + try (PinnedObject pinnedPubkey = PinnedObject.create(pubkey); + PinnedObject pinnedInput = PinnedObject.create(input)) { + return secp256k1EcPubkeyParse( + ctx, + pinnedPubkey.addressOfArrayElement(0), + pinnedInput.addressOfArrayElement(0), + input.length); + } + } + + /** + * Java-friendly wrapper for public key serialization. + * + * @param ctx context object + * @param pubkey 64-byte internal pubkey representation + * @param compressed true for compressed (33 bytes), false for uncompressed (65 bytes) + * @return serialized public key + */ + public static byte[] ecPubkeySerialize(PointerBase ctx, byte[] pubkey, boolean compressed) { + if (pubkey.length != SECP256K1_PUBKEY_SIZE) { + throw new IllegalArgumentException("Public key must be " + SECP256K1_PUBKEY_SIZE + " bytes"); + } + + int flags = compressed ? SECP256K1_EC_COMPRESSED : SECP256K1_EC_UNCOMPRESSED; + byte[] output = new byte[compressed ? 33 : 65]; + int[] outputLen = new int[] { output.length }; + + try (PinnedObject pinnedPubkey = PinnedObject.create(pubkey); + PinnedObject pinnedOutput = PinnedObject.create(output); + PinnedObject pinnedOutputLen = PinnedObject.create(outputLen)) { + secp256k1EcPubkeySerialize( + ctx, + pinnedOutput.addressOfArrayElement(0), + (CIntPointer) pinnedOutputLen.addressOfArrayElement(0), + pinnedPubkey.addressOfArrayElement(0), + flags); + } + + return output; + } + + /** + * Java-friendly wrapper for signature verification. + * + * @param ctx context object + * @param signature 64-byte compact signature + * @param message 32-byte message hash + * @param pubkey 64-byte internal pubkey representation + * @return 1 if valid signature, 0 otherwise + */ + public static int ecdsaVerify(PointerBase ctx, byte[] signature, byte[] message, byte[] pubkey) { + if (signature.length != 64) { + throw new IllegalArgumentException("Signature must be 64 bytes"); + } + if (message.length != 32) { + throw new IllegalArgumentException("Message must be 32 bytes"); + } + if (pubkey.length != SECP256K1_PUBKEY_SIZE) { + throw new IllegalArgumentException("Public key must be " + SECP256K1_PUBKEY_SIZE + " bytes"); + } + + byte[] sig = new byte[SECP256K1_ECDSA_SIGNATURE_SIZE]; + try (PinnedObject pinnedSig = PinnedObject.create(sig); + PinnedObject pinnedSignature = PinnedObject.create(signature); + PinnedObject pinnedMessage = PinnedObject.create(message); + PinnedObject pinnedPubkey = PinnedObject.create(pubkey)) { + + // Parse the signature + int parseResult = secp256k1EcdsaSignatureParseCompact( + ctx, + pinnedSig.addressOfArrayElement(0), + pinnedSignature.addressOfArrayElement(0)); + + if (parseResult != 1) { + return 0; + } + + // Verify the signature + return secp256k1EcdsaVerify( + ctx, + pinnedSig.addressOfArrayElement(0), + pinnedMessage.addressOfArrayElement(0), + pinnedPubkey.addressOfArrayElement(0)); + } + } + + /** + * Java-friendly wrapper for public key recovery from signature. + * + * @param ctx context object + * @param signature 64-byte compact signature (r || s) + * @param message 32-byte message hash + * @param recid recovery id (0 or 1) + * @return 64-byte recovered public key, or null if recovery failed + */ + public static byte[] ecdsaRecover(PointerBase ctx, byte[] signature, byte[] message, int recid) { + if (signature.length != 64) { + throw new IllegalArgumentException("Signature must be 64 bytes"); + } + if (message.length != 32) { + throw new IllegalArgumentException("Message must be 32 bytes"); + } + + byte[] recoverableSig = new byte[SECP256K1_ECDSA_RECOVERABLE_SIGNATURE_SIZE]; + byte[] pubkey = new byte[SECP256K1_PUBKEY_SIZE]; + + try (PinnedObject pinnedRecSig = PinnedObject.create(recoverableSig); + PinnedObject pinnedSignature = PinnedObject.create(signature); + PinnedObject pinnedMessage = PinnedObject.create(message); + PinnedObject pinnedPubkey = PinnedObject.create(pubkey)) { + + // Parse recoverable signature + int parseResult = secp256k1EcdsaRecoverableSignatureParseCompact( + ctx, + pinnedRecSig.addressOfArrayElement(0), + pinnedSignature.addressOfArrayElement(0), + recid); + + if (parseResult != 1) { + return null; + } + + // Recover public key + int recoverResult = secp256k1EcdsaRecover( + ctx, + pinnedPubkey.addressOfArrayElement(0), + pinnedRecSig.addressOfArrayElement(0), + pinnedMessage.addressOfArrayElement(0)); + + return recoverResult == 1 ? pubkey : null; + } + } + + /** + * Java-friendly wrapper for public key creation from private key. + * + * @param ctx context object + * @param seckey 32-byte private key + * @return 64-byte internal public key representation, or null if invalid + */ + public static byte[] ecPubkeyCreate(PointerBase ctx, byte[] seckey) { + if (seckey.length != 32) { + throw new IllegalArgumentException("Private key must be 32 bytes"); + } + + byte[] pubkey = new byte[SECP256K1_PUBKEY_SIZE]; + try (PinnedObject pinnedPubkey = PinnedObject.create(pubkey); + PinnedObject pinnedSeckey = PinnedObject.create(seckey)) { + int result = secp256k1EcPubkeyCreate( + ctx, + pinnedPubkey.addressOfArrayElement(0), + pinnedSeckey.addressOfArrayElement(0)); + + return result == 1 ? pubkey : null; + } + } + + /** + * Java-friendly wrapper for signature parsing. + * + * @param ctx context object + * @param signature 64-byte array to receive parsed signature + * @param compact64 64-byte compact signature to parse + * @return 1 if valid, 0 otherwise + */ + public static int ecdsaSignatureParseCompact(PointerBase ctx, byte[] signature, byte[] compact64) { + if (signature.length != SECP256K1_ECDSA_SIGNATURE_SIZE) { + throw new IllegalArgumentException("Signature buffer must be " + SECP256K1_ECDSA_SIGNATURE_SIZE + " bytes"); + } + if (compact64.length != 64) { + throw new IllegalArgumentException("Compact signature must be 64 bytes"); + } + try (PinnedObject pinnedSignature = PinnedObject.create(signature); + PinnedObject pinnedCompact = PinnedObject.create(compact64)) { + return secp256k1EcdsaSignatureParseCompact( + ctx, + pinnedSignature.addressOfArrayElement(0), + pinnedCompact.addressOfArrayElement(0)); + } + } + + /** + * Java-friendly wrapper for signature serialization. + * + * @param ctx context object + * @param signature 64-byte internal signature representation + * @return 64-byte compact signature + */ + public static byte[] ecdsaSignatureSerializeCompact(PointerBase ctx, byte[] signature) { + if (signature.length != SECP256K1_ECDSA_SIGNATURE_SIZE) { + throw new IllegalArgumentException("Signature must be " + SECP256K1_ECDSA_SIGNATURE_SIZE + " bytes"); + } + + byte[] compact = new byte[64]; + try (PinnedObject pinnedSignature = PinnedObject.create(signature); + PinnedObject pinnedCompact = PinnedObject.create(compact)) { + secp256k1EcdsaSignatureSerializeCompact( + ctx, + pinnedCompact.addressOfArrayElement(0), + pinnedSignature.addressOfArrayElement(0)); + } + return compact; + } + + /** + * Serialize an ECDSA signature in compact format (64 bytes). + * + * @param ctx a secp256k1 context object + * @param output64 pointer to a 64-byte array (output) + * @param sig pointer to 64-byte signature buffer + */ + @CFunction(value = "secp256k1_ecdsa_signature_serialize_compact") + static native void secp256k1EcdsaSignatureSerializeCompact( + PointerBase ctx, + CCharPointer output64, + CCharPointer sig); + + /** + * Java-friendly wrapper for signature normalization. + * + * @param ctx context object + * @param normalized 64-byte array to receive normalized signature + * @param signature 64-byte internal signature representation + * @return 1 if signature was not normalized, 0 if it already was + */ + public static int ecdsaSignatureNormalize(PointerBase ctx, byte[] normalized, byte[] signature) { + if (normalized.length != SECP256K1_ECDSA_SIGNATURE_SIZE) { + throw new IllegalArgumentException("Normalized buffer must be " + SECP256K1_ECDSA_SIGNATURE_SIZE + " bytes"); + } + if (signature.length != SECP256K1_ECDSA_SIGNATURE_SIZE) { + throw new IllegalArgumentException("Signature must be " + SECP256K1_ECDSA_SIGNATURE_SIZE + " bytes"); + } + try (PinnedObject pinnedNormalized = PinnedObject.create(normalized); + PinnedObject pinnedSignature = PinnedObject.create(signature)) { + return secp256k1EcdsaSignatureNormalize( + ctx, + pinnedNormalized.addressOfArrayElement(0), + pinnedSignature.addressOfArrayElement(0)); + } + } + + /** + * Java-friendly wrapper for recoverable signature parsing. + * + * @param ctx context object + * @param recoverableSignature 65-byte array to receive parsed signature + * @param compact64 64-byte compact signature to parse + * @param recoveryId recovery ID (0-3) + * @return 1 if valid, 0 otherwise + */ + public static int ecdsaRecoverableSignatureParseCompact( + PointerBase ctx, + byte[] recoverableSignature, + byte[] compact64, + int recoveryId) { + if (recoverableSignature.length != SECP256K1_ECDSA_RECOVERABLE_SIGNATURE_SIZE) { + throw new IllegalArgumentException( + "Recoverable signature buffer must be " + SECP256K1_ECDSA_RECOVERABLE_SIGNATURE_SIZE + " bytes"); + } + if (compact64.length != 64) { + throw new IllegalArgumentException("Compact signature must be 64 bytes"); + } + try (PinnedObject pinnedRecSig = PinnedObject.create(recoverableSignature); + PinnedObject pinnedCompact = PinnedObject.create(compact64)) { + return secp256k1EcdsaRecoverableSignatureParseCompact( + ctx, + pinnedRecSig.addressOfArrayElement(0), + pinnedCompact.addressOfArrayElement(0), + recoveryId); + } + } + + /** + * Java-friendly wrapper for recoverable signature serialization. + * + * @param ctx context object + * @param compact64 64-byte array to receive compact signature (output) + * @param recoveryId single-element array to receive recovery ID (output) + * @param recoverableSignature 65-byte internal recoverable signature representation + */ + public static void ecdsaRecoverableSignatureSerializeCompact( + PointerBase ctx, + byte[] compact64, + int[] recoveryId, + byte[] recoverableSignature) { + if (compact64.length != 64) { + throw new IllegalArgumentException("Compact signature buffer must be 64 bytes"); + } + if (recoveryId.length != 1) { + throw new IllegalArgumentException("Recovery ID array must have length 1"); + } + if (recoverableSignature.length != SECP256K1_ECDSA_RECOVERABLE_SIGNATURE_SIZE) { + throw new IllegalArgumentException( + "Recoverable signature must be " + SECP256K1_ECDSA_RECOVERABLE_SIGNATURE_SIZE + " bytes"); + } + try (PinnedObject pinnedCompact = PinnedObject.create(compact64); + PinnedObject pinnedRecId = PinnedObject.create(recoveryId); + PinnedObject pinnedRecSig = PinnedObject.create(recoverableSignature)) { + secp256k1EcdsaRecoverableSignatureSerializeCompact( + ctx, + pinnedCompact.addressOfArrayElement(0), + (CIntPointer) pinnedRecId.addressOfArrayElement(0), + pinnedRecSig.addressOfArrayElement(0)); + } + } + + /** + * Java-friendly wrapper for creating a recoverable ECDSA signature. + * Uses the default nonce generation function (RFC 6979). + * + * @param ctx context object (must be initialized with SECP256K1_CONTEXT_SIGN) + * @param seckey 32-byte private key + * @param message 32-byte message hash + * @return 65-byte signature (64 bytes compact signature + 1 byte recovery id), or null if signing failed + */ + public static byte[] ecdsaSignRecoverable(PointerBase ctx, byte[] seckey, byte[] message) { + if (seckey.length != 32) { + throw new IllegalArgumentException("Secret key must be 32 bytes"); + } + if (message.length != 32) { + throw new IllegalArgumentException("Message must be 32 bytes"); + } + + byte[] recSig = new byte[SECP256K1_ECDSA_RECOVERABLE_SIGNATURE_SIZE]; + try (PinnedObject pinnedRecSig = PinnedObject.create(recSig); + PinnedObject pinnedMessage = PinnedObject.create(message); + PinnedObject pinnedSeckey = PinnedObject.create(seckey)) { + + int result = secp256k1EcdsaSignRecoverable( + ctx, + pinnedRecSig.addressOfArrayElement(0), + pinnedMessage.addressOfArrayElement(0), + pinnedSeckey.addressOfArrayElement(0), + WordFactory.nullPointer(), // use default nonce function + WordFactory.nullPointer()); // no extra nonce data + + if (result != 1) { + return null; + } + + // Serialize to compact format with recovery id + byte[] compact64 = new byte[64]; + int[] recid = new int[1]; + try (PinnedObject pinnedCompact = PinnedObject.create(compact64); + PinnedObject pinnedRecid = PinnedObject.create(recid)) { + + secp256k1EcdsaRecoverableSignatureSerializeCompact( + ctx, + pinnedCompact.addressOfArrayElement(0), + (CIntPointer) pinnedRecid.addressOfArrayElement(0), + pinnedRecSig.addressOfArrayElement(0)); + + // Return 65 bytes: 64-byte compact + 1-byte recid + byte[] result65 = new byte[65]; + System.arraycopy(compact64, 0, result65, 0, 64); + result65[64] = (byte) recid[0]; + return result65; + } + } + } + + /** + * Convert a recoverable ECDSA signature to a regular ECDSA signature. + * + * @param ctx a secp256k1 context object + * @param sig pointer to 64-byte signature buffer (output) + * @param recoverableSig pointer to 65-byte recoverable signature buffer + */ + @CFunction(value = "secp256k1_ecdsa_recoverable_signature_convert") + static native void secp256k1EcdsaRecoverableSignatureConvert( + PointerBase ctx, + CCharPointer sig, + CCharPointer recoverableSig); + + /** + * Java-friendly wrapper for converting recoverable signature to regular signature. + * + * @param ctx context object + * @param signature 64-byte array to receive regular signature (output) + * @param recoverableSignature 65-byte internal recoverable signature representation + */ + public static void ecdsaRecoverableSignatureConvert( + PointerBase ctx, + byte[] signature, + byte[] recoverableSignature) { + if (signature.length != SECP256K1_ECDSA_SIGNATURE_SIZE) { + throw new IllegalArgumentException("Signature buffer must be " + SECP256K1_ECDSA_SIGNATURE_SIZE + " bytes"); + } + if (recoverableSignature.length != SECP256K1_ECDSA_RECOVERABLE_SIGNATURE_SIZE) { + throw new IllegalArgumentException( + "Recoverable signature must be " + SECP256K1_ECDSA_RECOVERABLE_SIGNATURE_SIZE + " bytes"); + } + try (PinnedObject pinnedSig = PinnedObject.create(signature); + PinnedObject pinnedRecSig = PinnedObject.create(recoverableSignature)) { + secp256k1EcdsaRecoverableSignatureConvert( + ctx, + pinnedSig.addressOfArrayElement(0), + pinnedRecSig.addressOfArrayElement(0)); + } + } + + /** + * Java-friendly wrapper for public key recovery from recoverable signature. + * + * @param ctx context object + * @param recoverableSignature 65-byte internal recoverable signature representation + * @param message 32-byte message hash + * @return 64-byte recovered public key, or null if recovery failed + */ + public static byte[] ecdsaRecover(PointerBase ctx, byte[] recoverableSignature, byte[] message) { + if (recoverableSignature.length != SECP256K1_ECDSA_RECOVERABLE_SIGNATURE_SIZE) { + throw new IllegalArgumentException( + "Recoverable signature must be " + SECP256K1_ECDSA_RECOVERABLE_SIGNATURE_SIZE + " bytes"); + } + if (message.length != 32) { + throw new IllegalArgumentException("Message must be 32 bytes"); + } + + byte[] pubkey = new byte[SECP256K1_PUBKEY_SIZE]; + try (PinnedObject pinnedPubkey = PinnedObject.create(pubkey); + PinnedObject pinnedRecSig = PinnedObject.create(recoverableSignature); + PinnedObject pinnedMessage = PinnedObject.create(message)) { + + int recoverResult = secp256k1EcdsaRecover( + ctx, + pinnedPubkey.addressOfArrayElement(0), + pinnedRecSig.addressOfArrayElement(0), + pinnedMessage.addressOfArrayElement(0)); + + return recoverResult == 1 ? pubkey : null; + } + } +} From c0b9e1aec52d35e40f075d3eea4bb5c4970660e3 Mon Sep 17 00:00:00 2001 From: garyschulte Date: Wed, 15 Oct 2025 17:08:50 -0700 Subject: [PATCH 2/4] static artifacts for boringssl p256_verify Signed-off-by: garyschulte --- boringssl/boringssl_jni/Makefile | 17 ++- boringssl/build.gradle | 68 +++++++++++ .../boringssl/BoringSSLPrecompiles.java | 80 +++---------- .../boringssl/BoringSSLPrecompilesCommon.java | 109 +++++++++++++++++ .../boringssl/BoringSSLPrecompilesGraal.java | 111 ++++++++++++++++++ build.sh | 13 +- 6 files changed, 329 insertions(+), 69 deletions(-) create mode 100644 boringssl/src/main/java/org/hyperledger/besu/nativelib/boringssl/BoringSSLPrecompilesCommon.java create mode 100644 boringssl/src/main/java/org/hyperledger/besu/nativelib/boringssl/BoringSSLPrecompilesGraal.java diff --git a/boringssl/boringssl_jni/Makefile b/boringssl/boringssl_jni/Makefile index 2fc099e7..ca50887b 100644 --- a/boringssl/boringssl_jni/Makefile +++ b/boringssl/boringssl_jni/Makefile @@ -7,12 +7,16 @@ ifeq ($(UNAME_S),Darwin) LIB_EXT = dylib JNI_OS_DIR = darwin CC ?= clang - LDFLAGS = -dynamiclib -undefined dynamic_lookup + # Use MACOSX_DEPLOYMENT_TARGET from environment or default to 11.0 (first Apple Silicon support) + MACOSX_DEPLOYMENT_TARGET ?= 11.0 + LDFLAGS = -dynamiclib -undefined dynamic_lookup -mmacosx-version-min=$(MACOSX_DEPLOYMENT_TARGET) + CFLAGS_EXTRA = -mmacosx-version-min=$(MACOSX_DEPLOYMENT_TARGET) else LIB_EXT = so JNI_OS_DIR = linux CC ?= gcc LDFLAGS = -shared + CFLAGS_EXTRA = endif # Directories @@ -22,25 +26,32 @@ BSSL_BUILD_DIR = $(BSSL_DIR)/build INCLUDES = -I$(BSSL_DIR)/include -I$(JAVA_HOME)/include -I$(JAVA_HOME)/include/$(JNI_OS_DIR) # Flags -CFLAGS = -O2 -fPIC $(INCLUDES) +CFLAGS = -O2 -fPIC $(INCLUDES) $(CFLAGS_EXTRA) LIBS = $(BSSL_BUILD_DIR)/libcrypto.a # Files LIB_NAME = libboringssl_precompiles.$(LIB_EXT) +STATIC_LIB_NAME = libboringssl_precompiles.a SRCS = p256_verify.c ecrecover.c OBJS = $(SRCS:.c=.o) BUILD_OBJS = $(addprefix $(BUILD_DIR)/, $(OBJS)) BUILD_LIB = $(BUILD_DIR)/$(LIB_NAME) +BUILD_STATIC_LIB = $(BUILD_DIR)/$(STATIC_LIB_NAME) # Default target -all: $(BUILD_DIR) $(BUILD_LIB) +all: $(BUILD_DIR) $(BUILD_LIB) $(BUILD_STATIC_LIB) $(BUILD_DIR): mkdir -p $@ +# Dynamic library $(BUILD_LIB): $(BUILD_OBJS) $(CC) $(LDFLAGS) -o $@ $^ $(LIBS) +# Static library +$(BUILD_STATIC_LIB): $(BUILD_OBJS) + ar rcs $@ $^ $(LIBS) + $(BUILD_DIR)/%.o: %.c | $(BUILD_DIR) $(CC) $(CFLAGS) -c $< -o $@ diff --git a/boringssl/build.gradle b/boringssl/build.gradle index 625a26b3..1fe10725 100644 --- a/boringssl/build.gradle +++ b/boringssl/build.gradle @@ -19,9 +19,31 @@ plugins { id 'com.jfrog.artifactory' version '5.2.3' } +// Configure source sets - separate GraalVM classes from main JAR +sourceSets { + graal { + java { + srcDir 'src/main/java' + include '**/*Graal*.java' + include '**/BoringSSLPrecompilesCommon.java' + } + // Graal source set needs access to main source set classes + compileClasspath += sourceSets.main.output + } + main { + java { + exclude '**/*Graal*.java' + } + } +} + dependencies { implementation 'net.java.dev.jna:jna:5.12.1' implementation project(':common') + + // GraalVM SDK only for graal source set + graalImplementation 'org.graalvm.sdk:graal-sdk:24.0.2' + graalImplementation project(':common') testImplementation 'com.google.guava:guava:31.1-jre' testImplementation 'net.java.dev.jna:jna:5.12.1' testImplementation 'io.consensys.tuweni:tuweni-bytes:2.7.1' @@ -61,6 +83,43 @@ task linuxRiscv64LibCopy(type: Copy) { } processResources.dependsOn linuxRiscv64LibCopy +// Static library copy tasks (for GraalVM native-image) +task macArmStaticLibCopy(type: Copy) { + from 'build/darwin-aarch64/lib/libboringssl_precompiles.a' + from 'boringssl_jni/p256_verify.h' + into 'build/resources-static/lib/aarch64' +} + +task macStaticLibCopy(type: Copy) { + from 'build/darwin-x86-64/lib/libboringssl_precompiles.a' + from 'boringssl_jni/p256_verify.h' + into 'build/resources-static/lib/x86-64' +} + +task linuxStaticLibCopy(type: Copy) { + from 'build/linux-gnu-x86_64/lib/libboringssl_precompiles.a' + from 'boringssl_jni/p256_verify.h' + into 'build/resources-static/lib/x86-64' +} + +task linuxArm64StaticLibCopy(type: Copy) { + from 'build/linux-gnu-aarch64/lib/libboringssl_precompiles.a' + from 'boringssl_jni/p256_verify.h' + into 'build/resources-static/lib/aarch64' +} + +task linuxRiscv64StaticLibCopy(type: Copy) { + from 'build/linux-gnu-riscv64/lib/libboringssl_precompiles.a' + from 'boringssl_jni/p256_verify.h' + into 'build/resources-static/lib/riscv64' +} + +// Task to prepare static resources +task prepareStaticResources { + dependsOn macArmStaticLibCopy, macStaticLibCopy, linuxStaticLibCopy, + linuxArm64StaticLibCopy, linuxRiscv64StaticLibCopy +} + jar { @@ -89,6 +148,14 @@ task javadocJar(type: Jar, dependsOn: javadoc) { from javadoc.destinationDir } +task staticJar(type: Jar, dependsOn: [prepareStaticResources, graalClasses]) { + archiveBaseName = 'besu-native-boringssl' + archiveClassifier = 'static' + from 'build/resources-static' + from sourceSets.graal.output + includeEmptyDirs = false +} + publishing { publications { @@ -100,6 +167,7 @@ publishing { from components.java artifact sourcesJar artifact javadocJar + artifact staticJar pom { name = "Besu Native - ${project.name}" diff --git a/boringssl/src/main/java/org/hyperledger/besu/nativelib/boringssl/BoringSSLPrecompiles.java b/boringssl/src/main/java/org/hyperledger/besu/nativelib/boringssl/BoringSSLPrecompiles.java index b67cd80a..bfef7243 100644 --- a/boringssl/src/main/java/org/hyperledger/besu/nativelib/boringssl/BoringSSLPrecompiles.java +++ b/boringssl/src/main/java/org/hyperledger/besu/nativelib/boringssl/BoringSSLPrecompiles.java @@ -17,8 +17,6 @@ import org.hyperledger.besu.nativelib.common.BesuNativeLibraryLoader; -import java.math.BigInteger; -import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Optional; @@ -26,9 +24,9 @@ public class BoringSSLPrecompiles { public static final boolean ENABLED; - public static final int STATUS_SUCCESS = 0; - public static final int STATUS_FAIL = 1; - public static final int STATUS_ERROR = 2; + public static final int STATUS_SUCCESS = BoringSSLPrecompilesCommon.STATUS_SUCCESS; + public static final int STATUS_FAIL = BoringSSLPrecompilesCommon.STATUS_FAIL; + public static final int STATUS_ERROR = BoringSSLPrecompilesCommon.STATUS_ERROR; static { @@ -62,18 +60,13 @@ static native int ecrecover_r1( - // Wrapper result classes - public static class P256VerifyResult { - public final int status; - public final String error; - + // Wrapper result classes - use common implementations + public static class P256VerifyResult extends BoringSSLPrecompilesCommon.P256VerifyResult { public P256VerifyResult(final int status, final String message) { - this.status = status; - this.error = message; + super(status, message); } } - public record ECRecoverResult( int status, Optional publicKey, @@ -81,7 +74,7 @@ public record ECRecoverResult( // Safe, wrapped version of the native calls - final static int ERROR_BUF_SIZE = 256; + final static int ERROR_BUF_SIZE = BoringSSLPrecompilesCommon.ERROR_BUF_SIZE; public static P256VerifyResult p256Verify(final byte[] input, final int inputLength) { @@ -107,71 +100,32 @@ public static P256VerifyResult p256Verify(final byte[] input, final int inputLen uncompressedPubKey, uncompressedPubKey.length, errorBuf, ERROR_BUF_SIZE); - return new P256VerifyResult(status, bytesToNullTermString(errorBuf)); + return new P256VerifyResult(status, BoringSSLPrecompilesCommon.bytesToNullTermString(errorBuf)); } - // secp256r1 curve order - private static final BigInteger SECP256R1_ORDER = - new BigInteger("FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551", 16); - public static ECRecoverResult ecrecover(final byte[] hash, final byte[] sig, final int recovery_id) { - // Validate signature length - if (sig == null || sig.length != 64) { - return new ECRecoverResult(STATUS_ERROR, Optional.empty(), - Optional.of("invalid signature length")); - } - - if (hash == null || hash.length != 32) { - return new ECRecoverResult(STATUS_ERROR, Optional.empty(), - Optional.of("invalid hash length")); - } - byte[] errorBuf = new byte[ERROR_BUF_SIZE]; - - // Extract r and s values - byte[] rBytes = new byte[32]; - byte[] sBytes = new byte[32]; - System.arraycopy(sig, 0, rBytes, 0, 32); - System.arraycopy(sig, 32, sBytes, 0, 32); - - BigInteger r = new BigInteger(1, rBytes); - BigInteger s = new BigInteger(1, sBytes); - // Validate r and s are in range [1, n-1] before calling native method - if (r.equals(BigInteger.ZERO) || r.compareTo(SECP256R1_ORDER) >= 0) { - return new ECRecoverResult(STATUS_ERROR, Optional.empty(), - Optional.of("invalid signature r value")); + // Validate input using common validation logic + Optional validationError = + BoringSSLPrecompilesCommon.validateEcrecoverInput(hash, sig, recovery_id); + if (validationError.isPresent()) { + BoringSSLPrecompilesCommon.ECRecoverResult err = validationError.get(); + return new ECRecoverResult(err.status(), err.publicKey(), err.error()); } - if (s.equals(BigInteger.ZERO) || s.compareTo(SECP256R1_ORDER) >= 0) { - return new ECRecoverResult(STATUS_ERROR, Optional.empty(), - Optional.of("invalid signature s value")); - } - - if (recovery_id < 0 || recovery_id > 1) { - return new ECRecoverResult(STATUS_ERROR, Optional.empty(), - Optional.of("invalid recovery id " + recovery_id + " is not 0 or 1")); - } + byte[] errorBuf = new byte[ERROR_BUF_SIZE]; byte[] output = new byte[65]; byte[] error_buf = new byte[ERROR_BUF_SIZE]; int status = ecrecover_r1(hash, hash.length, sig, sig.length, recovery_id, output, output.length, errorBuf, ERROR_BUF_SIZE); - if (status == 0) { + if (status == STATUS_SUCCESS) { return new ECRecoverResult(status, Optional.of(output), Optional.empty()); } else { - String errorMessage = bytesToNullTermString(error_buf); + String errorMessage = BoringSSLPrecompilesCommon.bytesToNullTermString(error_buf); return new ECRecoverResult(status, Optional.empty(), Optional.of(errorMessage)); } } - - static String bytesToNullTermString(final byte[] buffer) { - int nullTerminator = 0; - while (nullTerminator < buffer.length && buffer[nullTerminator] != 0) { - nullTerminator++; - } - return new String(buffer, 0, nullTerminator, StandardCharsets.UTF_8); - } - } diff --git a/boringssl/src/main/java/org/hyperledger/besu/nativelib/boringssl/BoringSSLPrecompilesCommon.java b/boringssl/src/main/java/org/hyperledger/besu/nativelib/boringssl/BoringSSLPrecompilesCommon.java new file mode 100644 index 00000000..8eb8ec87 --- /dev/null +++ b/boringssl/src/main/java/org/hyperledger/besu/nativelib/boringssl/BoringSSLPrecompilesCommon.java @@ -0,0 +1,109 @@ +/* + * Copyright contributors to Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * + */ +package org.hyperledger.besu.nativelib.boringssl; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.util.Optional; + +/** + * Common logic shared between JNA and GraalVM implementations of BoringSSL precompiles + */ +public class BoringSSLPrecompilesCommon { + + public static final int STATUS_SUCCESS = 0; + public static final int STATUS_FAIL = 1; + public static final int STATUS_ERROR = 2; + + public static final int ERROR_BUF_SIZE = 256; + + // secp256r1 curve order + public static final BigInteger SECP256R1_ORDER = + new BigInteger("FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551", 16); + + // Wrapper result classes + public static class P256VerifyResult { + public final int status; + public final String error; + + public P256VerifyResult(final int status, final String message) { + this.status = status; + this.error = message; + } + } + + public record ECRecoverResult( + int status, + Optional publicKey, + Optional error) {} + + /** + * Validates ecrecover input parameters + * @return Optional containing error result if validation fails, empty if valid + */ + public static Optional validateEcrecoverInput( + final byte[] hash, final byte[] sig, final int recovery_id) { + + // Validate signature length + if (sig == null || sig.length != 64) { + return Optional.of(new ECRecoverResult(STATUS_ERROR, Optional.empty(), + Optional.of("invalid signature length"))); + } + + if (hash == null || hash.length != 32) { + return Optional.of(new ECRecoverResult(STATUS_ERROR, Optional.empty(), + Optional.of("invalid hash length"))); + } + + // Extract r and s values + byte[] rBytes = new byte[32]; + byte[] sBytes = new byte[32]; + System.arraycopy(sig, 0, rBytes, 0, 32); + System.arraycopy(sig, 32, sBytes, 0, 32); + + BigInteger r = new BigInteger(1, rBytes); + BigInteger s = new BigInteger(1, sBytes); + + // Validate r and s are in range [1, n-1] before calling native method + if (r.equals(BigInteger.ZERO) || r.compareTo(SECP256R1_ORDER) >= 0) { + return Optional.of(new ECRecoverResult(STATUS_ERROR, Optional.empty(), + Optional.of("invalid signature r value"))); + } + + if (s.equals(BigInteger.ZERO) || s.compareTo(SECP256R1_ORDER) >= 0) { + return Optional.of(new ECRecoverResult(STATUS_ERROR, Optional.empty(), + Optional.of("invalid signature s value"))); + } + + if (recovery_id < 0 || recovery_id > 1) { + return Optional.of(new ECRecoverResult(STATUS_ERROR, Optional.empty(), + Optional.of("invalid recovery id " + recovery_id + " is not 0 or 1"))); + } + + return Optional.empty(); + } + + /** + * Converts a null-terminated byte buffer to a Java String + */ + public static String bytesToNullTermString(final byte[] buffer) { + int nullTerminator = 0; + while (nullTerminator < buffer.length && buffer[nullTerminator] != 0) { + nullTerminator++; + } + return new String(buffer, 0, nullTerminator, StandardCharsets.UTF_8); + } +} diff --git a/boringssl/src/main/java/org/hyperledger/besu/nativelib/boringssl/BoringSSLPrecompilesGraal.java b/boringssl/src/main/java/org/hyperledger/besu/nativelib/boringssl/BoringSSLPrecompilesGraal.java new file mode 100644 index 00000000..5c4ffd07 --- /dev/null +++ b/boringssl/src/main/java/org/hyperledger/besu/nativelib/boringssl/BoringSSLPrecompilesGraal.java @@ -0,0 +1,111 @@ +/* + * Copyright contributors to Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * + */ +package org.hyperledger.besu.nativelib.boringssl; + +import org.graalvm.nativeimage.PinnedObject; +import org.graalvm.nativeimage.c.CContext; +import org.graalvm.nativeimage.c.function.CFunction; +import org.graalvm.nativeimage.c.type.CCharPointer; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +/** + * GraalVM native-image compatible interface to BoringSSL static library + * + * Note: This implementation only includes p256_verify, which is required for Ethereum mainnet. + * The ecrecover_r1 function is not included as it is not necessary for mainnet configuration + * and remains available in the standard JNA-based implementation if needed. + */ +public class BoringSSLPrecompilesGraal { + + public static final int STATUS_SUCCESS = BoringSSLPrecompilesCommon.STATUS_SUCCESS; + public static final int STATUS_FAIL = BoringSSLPrecompilesCommon.STATUS_FAIL; + public static final int STATUS_ERROR = BoringSSLPrecompilesCommon.STATUS_ERROR; + + @CContext(BoringSSLPrecompilesGraal.Directives.class) + public static class Directives implements CContext.Directives { + @Override + public List getHeaderFiles() { + return Collections.singletonList(""); + } + + @Override + public List getLibraries() { + return Collections.singletonList("boringssl_precompiles"); + } + + @Override + public List getLibraryPaths() { + // Library paths should be configured via native-image build arguments + return Collections.emptyList(); + } + } + + @CFunction(value = "p256_verify") + public static native int p256VerifyNative( + CCharPointer dataHash, int dataHashLength, + CCharPointer signatureR, int signatureRLength, + CCharPointer signatureS, int signatureSLength, + CCharPointer publicKeyData, int publicKeyDataLength, + CCharPointer errorMessageBuf, int errorMessageBufLen); + + // Wrapper result class - use common implementation + public static class P256VerifyResult extends BoringSSLPrecompilesCommon.P256VerifyResult { + public P256VerifyResult(final int status, final String message) { + super(status, message); + } + } + + final static int ERROR_BUF_SIZE = BoringSSLPrecompilesCommon.ERROR_BUF_SIZE; + + /** + * Java-friendly wrapper for p256_verify + */ + public static P256VerifyResult p256Verify(final byte[] input, final int inputLength) { + if (inputLength != 160) { + return new P256VerifyResult(STATUS_ERROR, "incorrect input size"); + } + + byte[] dataHash = Arrays.copyOfRange(input, 0, 32); + byte[] signatureR = Arrays.copyOfRange(input, 32, 64); + byte[] signatureS = Arrays.copyOfRange(input, 64, 96); + byte[] uncompressedPubKey = new byte[65]; + // uncompressed point prefix + uncompressedPubKey[0] = 0x04; + System.arraycopy(input, 96, uncompressedPubKey, 1, 64); + + byte[] errorBuf = new byte[ERROR_BUF_SIZE]; + + try (PinnedObject pinnedDataHash = PinnedObject.create(dataHash); + PinnedObject pinnedSignatureR = PinnedObject.create(signatureR); + PinnedObject pinnedSignatureS = PinnedObject.create(signatureS); + PinnedObject pinnedPublicKey = PinnedObject.create(uncompressedPubKey); + PinnedObject pinnedErrorBuf = PinnedObject.create(errorBuf)) { + + int status = p256VerifyNative( + pinnedDataHash.addressOfArrayElement(0), dataHash.length, + pinnedSignatureR.addressOfArrayElement(0), signatureR.length, + pinnedSignatureS.addressOfArrayElement(0), signatureS.length, + pinnedPublicKey.addressOfArrayElement(0), uncompressedPubKey.length, + pinnedErrorBuf.addressOfArrayElement(0), ERROR_BUF_SIZE); + + return new P256VerifyResult(status, BoringSSLPrecompilesCommon.bytesToNullTermString(errorBuf)); + } + } +} diff --git a/build.sh b/build.sh index b7be136a..650bb763 100755 --- a/build.sh +++ b/build.sh @@ -42,12 +42,14 @@ fi if [[ "$OSTYPE" == "darwin"* ]]; then CORE_COUNT=$(sysctl -n hw.ncpu) + # Set deployment target for Apple Silicon support (macOS 11.0+) + export MACOSX_DEPLOYMENT_TARGET=11.0 if [[ "`machine`" == "arm"* ]]; then arch_name="aarch64" - export CFLAGS="-arch arm64" + export CFLAGS="-arch arm64 -mmacosx-version-min=${MACOSX_DEPLOYMENT_TARGET}" else arch_name="x86-64" - export CFLAGS="-arch x86_64" + export CFLAGS="-arch x86_64 -mmacosx-version-min=${MACOSX_DEPLOYMENT_TARGET}" fi OSARCH="darwin-$arch_name" fi @@ -403,7 +405,12 @@ EOF cd "$SCRIPTDIR/boringssl/google-boringssl/" rm -rf build && mkdir build #-fPIC is redundant on macos, but required for building on linux - cmake -B build -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DCMAKE_BUILD_TYPE=Release + # Set deployment target explicitly to match JNI build + if [[ "$OSTYPE" == "darwin"* ]]; then + cmake -B build -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_DEPLOYMENT_TARGET=${MACOSX_DEPLOYMENT_TARGET} + else + cmake -B build -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DCMAKE_BUILD_TYPE=Release + fi make -C build # build boringssl_jni shared lib linked against boringssl static lib From 33e68cee6f3bc9754482b15e68ecc7a2c09f4007 Mon Sep 17 00:00:00 2001 From: garyschulte Date: Thu, 16 Oct 2025 12:59:39 -0700 Subject: [PATCH 3/4] fix packaging problems with boringssl and gnark Signed-off-by: garyschulte --- boringssl/boringssl_jni/Makefile | 7 +++++-- gnark/build.gradle | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/boringssl/boringssl_jni/Makefile b/boringssl/boringssl_jni/Makefile index ca50887b..f21bcd90 100644 --- a/boringssl/boringssl_jni/Makefile +++ b/boringssl/boringssl_jni/Makefile @@ -48,9 +48,12 @@ $(BUILD_DIR): $(BUILD_LIB): $(BUILD_OBJS) $(CC) $(LDFLAGS) -o $@ $^ $(LIBS) -# Static library +# Static library - properly merge libcrypto.a object files instead of nesting $(BUILD_STATIC_LIB): $(BUILD_OBJS) - ar rcs $@ $^ $(LIBS) + @mkdir -p $(BUILD_DIR)/merge_tmp + @cd $(BUILD_DIR)/merge_tmp && ar x ../../$(BSSL_BUILD_DIR)/libcrypto.a + ar rcs $@ $^ $(BUILD_DIR)/merge_tmp/*.o + @rm -rf $(BUILD_DIR)/merge_tmp $(BUILD_DIR)/%.o: %.c | $(BUILD_DIR) $(CC) $(CFLAGS) -c $< -o $@ diff --git a/gnark/build.gradle b/gnark/build.gradle index ecedf674..7ed9a7d4 100644 --- a/gnark/build.gradle +++ b/gnark/build.gradle @@ -25,6 +25,7 @@ sourceSets { java { srcDir 'src/main/java' include '**/*Graal*.java' + include '**/LibGnarkUtils.java' } // Graal source set needs access to main source set classes compileClasspath += sourceSets.main.output From 6ed554a001dfcbf6a425f4a3042b6eaddeb7832f Mon Sep 17 00:00:00 2001 From: garyschulte Date: Thu, 16 Oct 2025 17:02:47 -0700 Subject: [PATCH 4/4] fix boringssl libcrypto build for riscv64 Signed-off-by: garyschulte --- build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sh b/build.sh index 650bb763..f555a525 100755 --- a/build.sh +++ b/build.sh @@ -409,7 +409,7 @@ EOF if [[ "$OSTYPE" == "darwin"* ]]; then cmake -B build -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_DEPLOYMENT_TARGET=${MACOSX_DEPLOYMENT_TARGET} else - cmake -B build -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DCMAKE_BUILD_TYPE=Release + cmake -B build -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=OFF fi make -C build