diff --git a/build.gradle b/build.gradle index 3429a528..62eeddde 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,9 @@ buildscript { + repositories { + maven { url "https://repo.grails.org/grails/core/" } + } + dependencies { classpath 'com.netflix.nebula:gradle-aggregate-javadocs-plugin:3.0.1' } @@ -15,7 +19,7 @@ apply plugin: 'jacoco' allprojects { group = 'io.emeraldpay.polkaj' - version = "0.5.0-SNAPSHOT" + version = "0.5.5-SNAPSHOT" repositories { mavenLocal() @@ -50,9 +54,9 @@ task syncJars(type: Sync) { // Skip Bintray for the root module bintray { - dryRun=true - publish=false - override=true + dryRun = true + publish = false + override = true publications = [] configurations = [] pkg { @@ -62,4 +66,4 @@ bintray { name = 'none' } } -} \ No newline at end of file +} diff --git a/polkaj-adapter-tests/build.gradle b/polkaj-adapter-tests/build.gradle index 54b154d3..17b8b2d1 100644 --- a/polkaj-adapter-tests/build.gradle +++ b/polkaj-adapter-tests/build.gradle @@ -17,4 +17,5 @@ dependencies { api 'org.codehaus.groovy:groovy-all:3.0.3' api 'commons-codec:commons-codec:1.14' api 'cglib:cglib-nodep:3.3.0' + api 'org.junit.jupiter:junit-jupiter-api:5.9.2' } \ No newline at end of file diff --git a/polkaj-api-base/build.gradle b/polkaj-api-base/build.gradle index 41ee3c85..87e8ce34 100644 --- a/polkaj-api-base/build.gradle +++ b/polkaj-api-base/build.gradle @@ -5,4 +5,5 @@ apply from: '../common_java_app.gradle' dependencies { api project(":polkaj-json-types") + api 'org.junit.jupiter:junit-jupiter-api:5.9.2' } \ No newline at end of file diff --git a/polkaj-api-http/build.gradle b/polkaj-api-http/build.gradle index 64c53ecd..0518e2fd 100644 --- a/polkaj-api-http/build.gradle +++ b/polkaj-api-http/build.gradle @@ -8,6 +8,7 @@ compileJava { dependencies { api project(":polkaj-json-types") api project(":polkaj-api-base") + api 'org.junit.jupiter:junit-jupiter-api:5.9.2' testImplementation 'org.mock-server:mockserver-netty:5.10' testImplementation project(":polkaj-adapter-tests") diff --git a/polkaj-api-ws/build.gradle b/polkaj-api-ws/build.gradle index 496ee64b..aedbae92 100644 --- a/polkaj-api-ws/build.gradle +++ b/polkaj-api-ws/build.gradle @@ -9,6 +9,7 @@ compileJava { dependencies { api project(":polkaj-json-types") api project(":polkaj-api-base") + api 'org.junit.jupiter:junit-jupiter-api:5.9.2' testImplementation 'org.java-websocket:Java-WebSocket:1.5.1' testImplementation project(":polkaj-adapter-tests") diff --git a/polkaj-common-types/build.gradle b/polkaj-common-types/build.gradle index 77c8cd4c..57ff5d2e 100644 --- a/polkaj-common-types/build.gradle +++ b/polkaj-common-types/build.gradle @@ -2,4 +2,5 @@ apply from: '../common_java_app.gradle' dependencies { api project(":polkaj-ss58") + api 'org.junit.jupiter:junit-jupiter-api:5.9.2' } \ No newline at end of file diff --git a/polkaj-common-types/src/main/java/io/emeraldpay/polkaj/types/FixedBytes.java b/polkaj-common-types/src/main/java/io/emeraldpay/polkaj/types/FixedBytes.java index 1f509edd..0b113dda 100644 --- a/polkaj-common-types/src/main/java/io/emeraldpay/polkaj/types/FixedBytes.java +++ b/polkaj-common-types/src/main/java/io/emeraldpay/polkaj/types/FixedBytes.java @@ -4,6 +4,13 @@ abstract public class FixedBytes extends ByteData { + /** + * Needed for serialization/deserialization. + */ + public FixedBytes() { + super(new byte[0]); + } + protected FixedBytes(byte[] value, int expectedSize) { super(value); if (value.length != expectedSize) { @@ -34,7 +41,7 @@ protected static byte[] parseHex(String hex) { } byte[] data = new byte[len / 2]; for (int i = 0; i < len; i += 2) { - data[i / 2] = Integer.valueOf(hex.substring(i, i+2), 16).byteValue(); + data[i / 2] = Integer.valueOf(hex.substring(i, i + 2), 16).byteValue(); } return data; } diff --git a/polkaj-common-types/src/main/java/io/emeraldpay/polkaj/types/Hash256.java b/polkaj-common-types/src/main/java/io/emeraldpay/polkaj/types/Hash256.java index 8d162e5b..8cecb49b 100644 --- a/polkaj-common-types/src/main/java/io/emeraldpay/polkaj/types/Hash256.java +++ b/polkaj-common-types/src/main/java/io/emeraldpay/polkaj/types/Hash256.java @@ -1,9 +1,14 @@ package io.emeraldpay.polkaj.types; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; + /** * A 256 bit value, commonly used as a hash */ -public class Hash256 extends FixedBytes implements Comparable { +public class Hash256 extends FixedBytes implements Comparable, Serializable { /** * Length in bytes (32 byte) @@ -14,7 +19,7 @@ public class Hash256 extends FixedBytes implements Comparable { * Create a new value. Makes sure the input is correct, if not throws an exception * * @param value 32 byte value - * @throws NullPointerException if value is null + * @throws NullPointerException if value is null * @throws IllegalArgumentException is size is not 32 bytes */ public Hash256(byte[] value) { @@ -36,7 +41,7 @@ public static Hash256 empty() { * @param hex hex value, may optionally start with 0x prefix * @return hash instance * @throws IllegalArgumentException if value has invalid length - * @throws NumberFormatException if value has invalid format (non-hex characters, etc) + * @throws NumberFormatException if value has invalid format (non-hex characters, etc) */ public static Hash256 from(String hex) { byte[] parsed = parseHex(hex, SIZE_BYTES); @@ -47,4 +52,38 @@ public static Hash256 from(String hex) { public int compareTo(Hash256 o) { return super.compareTo(o); } + + /** + * Custom serialization logic. Writes the length and value of the underlying byte array for the hash. + * + * @param out the {@link ObjectOutputStream} to write this object to + * @throws IOException if an I/O error occurs while writing to the stream + */ + private void writeObject(ObjectOutputStream out) throws IOException { + out.writeInt(value.length); + out.write(value); + } + + /** + * Custom deserialization logic. Validates the required length and sets the underlying byte array using reflection. + * + * @param in the {@link ObjectInputStream} to read this object from + * @throws IOException if the length of the serialized data is invalid or if an I/O error occurs + * @throws ClassNotFoundException if the class of a serialized object cannot be found + */ + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + int length = in.readInt(); + if (length != SIZE_BYTES) { + throw new IOException("Invalid Hash256 length: " + length); + } + byte[] value = new byte[length]; + in.readFully(value); + try { + java.lang.reflect.Field valueField = ByteData.class.getDeclaredField("value"); + valueField.setAccessible(true); + valueField.set(this, value.clone()); + } catch (IllegalAccessException | NoSuchFieldException e) { + throw new IOException("Failed to initialize Hash256", e); + } + } } diff --git a/polkaj-common-types/src/main/java/io/emeraldpay/polkaj/types/Hash264.java b/polkaj-common-types/src/main/java/io/emeraldpay/polkaj/types/Hash264.java new file mode 100644 index 00000000..fb4fb1e9 --- /dev/null +++ b/polkaj-common-types/src/main/java/io/emeraldpay/polkaj/types/Hash264.java @@ -0,0 +1,89 @@ +package io.emeraldpay.polkaj.types; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; + +/** + * A 264 bit value, commonly used as a hash + */ +public class Hash264 extends FixedBytes implements Comparable, Serializable { + + /** + * Length in bytes (33 byte) + */ + public static final int SIZE_BYTES = 33; + + /** + * Create a new value. Makes sure the input is correct, if not throws an exception + * + * @param value 33 byte value + * @throws NullPointerException if value is null + * @throws IllegalArgumentException is size is not 33 bytes + */ + public Hash264(byte[] value) { + super(value, SIZE_BYTES); + } + + /** + * Creates an empty zeroed instance + * + * @return empty hash + */ + public static Hash264 empty() { + return new Hash264(new byte[SIZE_BYTES]); + } + + /** + * Parse hex value and create a new instance + * + * @param hex hex value, may optionally start with 0x prefix + * @return hash instance + * @throws IllegalArgumentException if value has invalid length + * @throws NumberFormatException if value has invalid format (non-hex characters, etc) + */ + public static Hash264 from(String hex) { + byte[] parsed = parseHex(hex, SIZE_BYTES); + return new Hash264(parsed); + } + + @Override + public int compareTo(Hash264 o) { + return super.compareTo(o); + } + + /** + * Custom serialization logic. Writes the length and value of the underlying byte array for the hash. + * + * @param out the {@link ObjectOutputStream} to write this object to + * @throws IOException if an I/O error occurs while writing to the stream + */ + private void writeObject(ObjectOutputStream out) throws IOException { + out.writeInt(value.length); + out.write(value); + } + + /** + * Custom deserialization logic. Validates the required length and sets the underlying byte array using reflection. + * + * @param in the {@link ObjectInputStream} to read this object from + * @throws IOException if the length of the serialized data is invalid or if an I/O error occurs + * @throws ClassNotFoundException if the class of a serialized object cannot be found + */ + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + int length = in.readInt(); + if (length != SIZE_BYTES) { + throw new IOException("Invalid Hash264 length: " + length); + } + byte[] value = new byte[length]; + in.readFully(value); + try { + java.lang.reflect.Field valueField = ByteData.class.getDeclaredField("value"); + valueField.setAccessible(true); + valueField.set(this, value.clone()); + } catch (IllegalAccessException | NoSuchFieldException e) { + throw new IOException("Failed to initialize Hash264", e); + } + } +} diff --git a/polkaj-common-types/src/main/java/io/emeraldpay/polkaj/types/Hash512.java b/polkaj-common-types/src/main/java/io/emeraldpay/polkaj/types/Hash512.java index 9aa4444e..3ebb45b2 100644 --- a/polkaj-common-types/src/main/java/io/emeraldpay/polkaj/types/Hash512.java +++ b/polkaj-common-types/src/main/java/io/emeraldpay/polkaj/types/Hash512.java @@ -1,9 +1,14 @@ package io.emeraldpay.polkaj.types; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; + /** * A 512 bit value, commonly used as a hash */ -public class Hash512 extends FixedBytes implements Comparable { +public class Hash512 extends FixedBytes implements Comparable, Serializable { /** * Length in bytes (64 byte) @@ -14,7 +19,7 @@ public class Hash512 extends FixedBytes implements Comparable { * Create a new value. Makes sure the input is correct, if not throws an exception * * @param value 64 byte value - * @throws NullPointerException if value is null + * @throws NullPointerException if value is null * @throws IllegalArgumentException is size is not 64 bytes */ public Hash512(byte[] value) { @@ -36,7 +41,7 @@ public static Hash512 empty() { * @param hex hex value, may optionally start with 0x prefix * @return hash instance * @throws IllegalArgumentException if value has invalid length - * @throws NumberFormatException if value has invalid format (non-hex characters, etc) + * @throws NumberFormatException if value has invalid format (non-hex characters, etc) */ public static Hash512 from(String hex) { byte[] parsed = parseHex(hex, SIZE_BYTES); @@ -47,4 +52,38 @@ public static Hash512 from(String hex) { public int compareTo(Hash512 o) { return super.compareTo(o); } + + /** + * Custom serialization logic. Writes the length and value of the underlying byte array for the hash. + * + * @param out the {@link ObjectOutputStream} to write this object to + * @throws IOException if an I/O error occurs while writing to the stream + */ + private void writeObject(ObjectOutputStream out) throws IOException { + out.writeInt(value.length); + out.write(value); + } + + /** + * Custom deserialization logic. Validates the required length and sets the underlying byte array using reflection. + * + * @param in the {@link ObjectInputStream} to read this object from + * @throws IOException if the length of the serialized data is invalid or if an I/O error occurs + * @throws ClassNotFoundException if the class of a serialized object cannot be found + */ + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + int length = in.readInt(); + if (length != SIZE_BYTES) { + throw new IOException("Invalid Hash512 length: " + length); + } + byte[] value = new byte[length]; + in.readFully(value); + try { + java.lang.reflect.Field valueField = ByteData.class.getDeclaredField("value"); + valueField.setAccessible(true); + valueField.set(this, value.clone()); + } catch (IllegalAccessException | NoSuchFieldException e) { + throw new IOException("Failed to initialize Hash512", e); + } + } } diff --git a/polkaj-common-types/src/test/groovy/io/emeraldpay/polkaj/types/FixedBytesSpec.groovy b/polkaj-common-types/src/test/groovy/io/emeraldpay/polkaj/types/FixedBytesSpec.groovy index 6f7281b2..a992d32e 100644 --- a/polkaj-common-types/src/test/groovy/io/emeraldpay/polkaj/types/FixedBytesSpec.groovy +++ b/polkaj-common-types/src/test/groovy/io/emeraldpay/polkaj/types/FixedBytesSpec.groovy @@ -17,6 +17,4 @@ class FixedBytesSpec extends Specification { def t = thrown(IllegalStateException) t.message.contains("Different size") } - - } diff --git a/polkaj-common-types/src/test/groovy/io/emeraldpay/polkaj/types/Hash256Spec.groovy b/polkaj-common-types/src/test/groovy/io/emeraldpay/polkaj/types/Hash256Spec.groovy index 82947b5f..c086fa6d 100644 --- a/polkaj-common-types/src/test/groovy/io/emeraldpay/polkaj/types/Hash256Spec.groovy +++ b/polkaj-common-types/src/test/groovy/io/emeraldpay/polkaj/types/Hash256Spec.groovy @@ -189,4 +189,24 @@ class Hash256Spec extends Specification { then: hash1.hashCode() != hash2.hashCode() } + + def "Deserialization produces expected Hash256"() { + setup: + def hash = Hash256.from("0x63c2499de640b43c924bc2bfc9ea89730e7c4790e24d126906e7af6c99cb506b") + def serialized = SerializationUtil.serialize(hash) + when: + def deserialized = SerializationUtil.deserialize(serialized) + then: + deserialized instanceof Hash256 + deserialized.toString() == "0x63c2499de640b43c924bc2bfc9ea89730e7c4790e24d126906e7af6c99cb506b" + } + + def "Cannot deserialize invalid length bytes"() { + setup: + def serialized = new byte[]{1, 2, 3, 4} + when: + SerializationUtil.deserialize(serialized) + then: + thrown(IllegalArgumentException) + } } diff --git a/polkaj-common-types/src/test/groovy/io/emeraldpay/polkaj/types/Hash512Spec.groovy b/polkaj-common-types/src/test/groovy/io/emeraldpay/polkaj/types/Hash512Spec.groovy index b143b501..56a9667f 100644 --- a/polkaj-common-types/src/test/groovy/io/emeraldpay/polkaj/types/Hash512Spec.groovy +++ b/polkaj-common-types/src/test/groovy/io/emeraldpay/polkaj/types/Hash512Spec.groovy @@ -189,4 +189,18 @@ class Hash512Spec extends Specification { then: hash1.hashCode() != hash2.hashCode() } + + def "Deserialization produces expected Hash512"() { + setup: + def hash = Hash512.from( + "0x63c2499de640b43c924bc2bfc9ea89730e7c4790e24d126906e7af6c99cb506b" + + "63c2499de640b43c924bc2bfc9ea89730e7c4790e24d126906e7af6c99cb506b") + def serialized = SerializationUtil.serialize(hash) + when: + def deserialized = SerializationUtil.deserialize(serialized) + then: + deserialized instanceof Hash512 + deserialized.toString() == "0x63c2499de640b43c924bc2bfc9ea89730e7c4790e24d126906e7af6c99cb506b" + + "63c2499de640b43c924bc2bfc9ea89730e7c4790e24d126906e7af6c99cb506b" + } } diff --git a/polkaj-common-types/src/test/groovy/io/emeraldpay/polkaj/types/SerializationUtil.java b/polkaj-common-types/src/test/groovy/io/emeraldpay/polkaj/types/SerializationUtil.java new file mode 100644 index 00000000..0abda4ff --- /dev/null +++ b/polkaj-common-types/src/test/groovy/io/emeraldpay/polkaj/types/SerializationUtil.java @@ -0,0 +1,37 @@ +package io.emeraldpay.polkaj.types; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +public class SerializationUtil { + + public static byte[] serialize(Object object) { + if (object == null) { + return null; + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(1024); + try (ObjectOutputStream oos = new ObjectOutputStream(baos)) { + oos.writeObject(object); + oos.flush(); + } catch (IOException ex) { + throw new IllegalArgumentException("Failed to serialize object of type: " + object.getClass(), ex); + } + return baos.toByteArray(); + } + + public static Object deserialize(byte[] bytes) { + if (bytes == null) { + return null; + } + try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes))) { + return ois.readObject(); + } catch (IOException ex) { + throw new IllegalArgumentException("Failed to deserialize object", ex); + } catch (ClassNotFoundException ex) { + throw new IllegalStateException("Failed to deserialize object type", ex); + } + } +} diff --git a/polkaj-json-types/build.gradle b/polkaj-json-types/build.gradle index 72e5c404..b1d54d14 100644 --- a/polkaj-json-types/build.gradle +++ b/polkaj-json-types/build.gradle @@ -5,5 +5,6 @@ dependencies { api project(":polkaj-ss58") api 'com.fasterxml.jackson.core:jackson-databind:2.11.0' + api 'org.junit.jupiter:junit-jupiter-api:5.9.2' } \ No newline at end of file diff --git a/polkaj-scale-types/build.gradle b/polkaj-scale-types/build.gradle index f4596dc0..469959bb 100644 --- a/polkaj-scale-types/build.gradle +++ b/polkaj-scale-types/build.gradle @@ -3,4 +3,6 @@ apply from: '../common_java_app.gradle' dependencies { api project(":polkaj-scale") api project(":polkaj-common-types") + api 'org.junit.jupiter:junit-jupiter-api:5.9.2' + testImplementation 'org.testng:testng:7.1.0' } \ No newline at end of file diff --git a/polkaj-scale-types/src/main/java/io/emeraldpay/polkaj/scaletypes/Result.java b/polkaj-scale-types/src/main/java/io/emeraldpay/polkaj/scaletypes/Result.java new file mode 100644 index 00000000..6a8e03e4 --- /dev/null +++ b/polkaj-scale-types/src/main/java/io/emeraldpay/polkaj/scaletypes/Result.java @@ -0,0 +1,44 @@ +package io.emeraldpay.polkaj.scaletypes; + + +public class Result { + + public enum ResultMode { + OK((byte) 0), ERR((byte) 1); + private byte value; + + ResultMode(byte value) { + this.value = value; + } + + public byte getValue() { + return this.value; + } + } + + T ok; + E error; + ResultMode mode; + + public Result(ResultMode mode, T ok, E error) { + this.mode = mode; + this.ok = ok; + this.error = error; + } + + public boolean isOk() { + return mode == ResultMode.OK; + } + + public boolean isError() { + return mode == ResultMode.ERR; + } + + public T getOkValue() { + return ok; + } + + public E getErrorValue() { + return error; + } +} diff --git a/polkaj-scale-types/src/main/java/io/emeraldpay/polkaj/scaletypes/ResultReader.java b/polkaj-scale-types/src/main/java/io/emeraldpay/polkaj/scaletypes/ResultReader.java new file mode 100644 index 00000000..17c859a2 --- /dev/null +++ b/polkaj-scale-types/src/main/java/io/emeraldpay/polkaj/scaletypes/ResultReader.java @@ -0,0 +1,21 @@ +package io.emeraldpay.polkaj.scaletypes; + +import io.emeraldpay.polkaj.scale.ScaleCodecReader; +import io.emeraldpay.polkaj.scale.ScaleReader; + +public class ResultReader { + static final String unsupportedValueMessage = "Reading unsupported result mode value"; + + public Result readResult(ScaleCodecReader reader, ScaleReader okScaleReader, ScaleReader errorScaleReader) { + byte mode = reader.readByte(); + if (mode == Result.ResultMode.OK.getValue()) { + T ok = reader.read(okScaleReader); + return new Result(Result.ResultMode.OK, ok, null); + } + if (mode == Result.ResultMode.ERR.getValue()) { + E error = reader.read(errorScaleReader); + return new Result(Result.ResultMode.ERR, null, error); + } + throw new IllegalStateException(unsupportedValueMessage); + } +} diff --git a/polkaj-scale-types/src/main/java/io/emeraldpay/polkaj/scaletypes/ResultWriter.java b/polkaj-scale-types/src/main/java/io/emeraldpay/polkaj/scaletypes/ResultWriter.java new file mode 100644 index 00000000..94b1f6b3 --- /dev/null +++ b/polkaj-scale-types/src/main/java/io/emeraldpay/polkaj/scaletypes/ResultWriter.java @@ -0,0 +1,18 @@ +package io.emeraldpay.polkaj.scaletypes; + +import io.emeraldpay.polkaj.scale.ScaleCodecWriter; +import io.emeraldpay.polkaj.scale.ScaleWriter; + +import java.io.IOException; + +public class ResultWriter { + public void writeResult(ScaleCodecWriter writer, ScaleWriter okScaleWriter, + ScaleWriter errorScaleWriter, Result result) throws IOException { + writer.writeByte(result.mode.getValue()); + if (result.mode == Result.ResultMode.OK) { + writer.write(okScaleWriter, result.ok); + return; + } + writer.write(errorScaleWriter, result.error); + } +} diff --git a/polkaj-scale-types/src/test/java/io/emeraldpay/polkaj/scaletypes/EncodeDecodeResultTest.java b/polkaj-scale-types/src/test/java/io/emeraldpay/polkaj/scaletypes/EncodeDecodeResultTest.java new file mode 100644 index 00000000..59ffc4b1 --- /dev/null +++ b/polkaj-scale-types/src/test/java/io/emeraldpay/polkaj/scaletypes/EncodeDecodeResultTest.java @@ -0,0 +1,34 @@ +package io.emeraldpay.polkaj.scaletypes; + +import io.emeraldpay.polkaj.scale.ScaleCodecReader; +import io.emeraldpay.polkaj.scale.ScaleCodecWriter; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import org.junit.jupiter.api.Assertions; + +public class EncodeDecodeResultTest { + + @Test + public void EncodeDecodeResultTest() { + Result dataToEncode = new Result<>(Result.ResultMode.OK, 10, 5); + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + try (ScaleCodecWriter writer = new ScaleCodecWriter(buf)) { + ResultWriter resultWriter = new ResultWriter(); + resultWriter.writeResult(writer, ScaleCodecWriter::writeCompact, ScaleCodecWriter::writeCompact, dataToEncode); + + byte[] decodeBuf = buf.toByteArray(); + ScaleCodecReader reader = new ScaleCodecReader(decodeBuf); + ResultReader resultReader = new ResultReader<>(); + Result result = resultReader.readResult(reader, ScaleCodecReader::readCompactInt, ScaleCodecReader::readCompactInt); + + Assertions.assertEquals(dataToEncode.mode, result.mode); + Assertions.assertEquals(dataToEncode.getOkValue(), result.getOkValue()); + Assertions.assertEquals(null, result.getErrorValue()); + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/polkaj-scale/build.gradle b/polkaj-scale/build.gradle index 64cdaea9..d050bcee 100644 --- a/polkaj-scale/build.gradle +++ b/polkaj-scale/build.gradle @@ -2,4 +2,5 @@ apply from: '../common_java_app.gradle' dependencies { api project(":polkaj-common-types") + api 'org.junit.jupiter:junit-jupiter-api:5.9.2' } \ No newline at end of file diff --git a/polkaj-schnorrkel/build.gradle b/polkaj-schnorrkel/build.gradle index f959f146..f75b4115 100644 --- a/polkaj-schnorrkel/build.gradle +++ b/polkaj-schnorrkel/build.gradle @@ -1,6 +1,7 @@ apply from: '../common_java_app.gradle' dependencies { +api 'org.junit.jupiter:junit-jupiter-api:5.9.2' } test { @@ -12,19 +13,4 @@ task compileRust(type:Exec) { commandLine 'cargo', 'build', '--release', '--target-dir=../../build/rust' } -compileJava.dependsOn(compileRust) - -jar { - from("${buildDir}/rust/release") { - into "native/macos" - include '*.dylib' - } - from("${buildDir}/rust/release") { - into "native/linux" - include '*.so' - } - from("${buildDir}/rust/release") { - into "native/windows" - include '*.dll' - } -} +compileJava.dependsOn(compileRust) \ No newline at end of file diff --git a/polkaj-schnorrkel/src/main/java/io/emeraldpay/polkaj/merlin/TranscriptData.java b/polkaj-schnorrkel/src/main/java/io/emeraldpay/polkaj/merlin/TranscriptData.java new file mode 100644 index 00000000..c1ab2049 --- /dev/null +++ b/polkaj-schnorrkel/src/main/java/io/emeraldpay/polkaj/merlin/TranscriptData.java @@ -0,0 +1,59 @@ +package io.emeraldpay.polkaj.merlin; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; + +/** + * A container class to simply hold all the necessary input data (label + messages) to construct the actual + * transcript on Rust's side. Think of this as "the bag of currently necessary arguments". + * The main idea of this class is to be easily portable to Rust using JNI. + * It has no usage on its own on the Java side alone. + */ +@SuppressWarnings({"MismatchedQueryAndUpdateOfCollection", "FieldCanBeLocal"}) +public class TranscriptData { + // INTENTIONALITY: Those fields being of type ArrayList is essential for the JNI mappings on the Rust side + + // HACK: + // Semantically, the internal representation of the domainSeparationLabel should be a single byte[] + // Due to an unresolved issue with robusta (the rust lib) though, + // the field mapping from byte[] doesn't work as expected, so this is the current workaround. + private final ArrayList domainSeparationLabel; + + private final ArrayList labels; + private final ArrayList messages; + + public TranscriptData(byte[] domainSeparationLabel) { + this.domainSeparationLabel = new ArrayList<>(); + this.domainSeparationLabel.add(domainSeparationLabel); + this.labels = new ArrayList<>(); + this.messages = new ArrayList<>(); + } + + /** + * Appends an ASCII encoded string message to the transcript with an ASCII encoded string label. + * @param label the ASCII encoded label + * @param message the ASCII encoded message + */ + public void appendMessage(String label, String message) { + appendMessage(label, message.getBytes(StandardCharsets.US_ASCII)); + } + + /** + * Appends a message to the transcript with an ASCII encoded string label. + * @param label the ASCII encoded label + * @param message the actual message (content) + */ + public void appendMessage(String label, byte[] message) { + appendMessage(label.getBytes(StandardCharsets.US_ASCII), message); + } + + /** + * Appends a message to the transcript. + * @param label the label of the message + * @param message the actual message (content) + */ + public void appendMessage(byte[] label, byte[] message) { + labels.add(label); + messages.add(message); + } +} diff --git a/polkaj-schnorrkel/src/main/java/io/emeraldpay/polkaj/schnorrkel/Schnorrkel.java b/polkaj-schnorrkel/src/main/java/io/emeraldpay/polkaj/schnorrkel/Schnorrkel.java index 0ce82b1d..eb654104 100644 --- a/polkaj-schnorrkel/src/main/java/io/emeraldpay/polkaj/schnorrkel/Schnorrkel.java +++ b/polkaj-schnorrkel/src/main/java/io/emeraldpay/polkaj/schnorrkel/Schnorrkel.java @@ -1,18 +1,9 @@ package io.emeraldpay.polkaj.schnorrkel; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodHandles; -import java.lang.invoke.MethodType; -import java.lang.invoke.VarHandle; -import java.nio.file.Files; -import java.nio.file.Path; -import java.security.NoSuchAlgorithmException; +import io.emeraldpay.polkaj.merlin.TranscriptData; + import java.security.SecureRandom; import java.util.Arrays; -import java.util.logging.MemoryHandler; /** * Schnorrkel implements Schnorr signature on Ristretto compressed Ed25519 points, as well as related protocols like @@ -141,6 +132,12 @@ public static Schnorrkel getInstance() { */ public abstract Schnorrkel.PublicKey derivePublicKeySoft(Schnorrkel.PublicKey base, Schnorrkel.ChainCode chainCode) throws SchnorrkelException; + public abstract boolean vrfVerify(PublicKey sk, TranscriptData transcript, VrfOutputAndProof vrfOutputAndProof); + + public abstract VrfOutputAndProof vrfSign(KeyPair keyPair, TranscriptData transcript); + + public abstract byte[] makeBytes(Schnorrkel.PublicKey publicKey, TranscriptData transcript, VrfOutputAndProof vrfOutputAndProof); + // ====================== Supporting Classes ====================== /** @@ -248,5 +245,4 @@ public int hashCode() { return Arrays.hashCode(value); } } - } diff --git a/polkaj-schnorrkel/src/main/java/io/emeraldpay/polkaj/schnorrkel/SchnorrkelNative.java b/polkaj-schnorrkel/src/main/java/io/emeraldpay/polkaj/schnorrkel/SchnorrkelNative.java index 732f0eed..fadbfaf5 100644 --- a/polkaj-schnorrkel/src/main/java/io/emeraldpay/polkaj/schnorrkel/SchnorrkelNative.java +++ b/polkaj-schnorrkel/src/main/java/io/emeraldpay/polkaj/schnorrkel/SchnorrkelNative.java @@ -1,5 +1,7 @@ package io.emeraldpay.polkaj.schnorrkel; +import io.emeraldpay.polkaj.merlin.TranscriptData; + import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; @@ -18,57 +20,24 @@ */ public class SchnorrkelNative extends Schnorrkel { - @Override - public byte[] sign(byte[] message, KeyPair keypair) throws SchnorrkelException { - return SchnorrkelNative.sign(keypair.getPublicKey(), keypair.getSecretKey(), message); - } + private static final String LIBNAME = "polkaj_schnorrkel"; - @Override - public boolean verify(byte[] signature, byte[] message, PublicKey publicKey) throws SchnorrkelException { - return SchnorrkelNative.verify(signature, message, publicKey.getPublicKey()); - } + static { - @Override - public KeyPair generateKeyPair() throws SchnorrkelException { try { - return generateKeyPair(SecureRandom.getInstanceStrong()); - } catch (NoSuchAlgorithmException e) { - throw new SchnorrkelException("Secure Random is not available"); + // JVM needs native libraries to be loaded from filesystem, so first we need to extract + // files for current OS into a temp dir then load the file. + if (!extractAndLoadJNI()) { + // load the native library, this is for running tests + System.loadLibrary(LIBNAME); + } + } catch (IOException e) { + System.err.println("Failed to extract JNI library from Jar file. " + e.getClass() + ":" + e.getMessage()); + } catch (UnsatisfiedLinkError e) { + System.err.println("Failed to load native library. Polkaj Schnorrkel methods are unavailable. Error: " + e.getMessage()); } } - @Override - public KeyPair generateKeyPair(SecureRandom random) throws SchnorrkelException { - byte[] seed = new byte[32]; - random.nextBytes(seed); - byte[] key = keypairFromSeed(seed); - return decodeKeyPair(key); - } - - @Override - public KeyPair generateKeyPairFromSeed(byte[] seed) throws SchnorrkelException { - byte[] key = keypairFromSeed(seed); - return decodeKeyPair(key); - } - - @Override - public KeyPair deriveKeyPair(KeyPair base, ChainCode chainCode) throws SchnorrkelException { - byte[] key = deriveHard(encodeKeyPair(base), chainCode.getValue()); - return decodeKeyPair(key); - } - - @Override - public KeyPair deriveKeyPairSoft(KeyPair base, ChainCode chainCode) throws SchnorrkelException { - byte[] key = deriveSoft(encodeKeyPair(base), chainCode.getValue()); - return decodeKeyPair(key); - } - - @Override - public PublicKey derivePublicKeySoft(PublicKey base, ChainCode chainCode) throws SchnorrkelException { - byte[] key = derivePublicKeySoft(base.getPublicKey(), chainCode.getValue()); - return new Schnorrkel.PublicKey(key); - } - private static Schnorrkel.KeyPair decodeKeyPair(byte[] key) throws SchnorrkelException { if (key.length != KEYPAIR_LENGTH) { throw new SchnorrkelException("Invalid key generated"); @@ -91,41 +60,44 @@ private static byte[] encodeKeyPair(Schnorrkel.KeyPair keyPair) { // ====================== Mapping to the Native Library ====================== private static native byte[] sign(byte[] publicKey, byte[] secretKey, byte[] message); + private static native byte[] keypairFromSeed(byte[] seed); + private static native boolean verify(byte[] signature, byte[] message, byte[] publicKey); + private static native byte[] deriveHard(byte[] secret, byte[] cc); + private static native byte[] deriveSoft(byte[] secret, byte[] cc); + private static native byte[] derivePublicKeySoft(byte[] publicKey, byte[] cc); - // ====================== LOAD NATIVE LIBRARY ====================== + private static native boolean vrfVerify(byte[] publicKey, TranscriptData transcript, byte[] vrfOutput, byte[] vrfProof); - private static final String LIBNAME = "polkaj_schnorrkel"; + private static native byte[] vrfSign(byte[] secretKey, TranscriptData transcript); - static { - - try { - // JVM needs native libraries to be loaded from filesystem, so first we need to extract - // files for current OS into a temp dir then load the file. - if(!extractAndLoadJNI()) { - // load the native library, this is for running tests - System.loadLibrary(LIBNAME); - } - } catch (IOException e) { - System.err.println("Failed to extract JNI library from Jar file. " + e.getClass() + ":" + e.getMessage()); - } catch (UnsatisfiedLinkError e){ - System.err.println("Failed to load native library. Polkaj Schnorrkel methods are unavailable. Error: " + e.getMessage()); - } - } + private static native byte[] makeBytes(byte[] publicKey, TranscriptData transcript, byte[] vrfOutput); private static boolean extractAndLoadJNI() throws IOException { // define which of files bundled with Jar to extract String os = System.getProperty("os.name", "unknown").toLowerCase(); if (os.contains("win")) { - os = "windows"; + os = "windows/amd"; } else if (os.contains("mac")) { - os = "macos"; + String arch = System.getProperty("os.arch").toLowerCase(); + if (arch.equalsIgnoreCase("aarch64")) { + os = "macos/arm"; + } else { + os = "macos/amd"; + } } else if (os.contains("nux")) { - os = "linux"; + String arch = System.getProperty("os.arch").toLowerCase(); + if (arch.equalsIgnoreCase("aarch64")) { + os = "linux/arm"; + } else if (arch.equalsIgnoreCase("amd64")) { + os = "linux/amd"; + } else { + os = "linux/i686"; + } } else { System.err.println("Unknown OS: " + os + ". Unable to setup native library for Polkaj Schnorrkel"); return false; @@ -135,7 +107,6 @@ private static boolean extractAndLoadJNI() throws IOException { // extract native lib to the filesystem InputStream lib = Schnorrkel.class.getResourceAsStream(classpathFile); - System.out.println(classpathFile); if (lib == null) { System.err.println("Library " + classpathFile + " is not found in the classpath"); return false; @@ -145,7 +116,7 @@ private static boolean extractAndLoadJNI() throws IOException { Files.copy(lib, target); System.load(target.toFile().getAbsolutePath()); - System.out.println("library " + classpathFile + " is loaded"); + System.out.println("Library " + classpathFile + " is loaded"); // setup JVM to delete files on exit, when possible target.toFile().deleteOnExit(); @@ -153,4 +124,87 @@ private static boolean extractAndLoadJNI() throws IOException { return true; } + @Override + public byte[] sign(byte[] message, KeyPair keypair) throws SchnorrkelException { + return SchnorrkelNative.sign(keypair.getPublicKey(), keypair.getSecretKey(), message); + } + + @Override + public boolean verify(byte[] signature, byte[] message, PublicKey publicKey) throws SchnorrkelException { + return SchnorrkelNative.verify(signature, message, publicKey.getPublicKey()); + } + + @Override + public KeyPair generateKeyPair() throws SchnorrkelException { + try { + return generateKeyPair(SecureRandom.getInstanceStrong()); + } catch (NoSuchAlgorithmException e) { + throw new SchnorrkelException("Secure Random is not available"); + } + } + + @Override + public KeyPair generateKeyPair(SecureRandom random) throws SchnorrkelException { + byte[] seed = new byte[32]; + random.nextBytes(seed); + byte[] key = keypairFromSeed(seed); + return decodeKeyPair(key); + } + + @Override + public KeyPair generateKeyPairFromSeed(byte[] seed) throws SchnorrkelException { + byte[] key = keypairFromSeed(seed); + return decodeKeyPair(key); + } + + // ====================== LOAD NATIVE LIBRARY ====================== + + @Override + public KeyPair deriveKeyPair(KeyPair base, ChainCode chainCode) throws SchnorrkelException { + byte[] key = deriveHard(encodeKeyPair(base), chainCode.getValue()); + return decodeKeyPair(key); + } + + @Override + public KeyPair deriveKeyPairSoft(KeyPair base, ChainCode chainCode) throws SchnorrkelException { + byte[] key = deriveSoft(encodeKeyPair(base), chainCode.getValue()); + return decodeKeyPair(key); + } + + @Override + public PublicKey derivePublicKeySoft(PublicKey base, ChainCode chainCode) throws SchnorrkelException { + byte[] key = derivePublicKeySoft(base.getPublicKey(), chainCode.getValue()); + return new Schnorrkel.PublicKey(key); + } + + @Override + public boolean vrfVerify(PublicKey pk, TranscriptData transcript, VrfOutputAndProof vrfOutputAndProof) { + return vrfVerify( + pk.getPublicKey(), + transcript, + vrfOutputAndProof.getOutput(), + vrfOutputAndProof.getProof() + ); + } + + @Override + public VrfOutputAndProof vrfSign(KeyPair keyPair, TranscriptData transcript) { + byte[] vrfOutputAndProofBytes = vrfSign(keyPair.getSecretKey(), transcript); + final int OUTPUT_LEN = VrfOutputAndProof.OUTPUT_BYTE_LEN; + final int PROOF_LEN = VrfOutputAndProof.PROOF_BYTE_LEN; + + // effectively, split the array + byte[] vrfOutput = new byte[OUTPUT_LEN]; + byte[] vrfProof = new byte[PROOF_LEN]; + System.arraycopy(vrfOutputAndProofBytes, 0, vrfOutput, 0, OUTPUT_LEN); + System.arraycopy(vrfOutputAndProofBytes, OUTPUT_LEN, vrfProof, 0, PROOF_LEN); + + return VrfOutputAndProof.wrap(vrfOutput, vrfProof); + } + + @Override + public byte[] makeBytes(Schnorrkel.PublicKey publicKey, TranscriptData transcript, VrfOutputAndProof vrfOutputAndProof) { + byte[] vrfOutput = vrfOutputAndProof.getOutput(); + return makeBytes(publicKey.getPublicKey(), transcript, vrfOutput); + } } diff --git a/polkaj-schnorrkel/src/main/java/io/emeraldpay/polkaj/schnorrkel/VrfOutputAndProof.java b/polkaj-schnorrkel/src/main/java/io/emeraldpay/polkaj/schnorrkel/VrfOutputAndProof.java new file mode 100644 index 00000000..bc2f23c1 --- /dev/null +++ b/polkaj-schnorrkel/src/main/java/io/emeraldpay/polkaj/schnorrkel/VrfOutputAndProof.java @@ -0,0 +1,40 @@ +package io.emeraldpay.polkaj.schnorrkel; + +/** + * Essentially a record to hold the VRF output together with its proof. + */ +public class VrfOutputAndProof { + public static final int OUTPUT_BYTE_LEN = 32; + public static final int PROOF_BYTE_LEN = 64; + + private final byte[] output; + + private final byte[] proof; + + public static VrfOutputAndProof wrap(byte[] output, byte[] proof) { + if (output.length != OUTPUT_BYTE_LEN) { + throw new IllegalArgumentException( + String.format("VRF output must be %d bytes (compressed ristretto point).", OUTPUT_BYTE_LEN)); + } + + if (proof.length != PROOF_BYTE_LEN) { + throw new IllegalArgumentException( + String.format("VRF proof must be %d bytes (compressed ristretto point).", PROOF_BYTE_LEN)); + } + + return new VrfOutputAndProof(output, proof); + } + + private VrfOutputAndProof(byte[] output, byte[] proof) { + this.output = output; + this.proof = proof; + } + + public byte[] getOutput() { + return output; + } + + public byte[] getProof() { + return proof; + } +} diff --git a/polkaj-schnorrkel/src/main/resources/native/linux/amd/libpolkaj_schnorrkel.so b/polkaj-schnorrkel/src/main/resources/native/linux/amd/libpolkaj_schnorrkel.so new file mode 100755 index 00000000..9add0a0e Binary files /dev/null and b/polkaj-schnorrkel/src/main/resources/native/linux/amd/libpolkaj_schnorrkel.so differ diff --git a/polkaj-schnorrkel/src/main/resources/native/linux/arm/libpolkaj_schnorrkel.so b/polkaj-schnorrkel/src/main/resources/native/linux/arm/libpolkaj_schnorrkel.so new file mode 100755 index 00000000..51cf88df Binary files /dev/null and b/polkaj-schnorrkel/src/main/resources/native/linux/arm/libpolkaj_schnorrkel.so differ diff --git a/polkaj-schnorrkel/src/main/resources/native/linux/i686/libpolkaj_schnorrkel.so b/polkaj-schnorrkel/src/main/resources/native/linux/i686/libpolkaj_schnorrkel.so new file mode 100755 index 00000000..bd5a42b4 Binary files /dev/null and b/polkaj-schnorrkel/src/main/resources/native/linux/i686/libpolkaj_schnorrkel.so differ diff --git a/polkaj-schnorrkel/src/main/resources/native/macos/amd/libpolkaj_schnorrkel.dylib b/polkaj-schnorrkel/src/main/resources/native/macos/amd/libpolkaj_schnorrkel.dylib new file mode 100755 index 00000000..c8c9dfe1 Binary files /dev/null and b/polkaj-schnorrkel/src/main/resources/native/macos/amd/libpolkaj_schnorrkel.dylib differ diff --git a/polkaj-schnorrkel/src/main/resources/native/macos/arm/libpolkaj_schnorrkel.dylib b/polkaj-schnorrkel/src/main/resources/native/macos/arm/libpolkaj_schnorrkel.dylib new file mode 100755 index 00000000..336ea54e Binary files /dev/null and b/polkaj-schnorrkel/src/main/resources/native/macos/arm/libpolkaj_schnorrkel.dylib differ diff --git a/polkaj-schnorrkel/src/main/resources/native/windows/amd/polkaj_schnorrkel.dll b/polkaj-schnorrkel/src/main/resources/native/windows/amd/polkaj_schnorrkel.dll new file mode 100755 index 00000000..45d67e35 Binary files /dev/null and b/polkaj-schnorrkel/src/main/resources/native/windows/amd/polkaj_schnorrkel.dll differ diff --git a/polkaj-schnorrkel/src/rust/.gitignore b/polkaj-schnorrkel/src/rust/.gitignore index b354aec7..9f970225 100644 --- a/polkaj-schnorrkel/src/rust/.gitignore +++ b/polkaj-schnorrkel/src/rust/.gitignore @@ -1,2 +1 @@ -Cargo.lock target/ \ No newline at end of file diff --git a/polkaj-schnorrkel/src/rust/.vscode/settings.json b/polkaj-schnorrkel/src/rust/.vscode/settings.json new file mode 100644 index 00000000..80121080 --- /dev/null +++ b/polkaj-schnorrkel/src/rust/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "rust-analyzer.linkedProjects": [ + "./Cargo.toml" + ] +} \ No newline at end of file diff --git a/polkaj-schnorrkel/src/rust/Cargo.lock b/polkaj-schnorrkel/src/rust/Cargo.lock new file mode 100644 index 00000000..25a5bada --- /dev/null +++ b/polkaj-schnorrkel/src/rust/Cargo.lock @@ -0,0 +1,767 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static", + "regex", +] + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "arrayref" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "combine" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a677b8922c94e01bdbb12126b0bc852f00447528dee1782229af9c720c3f348" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "platforms", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "darling" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" +dependencies = [ + "darling_core", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1676f435fc1dadde4d03e43f5d62b259e1ce5f40bd4ffb21db2b42ebe59c1382" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom_or_panic" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea1015b5a70616b688dc230cfe50c8af89d972cb132d5a622814d29773b10b9" +dependencies = [ + "rand", + "rand_core", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "jni" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", + "windows-sys", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + +[[package]] +name = "memchr" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" + +[[package]] +name = "merlin" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" +dependencies = [ + "byteorder", + "keccak", + "rand_core", + "zeroize", +] + +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + +[[package]] +name = "platforms" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626dec3cac7cc0e1577a2ec3fc496277ec2baa084bebad95bb6fdbfae235f84c" + +[[package]] +name = "polkaj-schnorrkel" +version = "0.3.0" +dependencies = [ + "hex", + "jni 0.21.1", + "merlin", + "rand", + "robusta_jni", + "schnorrkel", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "regex" +version = "1.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "robusta-codegen" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb512b451472948a204452dfad582bdc48d69caacdd3b1b4571d5e3f11707f3" +dependencies = [ + "Inflector", + "darling", + "proc-macro-error", + "proc-macro2", + "quote", + "rand", + "syn 1.0.109", +] + +[[package]] +name = "robusta_jni" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c080146e0cc733697fe500413871142af91bd879641205c2febbe5f982f304e3" +dependencies = [ + "jni 0.19.0", + "paste", + "robusta-codegen", + "static_assertions", +] + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schnorrkel" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de18f6d8ba0aad7045f5feae07ec29899c1112584a38509a84ad7b04451eaa0" +dependencies = [ + "aead", + "arrayref", + "arrayvec", + "curve25519-dalek", + "getrandom_or_panic", + "merlin", + "rand_core", + "serde_bytes", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" + +[[package]] +name = "serde" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_bytes" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b8497c313fd43ab992087548117643f6fcd935cbf36f176ffda0aacf9591734" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "zeroize" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] diff --git a/polkaj-schnorrkel/src/rust/Cargo.toml b/polkaj-schnorrkel/src/rust/Cargo.toml index daa3d77f..36a72b77 100644 --- a/polkaj-schnorrkel/src/rust/Cargo.toml +++ b/polkaj-schnorrkel/src/rust/Cargo.toml @@ -3,10 +3,12 @@ name = "polkaj-schnorrkel" version = "0.3.0" [lib] -crate_type = ["cdylib"] +crate-type = ["cdylib"] [dependencies] -jni = "0.17.0" -schnorrkel = "0.9.1" +robusta_jni = "0.2" +jni = "0.21.1" +schnorrkel = "0.11.4" hex = "0.4.2" -rand = "0.7.3" \ No newline at end of file +rand = "0.8.5" +merlin = "3.0" \ No newline at end of file diff --git a/polkaj-schnorrkel/src/rust/src/lib.rs b/polkaj-schnorrkel/src/rust/src/lib.rs index 5c895ae0..258926e0 100644 --- a/polkaj-schnorrkel/src/rust/src/lib.rs +++ b/polkaj-schnorrkel/src/rust/src/lib.rs @@ -2,46 +2,55 @@ // Based on https://github.com/polkadot-js/wasm/blob/master/packages/wasm-crypto/src/sr25519.rs // -extern crate jni; -extern crate schnorrkel; extern crate hex; +extern crate merlin; extern crate rand; +extern crate robusta_jni; +extern crate schnorrkel; + +mod merlin_jni; -use jni::JNIEnv; -use jni::objects::{JClass}; -use jni::sys::{jbyteArray, jboolean}; -use schnorrkel::{SecretKey, PublicKey, Signature, SignatureError, MiniSecretKey, ExpansionMode, Keypair}; -use schnorrkel::derive::{ChainCode, CHAIN_CODE_LENGTH, Derivation}; +use merlin::Transcript; +use robusta_jni::jni::objects::JClass; +use robusta_jni::jni::objects::JObject; +use robusta_jni::jni::sys::{jboolean, jbyteArray}; +use robusta_jni::jni::JNIEnv; +use schnorrkel::context::SigningTranscript; +use schnorrkel::derive::{ChainCode, Derivation, CHAIN_CODE_LENGTH}; +use schnorrkel::vrf::{VRFInOut, VRFPreOut, VRFProof, VRFProofBatchable, VRFSigningTranscript}; +use schnorrkel::{ + ExpansionMode, Keypair, MiniSecretKey, PublicKey, SecretKey, Signature, SignatureError, +}; use std::string::String; -const SIGNING_CTX: &'static [u8] = b"substrate"; +use merlin_jni::TranscriptData; +use robusta_jni::convert::TryFromJavaValue; + +const SIGNING_CTX: &[u8] = b"substrate"; +const AUTHORING_SCORE_VRF_CONTEXT: &[u8] = b"substrate-babe-vrf"; /// ChainCode construction helper fn create_cc(data: &[u8]) -> ChainCode { let mut cc = [0u8; CHAIN_CODE_LENGTH]; - cc.copy_from_slice(&data); + cc.copy_from_slice(data); ChainCode(cc) } fn sign(message: Vec, sk: Vec, pubkey: Vec) -> Result, String> { - let pubkey = PublicKey::from_bytes(pubkey.as_slice()) - .map_err(|e| e.to_string())?; + let pubkey = PublicKey::from_bytes(pubkey.as_slice()).map_err(|e| e.to_string())?; let signature = SecretKey::from_ed25519_bytes(sk.as_slice()) .map_err(|e| e.to_string())? - .sign_simple(SIGNING_CTX, - message.as_slice(), - &pubkey) + .sign_simple(SIGNING_CTX, message.as_slice(), &pubkey) .to_bytes() .to_vec(); Ok(signature) } fn verify(signature: &[u8], message: &[u8], public: &[u8]) -> Result { - let signature = Signature::from_bytes(signature) - .map_err(|e| e.to_string())?; + let signature = Signature::from_bytes(signature).map_err(|e| e.to_string())?; let result = PublicKey::from_bytes(public) .map_err(|e| e.to_string())? .verify_simple(SIGNING_CTX, message, &signature) @@ -50,8 +59,8 @@ fn verify(signature: &[u8], message: &[u8], public: &[u8]) -> Result Ok(value), Err(err) => match err { SignatureError::EquationFalse => Ok(false), - _ => Err(err.to_string()) - } + _ => Err(err.to_string()), + }, } } @@ -68,7 +77,8 @@ pub fn derive_keypair_hard(pair: &[u8], cc: &[u8]) -> Result, String> { let result = Keypair::from_half_ed25519_bytes(pair) .map_err(|e| e.to_string())? .secret - .hard_derive_mini_secret_key(Some(create_cc(cc)), &[]).0 + .hard_derive_mini_secret_key(Some(create_cc(cc)), []) + .0 .expand_to_keypair(ExpansionMode::Ed25519) .to_half_ed25519_bytes() .to_vec(); @@ -78,41 +88,49 @@ pub fn derive_keypair_hard(pair: &[u8], cc: &[u8]) -> Result, String> { pub fn derive_keypair_soft(pair: &[u8], cc: &[u8]) -> Result, String> { let result = Keypair::from_half_ed25519_bytes(pair) .map_err(|e| e.to_string())? - .derived_key_simple(create_cc(cc), &[]).0 + .derived_key_simple(create_cc(cc), []) + .0 .to_half_ed25519_bytes() .to_vec(); Ok(result) } pub fn derive_pubkey_soft(pubkey: &[u8], cc: &[u8]) -> Result, String> { - let result = PublicKey::from_bytes(pubkey) + let result = PublicKey::from_bytes(pubkey) .map_err(|e| e.to_string())? - .derived_key_simple(create_cc(cc), &[]).0 + .derived_key_simple(create_cc(cc), []) + .0 .to_bytes() .to_vec(); Ok(result) } #[no_mangle] -pub extern "system" fn Java_io_emeraldpay_polkaj_schnorrkel_SchnorrkelNative_sign - (env: JNIEnv, _class: JClass, pubkey: jbyteArray, sk: jbyteArray, message: jbyteArray) -> jbyteArray { - - let message = env.convert_byte_array(message) +pub extern "system" fn Java_io_emeraldpay_polkaj_schnorrkel_SchnorrkelNative_sign( + env: JNIEnv, + _class: JClass, + pubkey: jbyteArray, + sk: jbyteArray, + message: jbyteArray, +) -> jbyteArray { + let message = env + .convert_byte_array(message) .expect("Message is not provided"); - let sk = env.convert_byte_array(sk) + let sk = env + .convert_byte_array(sk) .expect("Secret Key is not provided"); - let pubkey = env.convert_byte_array(pubkey) + let pubkey = env + .convert_byte_array(pubkey) .expect("Public Key is not provided"); let output = match sign(message, sk, pubkey) { - Ok(signature) => { - env.byte_array_from_slice(signature.as_slice()) - .expect("Couldn't create result") - }, + Ok(signature) => env + .byte_array_from_slice(signature.as_slice()) + .expect("Couldn't create result"), Err(msg) => { - let none = env.new_byte_array(0) - .expect("Couldn't create empty result"); - env.throw_new("io/emeraldpay/polkaj/schnorrkel/SchnorrkelException", msg).unwrap(); + let none = env.new_byte_array(0).expect("Couldn't create empty result"); + env.throw_new("io/emeraldpay/polkaj/schnorrkel/SchnorrkelException", msg) + .unwrap(); none } }; @@ -120,23 +138,29 @@ pub extern "system" fn Java_io_emeraldpay_polkaj_schnorrkel_SchnorrkelNative_sig } #[no_mangle] -pub extern "system" fn Java_io_emeraldpay_polkaj_schnorrkel_SchnorrkelNative_verify -(env: JNIEnv, _class: JClass, signature: jbyteArray, message: jbyteArray, pubkey: jbyteArray) -> jboolean { - - let message = env.convert_byte_array(message) +pub extern "system" fn Java_io_emeraldpay_polkaj_schnorrkel_SchnorrkelNative_verify( + env: JNIEnv, + _class: JClass, + signature: jbyteArray, + message: jbyteArray, + pubkey: jbyteArray, +) -> jboolean { + let message = env + .convert_byte_array(message) .expect("Message is not provided"); - let pubkey = env.convert_byte_array(pubkey) + let pubkey = env + .convert_byte_array(pubkey) .expect("Public Key is not provided"); - let signature = env.convert_byte_array(signature) + let signature = env + .convert_byte_array(signature) .expect("Signature is not provided"); let output = match verify(signature.as_slice(), message.as_slice(), pubkey.as_slice()) { - Ok(valid) => { - valid as jboolean - }, + Ok(valid) => valid as jboolean, Err(msg) => { let none = false as jboolean; - env.throw_new("io/emeraldpay/polkaj/schnorrkel/SchnorrkelException", msg).unwrap(); + env.throw_new("io/emeraldpay/polkaj/schnorrkel/SchnorrkelException", msg) + .unwrap(); none } }; @@ -144,21 +168,21 @@ pub extern "system" fn Java_io_emeraldpay_polkaj_schnorrkel_SchnorrkelNative_ver } #[no_mangle] -pub extern "system" fn Java_io_emeraldpay_polkaj_schnorrkel_SchnorrkelNative_keypairFromSeed -(env: JNIEnv, _class: JClass, seed: jbyteArray) -> jbyteArray { - - let seed = env.convert_byte_array(seed) - .expect("Seed is not provided"); +pub extern "system" fn Java_io_emeraldpay_polkaj_schnorrkel_SchnorrkelNative_keypairFromSeed( + env: JNIEnv, + _class: JClass, + seed: jbyteArray, +) -> jbyteArray { + let seed = env.convert_byte_array(seed).expect("Seed is not provided"); let output = match keypair_from_seed(seed.as_slice()) { - Ok(value) => { - env.byte_array_from_slice(value.as_slice()) - .expect("Couldn't create result") - }, + Ok(value) => env + .byte_array_from_slice(value.as_slice()) + .expect("Couldn't create result"), Err(msg) => { - let none = env.new_byte_array(0) - .expect("Couldn't create empty result"); - env.throw_new("io/emeraldpay/polkaj/schnorrkel/SchnorrkelException", msg).unwrap(); + let none = env.new_byte_array(0).expect("Couldn't create empty result"); + env.throw_new("io/emeraldpay/polkaj/schnorrkel/SchnorrkelException", msg) + .unwrap(); none } }; @@ -166,23 +190,27 @@ pub extern "system" fn Java_io_emeraldpay_polkaj_schnorrkel_SchnorrkelNative_key } #[no_mangle] -pub extern "system" fn Java_io_emeraldpay_polkaj_schnorrkel_SchnorrkelNative_deriveHard -(env: JNIEnv, _class: JClass, keypair: jbyteArray, cc: jbyteArray) -> jbyteArray { - - let keypair = env.convert_byte_array(keypair) +pub extern "system" fn Java_io_emeraldpay_polkaj_schnorrkel_SchnorrkelNative_deriveHard( + env: JNIEnv, + _class: JClass, + keypair: jbyteArray, + cc: jbyteArray, +) -> jbyteArray { + let keypair = env + .convert_byte_array(keypair) .expect("Keypair is not provided"); - let cc = env.convert_byte_array(cc) + let cc = env + .convert_byte_array(cc) .expect("ChainCode is not provided"); let output = match derive_keypair_hard(keypair.as_slice(), cc.as_slice()) { - Ok(value) => { - env.byte_array_from_slice(value.as_slice()) - .expect("Couldn't create result") - }, + Ok(value) => env + .byte_array_from_slice(value.as_slice()) + .expect("Couldn't create result"), Err(msg) => { - let none = env.new_byte_array(0) - .expect("Couldn't create empty result"); - env.throw_new("io/emeraldpay/polkaj/schnorrkel/SchnorrkelException", msg).unwrap(); + let none = env.new_byte_array(0).expect("Couldn't create empty result"); + env.throw_new("io/emeraldpay/polkaj/schnorrkel/SchnorrkelException", msg) + .unwrap(); none } }; @@ -190,23 +218,27 @@ pub extern "system" fn Java_io_emeraldpay_polkaj_schnorrkel_SchnorrkelNative_der } #[no_mangle] -pub extern "system" fn Java_io_emeraldpay_polkaj_schnorrkel_SchnorrkelNative_deriveSoft -(env: JNIEnv, _class: JClass, keypair: jbyteArray, cc: jbyteArray) -> jbyteArray { - - let keypair = env.convert_byte_array(keypair) +pub extern "system" fn Java_io_emeraldpay_polkaj_schnorrkel_SchnorrkelNative_deriveSoft( + env: JNIEnv, + _class: JClass, + keypair: jbyteArray, + cc: jbyteArray, +) -> jbyteArray { + let keypair = env + .convert_byte_array(keypair) .expect("Keypair is not provided"); - let cc = env.convert_byte_array(cc) + let cc = env + .convert_byte_array(cc) .expect("ChainCode is not provided"); let output = match derive_keypair_soft(keypair.as_slice(), cc.as_slice()) { - Ok(value) => { - env.byte_array_from_slice(value.as_slice()) - .expect("Couldn't create result") - }, + Ok(value) => env + .byte_array_from_slice(value.as_slice()) + .expect("Couldn't create result"), Err(msg) => { - let none = env.new_byte_array(0) - .expect("Couldn't create empty result"); - env.throw_new("io/emeraldpay/polkaj/schnorrkel/SchnorrkelException", msg).unwrap(); + let none = env.new_byte_array(0).expect("Couldn't create empty result"); + env.throw_new("io/emeraldpay/polkaj/schnorrkel/SchnorrkelException", msg) + .unwrap(); none } }; @@ -214,25 +246,244 @@ pub extern "system" fn Java_io_emeraldpay_polkaj_schnorrkel_SchnorrkelNative_der } #[no_mangle] -pub extern "system" fn Java_io_emeraldpay_polkaj_schnorrkel_SchnorrkelNative_derivePublicKeySoft -(env: JNIEnv, _class: JClass, pubkey: jbyteArray, cc: jbyteArray) -> jbyteArray { - - let pubkey = env.convert_byte_array(pubkey) +pub extern "system" fn Java_io_emeraldpay_polkaj_schnorrkel_SchnorrkelNative_derivePublicKeySoft( + env: JNIEnv, + _class: JClass, + pubkey: jbyteArray, + cc: jbyteArray, +) -> jbyteArray { + let pubkey = env + .convert_byte_array(pubkey) .expect("Keypair is not provided"); - let cc = env.convert_byte_array(cc) + let cc = env + .convert_byte_array(cc) .expect("ChainCode is not provided"); let output = match derive_pubkey_soft(pubkey.as_slice(), cc.as_slice()) { - Ok(value) => { - env.byte_array_from_slice(value.as_slice()) - .expect("Couldn't create result") - }, + Ok(value) => env + .byte_array_from_slice(value.as_slice()) + .expect("Couldn't create result"), Err(msg) => { - let none = env.new_byte_array(0) - .expect("Couldn't create empty result"); - env.throw_new("io/emeraldpay/polkaj/schnorrkel/SchnorrkelException", msg).unwrap(); + let none = env.new_byte_array(0).expect("Couldn't create empty result"); + env.throw_new("io/emeraldpay/polkaj/schnorrkel/SchnorrkelException", msg) + .unwrap(); none } }; output -} \ No newline at end of file +} + +#[no_mangle] +pub extern "system" fn Java_io_emeraldpay_polkaj_schnorrkel_SchnorrkelNative_vrfVerify( + env: JNIEnv, + _class: JClass, + pk_raw: jbyteArray, + transcript_data_raw: JObject, + vrf_output_raw: jbyteArray, + vrf_proof_raw: jbyteArray, +) -> jboolean { + let pk_bytes = env + .convert_byte_array(pk_raw) + .expect("Public key bytes not provided."); + + let transcript_data = match TranscriptData::try_from(transcript_data_raw, &env) { + Ok(data) => data, + Err(msg) => { + env.throw_new( + "io/emeraldpay/polkaj/schnorrkel/SchnorrkelException", + msg.to_string(), + ) + .unwrap(); + return false as jboolean; + } + }; + + let transcript = Transcript::from(transcript_data); + + let vrf_output_bytes = env + .convert_byte_array(vrf_output_raw) + .expect("Vrf output bytes not provided."); + + let vrf_proof_bytes = env + .convert_byte_array(vrf_proof_raw) + .expect("Vrf proof bytes not provided."); + + let output = match vrf_verify(&pk_bytes, transcript, &vrf_output_bytes, &vrf_proof_bytes) { + Ok(_) => true, + Err(SignatureError::EquationFalse) => false, + Err(err) => { + env.throw_new( + "io/emeraldpay/polkaj/schnorrkel/SchnorrkelException", + err.to_string(), + ) + .unwrap(); + false + } + }; + + output as jboolean +} + +fn vrf_verify( + pk_bytes: &[u8], + transcript: Transcript, + vrf_output_bytes: &[u8], + vrf_proof_bytes: &[u8], +) -> Result<(), SignatureError> { + let signing_public_key = schnorrkel::PublicKey::from_bytes(pk_bytes)?; + + // NOTE: + // These `from_bytes`s can only panic if `vrf_output_bytes` or `vrf_proof_bytes` are of the wrong + // length, which is the Java caller's responsibility. In any case, errors are properly accounted for. + let vrf_output = schnorrkel::vrf::VRFPreOut::from_bytes(vrf_output_bytes)?; + let vrf_proof = schnorrkel::vrf::VRFProof::from_bytes(vrf_proof_bytes)?; + + signing_public_key.vrf_verify(transcript, &vrf_output, &vrf_proof)?; + Ok(()) +} + +#[no_mangle] +pub extern "system" fn Java_io_emeraldpay_polkaj_schnorrkel_SchnorrkelNative_vrfSign( + env: JNIEnv, + _class: JClass, + sk_raw: jbyteArray, + transcript_data_raw: JObject, +) -> jbyteArray { + let sk_bytes = env + .convert_byte_array(sk_raw) + .expect("Secret key bytes not provided."); + + let transcript_data = match TranscriptData::try_from(transcript_data_raw, &env) { + Ok(data) => data, + Err(msg) => { + env.throw_new( + "io/emeraldpay/polkaj/schnorrkel/SchnorrkelException", + msg.to_string(), + ) + .unwrap(); + return *JObject::null(); + } + }; + + let transcript = Transcript::from(transcript_data); + + match vrf_sign(&sk_bytes, transcript) { + Err(err) => { + env.throw_new( + "io/emeraldpay/polkaj/schnorrkel/SchnorrkelException", + err.to_string(), + ) + .unwrap(); + *JObject::null() + } + Ok((VRFInOut { output, .. }, vrf_proof, _)) => { + let output_bytes = output.to_bytes(); + let proof_bytes = vrf_proof.to_bytes(); + + // HACK: + // Constructing a Java class instance from Rust is a bit of a pain. + // So for now, we're just concatenating the two byte arrays and returning them as one as a hacky serialization workaround. + // If, however, in future we'd want to expand this to a more complex data structure, additional work would be needed. + let output_and_proof: Vec = output_bytes + .iter() + .chain(proof_bytes.iter()) + .copied() + .collect(); + env.byte_array_from_slice(output_and_proof.as_slice()) + .expect("Couldn't create result") + } + } +} + +fn vrf_sign( + sk_bytes: &[u8], + transcript: Transcript, +) -> Result<(VRFInOut, VRFProof, VRFProofBatchable), SignatureError> { + let sk = SecretKey::from_ed25519_bytes(sk_bytes)?; + let keypair = sk.to_keypair(); + Ok(keypair.vrf_sign(transcript)) +} + +fn make_bytes( + pk_bytes: &[u8], + context: &[u8], + vrf_input: T, + vrf_pre_output: &VRFPreOut, +) -> Result<[u8; 16], SignatureError> +where + T: VRFSigningTranscript + SigningTranscript, +{ + let pk = PublicKey::from_bytes(pk_bytes)?; + let inout = vrf_pre_output.attach_input_hash(&pk, vrf_input)?; + Ok(inout.make_bytes::<[u8; 16]>(context)) +} + +#[no_mangle] +pub extern "system" fn Java_io_emeraldpay_polkaj_schnorrkel_SchnorrkelNative_makeBytes( + env: JNIEnv, + _class: JClass, + pk_bytes: jbyteArray, + transcript_data: JObject, + vrf_output_bytes: jbyteArray, +) -> jbyteArray { + let pk_bytes = env + .convert_byte_array(pk_bytes) + .expect("Public key bytes not provided."); + + let transcript_data = match TranscriptData::try_from(transcript_data, &env) { + Ok(data) => data, + Err(msg) => { + env.throw_new( + "io/emeraldpay/polkaj/schnorrkel/SchnorrkelException", + msg.to_string(), + ) + .unwrap(); + return *JObject::null(); + } + }; + + let transcript = Transcript::from(transcript_data); + + let vrf_output_bytes = env + .convert_byte_array(vrf_output_bytes) + .expect("Invalid pre-output"); + + let vrf_output_bytes = match VRFPreOut::from_bytes(&vrf_output_bytes) { + Ok(output) => output, + Err(msg) => { + env.throw_new( + "io/emeraldpay/polkaj/schnorrkel/SchnorrkelException", + msg.to_string(), + ) + .unwrap(); + return *JObject::null(); + } + }; + + match make_bytes( + &pk_bytes, + AUTHORING_SCORE_VRF_CONTEXT, + transcript, + &vrf_output_bytes, + ) { + Ok(bytes) => match env.byte_array_from_slice(&bytes) { + Ok(jbytes) => jbytes, + Err(_) => { + env.throw_new( + "io/emeraldpay/polkaj/schnorrkel/SchnorrkelException", + "Failed to convert byte array", + ) + .unwrap(); + *JObject::null() + } + }, + Err(e) => { + env.throw_new( + "io/emeraldpay/polkaj/schnorrkel/SchnorrkelException", + format!("Error: {}", e), + ) + .unwrap(); + *JObject::null() + } + } +} diff --git a/polkaj-schnorrkel/src/rust/src/merlin_jni/mod.rs b/polkaj-schnorrkel/src/rust/src/merlin_jni/mod.rs new file mode 100644 index 00000000..23bb936a --- /dev/null +++ b/polkaj-schnorrkel/src/rust/src/merlin_jni/mod.rs @@ -0,0 +1,60 @@ +use robusta_jni::bridge; + +pub use self::jni::TranscriptData; + +#[bridge] +pub mod jni { + use robusta_jni::convert::{Field, Signature, TryFromJavaValue}; + use robusta_jni::jni::objects::AutoLocal; + + #[derive(Signature, TryFromJavaValue)] + #[package(io.emeraldpay.polkaj.merlin)] + pub struct TranscriptData<'env: 'borrow, 'borrow> { + #[allow(dead_code)] + #[instance] + raw: AutoLocal<'env, 'borrow>, + + // HACK: + // Figure out how to make this simply a `Box<[u8]>` instead of a `Vec>`. + // Currently, this is a blocker with byte[] fields; seems to be an issue with robusta itself, perhaps some missing impls... + // see: https://github.com/giovanniberti/robusta/issues/69 + #[field] + pub domainSeparationLabel: Field<'env, 'borrow, Vec>>, + + #[field] + pub labels: Field<'env, 'borrow, Vec>>, + + #[field] + pub messages: Field<'env, 'borrow, Vec>>, + } +} + +impl<'env, 'borrow> From> for merlin::Transcript { + // SAFETY: + // Since merlin's API requires labels to have a static lifetime, a `Box::leak` invocation is necessary. + // This is safe enough, however, since the boxed values are managed by the Java runtime and will be GC'd there when needed, + // so Rust doesn't have to worry about that (although it wants to). + fn from(value: TranscriptData) -> Self { + // SAFETY: We're purposefully dropping mutability since we want to read only + let domain_separation_label: &[u8] = Box::leak( + value + .domainSeparationLabel + .get() + .unwrap() + .first() + .cloned() + .unwrap(), + ); + + let mut transcript = merlin::Transcript::new(domain_separation_label); + + let message_labels = value.labels.get().unwrap(); + let messages = value.messages.get().unwrap(); + + for (label, message) in message_labels.iter().cloned().zip(messages.iter().cloned()) { + transcript.append_message(Box::leak(label), message.as_ref()) + } + + transcript + } +} diff --git a/polkaj-schnorrkel/src/test/groovy/io/emeraldpay/polkaj/schnorrkel/SchnorrkelNativeSpec.groovy b/polkaj-schnorrkel/src/test/groovy/io/emeraldpay/polkaj/schnorrkel/SchnorrkelNativeSpec.groovy index ab08023a..1c87e19d 100644 --- a/polkaj-schnorrkel/src/test/groovy/io/emeraldpay/polkaj/schnorrkel/SchnorrkelNativeSpec.groovy +++ b/polkaj-schnorrkel/src/test/groovy/io/emeraldpay/polkaj/schnorrkel/SchnorrkelNativeSpec.groovy @@ -1,5 +1,6 @@ package io.emeraldpay.polkaj.schnorrkel +import io.emeraldpay.polkaj.merlin.TranscriptData import org.apache.commons.codec.binary.Hex import spock.lang.Specification @@ -33,7 +34,7 @@ class SchnorrkelNativeSpec extends Specification { Hex.decodeHex( "28b0" ) - )); + )) then: def t = thrown(SchnorrkelException) t.message.length() > 0 @@ -47,7 +48,7 @@ class SchnorrkelNativeSpec extends Specification { byte[] signature = schnorrkel.sign(msg, key1) def act = schnorrkel.verify(signature, msg, key1) then: - act == true + act } def "Modified signature is invalid"() { @@ -57,24 +58,23 @@ class SchnorrkelNativeSpec extends Specification { byte[] signature = schnorrkel.sign(msg, key1) def initial = schnorrkel.verify(signature, msg, key1) then: - initial == true + initial when: signature[0] = (byte)(signature[0] + 1) def act = schnorrkel.verify(signature, msg, key1) then: - act == false + !act } def "Different signature is invalid"() { setup: byte[] msg = "hello".bytes when: - byte[] signature = schnorrkel.sign(msg, key1) byte[] signature2 = schnorrkel.sign("hello2".bytes, key1) def act = schnorrkel.verify(signature2, msg, key1) then: - act == false + !act } def "Throws error on invalid signature"() { @@ -170,4 +170,73 @@ class SchnorrkelNativeSpec extends Specification { then: Hex.encodeHexString(act.getPublicKey()) == "40b9675df90efa6069ff623b0fdfcf706cd47ca7452a5056c7ad58194d23440a" } + + // taken from https://github.com/ChainSafe/go-schnorrkel/blob/d1354d86e41dc066cdf6c755a9d08caffc55b542/vrf_test.go#L90 + def "Test VRF sign and verify"() { + setup: + def signTranscript = new TranscriptData("vrf-test".getBytes()) + def verifyTranscript = new TranscriptData("vrf-test".getBytes()) + + def keyPair = schnorrkel.generateKeyPair() + + def vrfOutputAndProof = schnorrkel.vrfSign(keyPair, signTranscript) + + when: + boolean verified = schnorrkel.vrfVerify(keyPair, verifyTranscript, vrfOutputAndProof) + + then: + verified + } + + // translated from https://github.com/ChainSafe/go-schnorrkel/blob/d1354d86e41dc066cdf6c755a9d08caffc55b542/vrf_test.go#L172 + def "Test VRF verify from Rust"() { + setup: + byte[] pubKeyBytes = new byte[]{-64, 42, 72, -70, 20, 11, 83, -106, -11, 69, -88, -34, 22, -90, -89, 95, 125, -8, -72, 67, -59, 10, -95, 107, -51, 116, -113, -92, -113, 127, -90, 84} + Schnorrkel.PublicKey pubKey = new Schnorrkel.PublicKey(pubKeyBytes) + + TranscriptData transcript = new TranscriptData("SigningContext".getBytes()) + transcript.appendMessage("", "yo!") + transcript.appendMessage("sign-bytes", "meow") + + byte[] outputBytes = new byte[] {0, 91, 50, 25, -42, 94, 119, 36, 71, -40, 33, -104, 85, -72, 34, 120, 61, -95, -92, -33, 76, 53, 40, -10, 76, 38, -21, -52, 43, 31, -77, 28} + + byte[] cbytes = new byte[] {120, 23, -21, -97, 115, 122, -49, -50, 123, -24, 75, -13, 115, -1, -125, -75, -37, -15, -56, -50, 21, 22, -18, 16, 68, 49, 86, 99, 76, -117, 39, 0} + byte[] sbytes = new byte[] {102, 106, -75, -120, 97, -115, -69, 1, -22, -73, -15, 28, 27, -27, -123, 8, 32, -10, -11, -50, -57, -114, -122, 124, -30, -39, 95, 30, -80, -10, 5, 3} + byte[] proofBytes = new byte[cbytes.length + sbytes.length] + System.arraycopy(cbytes, 0, proofBytes, 0, cbytes.length) + System.arraycopy(sbytes, 0, proofBytes, cbytes.length, sbytes.length) + + when: + boolean verified = schnorrkel.vrfVerify(pubKey, transcript, VrfOutputAndProof.wrap(outputBytes, proofBytes)) + + then: + verified + } + + // NOTE: This test is currently only a sanity check against trivially false positives + // translated from https://github.com/ChainSafe/go-schnorrkel/blob/d1354d86e41dc066cdf6c755a9d08caffc55b542/vrf_test.go#L149 + def "Test VRF verify invalid proof fails"() { + setup: + def transcript = new TranscriptData("vrf-test".getBytes()) + // Ideally, we'd want this test case to corroborate that "choosing any other than the right scalar would invalidate the proof", + // i.e. we'd only want to change one of the scalars in the proof. + // But since we don't have that granularity on our Java side, for now, + // I've hardcoded this "fake proof", generated from another secret key and another scalar picked at random. + def invalidProof = new byte[]{117, 87, 1, 77, 44, 43, -13, 116, 47, 125, -44, 124, 47, -113, 46, -126, -76, 68, 46, -11, 42, 64, 7, 43, -121, 107, -18, 123, 19, 66, 115, 7, 107, -104, 95, 62, 4, -100, 111, -23, 79, 56, 108, 113, -127, -85, -38, -35, -33, -97, -43, 85, 52, 106, 71, -53, 11, 81, 96, 35, -80, -119, -14, 1} + + def keyPair = schnorrkel.generateKeyPair() + def vrfOutputAndProof = schnorrkel.vrfSign(keyPair, transcript) + + when: + def verifiedValidProof = schnorrkel.vrfVerify(keyPair, transcript, vrfOutputAndProof) + + then: + verifiedValidProof + + when: + def verifiedInvalidProof = schnorrkel.vrfVerify(keyPair, transcript, VrfOutputAndProof.wrap(vrfOutputAndProof.getOutput(), invalidProof)) + + then: + !verifiedInvalidProof + } } diff --git a/polkaj-ss58/build.gradle b/polkaj-ss58/build.gradle index 42f765a9..e0b83816 100644 --- a/polkaj-ss58/build.gradle +++ b/polkaj-ss58/build.gradle @@ -2,5 +2,6 @@ apply from: '../common_java_app.gradle' dependencies { api 'com.github.multiformats:java-multibase:v1.0.0' + api 'org.junit.jupiter:junit-jupiter-api:5.9.2' implementation 'org.bouncycastle:bcprov-jdk15on:1.65' } \ No newline at end of file diff --git a/polkaj-tx/build.gradle b/polkaj-tx/build.gradle index b57bf4c3..210fd49c 100644 --- a/polkaj-tx/build.gradle +++ b/polkaj-tx/build.gradle @@ -11,4 +11,5 @@ dependencies { // for xxHash api 'net.openhft:zero-allocation-hashing:0.11' api 'org.bouncycastle:bcprov-jdk15on:1.65' + api 'org.junit.jupiter:junit-jupiter-api:5.9.2' } \ No newline at end of file