Adding to Secp256k1 JNI
Bitcoin-S uses a Java Native Interface (JNI) to execute functions in secp256k1-zkp from java/scala. The native java bindings used to be a part of the secp256k1 library that was maintained by bitcoin-core, but it was removed in October 2019. We maintain a fork of secp256k1 which forks off of bitcoin-core's master but re-introduces the jni. This is also the easiest way to add functionality from new projects such as Schnorr signatures and ECDSA adaptor signatures by rebasing the bitcoin-s branch with the JNI on top of these experimental branches. That said, it is quite tricky to hook up new functionality in secp256k1 into bitcoin-s and specifically NativeSecp256k1.java. The following is a description of this process.
Adding a new function to NativeSecp256k1.java
Adding to src/java/org_bitcoin_NativeSecp256k1.c
Add an
#includeimport at the top (if applicable)If your secp256k1 functions are not already included, you will need to
#includethe header file (should be in thesecp256k1/includedirectory).Function signature
Your new function signature should begin with
SECP256K1_API jint JNICALL Java_org_bitcoin_NativeSecp256k1_followed by the secp256k1 function name where
_are replaced with_1(it's a weird jni thing). Finally, you add a parameter list that begins with(JNIEnv* env, jclass classObject, jobject byteBufferObject, jlong ctx_land ends with any
jints in the case that any of the secp256k1 function inputs have variable length (such as public keys which can be either33or65bytes, or ECDSA signatures), and lastly anyjbooleans in case there is some flag likecompressedpassed in.As an example that includes everything, if you are making a call to
secp256k1_pubkey_tweak_addwhich takes in public keys that could be33or65bytes and outputs a public key that will either be compressed or decompressed based on an input flag, then the function signature would beSECP256K1_API jobjectArray JNICALL Java_org_bitcoin_NativeSecp256k1_secp256k1_1pubkey_1tweak_1add (JNIEnv* env, jclass classObject, jobject byteBufferObject, jlong ctx_l, jint publen, jboolean compressed)Reading
unsigned char*inputsIt is now time to create pointers for each of the secp256k1 function inputs that where passed in via the
byteBufferObject. We must first read in theSecp256k1Contextwith the linesecp256k1_context *ctx = (secp256k1_context*)(uintptr_t)ctx_l;and we can then initialize the first pointer to be the beginning of the
byteBufferObjectwith the lineunsigned char* firstArg = (*env)->GetDirectBufferAddress(env, byteBufferObject);and subsequent arguments' pointers where the previous argument's length is known (say
32bytes for example) can be instantiated usingunsigned char* prevArg = ... unsigned char* nextArg = (unsigned char*) (prevArg + 32);and in the case that a previous argument has variable length, then a
jinthas been provided as an input and can be used instead, such as in the exampleunsigned char* prevArg = ... unsigned char* nextArg = (unsigned char*) (prevArg + publen);where
publenis ajintpassed to this C function.As an example that includes everything, consider the function
secp256k1_ecdsa_verifywhich takes as input a32byte message, a variable length signature and a public key (of length33or65bytes). Our function will begin with{ secp256k1_context *ctx = (secp256k1_context*)(uintptr_t)ctx_l; unsigned char* data = (unsigned char*) (*env)->GetDirectBufferAddress(env, byteBufferObject); const unsigned char* sigdata = (unsigned char*) (data + 32); const unsigned char* pubdata = (unsigned char*) (sigdata + siglen);where
siglenis ajintpassed into the C function.Initialize variables
Next we must declare all variables. We put all decelerations here as it is required by the C framework used by
libsecp256k1that definitions and assignments/function calls cannot be interleaved.Specifically you will need to declare any secp256k1 specific structs here as well as all outputs (such as
jobjectArraysandjByteArrays). Generally speaking this will include all inputs which are not raw data (public keys, signatures, etc). Lastly, you will also have anint retwhich will store0if an error occurred and1otherwise.As an example that includes everything, consider again the function
secp256k1_pubkey_tweak_addhas the following declarationsjobjectArray retArray; jbyteArray pubArray, intsByteArray; unsigned char intsarray[2]; unsigned char outputSer[65]; size_t outputLen = 65; secp256k1_pubkey pubkey; int ret;Where
retArrayis eventually going to be the data returned, which will contain thejbyteArrayspubArrayandintsByteArray, which will containoutputSerandintsarrayrespectively. Lastlypubkeywill store a deserializedsecp256k1_pubkeycorresponding to the inputunsigned char*public key.Parse inputs when applicable
In the case where there are
unsigned char*inputs which need to be deserialized into secp256k1 structs, this is done now. As an example,secp256k1_pubkey_tweak_addtakes a public key as input:unsigned char* pkey = (*env)->GetDirectBufferAddress(env, byteBufferObject);where a
jint publenis passed in as a function parameter. This function already has a declaration forsecp256k1_pubkey pubkey;. The first call made after the above declarations isret = secp256k1_ec_pubkey_parse(ctx, &pubkey, pkey, publen)and if further parsing is necessary, it is put inside of
if (ret) { ret = [further parsing here] }.Make calls to secp256k1 functions to instantiate outputs
It is finally time to actually call the secp256k1 function we are binding to the jni! This is done by simply calling
ret = [call to secp function here];orif (ret) { ret = [secp function call] };if there were any inputs that needed to be parsed. Note that some secp256k1 functions return outputs by populating variables you should have declared and for which pointers are passed as inputs, while other functions will mutate their inputs rather than returning outputs.Serialize variable length outputs if applicable
When dealing with variable length outputs such as signatures, you will likely need to serialize these outputs. This is done by having already instantiated such a variable as
unsigned char outputSer[72]; size_t outputLen = 72;where in this case
72is an upper bound on signature length. With these variables existing (as well as asecp256k1_ecdsa_signature sigwhich has been populated), we call a secp256k1 serialization function to populateoutputSerandoutputLenfromsigif(ret) { int ret2 = secp256k1_ecdsa_signature_serialize_der(ctx, outputSer, &outputLen, &sig); (void)ret2; }As you can see, in this case we do not which to alter the value returned in
retif serialization fails. If we did thenret2would not be introduced and we would instead doret = [serialize].Populate return array when applicable
We now begin translating our serialized results back into Java entities. If you are returning any
ints containing meta-data (usuallyretis included here, as are the variable lengths of outputs when applicable), you will want anunsigned char intsarray[n]to be already declared wherenis the number of pieces of meta-data. For example, insecp256k1_ecdsa_sign, we wish to return whether there were any errors (stored inint ret) and the output signature's length,size_t outputLen. Hence we have anunsigned char intsarray[2]and we populate it as followsintsarray[0] = outputLen; intsarray[1] = ret;Next we populate the
jobjectArraywe wish to return, this will always begin with a callretArray = (*env)->NewObjectArray(env, 2, (*env)->FindClass(env, "[B"), (*env)->NewByteArray(env, 1));to instantiate an empty return array. Next we instantiate our
jbyteArrays with calls tomyByteArray = (*env)->NewByteArray(env, len);where
myByteArrayis replaced with a real name (such asintsByteArray) andlenis replaced with the length of this array (either a constant or a populatedsize_tvariable). Next we populate this array with our data by calling(*env)->SetByteArrayRegion(env, myByteArray, 0, len, (jbyte*)myData);where
myDatais a C array ofunsigned char(of lengthlen). Lastly, we placemyByteArrayinto its place inretArraywith(*env)->SetObjectArrayElement(env, retArray, index, myByteArray);where
indexis a constant (0,1,2, etc.) for the index ofmyByteArraywithinretArray. Note that you should follow our conventions and have index0contain the actual data to be returned and index1(and onward) contain any meta-data.Please note that if you wish not to return such meta-data (such as if you wish to return only a
boolean), then none of the code in this subsection is requiredvoid
classObjectOnce we are ready to return, we first void the input
classObjectby making the call(void)classObject;Return array when applicable,
retwhen applicableLastly, we return
retArrayin the case where we wish to return abyte[], orretin the case that we wish to return aboolean.
Adding to src/java/org_bitcoin_NativeSecp256k1.h
Once your function is defined in src/java/org_bitcoin_NativeSecp256k1.c, you must define them in the corresponding header files by simply copying the function signature but without parameter names. For example, if secp256k1_pubkey_tweak_add has the function signature
SECP256K1_API jobjectArray JNICALL Java_org_bitcoin_NativeSecp256k1_secp256k1_1pubkey_1tweak_1add
(JNIEnv* env, jclass classObject, jobject byteBufferObject, jlong ctx_l, jint publen, jboolean compressed)
then in the header file we include
/*
* Class: org_bitcoin_NativeSecp256k1
* Method: secp256k1_pubkey_tweak_add
* Signature: (Ljava/nio/ByteBuffer;JI)[[B
*/
SECP256K1_API jobjectArray JNICALL Java_org_bitcoin_NativeSecp256k1_secp256k1_1pubkey_1tweak_1add
(JNIEnv *, jclass, jobject, jlong, jint, jboolean);
Adding to src/java/org/bitcoin/NativeSecp256k1.java
We are now done writing C code! We have completed an interface in C for the JNI to hook up to. However, we must now write the corresponding Java code which hides the Java to C (and back) conversions from other Java code. We accomplish this with a class of static methods called NativeSecp256k1.
Add
private static nativesecp256k1 functionWe begin by adding a
private static nativemethod at the bottom of the file corresponding to our secp256k1 function. Notice that the syntax fornativemethods is similar to that of Java abstract interface methods where instead of providing an implementation we simply end with a semi-colon.For functions returning
booleans, we have theirnativemethods returnint(will be0or1). Otherwise, for functions returningbyte[]s, we have theirnativemethods returnbyte[][](two dimensional array to allow for meta-data).Method signature
Next we add a method to the
NativeSecp256k1classpublic static byte[] myFunc(byte[] input1, byte[] input2, boolean input3) throws AssertFailExceptionwhere
booleancould also be the return type instead ofbyte[].checkArgumentsWe begin implementing this function by checking the input argument lengths using the
checkArumentfunctioncheckArgument(input1.length == 32 && (input2.length == 33 || input2.length == 65));Initialize
ByteBufferWe now initialize the
ByteBufferwhich we will be passing through the JNI as an input. This is done with a call toByteBuffer byteBuff = nativeECDSABuffer.get();followed by allocation when necessary as follows
if (byteBuff == null || byteBuff.capacity() < input1.length + input2.length) { byteBuff = ByteBuffer.allocateDirect(input1.length + input2.length); byteBuff.order(ByteOrder.nativeOrder()); nativeECDSABuffer.set(byteBuff); }where
input1.length + input2.lengthis replaced by whatever the totalByteBufferlength needed.Fill
ByteBufferWe now populate the
ByteBufferas followsbyteBuff.rewind(); byteBuff.put(input1); byteBuff.put(input2);where generally, you will
rewind()and thenput()all inputs (in order).Make
nativecallIt is now time to make a call to our
nativeC function.In the case where we are returning a
byte[], this is done by first declaring abyte[][]to store the output and then locking the read lock,r. Then we call thenativefunction within atryclause which releases the lock in thefinallyclause.byte[][] retByteArray; r.lock(); try { retByteArray = secp256k1_my_call(byteBuff, Secp256k1Context.getContext(), input3); } finally { r.unlock(); }In the case where we are returning a
boolean, simply make the call in thetryand compare the output to1like sor.lock(); try { return secp256k1_my_bool_call(byteBuff, Secp256k1Context.getContext()) == 1; } finally { r.unlock(); }If this is the case, you are now done and can ignore the following steps.
Parse outputs
retByteArrayshould now be populated and we want to read its two parts (data and meta-data). Getting the data should be as easy asbyte[] resultArr = retByteArr[0];while for each piece of meta-data, you can read the corresponding
intas followsint metaVal = new BigInteger(new byte[] { retByteArray[1][index] }).intValue();where
indexis replaced with the index in the meta-data array.Validate outputs
In the case where we now have meta-data, we validate it with calls to
assertEquals.Return output
Finally, we return
resultArr.
Adding to src/java/org/bitcoin/NativeSecp256k1Test.java
I normally first build the C binaries and add to Bitcoin-S before coming back to this section because I use sbt core/console to generate values and make calls below, but this is not a requirement.
Generate values and make calls to
org.bitcoin.NativeSecp256k1to generate inputs and their expected outputsCreate regression unit tests with these values in NativeSecp256k1Test
Note that you can use
DatatypeConverter.parseHexBinaryto convertStringhex to abyte[], and you can useDatatypeConverter.printHexBinaryto convert abyte[]to itsStringhex. Lastly you will make assertions with calls toassertEquals.Add test to
main
Adding to Bitcoin-S
Translate
NativeSecp256k1andNativeSecp256k1Testto jni projectBy translate I mean to say that you must copy the functions from those files to the corresponding files in the
bitcoin-s/secp256k1jniproject. For tests this will require changing the methods to be non-staticas well as adding the@Testannotation above each method (rather than adding to amainmethod).Configure and build
secp256k1You will need to go to the
bitcoin-s/secp256k1directory in a terminal and running the following where you may need to add to the./configurecommand if you are introducing a new module.For Linux or OSx (64-bit)
You will have to make sure
JAVA_HOMEis set, and build tools are installed, for Linux this requires:echo JAVA_HOME sudo apt install build-essential autotools-dev libtool automakeand for Mac this requires:
brew install automake libtoolYou should then be able to build
libsecp256k1with the following:./autogen.sh ./configure --enable-jni --enable-experimental --enable-module-ecdh --enable-module-schnorrsig --enable-module-ecdsa-adaptor make CFLAGS="-std=c99" make check make check-javaFor Windows (64-bit)
Windows bindings are cross-built on Linux. You need to install the
mingwtoolchain and haveJAVA_HOMEpoint to a Windows JDK:sudo apt install g++-mingw-w64-x86-64 sudo update-alternatives --config x86_64-w64-mingw32-g++You should then be able to build
libsecp256k1with the following:echo "LDFLAGS = -no-undefined" >> Makefile.am ./configure --host=x86_64-w64-mingw32 --enable-jni --enable-experimental --enable-module-ecdh --enable-module-schnorrsig --enable-module-ecdsa-adaptor && make clean && make CFLAGS="-std=c99"There may be some errors that can be ignored:
Could not determine the host path corresponding toredeclared without dllimport attribute: previous dllimport ignored
Copy binaries into bitcoin-s natives for your system
You have now built the C binaries for your JNI bindings for your operating system and you should now find your operating system's directory in
bitcoin-s/secp256k1jni/nativesand replace its contents with the contents ofsecp256k1/.libs(which contains the compiled binaries).Run
secp256k1jnitestsIf you have not yet implemented tests, you should now be able to go back and do so as calls to
NativeSecp256k1should now succeed.Once you have tests implemented, and assuming you've copied them correctly to the
bitcoin-s/secp256k1jniproject, you should be able to run them usingsbt secp256k1jni/test
Further Work to Enable Typed Invocations and Nice Tests
Add new
NetworkElements where applicableIn the case where you are dealing in new kinds of data that are not yet defined in Bitcoin-S, you should add these as
case classes extending theNetworkElementtrait, and give them companion objects extendingFactoryfor easy serialization and deserialization.This step is not necessary if you are only dealing in raw data,
ECPrivateKeys,ECPublicKeys, etc.Add new typed functions to relevant data types where applicable
In the case where your new function should be a static method, find a good
object(or introduce one) and give it adefwhich takes in typed arguments and outputs typed arguments (usingByteVectorin all places dealing with raw data rather than usingArray[Byte]). You will then implement these methods using calls toNativeSecp256k1methods and getting the inputs intoArray[Byte]form by getting theirByteVectors (usually through a call to_.bytes) and then calling_.toArray.You will then need to take the data returned and deserialize it.
In the case where your new function belongs naturally as an action performed by some existing or newly introduced type, you can implement your new function as a call made by that class as described for the previous case but where the class will pass a serialized version of itself into the
NativeSecp256k1call.It is often acceptable to implement the call in an
objectand then also add the call (via a call to the object, passingthis) to the interface of relevant types.Implement Bouncy Castle fallback in
BouncyCastleUtil.scalaif you can.Add unit and property-based tests.
If you implemented Bouncy Castle fallback, add tests to
BouncyCastleSecp256k1Testto compare implementations