diff --git a/.gitignore b/.gitignore
new file mode 100644
index 000000000..703d3eacd
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,100 @@
+*.dSYM
+.vscode
+**/proving_material
+**/verification_material
+**/prover_verifier_shared
+**/notary/bin/*
+**/workflows/bin/*
+#**/block_stores/**/LOCK
+**/libpv*.so
+
+.idea/*
+
+corda/
+
+.DS_Store
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn. Uncomment if using
+# auto-import.
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+# *.iml
+# *.ipr
+
+# CMake
+cmake-build-*/
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+# Android studio 3.1+ serialized cache file
+.idea/caches/build_file_checksums.ser
+
+### Kotlin template
+# Compiled class file
+*.class
+
+# Log file
+*.log
+**/logs/
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+!pepper/*.jar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+
+### Gradle template
+.gradle
+**/build/
+
+# Ignore Gradle GUI config
+gradle-app.setting
+
+# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
+!gradle-wrapper.jar
+
+# Cache of project
+.gradletasknamecache
+
+# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
+# gradle/wrapper/gradle-wrapper.properties
+
diff --git a/README.md b/README.md
new file mode 100644
index 000000000..78c8b43f5
--- /dev/null
+++ b/README.md
@@ -0,0 +1,36 @@
+# For victor
+
+## Tests to use:
+
+`com.ing.zknotary.notary.transactions.VictorsSerializeProveVerifyTest`
+
+This test will give you:
+
+* a proper transaction data structure
+* a place to test serialization logic
+* an opportunity to test proving and verifying e2e between Kotlin and Zinc.
+
+Feel free to created dedicated unit tests for the serializer.
+
+## Serializer to implement:
+
+`com.ing.zknotary.common.serializer.VictorsZKInputSerializer`
+
+If you have a better name once you know how you will do it, please feel free to rename it. :-)
+
+I (Matthijs) will also implement a naive JSON/CordaSerialized serializer for inspiration/reference: `com.ing.zknotary.common.serializer.JsonZKInputSerializer`.
+If we can deserialize CordaSerialized components in Zinc into meaningful structures, this might even work.
+
+## Prover/Verifier to implement:
+
+Prover: `com.ing.zknotary.common.zkp.ZincProverCLI`
+
+Verifier: `com.ing.zknotary.common.zkp.ZincVerifierCLI`
+
+We have agreed to initially do it the CLI way, so that we can focus on the serialization/deserialization logic first.
+Once we have that in place, we will move to `ZincProverNative` and `ZincVerifierNative`.
+
+> Please note that the ZKId of a transaction (our custom Merkle root) is currently calculated based on the
+> CordaSerialized form of transaction components. We may be able to change that to another format, but if not, we will have to
+> pass that format to Zinc as well to recalculate the ZKId. Then we will have to deserialize it to verify the validity of the contents.
+
diff --git a/bin/compile_contract.sh b/bin/compile_contract.sh
new file mode 100755
index 000000000..72c84ea38
--- /dev/null
+++ b/bin/compile_contract.sh
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+
+cd $(git rev-parse --show-toplevel)/pepper
+
+contract_name=$1
+debug_flag=$2 # DEBUG=1, default is DEBUG=0
+
+if [[ -z "${contract_name}" ]]; then echo "Contract name is a required argument"; exit 1; fi
+
+docker run -v "$(pwd)":/opt/pequin/pepper -it mvdbos/corda-zk-notary bash -c "export PEPPER_BIN_PATH=\"/opt/pequin/pepper/bin\" && cd /opt/pequin/pepper && ./compile_contract.sh ${contract_name} ${debug_flag}"
diff --git a/bin/deploy_contract.sh b/bin/deploy_contract.sh
new file mode 100755
index 000000000..4727f0435
--- /dev/null
+++ b/bin/deploy_contract.sh
@@ -0,0 +1,47 @@
+#!/usr/bin/env bash
+
+cd $(git rev-parse --show-toplevel)
+
+contract_name=$1
+
+if [[ -z "${contract_name}" ]]; then echo "Contract name is a required argument"; exit 1; fi
+
+
+mkdir -p ./{workflows,notary}/proving_material
+mkdir -p ./{workflows,notary}/verification_material
+mkdir -p ./{workflows,notary}/prover_verifier_shared
+mkdir -p ./{workflows,notary}/src/test/resources
+
+rm -rf ./{workflows,notary}/proving_material/*
+rm -rf ./{workflows,notary}/verification_material/*
+rm -rf ./{workflows,notary}/prover_verifier_shared/*
+rm -f ./{workflows,notary}/src/test/resources/libpv.so
+
+cp -r ./pepper/prover_verifier_shared ./workflows/
+cp -r ./pepper/prover_verifier_shared ./notary/
+
+cp ./pepper/bin/${contract_name}.params ./workflows/prover_verifier_shared/
+cp ./pepper/bin/${contract_name}.params ./notary/prover_verifier_shared/
+
+cp ./pepper/bin/${contract_name}.pws ./workflows/proving_material/
+cp ./pepper/bin/${contract_name}.pws ./notary/proving_material/
+
+cp ./pepper/proving_material/${contract_name}.pkey ./workflows/proving_material/
+cp ./pepper/proving_material/${contract_name}.pkey ./notary/proving_material/
+
+cp ./pepper/verification_material/${contract_name}.vkey ./workflows/verification_material/
+cp ./pepper/verification_material/${contract_name}.vkey ./notary/verification_material/
+
+# This should also be dynamically named for the contract
+cp ./pepper/compiled_libs/libpv.so workflows/src/test/resources/
+cp ./pepper/compiled_libs/libpv.so notary/src/test/resources/
+
+## Copy Jsnark circuit files
+#cp ./pepper/*.arith ./workflows/bin/
+#cp ./pepper/*.arith ./notary/bin/
+#cp ./pepper/*.in ./workflows/bin/
+#cp ./pepper/*.in ./notary/bin/
+
+# Copy executables
+cp ./pepper/bin/* ./notary/bin/
+cp ./pepper/bin/* ./workflows/bin/
diff --git a/bin/enter_docker.sh b/bin/enter_docker.sh
new file mode 100644
index 000000000..5cd454bad
--- /dev/null
+++ b/bin/enter_docker.sh
@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+cd $(git rev-parse --show-toplevel)/pepper
+
+docker run --rm -v /"$(pwd)":/opt/pequin/pepper -it mvdbos/corda-zk-notary bash
\ No newline at end of file
diff --git a/bin/run_c_unit_tests.sh b/bin/run_c_unit_tests.sh
new file mode 100755
index 000000000..d33ed5cb1
--- /dev/null
+++ b/bin/run_c_unit_tests.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+
+cd $(git rev-parse --show-toplevel)/pepper/apps
+
+if [ ! -f Makefile ]; then
+ cmake .
+fi
+make test
+#clang simple_contract_test.c -ldl -rdynamic -lcmocka -Ied25519 -std=c89 -o /tmp/simple_contract_test && /tmp/simple_contract_test && rm /tmp/simple_contract_test
diff --git a/bin/run_notary_test.sh b/bin/run_notary_test.sh
new file mode 100755
index 000000000..00b1f7c3c
--- /dev/null
+++ b/bin/run_notary_test.sh
@@ -0,0 +1,21 @@
+#!/usr/bin/env bash
+
+cd $(git rev-parse --show-toplevel)
+
+test_name=$1
+
+# if test_name is empty, set it to 'com.ing.zknotary.flows.SimpleZKNotaryFlowTest'
+if [[ -z "$test_name" ]]
+then
+ test_name="com.ing.zknotary.notary.transactions.ComplexZKProofTest"
+fi
+
+# Mounting pepper dir is necessary for our gadget to be able to find the .arith files.
+docker run \
+ --rm \
+ -v "$(pwd)":/src \
+ -v "$(pwd)/pepper":/opt/pequin/pepper \
+ -v ~/.gradle/caches:/root/.gradle/caches \
+ -v ~/.m2/repository:/root/.m2/repository \
+ mvdbos/corda-zk-notary \
+ bash -c "cd /src && export PEPPER_BIN_PATH=\"/src/notary/bin\" && gradle --no-daemon --info notary:cleanTest notary:test --tests \"${test_name}\""
diff --git a/bin/run_prove_verify_test-NOCOMPILE.sh b/bin/run_prove_verify_test-NOCOMPILE.sh
new file mode 100755
index 000000000..cad9335ef
--- /dev/null
+++ b/bin/run_prove_verify_test-NOCOMPILE.sh
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+
+cd $(git rev-parse --show-toplevel)/pepper
+
+contract_name=$1
+debug_flag=$2 # DEBUG=1, default is DEBUG=0
+
+if [[ -z "${contract_name}" ]]; then echo "Contract name is a required argument"; exit 1; fi
+
+docker run -v "$(pwd)":/opt/pequin/pepper -it mvdbos/corda-zk-notary bash -c "cd /opt/pequin/pepper && ./test_prove_verify-NOCOMPILE.sh ${contract_name} ${debug_flag}"
diff --git a/bin/run_prove_verify_test.sh b/bin/run_prove_verify_test.sh
new file mode 100755
index 000000000..8d2ef392c
--- /dev/null
+++ b/bin/run_prove_verify_test.sh
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+
+cd $(git rev-parse --show-toplevel)/pepper
+
+contract_name=$1
+debug_flag=$2 # DEBUG=1, default is DEBUG=0
+
+if [[ -z "${contract_name}" ]]; then echo "Contract name is a required argument"; exit 1; fi
+
+docker run -v "$(pwd)":/opt/pequin/pepper -it mvdbos/corda-zk-notary bash -c "cd /opt/pequin/pepper && ./test_prove_verify.sh ${contract_name} ${debug_flag}"
diff --git a/bin/run_tests-NOCOMPILE.sh b/bin/run_tests-NOCOMPILE.sh
new file mode 100644
index 000000000..c30b16d40
--- /dev/null
+++ b/bin/run_tests-NOCOMPILE.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+
+contract_name=$1
+
+if [[ -z "${contract_name}" ]]; then echo "Contract name is a required argument"; exit 1; fi
+
+sh ./bin/deploy_contract.sh ${contract_name} && sh ./bin/run_notary_test.sh
diff --git a/bin/run_tests.sh b/bin/run_tests.sh
new file mode 100755
index 000000000..853984a6a
--- /dev/null
+++ b/bin/run_tests.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+contract_name=$1
+debug_flag=$2 # DEBUG=1, default is DEBUG=0
+
+if [[ -z "${contract_name}" ]]; then echo "Contract name is a required argument"; exit 1; fi
+
+sh ./bin/compile_contract.sh ${contract_name} ${debug_flag} && sh ./bin/deploy_contract.sh ${contract_name} && sh ./bin/run_notary_test.sh
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 000000000..4365e5bc7
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,103 @@
+buildscript {
+ Properties constants = new Properties()
+ file("$projectDir/./constants.properties").withInputStream { constants.load(it) }
+
+ ext {
+
+ //corda_gradle_plugins_version = '4.0.45'
+
+ corda_release_group = constants.getProperty("cordaReleaseGroup")
+ corda_core_release_group = constants.getProperty("cordaCoreReleaseGroup")
+ corda_release_version = constants.getProperty("cordaVersion")
+ corda_core_release_version = constants.getProperty("cordaCoreVersion")
+ corda_gradle_plugins_version = constants.getProperty("gradlePluginsVersion")
+ kotlin_version = constants.getProperty("kotlinVersion")
+ junit_version = constants.getProperty("junitVersion")
+ quasar_version = constants.getProperty("quasarVersion")
+ log4j_version = constants.getProperty("log4jVersion")
+ slf4j_version = constants.getProperty("slf4jVersion")
+ corda_platform_version = constants.getProperty("platformVersion").toInteger()
+ //springboot
+ spring_boot_version = '2.0.2.RELEASE'
+ spring_boot_gradle_plugin_version = '2.0.2.RELEASE'
+
+ spotless_plugin_version = '3.23.1'
+ }
+
+
+ repositories {
+ mavenLocal()
+ mavenCentral()
+ jcenter()
+ maven { url 'https://ci-artifactory.corda.r3cev.com/artifactory/corda-releases' }
+ maven { url 'https://software.r3.com/artifactory/corda' }
+ maven { url 'https://repo.gradle.org/gradle/libs-releases' }
+ maven { url "https://plugins.gradle.org/m2/" }
+ }
+
+ dependencies {
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ classpath "net.corda.plugins:cordapp:$corda_gradle_plugins_version"
+ classpath "net.corda.plugins:cordformation:$corda_gradle_plugins_version"
+ classpath "net.corda.plugins:quasar-utils:$corda_gradle_plugins_version"
+ classpath "com.diffplug.spotless:spotless-plugin-gradle:$spotless_plugin_version"
+ classpath "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
+ classpath "org.springframework.boot:spring-boot-gradle-plugin:$spring_boot_gradle_plugin_version"
+ }
+}
+
+plugins {
+ id 'com.cosminpolifronie.gradle.plantuml' version '1.6.0'
+}
+
+allprojects {
+ apply from: "${rootProject.projectDir}/repositories.gradle"
+ apply plugin: 'kotlin'
+ apply plugin: 'com.diffplug.gradle.spotless'
+
+ repositories {
+ mavenLocal()
+ jcenter()
+ mavenCentral()
+ maven { url 'https://ci-artifactory.corda.r3cev.com/artifactory/corda-releases' }
+ maven { url 'https://jitpack.io' }
+ maven { url 'https://software.r3.com/artifactory/corda' }
+ maven { url 'https://repo.gradle.org/gradle/libs-releases' }
+ }
+
+ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
+ kotlinOptions {
+ languageVersion = "1.2"
+ apiVersion = "1.2"
+ jvmTarget = "1.8"
+ javaParameters = true // Useful for reflection.
+ }
+ }
+
+ jar {
+ // This makes the JAR's SHA-256 hash repeatable.
+ preserveFileTimestamps = false
+ reproducibleFileOrder = true
+ }
+
+ spotless {
+ kotlin {
+ ktlint()
+ }
+ }
+
+ // below you can specify any env vars, for instance the path to the prover lib
+ test {
+ // environment "LD_LIBRARY_PATH", "~/pepper_deps/lib/"
+ }
+}
+
+apply plugin: 'net.corda.plugins.cordapp'
+apply plugin: 'net.corda.plugins.cordformation'
+apply plugin: 'net.corda.plugins.quasar-utils'
+
+
+plantUml {
+ render input: 'docs/**/*.puml', output: "docs/build", format: 'png', withMetadata: false
+}
+
diff --git a/config/dev/log4j2.xml b/config/dev/log4j2.xml
new file mode 100644
index 000000000..34ba4d45a
--- /dev/null
+++ b/config/dev/log4j2.xml
@@ -0,0 +1,59 @@
+
+
+
+
+ logs
+ node-${hostName}
+ ${log-path}/archive
+
+
+
+
+
+
+
+
+ %highlight{%level{length=1} %d{HH:mm:ss} %T %c{1}.%M - %msg%n}{INFO=white,WARN=red,FATAL=bright red blink}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/config/test/log4j2.xml b/config/test/log4j2.xml
new file mode 100644
index 000000000..cd9926ca8
--- /dev/null
+++ b/config/test/log4j2.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ [%-5level] %d{HH:mm:ss.SSS} [%t] %c{1}.%M - %msg%n
+ >
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/constants.properties b/constants.properties
new file mode 100644
index 000000000..d84cc091c
--- /dev/null
+++ b/constants.properties
@@ -0,0 +1,13 @@
+cordaReleaseGroup=net.corda
+cordaCoreReleaseGroup=net.corda
+cordaVersion=4.5-SNAPSHOT
+cordaCoreVersion=4.5-SNAPSHOT
+gradlePluginsVersion=5.0.4
+kotlinVersion=1.2.71
+junitVersion=4.12
+quasarVersion=0.7.10
+log4jVersion =2.11.2
+platformVersion=5
+slf4jVersion=1.7.25
+nettyVersion=4.1.22.Final
+
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 000000000..1c8713807
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,4 @@
+name=ZKNotary
+group=com.ing.zknotary
+version=0.1
+kotlin.incremental=false
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..87b738cbd
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..7c4388a92
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100755
index 000000000..cccdd3d51
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/notary/build.gradle b/notary/build.gradle
new file mode 100644
index 000000000..e77fffe17
--- /dev/null
+++ b/notary/build.gradle
@@ -0,0 +1,42 @@
+apply plugin: 'net.corda.plugins.cordapp'
+apply plugin: 'net.corda.plugins.quasar-utils'
+
+cordapp {
+ targetPlatformVersion corda_platform_version.toInteger()
+ minimumPlatformVersion corda_platform_version.toInteger()
+ workflow {
+ name "Zk Notary App"
+ vendor "ING Bank NV"
+ licence "Apache License, Version 2.0"
+ versionId 1
+ }
+}
+
+sourceSets {
+ main {
+ resources {
+ srcDir rootProject.file("config/dev")
+ }
+ }
+ test {
+ resources {
+ srcDir rootProject.file("config/test")
+ }
+ }
+}
+
+dependencies {
+ compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
+ testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
+ testCompile "junit:junit:$junit_version"
+
+ // Corda dependencies.
+ cordaCompile "$corda_release_group:corda-core:$corda_release_version"
+ cordaRuntime "$corda_release_group:corda:$corda_release_version"
+ cordaCompile "$corda_release_group:corda-node:$corda_release_version"
+ testCompile "$corda_release_group:corda-node-driver:$corda_release_version"
+ testCompile "$corda_release_group:corda-test-utils:$corda_release_version"
+
+ compile group: 'net.java.dev.jna', name: 'jna', version: '5.3.1'
+}
+
diff --git a/notary/src/main/kotlin/com/ing/zknotary/client/flows/ZKFinalityFlow.kt b/notary/src/main/kotlin/com/ing/zknotary/client/flows/ZKFinalityFlow.kt
new file mode 100644
index 000000000..39f51f4bd
--- /dev/null
+++ b/notary/src/main/kotlin/com/ing/zknotary/client/flows/ZKFinalityFlow.kt
@@ -0,0 +1,176 @@
+package com.ing.zknotary.client.flows
+
+import co.paralleluniverse.fibers.Suspendable
+import com.ing.zknotary.common.zkp.ZKConfig
+import com.ing.zknotary.common.zkp.DefaultZKConfig
+import net.corda.core.crypto.isFulfilledBy
+import net.corda.core.flows.FlowLogic
+import net.corda.core.flows.FlowSession
+import net.corda.core.flows.InitiatingFlow
+import net.corda.core.flows.NotaryException
+import net.corda.core.flows.NotaryFlow
+import net.corda.core.flows.SendTransactionFlow
+import net.corda.core.flows.UnexpectedFlowEndException
+import net.corda.core.identity.Party
+import net.corda.core.identity.groupAbstractPartyByWellKnownParty
+import net.corda.core.node.StatesToRecord
+import net.corda.core.transactions.LedgerTransaction
+import net.corda.core.transactions.SignedTransaction
+import net.corda.core.utilities.ProgressTracker
+
+/**
+ * Verifies the given transaction, then sends it to the named notary. If the notary agrees that the transaction
+ * is acceptable then it is from that point onwards committed to the ledger, and will be written through to the
+ * vault. Additionally it will be distributed to the parties reflected in the participants list of the states.
+ *
+ * The transaction is expected to have already been resolved: if its dependencies are not available in local
+ * storage, verification will fail. It must have signatures from all necessary parties other than the notary.
+ *
+ * A list of [FlowSession]s is required for each non-local participant of the transaction. These participants will receive
+ * the final notarised transaction by calling [ReceiveFinalityFlow] in their counterpart com.ing.zknotary.flows. Sessions with non-participants
+ * can also be included, but they must specify [StatesToRecord.ALL_VISIBLE] for statesToRecord if they wish to record the
+ * contract states into their vaults.
+ *
+ * The flow returns the same transaction but with the additional signatures from the notary.
+ *
+ * NOTE: This is an inlined flow but for backwards compatibility is annotated with [InitiatingFlow].
+ */
+// To maintain backwards compatibility with the old API, FinalityFlow can act both as an initiating flow and as an inlined flow.
+// This is only possible because a flow is only truly initiating when the first call to initiateFlow is made (where the
+// presence of @InitiatingFlow is checked). So the new API is inlined simply because that code path doesn't call initiateFlow.
+@InitiatingFlow
+class ZKFinalityFlow private constructor(
+ val transaction: SignedTransaction,
+ override val progressTracker: ProgressTracker,
+ private val sessions: Collection,
+ private val zkConfig: ZKConfig = DefaultZKConfig
+) : FlowLogic() {
+
+ /**
+ * Notarise the given transaction and broadcast it to all the participants.
+ *
+ * @param transaction What to commit.
+ * @param sessions A collection of [FlowSession]s for each non-local participant of the transaction. Sessions to non-participants can
+ * also be provided.
+ */
+ @JvmOverloads
+ constructor(
+ transaction: SignedTransaction,
+ sessions: Collection,
+ progressTracker: ProgressTracker = tracker(),
+ zkConfig: ZKConfig = DefaultZKConfig
+ ) : this(transaction, progressTracker, sessions, zkConfig)
+
+ companion object {
+ object NOTARISING : ProgressTracker.Step("Requesting signature by notary service") {
+ override fun childProgressTracker() = NotaryFlow.Client.tracker()
+ }
+
+ object BROADCASTING : ProgressTracker.Step("Broadcasting transaction to participants")
+
+ @JvmStatic
+ fun tracker() = ProgressTracker(
+ NOTARISING,
+ BROADCASTING
+ )
+ }
+
+ @Suspendable
+ @Throws(NotaryException::class)
+ override fun call(): SignedTransaction {
+ require(sessions.none { serviceHub.myInfo.isLegalIdentity(it.counterparty) }) {
+ "Do not provide flow sessions for the local node. ZKFinalityFlow will record the notarised transaction locally."
+ }
+
+ // Note: this method is carefully broken up to minimize the amount of data reachable from the stack at
+ // the point where subFlow is invoked, as that minimizes the checkpointing work to be done.
+ //
+ // Lookup the resolved transactions and use them to map each signed transaction to the list of participants.
+ // Then send to the notary if needed, record locally and distribute.
+
+ logCommandData()
+ val ledgerTransaction = verifyTx()
+ val externalTxParticipants = extractExternalParticipants(ledgerTransaction)
+
+ val sessionParties = sessions.map { it.counterparty }
+ val missingRecipients = externalTxParticipants - sessionParties
+ require(missingRecipients.isEmpty()) {
+ "Flow sessions were not provided for the following transaction participants: $missingRecipients"
+ }
+
+ val notarised = notariseAndRecord()
+
+ progressTracker.currentStep =
+ BROADCASTING
+
+ for (session in sessions) {
+ try {
+ subFlow(SendTransactionFlow(session, notarised))
+ logger.info("Party ${session.counterparty} received the transaction.")
+ } catch (e: UnexpectedFlowEndException) {
+ throw UnexpectedFlowEndException(
+ "${session.counterparty} has finished prematurely and we're trying to send them the finalised transaction. " +
+ "Did they forget to call ReceiveFinalityFlow? (${e.message})",
+ e.cause,
+ e.originalErrorId
+ )
+ }
+ }
+
+ logger.info("All parties received the transaction successfully.")
+
+ return notarised
+ }
+
+ private fun logCommandData() {
+ if (logger.isDebugEnabled) {
+ val commandDataTypes =
+ transaction.tx.commands.asSequence().mapNotNull { it.value::class.qualifiedName }.distinct()
+ logger.debug("Started finalization, commands are ${commandDataTypes.joinToString(", ", "[", "]")}.")
+ }
+ }
+
+ @Suspendable
+ private fun notariseAndRecord(): SignedTransaction {
+ val notarised = if (needsNotarySignature(transaction)) {
+ progressTracker.currentStep =
+ NOTARISING
+ val notarySignatures = subFlow(ZKNotaryFlow(transaction, zkConfig))
+ transaction + notarySignatures
+ } else {
+ logger.info("No need to notarise this transaction.")
+ transaction
+ }
+ logger.info("Recording transaction locally.")
+ serviceHub.recordTransactions(notarised)
+ logger.info("Recorded transaction locally successfully.")
+ return notarised
+ }
+
+ private fun needsNotarySignature(stx: SignedTransaction): Boolean {
+ val wtx = stx.tx
+ val needsNotarisation = wtx.inputs.isNotEmpty() || wtx.references.isNotEmpty() || wtx.timeWindow != null
+ return needsNotarisation && hasNoNotarySignature(stx)
+ }
+
+ private fun hasNoNotarySignature(stx: SignedTransaction): Boolean {
+ val notaryKey = stx.tx.notary?.owningKey
+ val signers = stx.sigs.asSequence().map { it.by }.toSet()
+ return notaryKey?.isFulfilledBy(signers) != true
+ }
+
+ private fun extractExternalParticipants(ltx: LedgerTransaction): Set {
+ val participants = ltx.outputStates.flatMap { it.participants } + ltx.inputStates.flatMap { it.participants }
+ return groupAbstractPartyByWellKnownParty(serviceHub, participants).keys - serviceHub.myInfo.legalIdentities
+ }
+
+ // For this first version, we still resolve the entire plaintext history of the transaction
+ private fun verifyTx(): LedgerTransaction {
+ val notary = transaction.tx.notary
+ // The notary signature(s) are allowed to be missing but no others.
+ if (notary != null) transaction.verifySignaturesExcept(notary.owningKey) else transaction.verifyRequiredSignatures()
+ val ltx = transaction.toLedgerTransaction(serviceHub, false)
+ ltx.verify()
+ return ltx
+ }
+}
diff --git a/notary/src/main/kotlin/com/ing/zknotary/client/flows/ZKNotaryFlow.kt b/notary/src/main/kotlin/com/ing/zknotary/client/flows/ZKNotaryFlow.kt
new file mode 100644
index 000000000..ddd03e83c
--- /dev/null
+++ b/notary/src/main/kotlin/com/ing/zknotary/client/flows/ZKNotaryFlow.kt
@@ -0,0 +1,123 @@
+package com.ing.zknotary.client.flows
+
+import co.paralleluniverse.fibers.Suspendable
+import com.ing.zknotary.common.transactions.ZKFilteredTransaction
+import com.ing.zknotary.common.zkp.ZKConfig
+import com.ing.zknotary.common.zkp.DefaultZKConfig
+import net.corda.core.contracts.StateRef
+import net.corda.core.contracts.TimeWindow
+import net.corda.core.crypto.SecureHash
+import net.corda.core.crypto.TransactionSignature
+import net.corda.core.flows.FlowSession
+import net.corda.core.flows.NotarisationPayload
+import net.corda.core.flows.NotarisationRequest
+import net.corda.core.flows.NotarisationRequestSignature
+import net.corda.core.flows.NotarisationResponse
+import net.corda.core.flows.NotaryError
+import net.corda.core.flows.NotaryException
+import net.corda.core.flows.NotaryFlow
+import net.corda.core.identity.Party
+import net.corda.core.internal.NetworkParametersStorage
+import net.corda.core.internal.notary.generateSignature
+import net.corda.core.transactions.ContractUpgradeWireTransaction
+import net.corda.core.transactions.NetworkParametersHash
+import net.corda.core.transactions.NotaryChangeWireTransaction
+import net.corda.core.transactions.ReferenceStateRef
+import net.corda.core.transactions.SignedTransaction
+import net.corda.core.transactions.WireTransaction
+import net.corda.core.utilities.UntrustworthyData
+import java.util.function.Predicate
+
+open class ZKNotaryFlow(
+ private val stx: SignedTransaction,
+ private val zkConfig: ZKConfig = DefaultZKConfig
+) : NotaryFlow.Client(stx) {
+
+ @Suspendable
+ @Throws(NotaryException::class)
+ override fun call(): List {
+ val notaryParty = checkTransaction()
+ val response = zkNotarise(notaryParty)
+ return validateResponse(response, notaryParty)
+ }
+
+ /** Notarises the transaction with the [notaryParty], obtains the notary's signature(s). */
+ @Throws(NotaryException::class)
+ @Suspendable
+ protected fun zkNotarise(notaryParty: Party): UntrustworthyData {
+ val session = initiateFlow(notaryParty)
+ val requestSignature = generateRequestSignature()
+ return if (isValidating(notaryParty)) {
+ throw NotaryException(NotaryError.TransactionInvalid(Throwable("Validating notaries can never handle ZKTransactions")))
+ } else {
+ // TODO: find a way to check that this notary is actually running ZKNotaryServiceFlow (className property?)
+ sendAndReceiveNonValidatingWithZKProof(notaryParty, session, requestSignature)
+ }
+ }
+
+ @Suspendable
+ private fun sendAndReceiveNonValidatingWithZKProof(
+ notaryParty: Party,
+ session: FlowSession,
+ signature: NotarisationRequestSignature
+ ): UntrustworthyData {
+ val ctx = stx.coreTransaction
+ val tx = when (ctx) {
+ is ContractUpgradeWireTransaction -> ctx.buildFilteredTransaction()
+ is WireTransaction -> buildZKFilteredTransaction(stx, notaryParty)
+ else -> ctx
+ }
+ session.send(NotarisationPayload(tx, signature))
+ return receiveResultOrTiming(session)
+ }
+
+ private fun buildZKFilteredTransaction(stx: SignedTransaction, notaryParty: Party): ZKFilteredTransaction {
+ val wtx = stx.coreTransaction as WireTransaction
+
+ val ftx = wtx.buildFilteredTransaction(Predicate {
+ it is StateRef || it is ReferenceStateRef || it is TimeWindow || it == notaryParty || it is NetworkParametersHash
+ })
+
+ // TODO: create custom sigs, because we need a different scheme, and also it sigs of the additional merkle root and not of SignableData
+ val signatures = stx.sigs.map { it.bytes }
+
+ val witness = zkConfig.serializer.serializeWitness(wtx.toLedgerTransaction(serviceHub), signatures)
+ val instance = zkConfig.serializer.serializeInstance(wtx.id)
+
+ // TODO: inject the prover
+ val proof = zkConfig.prover.prove(witness, instance)
+ return ZKFilteredTransaction(proof, ftx)
+ }
+
+ /****************************************************
+ * Copies of private methods from NotaryFlow.Client *
+ ****************************************************/
+ private fun isValidating(notaryParty: Party): Boolean {
+ val onTheCurrentWhitelist = serviceHub.networkMapCache.isNotary(notaryParty)
+ return if (!onTheCurrentWhitelist) {
+ /*
+ Note that the only scenario where it's acceptable to use a notary not in the current network parameter whitelist is
+ when performing a notary change transaction after a network merge – the old notary won't be on the whitelist of the new network,
+ and can't be used for regular transactions.
+ */
+ check(stx.coreTransaction is NotaryChangeWireTransaction) {
+ "Notary $notaryParty is not on the network parameter whitelist. A non-whitelisted notary can only be used for notary change transactions"
+ }
+ val historicNotary =
+ (serviceHub.networkParametersService as NetworkParametersStorage).getHistoricNotary(notaryParty)
+ ?: throw IllegalStateException("The notary party $notaryParty specified by transaction ${stx.id}, is not recognised as a current or historic notary.")
+ historicNotary.validating
+ } else serviceHub.networkMapCache.isValidatingNotary(notaryParty)
+ }
+
+ /**
+ * Ensure that transaction ID instances are not referenced in the serialized form in case several input states are outputs of the
+ * same transaction.
+ */
+ private fun generateRequestSignature(): NotarisationRequestSignature {
+ // TODO: This is not required any more once our AMQP serialization supports turning off object referencing.
+ val notarisationRequest =
+ NotarisationRequest(stx.inputs.map { it.copy(txhash = SecureHash.parse(it.txhash.toString())) }, stx.id)
+ return notarisationRequest.generateSignature(serviceHub)
+ }
+}
diff --git a/notary/src/main/kotlin/com/ing/zknotary/common/contracts/TestContract.kt b/notary/src/main/kotlin/com/ing/zknotary/common/contracts/TestContract.kt
new file mode 100644
index 000000000..ca7d20873
--- /dev/null
+++ b/notary/src/main/kotlin/com/ing/zknotary/common/contracts/TestContract.kt
@@ -0,0 +1,71 @@
+package com.ing.zknotary.common.contracts
+
+import net.corda.core.contracts.BelongsToContract
+import net.corda.core.contracts.CommandAndState
+import net.corda.core.contracts.CommandData
+import net.corda.core.contracts.Contract
+import net.corda.core.contracts.ContractClassName
+import net.corda.core.contracts.OwnableState
+import net.corda.core.identity.AbstractParty
+import net.corda.core.transactions.LedgerTransaction
+import java.util.Random
+
+class TestContract : Contract {
+ companion object {
+ const val PROGRAM_ID: ContractClassName = "com.ing.zknotary.common.contracts.TestContract"
+ }
+
+ @BelongsToContract(TestContract::class)
+ data class TestState(override val owner: AbstractParty, val value: Int = Random().nextInt()) : OwnableState {
+ override val participants = listOf(owner)
+ override fun withNewOwner(newOwner: AbstractParty) = CommandAndState(Move(), copy(owner = newOwner))
+ }
+
+ // Commands
+ class Create : CommandData
+ class Move : CommandData
+
+ override fun verify(tx: LedgerTransaction) {
+ // The transaction may have only one command, of a type defined above
+ if (tx.commands.size != 1) throw IllegalArgumentException("Failed requirement: the tx has only one command")
+ val command = tx.commands[0]
+
+ when (command.value) {
+ is Create -> {
+ // Transaction structure
+ if (tx.outputs.size != 1) throw IllegalArgumentException("Failed requirement: the tx has only one output")
+ if (tx.inputs.isNotEmpty()) throw IllegalArgumentException("Failed requirement: the tx has no inputs")
+
+ // Transaction contents
+ val output = tx.getOutput(0) as TestState
+ if (output.owner.owningKey !in command.signers) throw IllegalArgumentException("Failed requirement: the output state is owned by the command signer")
+ }
+ is Move -> {
+ // Transaction structure
+ if (tx.outputs.size != 1) throw IllegalArgumentException("Failed requirement: the tx has only one output")
+ if (tx.inputs.size != 1) throw IllegalArgumentException("Failed requirement: the tx has only one output")
+
+ // Transaction contents
+ val output = tx.getOutput(0) as TestState
+ val input = tx.getInput(0) as TestState
+
+ /*
+ // Note: the fact that command.signers contains a certain required key, does not mean we can assume it has been
+ // verified that this signature is present. The validating notary does check this directly after the contract verification,
+ // but the non-validating notary never checks signatures. In that case, this check only means that we
+ // can enforce that the owner of e.g. the output is set as one of the required signers by the tx creator,
+ // but not that these signatures are actually present.
+ // Counterparties also do contract verification, and like a validating notary, do check signatures.
+ // In that case, this check equals saying that we require a signature to be present on the tx of the
+ // owner of the input and of the owner of the output.
+
+ */
+ if (input.owner.owningKey !in command.signers) throw IllegalArgumentException("Failed requirement: the input state is owned by a required command signer")
+ if (input.value != output.value) throw IllegalArgumentException("Failed requirement: the value of the input and out put should be equal")
+ }
+ else -> {
+ throw IllegalStateException("No valid command found")
+ }
+ }
+ }
+}
diff --git a/notary/src/main/kotlin/com/ing/zknotary/common/serializer/JsonZKInputSerializer.kt b/notary/src/main/kotlin/com/ing/zknotary/common/serializer/JsonZKInputSerializer.kt
new file mode 100644
index 000000000..2111dc355
--- /dev/null
+++ b/notary/src/main/kotlin/com/ing/zknotary/common/serializer/JsonZKInputSerializer.kt
@@ -0,0 +1,120 @@
+package com.ing.zknotary.common.serializer
+
+import net.corda.core.crypto.SecureHash
+import net.corda.core.serialization.serialize
+import net.corda.core.transactions.LedgerTransaction
+
+/**
+ * This ZKInputSerializer puts CordaSerialized componentents in a JSON structure like so:
+ * {
+ * "inputs": [
+ * "t43t43fg4rfgeg45tgr4vdffvdgfdgs3234534", <---- This is some encoded form of CordaSerialized binary data
+ * "fsd9nkfdshy789uj89fud9cndks"
+ * ],
+ * "outputs": [
+ * ...
+ * ],
+ * ...
+ * "privacySalt": "89r5uy43hinf4389h439",
+ * ...
+ * }
+ */
+object JsonZKInputSerializer : ZKInputSerializer {
+ // FIXME: should be turned into proper serialization of any tx generic data structure
+ override fun serializeWitness(tx: LedgerTransaction, signatures: List): ByteArray {
+ var witness = ByteArray(0) // Or perhaps this should be JSON?
+
+ /**
+ * We keep the same order as [ComponentGroupEnum]
+ * INPUTS_GROUP, // ordinal = 0.
+ * OUTPUTS_GROUP, // ordinal = 1.
+ * COMMANDS_GROUP, // ordinal = 2.
+ * ATTACHMENTS_GROUP, // ordinal = 3.
+ * NOTARY_GROUP, // ordinal = 4.
+ * TIMEWINDOW_GROUP, // ordinal = 5.
+ * SIGNERS_GROUP, // ordinal = 6.
+ * REFERENCES_GROUP, // ordinal = 7.
+ * PARAMETERS_GROUP // ordinal = 8.
+ */
+ witness += serializeInputs(tx)
+ witness += serializeOutputs(tx)
+ witness += serializeCommandData(tx) // Note that the Commands in a tx are made up out of two component groups in the Merkle tree: CommandData and commandSigners. They are serialized serparately.
+ // We will skip the attachments and only use its component group hash for merkle root recalculation
+ witness += serializeNotary(tx) // We don't need to validate that this is the correct notary as the NotaryServiceFlow already does this. But we might need it for other checks
+ witness += serializeTimeWindow(tx) // The TimeWindow is committed by the FilteredTransaction.verify, but we may still need it for business logic.
+ witness += serializeSigners(tx) // // Note that the Commands in a tx are made up out of two component groups in the Merkle tree: CommandData and commandSigners. They are serialized serparately.
+ witness += serializeReferenceStates(
+ tx
+ )
+ // We will skip the network parameters group and only use its component group hash for merkle root calculation
+
+ // Other components we need
+ witness += serializeSignatures(
+ signatures
+ )
+ witness += serializePrivacySalt(tx)
+ witness += serializeComponentGroupHashes(
+ tx
+ )
+
+ return witness
+ }
+
+ private fun serializeSigners(tx: LedgerTransaction): ByteArray {
+ return ByteArray(0)
+ }
+
+ private fun serializeTimeWindow(tx: LedgerTransaction): ByteArray {
+ return ByteArray(0)
+ }
+
+ private fun serializeNotary(tx: LedgerTransaction): ByteArray {
+ return ByteArray(0)
+ }
+
+ private fun serializeComponentGroupHashes(tx: LedgerTransaction): ByteArray {
+ // FIXME: This is impossible with a LedgerTransaction, unless we recalculate them here. We need a TraversableTransaction for this
+ return ByteArray(0)
+ }
+
+ private fun serializePrivacySalt(tx: LedgerTransaction): ByteArray {
+ // return tx.privacySalt.bytes
+ return ByteArray(0)
+ }
+
+ private fun serializeReferenceStates(tx: LedgerTransaction): ByteArray {
+ return ByteArray(0)
+ }
+
+ private fun serializeSignatures(signatures: List): ByteArray {
+ // return signatures.reduce { acc, sig -> acc + sig // 64 bytes per sig } }
+ return ByteArray(0)
+ }
+
+ private fun serializeCommandData(tx: LedgerTransaction): ByteArray {
+ // As an example if not using Corda serialization: how to extract meaningful data from a Corda data structure:
+ // val commandSigners = tx.commands.flatMap { command -> command.signers }
+ // commandSigners.forEach { pubkey ->
+ // pubkey as EdDSAPublicKey
+ // witness += pubkey.abyte // 32 bytes
+ // }
+ return ByteArray(0)
+ }
+
+ private fun serializeOutputs(tx: LedgerTransaction): ByteArray {
+ return ByteArray(0)
+ }
+
+ private fun serializeInputs(tx: LedgerTransaction): ByteArray {
+ // return ByteArray(0)
+ // For testing, only serialize one input and nothing else for the entire tx. Lets see if we can deserialize that in Zinc
+ return tx.inputStates[0].serialize().bytes
+ }
+
+ /**
+ * This seems overkill now, but later we will add more things to the instance
+ */
+ override fun serializeInstance(zkTransactionId: SecureHash): ByteArray {
+ return zkTransactionId.bytes // These are the raw bytes of the the transaction id hash (merkle root)
+ }
+}
\ No newline at end of file
diff --git a/notary/src/main/kotlin/com/ing/zknotary/common/serializer/NoopZKInputSerializer.kt b/notary/src/main/kotlin/com/ing/zknotary/common/serializer/NoopZKInputSerializer.kt
new file mode 100644
index 000000000..18262118d
--- /dev/null
+++ b/notary/src/main/kotlin/com/ing/zknotary/common/serializer/NoopZKInputSerializer.kt
@@ -0,0 +1,9 @@
+package com.ing.zknotary.common.serializer
+
+import net.corda.core.crypto.SecureHash
+import net.corda.core.transactions.LedgerTransaction
+
+object NoopZKInputSerializer : ZKInputSerializer {
+ override fun serializeWitness(tx: LedgerTransaction, signatures: List) = ByteArray(0)
+ override fun serializeInstance(zkTransactionId: SecureHash) = ByteArray(0)
+}
\ No newline at end of file
diff --git a/notary/src/main/kotlin/com/ing/zknotary/common/serializer/VictorsZKInputSerializer.kt b/notary/src/main/kotlin/com/ing/zknotary/common/serializer/VictorsZKInputSerializer.kt
new file mode 100644
index 000000000..75a382db1
--- /dev/null
+++ b/notary/src/main/kotlin/com/ing/zknotary/common/serializer/VictorsZKInputSerializer.kt
@@ -0,0 +1,109 @@
+package com.ing.zknotary.common.serializer
+
+import net.corda.core.crypto.SecureHash
+import net.corda.core.serialization.serialize
+import net.corda.core.transactions.LedgerTransaction
+
+object VictorsZKInputSerializer : ZKInputSerializer {
+ // FIXME: should be turned into proper serialization of any tx generic data structure
+ override fun serializeWitness(tx: LedgerTransaction, signatures: List): ByteArray {
+ var witness = ByteArray(0) // Or perhaps this should be JSON?
+
+ /**
+ * We keep the same order as [ComponentGroupEnum]
+ * INPUTS_GROUP, // ordinal = 0.
+ * OUTPUTS_GROUP, // ordinal = 1.
+ * COMMANDS_GROUP, // ordinal = 2.
+ * ATTACHMENTS_GROUP, // ordinal = 3.
+ * NOTARY_GROUP, // ordinal = 4.
+ * TIMEWINDOW_GROUP, // ordinal = 5.
+ * SIGNERS_GROUP, // ordinal = 6.
+ * REFERENCES_GROUP, // ordinal = 7.
+ * PARAMETERS_GROUP // ordinal = 8.
+ */
+ witness += serializeInputs(tx)
+ witness += serializeOutputs(tx)
+ witness += serializeCommandData(
+ tx
+ ) // Note that the Commands in a tx are made up out of two component groups in the Merkle tree: CommandData and commandSigners. They are serialized serparately.
+ // We will skip the attachments and only use its component group hash for merkle root recalculation
+ witness += serializeNotary(tx) // We don't need to validate that this is the correct notary as the NotaryServiceFlow already does this. But we might need it for other checks
+ witness += serializeTimeWindow(tx) // The TimeWindow is committed by the FilteredTransaction.verify, but we may still need it for business logic.
+ witness += serializeSigners(tx) // // Note that the Commands in a tx are made up out of two component groups in the Merkle tree: CommandData and commandSigners. They are serialized serparately.
+ witness += serializeReferenceStates(
+ tx
+ )
+ // We will skip the network parameters group and only use its component group hash for merkle root calculation
+
+ // Other components we need
+ witness += serializeSignatures(
+ signatures
+ )
+ witness += serializePrivacySalt(
+ tx
+ )
+ witness += serializeComponentGroupHashes(
+ tx
+ )
+
+ return witness
+ }
+
+ private fun serializeSigners(tx: LedgerTransaction): ByteArray {
+ return ByteArray(0)
+ }
+
+ private fun serializeTimeWindow(tx: LedgerTransaction): ByteArray {
+ return ByteArray(0)
+ }
+
+ private fun serializeNotary(tx: LedgerTransaction): ByteArray {
+ return ByteArray(0)
+ }
+
+ private fun serializeComponentGroupHashes(tx: LedgerTransaction): ByteArray {
+ // FIXME: This is impossible with a LedgerTransaction, unless we recalculate them here. We need a TraversableTransaction for this
+ return ByteArray(0)
+ }
+
+ private fun serializePrivacySalt(tx: LedgerTransaction): ByteArray {
+ // return tx.privacySalt.bytes
+ return ByteArray(0)
+ }
+
+ private fun serializeReferenceStates(tx: LedgerTransaction): ByteArray {
+ return ByteArray(0)
+ }
+
+ private fun serializeSignatures(signatures: List): ByteArray {
+ // return signatures.reduce { acc, sig -> acc + sig // 64 bytes per sig } }
+ return ByteArray(0)
+ }
+
+ private fun serializeCommandData(tx: LedgerTransaction): ByteArray {
+ // As an example if not using Corda serialization: how to extract meaningful data from a Corda data structure:
+ // val commandSigners = tx.commands.flatMap { command -> command.signers }
+ // commandSigners.forEach { pubkey ->
+ // pubkey as EdDSAPublicKey
+ // witness += pubkey.abyte // 32 bytes
+ // }
+ return ByteArray(0)
+ }
+
+ private fun serializeOutputs(tx: LedgerTransaction): ByteArray {
+ return ByteArray(0)
+ }
+
+ private fun serializeInputs(tx: LedgerTransaction): ByteArray {
+ // return ByteArray(0)
+ // For testing, only serialize one input and nothing else for the entire tx. Lets see if we can deserialize that in Zinc
+ return tx.inputStates[0].serialize().bytes
+ }
+
+ /**
+ * This seems overkill now, but later we will add more things to the instance
+ */
+ override fun serializeInstance(zkTransactionId: SecureHash): ByteArray {
+ return zkTransactionId.bytes // These are the raw bytes of the the transaction id hash (merkle root)
+ }
+}
\ No newline at end of file
diff --git a/notary/src/main/kotlin/com/ing/zknotary/common/serializer/ZKInputSerializer.kt b/notary/src/main/kotlin/com/ing/zknotary/common/serializer/ZKInputSerializer.kt
new file mode 100644
index 000000000..5853b5ef0
--- /dev/null
+++ b/notary/src/main/kotlin/com/ing/zknotary/common/serializer/ZKInputSerializer.kt
@@ -0,0 +1,11 @@
+package com.ing.zknotary.common.serializer
+
+import net.corda.core.crypto.SecureHash
+import net.corda.core.transactions.LedgerTransaction
+
+interface ZKInputSerializer {
+ fun serializeWitness(tx: LedgerTransaction, signatures: List): ByteArray
+
+ fun serializeInstance(zkTransactionId: SecureHash): ByteArray
+}
+
diff --git a/notary/src/main/kotlin/com/ing/zknotary/common/transactions/NamedByAdditionalMerkleTree.kt b/notary/src/main/kotlin/com/ing/zknotary/common/transactions/NamedByAdditionalMerkleTree.kt
new file mode 100644
index 000000000..cb3ffc1a2
--- /dev/null
+++ b/notary/src/main/kotlin/com/ing/zknotary/common/transactions/NamedByAdditionalMerkleTree.kt
@@ -0,0 +1,18 @@
+package com.ing.zknotary.common.transactions
+
+import net.corda.core.KeepForDJVM
+
+/**
+ * Implemented by all transactions. This merkle root is an additional identifier to [NamedByHash.id].
+ *
+ */
+@KeepForDJVM
+interface NamedByAdditionalMerkleTree {
+ /**
+ * A [WireTransactionMerkleTree] that identifies this transaction.
+ *
+ * This identifier is an additional merkle root of this transaction.
+ * This enables flexibility in using additional, potentially less trusted algorithms for calculating this root.
+ */
+ val additionalMerkleTree: ZKWireTransactionMerkleTree
+}
diff --git a/notary/src/main/kotlin/com/ing/zknotary/common/transactions/ZKFilteredTransaction.kt b/notary/src/main/kotlin/com/ing/zknotary/common/transactions/ZKFilteredTransaction.kt
new file mode 100644
index 000000000..9ce7635ee
--- /dev/null
+++ b/notary/src/main/kotlin/com/ing/zknotary/common/transactions/ZKFilteredTransaction.kt
@@ -0,0 +1,30 @@
+package com.ing.zknotary.common.transactions
+
+import com.ing.zknotary.common.serializer.VictorsZKInputSerializer
+import com.ing.zknotary.common.zkp.Proof
+import com.ing.zknotary.common.zkp.ZincVerifierNative
+import net.corda.core.KeepForDJVM
+import net.corda.core.contracts.ComponentGroupEnum
+import net.corda.core.crypto.SecureHash
+import net.corda.core.serialization.CordaSerializable
+import net.corda.core.transactions.FilteredTransaction
+import net.corda.core.transactions.TraversableTransaction
+
+@KeepForDJVM
+@CordaSerializable
+class ZKFilteredTransaction(val proof: Proof, private val ftx: FilteredTransaction) :
+ TraversableTransaction(ftx.filteredComponentGroups) {
+ override val id: SecureHash = ftx.id
+
+ fun verify() {
+ // Check that the merkle tree of the ftx is correct
+ ftx.verify()
+
+ // If the merkle tree is correct, confirm that the required components are visible
+ ftx.checkAllComponentsVisible(ComponentGroupEnum.INPUTS_GROUP)
+ ftx.checkAllComponentsVisible(ComponentGroupEnum.TIMEWINDOW_GROUP)
+ ftx.checkAllComponentsVisible(ComponentGroupEnum.REFERENCES_GROUP)
+ ftx.checkAllComponentsVisible(ComponentGroupEnum.PARAMETERS_GROUP)
+
+ }
+}
diff --git a/notary/src/main/kotlin/com/ing/zknotary/common/transactions/ZKWireTransaction.kt b/notary/src/main/kotlin/com/ing/zknotary/common/transactions/ZKWireTransaction.kt
new file mode 100644
index 000000000..2537b6392
--- /dev/null
+++ b/notary/src/main/kotlin/com/ing/zknotary/common/transactions/ZKWireTransaction.kt
@@ -0,0 +1,17 @@
+package com.ing.zknotary.common.transactions
+
+import net.corda.core.crypto.Algorithm
+import net.corda.core.crypto.DefaultDigestServiceFactory
+import net.corda.core.transactions.WireTransaction
+
+class ZKWireTransaction(val wtx: WireTransaction) :
+ NamedByAdditionalMerkleTree {
+ /** This additional merkle root is represented by the root hash of a Merkle tree over the transaction components. */
+ override val additionalMerkleTree: ZKWireTransactionMerkleTree by lazy {
+ ZKWireTransactionMerkleTree(
+ this,
+ componentGroupLeafDigestService = DefaultDigestServiceFactory.getService(Algorithm.BLAKE2s256()),
+ nodeDigestService = DefaultDigestServiceFactory.getService(Algorithm.BLAKE2s256())
+ )
+ }
+}
\ No newline at end of file
diff --git a/notary/src/main/kotlin/com/ing/zknotary/common/transactions/ZKWireTransactionMerkleTree.kt b/notary/src/main/kotlin/com/ing/zknotary/common/transactions/ZKWireTransactionMerkleTree.kt
new file mode 100644
index 000000000..2f2d77e2e
--- /dev/null
+++ b/notary/src/main/kotlin/com/ing/zknotary/common/transactions/ZKWireTransactionMerkleTree.kt
@@ -0,0 +1,114 @@
+package com.ing.zknotary.common.transactions
+
+import net.corda.core.contracts.ComponentGroupEnum
+import net.corda.core.contracts.PrivacySalt
+import net.corda.core.crypto.DigestService
+import net.corda.core.crypto.MerkleTree
+import net.corda.core.crypto.SecureHash
+import net.corda.core.transactions.ComponentGroup
+import net.corda.core.utilities.OpaqueBytes
+import java.nio.ByteBuffer
+
+interface TransactionMerkleTree {
+ val root: SecureHash
+
+ /**
+ * The full Merkle tree for a transaction.
+ * Each transaction component group has its own sub Merkle tree.
+ * All of the roots of these trees are used as leaves in the top level Merkle tree.
+ *
+ * Note that ordering of elements inside a [ComponentGroup] matters when computing the Merkle root.
+ * On the other hand, insertion group ordering does not affect the top level Merkle tree construction, as it is
+ * actually an ordered Merkle tree, where its leaves are ordered based on the group ordinal in [ComponentGroupEnum].
+ * If any of the groups is an empty list or a null object, then [SecureHash.allOnesHash] is used as its hash.
+ * Also, [privacySalt] is not a Merkle tree leaf, because it is already "inherently" included via the component nonces.
+ *
+ * It is possible to have the leafs of ComponentGroups use a different hash function than the nodes of the merkle trees.
+ * This allows optimisation in choosing a leaf hash function that is better suited to arbitrary length inputs and a node function
+ * that is suited to fixed length inputs.
+ */
+ val tree: MerkleTree
+}
+
+class ZKWireTransactionMerkleTree(
+ zkwtx: ZKWireTransaction,
+ val componentGroupLeafDigestService: DigestService,
+ val nodeDigestService: DigestService
+) : TransactionMerkleTree {
+ private val componentGroups: List = zkwtx.wtx.componentGroups
+ private val privacySalt: PrivacySalt = zkwtx.wtx.privacySalt
+
+ constructor(wtx: ZKWireTransaction, digestService: DigestService) : this(wtx, digestService, digestService)
+
+ override val root: SecureHash get() = tree.hash
+
+ override val tree: MerkleTree by lazy { MerkleTree.getMerkleTree(groupHashes, nodeDigestService) }
+
+ /**
+ * For each component group: the root hashes of the sub Merkle tree for that component group
+ *
+ * If a group's Merkle root is allOnesHash, it is a flag that denotes this group is empty (if list) or null (if single object)
+ * in the wire transaction.
+ */
+ internal val groupHashes: List by lazy {
+ val componentGroupHashes = mutableListOf()
+ // Even if empty and not used, we should at least send oneHashes for each known
+ // or received but unknown (thus, bigger than known ordinal) component groups.
+ for (i in 0..componentGroups.map { it.groupIndex }.max()!!) {
+ val root = groupsMerkleRoots[i] ?: nodeDigestService.allOnesHash
+ componentGroupHashes.add(root)
+ }
+ componentGroupHashes
+ }
+
+ /**
+ * Calculate the root hashes of the component groups that are used to build the transaction's Merkle tree.
+ * Each group has its own sub Merkle tree and the hash of the root of this sub tree works as a leaf of the top
+ * level Merkle tree. The root of the latter is the transaction identifier.
+ */
+ private val groupsMerkleRoots: Map by lazy {
+ componentHashes.map { (groupIndex: Int, componentHashesInGroup: List) ->
+ groupIndex to MerkleTree.getMerkleTree(componentHashesInGroup, nodeDigestService, componentGroupLeafDigestService).hash
+ }.toMap()
+ }
+
+ /**
+ * Nonces for every transaction component in [componentGroups], including new fields (due to backwards compatibility support) we cannot process.
+ * Nonce are computed in the following way:
+ * nonce1 = H(salt || path_for_1st_component)
+ * nonce2 = H(salt || path_for_2nd_component)
+ * etc.
+ * Thus, all of the nonces are "independent" in the sense that knowing one or some of them, you can learn nothing about the rest.
+ */
+ private val componentNonces: Map> by lazy {
+ componentGroups.map { group ->
+ group.groupIndex to group.components.mapIndexed { componentIndex, _ ->
+ computeNonce(privacySalt, group.groupIndex, componentIndex)
+ }
+ }.toMap()
+ }
+
+ /**
+ * The hash for every transaction component, per component group. These will be used to build the full Merkle tree.
+ */
+ private val componentHashes: Map> by lazy {
+ componentGroups.map { group ->
+ group.groupIndex to group.components.mapIndexed { componentIndex, component ->
+ computeHash(componentNonces[group.groupIndex]!![componentIndex], component)
+ }
+ }.toMap()
+ }
+
+ private fun computeHash(nonce: SecureHash, opaqueBytes: OpaqueBytes): SecureHash =
+ componentGroupLeafDigestService.hash(nonce.bytes + opaqueBytes.bytes)
+
+ /**
+ * Method to compute a nonce based on privacySalt, component group index and component internal index.
+ * @param privacySalt a [PrivacySalt].
+ * @param groupIndex the fixed index (ordinal) of this component group.
+ * @param internalIndex the internal index of this object in its corresponding components list.
+ * @return H(privacySalt || groupIndex || internalIndex))
+ */
+ private fun computeNonce(privacySalt: PrivacySalt, groupIndex: Int, internalIndex: Int) = componentGroupLeafDigestService.hash(privacySalt.bytes + ByteBuffer.allocate(8)
+ .putInt(groupIndex).putInt(internalIndex).array())
+}
\ No newline at end of file
diff --git a/notary/src/main/kotlin/com/ing/zknotary/common/util/Native.kt b/notary/src/main/kotlin/com/ing/zknotary/common/util/Native.kt
new file mode 100644
index 000000000..958ac1bca
--- /dev/null
+++ b/notary/src/main/kotlin/com/ing/zknotary/common/util/Native.kt
@@ -0,0 +1,26 @@
+package com.ing.zknotary.common.util
+
+import com.sun.jna.Memory
+import com.sun.jna.Native
+
+fun ArrayList.toNative(): Memory {
+ val arrayListAsNativeMemory = Memory(this.size.toLong() * Native.getNativeSize(Int::class.javaObjectType))
+ this.forEachIndexed { index, element ->
+ arrayListAsNativeMemory.setInt(
+ index.toLong() * Native.getNativeSize(Int::class.javaObjectType),
+ element
+ )
+ }
+ return arrayListAsNativeMemory
+}
+
+fun ByteArray.toNative(): Memory {
+ val byteArrayAsNativeMemory = Memory(this.size.toLong() * Native.getNativeSize(Byte::class.javaObjectType))
+ this.forEachIndexed { index, element ->
+ byteArrayAsNativeMemory.setByte(
+ index.toLong() * Native.getNativeSize(Byte::class.javaObjectType),
+ element
+ )
+ }
+ return byteArrayAsNativeMemory
+}
diff --git a/notary/src/main/kotlin/com/ing/zknotary/common/zkp/NoopProverVerifier.kt b/notary/src/main/kotlin/com/ing/zknotary/common/zkp/NoopProverVerifier.kt
new file mode 100644
index 000000000..2441a8bff
--- /dev/null
+++ b/notary/src/main/kotlin/com/ing/zknotary/common/zkp/NoopProverVerifier.kt
@@ -0,0 +1,14 @@
+package com.ing.zknotary.common.zkp
+
+internal class NoopProver : Prover {
+ override fun prove(witness: ByteArray, instance: ByteArray): Proof {
+ return Proof(ByteArray(0))
+ }
+}
+
+internal class NoopVerifier : Verifier {
+ override fun verify(proof: Proof, instance: ByteArray) {
+ // No exception is success
+ }
+}
+
diff --git a/notary/src/main/kotlin/com/ing/zknotary/common/zkp/Proof.kt b/notary/src/main/kotlin/com/ing/zknotary/common/zkp/Proof.kt
new file mode 100644
index 000000000..b58567b1b
--- /dev/null
+++ b/notary/src/main/kotlin/com/ing/zknotary/common/zkp/Proof.kt
@@ -0,0 +1,8 @@
+package com.ing.zknotary.common.zkp
+
+import net.corda.core.KeepForDJVM
+import net.corda.core.serialization.CordaSerializable
+
+@CordaSerializable
+@KeepForDJVM
+class Proof(val bytes: ByteArray)
\ No newline at end of file
diff --git a/notary/src/main/kotlin/com/ing/zknotary/common/zkp/Prover.kt b/notary/src/main/kotlin/com/ing/zknotary/common/zkp/Prover.kt
new file mode 100644
index 000000000..6032344a4
--- /dev/null
+++ b/notary/src/main/kotlin/com/ing/zknotary/common/zkp/Prover.kt
@@ -0,0 +1,5 @@
+package com.ing.zknotary.common.zkp
+
+interface Prover {
+ fun prove(witness: ByteArray, instance: ByteArray): Proof
+}
\ No newline at end of file
diff --git a/notary/src/main/kotlin/com/ing/zknotary/common/zkp/Verifier.kt b/notary/src/main/kotlin/com/ing/zknotary/common/zkp/Verifier.kt
new file mode 100644
index 000000000..096fb72c6
--- /dev/null
+++ b/notary/src/main/kotlin/com/ing/zknotary/common/zkp/Verifier.kt
@@ -0,0 +1,16 @@
+package com.ing.zknotary.common.zkp
+
+import net.corda.core.CordaException
+import net.corda.core.KeepForDJVM
+import net.corda.core.serialization.CordaSerializable
+
+interface Verifier {
+ @Throws(ZKProofVerificationException::class)
+ fun verify(proof: Proof, instance: ByteArray)
+}
+
+@KeepForDJVM
+@CordaSerializable
+class ZKProofVerificationException(reason: String) :
+ CordaException("Transaction cannot be verified. Reason: $reason")
+
diff --git a/notary/src/main/kotlin/com/ing/zknotary/common/zkp/ZKConfig.kt b/notary/src/main/kotlin/com/ing/zknotary/common/zkp/ZKConfig.kt
new file mode 100644
index 000000000..ff2272bc6
--- /dev/null
+++ b/notary/src/main/kotlin/com/ing/zknotary/common/zkp/ZKConfig.kt
@@ -0,0 +1,12 @@
+package com.ing.zknotary.common.zkp
+
+import com.ing.zknotary.common.serializer.NoopZKInputSerializer
+import com.ing.zknotary.common.serializer.ZKInputSerializer
+
+object DefaultZKConfig : ZKConfig()
+
+open class ZKConfig(
+ val prover: Prover = NoopProver(),
+ val verifier: Verifier = NoopVerifier(),
+ val serializer: ZKInputSerializer = NoopZKInputSerializer
+)
\ No newline at end of file
diff --git a/notary/src/main/kotlin/com/ing/zknotary/common/zkp/ZincProverCLI.kt b/notary/src/main/kotlin/com/ing/zknotary/common/zkp/ZincProverCLI.kt
new file mode 100644
index 000000000..24d566724
--- /dev/null
+++ b/notary/src/main/kotlin/com/ing/zknotary/common/zkp/ZincProverCLI.kt
@@ -0,0 +1,11 @@
+package com.ing.zknotary.common.zkp
+
+class ZincProverCLI(private val proverKeyPath: String) : Prover {
+ override fun prove(witness: ByteArray, instance: ByteArray): Proof {
+ // write witness to file
+ // write instance to file
+ // call zargo prove with arguments for witness, instance and prover key location and save result as proof ByteArray
+ return Proof(ByteArray(0))
+ }
+}
+
diff --git a/notary/src/main/kotlin/com/ing/zknotary/common/zkp/ZincProverNative.kt b/notary/src/main/kotlin/com/ing/zknotary/common/zkp/ZincProverNative.kt
new file mode 100644
index 000000000..4fba39738
--- /dev/null
+++ b/notary/src/main/kotlin/com/ing/zknotary/common/zkp/ZincProverNative.kt
@@ -0,0 +1,39 @@
+package com.ing.zknotary.common.zkp
+
+import com.ing.zknotary.common.util.toNative
+import com.sun.jna.Library
+import com.sun.jna.Native
+import com.sun.jna.Pointer
+import com.sun.jna.ptr.IntByReference
+import com.sun.jna.ptr.PointerByReference
+
+class ZincProverNative(private val proverKeyPath: String) : Prover {
+ override fun prove(witness: ByteArray, instance: ByteArray): Proof {
+ val proofRef = PointerByReference()
+ val proofSizeRef = IntByReference()
+ ZincProverLibrary.INSTANCE.prove(proverKeyPath, proofRef, proofSizeRef, witness.toNative(), witness.size, instance.toNative(), instance.size)
+
+ val proofSize = proofSizeRef.value
+ val proofBytes = proofRef.value.getByteArray(0, proofSize)
+
+ return Proof(proofBytes)
+ // return Proof(ByteArray(0))
+ }
+
+ private interface ZincProverLibrary : Library {
+ fun prove(
+ proverKeyPath: String,
+ proofRef: PointerByReference,
+ proofSizeRef: IntByReference,
+ witness: Pointer,
+ witnessSize: Int,
+ instance: Pointer,
+ instanceSize: Int
+ ): Int
+
+ companion object {
+ val INSTANCE = Native.load("zinc_prover", ZincProverLibrary::class.java) as ZincProverLibrary
+ }
+ }
+}
+
diff --git a/notary/src/main/kotlin/com/ing/zknotary/common/zkp/ZincVerifierCLI.kt b/notary/src/main/kotlin/com/ing/zknotary/common/zkp/ZincVerifierCLI.kt
new file mode 100644
index 000000000..502bdb357
--- /dev/null
+++ b/notary/src/main/kotlin/com/ing/zknotary/common/zkp/ZincVerifierCLI.kt
@@ -0,0 +1,11 @@
+package com.ing.zknotary.common.zkp
+
+class ZincVerifierCLI(private val verifierKeyPath: String) : Verifier {
+ override fun verify(proof: Proof, instance: ByteArray) {
+ // write proof to file
+ // write instance to file
+ // call zargo verify with arguments for proof, instance and verifier key location and save result
+ // if (result != 1) throw ZKProofVerificationException("ZK Proof verification failed: reason understandably not given. ;-)")
+ }
+}
+
diff --git a/notary/src/main/kotlin/com/ing/zknotary/common/zkp/ZincVerifierNative.kt b/notary/src/main/kotlin/com/ing/zknotary/common/zkp/ZincVerifierNative.kt
new file mode 100644
index 000000000..d1c6839be
--- /dev/null
+++ b/notary/src/main/kotlin/com/ing/zknotary/common/zkp/ZincVerifierNative.kt
@@ -0,0 +1,35 @@
+package com.ing.zknotary.common.zkp
+
+import com.ing.zknotary.common.util.toNative
+import com.sun.jna.Library
+import com.sun.jna.Native
+import com.sun.jna.Pointer
+
+class ZincVerifierNative(private val verifierKeyPath: String) :
+ Verifier {
+ override fun verify(proof: Proof, instance: ByteArray) {
+ val result = ZincVerifierLibrary.INSTANCE.verify(
+ verifierKeyPath,
+ proof.bytes.toNative(),
+ proof.bytes.size,
+ instance.toNative(),
+ instance.size
+ )
+ if (result != 1) throw ZKProofVerificationException("ZK Proof verification failed: reason understandably not given. ;-)")
+ }
+
+ interface ZincVerifierLibrary : Library {
+ fun verify(
+ verifierKeyPath: String,
+ proof: Pointer,
+ proofSize: Int,
+ instance: Pointer,
+ instanceSize: Int
+ ): Int
+
+ companion object {
+ val INSTANCE = Native.load("zinc_verifier", ZincVerifierLibrary::class.java) as ZincVerifierLibrary
+ }
+ }
+}
+
diff --git a/notary/src/main/kotlin/com/ing/zknotary/notary/ZKNotaryService.kt b/notary/src/main/kotlin/com/ing/zknotary/notary/ZKNotaryService.kt
new file mode 100644
index 000000000..b3b441685
--- /dev/null
+++ b/notary/src/main/kotlin/com/ing/zknotary/notary/ZKNotaryService.kt
@@ -0,0 +1,45 @@
+package com.ing.zknotary.notary
+
+import com.ing.zknotary.notary.flows.ZKNotaryServiceFlow
+import java.security.PublicKey
+import net.corda.core.flows.FlowLogic
+import net.corda.core.flows.FlowSession
+import net.corda.core.internal.notary.SinglePartyNotaryService
+import net.corda.core.schemas.MappedSchema
+import net.corda.core.utilities.seconds
+import net.corda.node.services.api.ServiceHubInternal
+import net.corda.node.services.transactions.NodeNotarySchema
+import net.corda.node.services.transactions.PersistentUniquenessProvider
+
+class ZKNotaryService(override val services: ServiceHubInternal, override val notaryIdentityKey: PublicKey) :
+ SinglePartyNotaryService() {
+ override val uniquenessProvider =
+ PersistentUniquenessProvider(services.clock, services.database, services.cacheFactory, ::signTransaction)
+
+ init {
+ if (services.networkParameters.minimumPlatformVersion < 5) {
+ throw IllegalStateException("The ZKNotaryService is compatible with Corda version 5 or greater")
+ }
+ }
+
+ override fun createServiceFlow(otherPartySession: FlowSession): FlowLogic = ZKNotaryServiceFlow(
+ otherPartySession,
+ this,
+ 5.seconds // in the real world, this should come from configuration
+ )
+
+ override fun start() {}
+ override fun stop() {}
+}
+
+object PersistentUniquenessProviderSchema : MappedSchema(
+ schemaFamily = NodeNotarySchema.javaClass, version = 1,
+ mappedTypes = listOf(
+ PersistentUniquenessProvider.BaseComittedState::class.java,
+ PersistentUniquenessProvider.Request::class.java,
+ PersistentUniquenessProvider.CommittedState::class.java,
+ PersistentUniquenessProvider.CommittedTransaction::class.java
+ )
+) {
+ override val migrationResource = "node-notary.changelog-master"
+}
diff --git a/notary/src/main/kotlin/com/ing/zknotary/notary/flows/ZKNotaryServiceFlow.kt b/notary/src/main/kotlin/com/ing/zknotary/notary/flows/ZKNotaryServiceFlow.kt
new file mode 100644
index 000000000..b815ef003
--- /dev/null
+++ b/notary/src/main/kotlin/com/ing/zknotary/notary/flows/ZKNotaryServiceFlow.kt
@@ -0,0 +1,112 @@
+package com.ing.zknotary.notary.flows
+
+import com.ing.zknotary.common.transactions.ZKFilteredTransaction
+import com.ing.zknotary.common.zkp.DefaultZKConfig
+import com.ing.zknotary.common.zkp.ZKConfig
+import net.corda.core.KeepForDJVM
+import net.corda.core.crypto.SecureHash
+import net.corda.core.flows.FlowSession
+import net.corda.core.flows.NotarisationPayload
+import net.corda.core.flows.NotaryError
+import net.corda.core.identity.Party
+import net.corda.core.internal.notary.NotaryInternalException
+import net.corda.core.internal.notary.NotaryServiceFlow
+import net.corda.core.internal.notary.SinglePartyNotaryService
+import net.corda.core.node.NetworkParameters
+import net.corda.core.serialization.CordaSerializable
+import net.corda.core.transactions.ContractUpgradeFilteredTransaction
+import net.corda.core.transactions.NotaryChangeWireTransaction
+import java.time.Duration
+
+// TODO: find out how to inject the ZKConfig
+class ZKNotaryServiceFlow(
+ otherSideSession: FlowSession,
+ service: SinglePartyNotaryService,
+ etaThreshold: Duration,
+ private val zkConfig: ZKConfig = DefaultZKConfig
+) :
+ NotaryServiceFlow(otherSideSession, service, etaThreshold) {
+ init {
+ if (service.services.networkParameters.minimumPlatformVersion < 5) {
+ throw IllegalStateException("The ZKNotaryService is compatible with Corda version 5 or greater")
+ }
+ }
+
+ override fun extractParts(requestPayload: NotarisationPayload): TransactionParts {
+ val tx = requestPayload.coreTransaction
+ return when (tx) {
+ is ZKFilteredTransaction -> TransactionParts(
+ tx.id,
+ tx.inputs,
+ tx.timeWindow,
+ tx.notary,
+ tx.references,
+ networkParametersHash = tx.networkParametersHash
+ )
+ is ContractUpgradeFilteredTransaction,
+ is NotaryChangeWireTransaction -> TransactionParts(
+ tx.id,
+ tx.inputs,
+ null,
+ tx.notary,
+ networkParametersHash = tx.networkParametersHash
+ )
+ else -> throw UnexpectedTransactionTypeException(tx)
+ }
+ }
+
+ override fun verifyTransaction(requestPayload: NotarisationPayload) {
+ val tx = requestPayload.coreTransaction
+ try {
+ when (tx) {
+ is ZKFilteredTransaction -> {
+ tx.verify()
+ // TODO: the instance should be the additional Merkle root
+ val instance = zkConfig.serializer.serializeInstance(tx.id)
+ zkConfig.verifier.verify(tx.proof, instance)
+
+ val notary = tx.notary
+ ?: throw IllegalArgumentException("Transaction does not specify a notary.")
+ checkNotaryWhitelisted(notary, tx.networkParametersHash)
+ }
+ is ContractUpgradeFilteredTransaction -> {
+ checkNotaryWhitelisted(tx.notary, tx.networkParametersHash)
+ }
+ is NotaryChangeWireTransaction -> {
+ checkNotaryWhitelisted(tx.newNotary, tx.networkParametersHash)
+ }
+ else -> throw UnexpectedTransactionTypeException(tx)
+ }
+ } catch (e: Exception) {
+ throw NotaryInternalException(NotaryError.TransactionInvalid(e))
+ }
+ }
+
+ /** Make sure the transaction notary is part of the network parameter whitelist. */
+ private fun checkNotaryWhitelisted(notary: Party, attachedParameterHash: SecureHash?) {
+ // Expecting network parameters to be attached for platform version 4 or later.
+ if (attachedParameterHash == null) {
+ throw IllegalArgumentException("Transaction must contain network parameters.")
+ }
+ val attachedParameters = serviceHub.networkParametersService.lookup(attachedParameterHash)
+ ?: throw IllegalStateException("Unable to resolve network parameters from hash: $attachedParameterHash")
+
+ checkInWhitelist(attachedParameters, notary)
+ }
+
+ private fun checkInWhitelist(networkParameters: NetworkParameters, notary: Party) {
+ val notaryWhitelist = networkParameters.notaries.map { it.identity }
+
+ check(notary in notaryWhitelist) {
+ "Notary specified by the transaction ($notary) is not on the network parameter whitelist: ${notaryWhitelist.joinToString()}"
+ }
+ }
+
+ @KeepForDJVM
+ @CordaSerializable
+ class UnexpectedTransactionTypeException(tx: Any) : IllegalArgumentException(
+ "Received unexpected transaction type: " +
+ "${tx::class.java.simpleName}, expected ${ZKFilteredTransaction::class.java.simpleName}, " +
+ "${ContractUpgradeFilteredTransaction::class.java.simpleName} or ${NotaryChangeWireTransaction::class.java.simpleName}"
+ )
+}
diff --git a/notary/src/test/kotlin/com/ing/zknotary/flows/DenialOfStateFlowTest.kt b/notary/src/test/kotlin/com/ing/zknotary/flows/DenialOfStateFlowTest.kt
new file mode 100644
index 000000000..f63f35923
--- /dev/null
+++ b/notary/src/test/kotlin/com/ing/zknotary/flows/DenialOfStateFlowTest.kt
@@ -0,0 +1,290 @@
+package com.ing.zknotary.flows
+
+import com.ing.zknotary.common.contracts.TestContract
+import com.ing.zknotary.common.contracts.TestContract.Companion.PROGRAM_ID
+import net.corda.core.contracts.Command
+import net.corda.core.contracts.PrivacySalt
+import net.corda.core.contracts.StateAndRef
+import net.corda.core.contracts.StateRef
+import net.corda.core.contracts.TransactionVerificationException
+import net.corda.core.crypto.Crypto
+import net.corda.core.crypto.SecureHash
+import net.corda.core.crypto.SignableData
+import net.corda.core.crypto.SignatureMetadata
+import net.corda.core.flows.FinalityFlow
+import net.corda.core.flows.NotaryError
+import net.corda.core.flows.NotaryException
+import net.corda.core.identity.CordaX500Name
+import net.corda.core.identity.Party
+import net.corda.core.internal.createComponentGroups
+import net.corda.core.serialization.SerializationFactory
+import net.corda.core.transactions.SignedTransaction
+import net.corda.core.transactions.TransactionBuilder
+import net.corda.core.transactions.WireTransaction
+import net.corda.core.utilities.getOrThrow
+import net.corda.testing.common.internal.testNetworkParameters
+import net.corda.testing.core.ALICE_NAME
+import net.corda.testing.core.BOB_NAME
+import net.corda.testing.core.CHARLIE_NAME
+import net.corda.testing.core.singleIdentity
+import net.corda.testing.node.MockNetwork
+import net.corda.testing.node.MockNetworkNotarySpec
+import net.corda.testing.node.MockNetworkParameters
+import net.corda.testing.node.StartedMockNode
+import net.corda.testing.node.internal.findCordapp
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertTrue
+
+class DenialOfStateFlowTest {
+ private lateinit var mockNet: MockNetwork
+ private lateinit var notaryNode: StartedMockNode
+ private lateinit var notary: Party
+ private lateinit var aliceNode: StartedMockNode
+ private lateinit var alice: Party
+ private lateinit var bobNode: StartedMockNode
+ private lateinit var bob: Party
+ private lateinit var charlieNode: StartedMockNode
+ private lateinit var charlie: Party
+
+ @Before
+ fun setup() {
+ mockNet = MockNetwork(
+ MockNetworkParameters(
+ cordappsForAllNodes = listOf(
+ findCordapp("com.ing.zknotary.notary"),
+ findCordapp("com.ing.zknotary.common.contracts")
+ ),
+ notarySpecs = listOf(
+ MockNetworkNotarySpec(
+ name = CordaX500Name("Custom Notary", "Amsterdam", "NL"),
+ validating = false
+ )
+ ),
+ networkParameters = testNetworkParameters(minimumPlatformVersion = 5)
+ )
+ )
+ aliceNode = mockNet.createPartyNode(ALICE_NAME)
+ alice = aliceNode.info.singleIdentity()
+ bobNode = mockNet.createPartyNode(BOB_NAME)
+ bob = bobNode.info.singleIdentity()
+ charlieNode = mockNet.createPartyNode(CHARLIE_NAME)
+ charlie = charlieNode.info.singleIdentity()
+ notaryNode = mockNet.defaultNotaryNode
+ notary = mockNet.defaultNotaryIdentity
+
+ bobNode.registerInitiatedFlow(MoveReplyFlow::class.java)
+ charlieNode.registerInitiatedFlow(MoveReplyFlow::class.java)
+ }
+
+ @After
+ fun tearDown() {
+ mockNet.stopNodes()
+ }
+
+ @Test
+ /*
+ * In this version of the attack, Alice was no partiipant in any earlier tx.
+ * Therefore she has no knowledge of the contents of any of these transactions or states.
+ * Alice wants to maliciously prevent Bob from using his assets on the ledger.
+ * Alice manages to discover the identifier of one of Bob's UTXO's.
+ * Alice handcrafts a tx that consumes the UTXO.
+ * This tx will of course not be signed by Bob, who is not aware of the attack.
+ * Alice request notarisation for this malicious tx from a non-validating notary.
+ * The notary signs the tx, because it does not check contract and sigs and it is not a double spend.
+ * Bob is now blocked from using the state. When he tries to do that, the notary will reject the tx
+ * as a double spend.
+ */
+ fun `only knowing state id is enough for denial of state attack`() {
+ // Bob has a state
+ val bobsState = runCreateTx(bobNode, bob).coreTransaction.outRef(0)
+
+ // Alice finds out the id of Bob's state.
+ val bobsStateRef = bobsState.ref
+
+ // Alice executes a malicious tx to consume Bob's state, the notary signs it.
+ val aliceConsumesTransaction = runDenialOfStateConsumeTx(aliceNode, bobsStateRef)
+ val signers = aliceConsumesTransaction.sigs.map { it.by }
+ assertTrue { notary.owningKey in signers }
+
+ // Bob tries to spend it (use it as input) and it will fail
+ // Charlie will accept this, as it is a valid tx chain from his perspective, but
+ // the notary will not sign it, as it has already seen the input in Alice's malicious tx.
+ val ex = assertFailsWith {
+ runMoveTx(bobNode, bobsState, charlie)
+ }
+ assertThat(ex.error).isInstanceOf(NotaryError.Conflict::class.java)
+ }
+
+ @Test
+ /*
+ * In this version of the attack, Alice is a participant in a transaction with Bob.
+ * Therefore she has knowledge of the output state that was the result of that transaction.
+ * Alice will try to maliciously regain ownership of the state she gave to bob.
+ * Alice handcrafts a tx that assigns ownership back to her, resulting in a new output state in her name.
+ * This tx will of course not be signed by Bob, who is not aware of the attack.
+ * Alice request notarisation for this malicious tx from a non-validating notary.
+ * The notary signs the tx, because it does not check contract and sigs and it is not a double spend.
+ * Bob is now blocked from using the state. When he tries to do that, the notary will reject the tx
+ * as a double spend.
+ * Now Alice tries to sell the maliciously created output state to Charlie in a next tx.
+ * Charlie rejects this, because even though the tx creating the state was notarised, unlike the
+ * non-validating notary, Charlie **will** check the smart contract rules and sigs for all txs leading to
+ * the existence of this state. Those checks will fail, because Bobs signature is missing.
+ * End result: Bob is denied the usage of his state, and Alice will not be able to use it either.
+ */
+ fun `denial of state is successful with non-validating notary`() {
+ // Alice issues a state. This is normal and notarised
+ val aliceCreated = runCreateTx(aliceNode, alice)
+
+ // Alice: execute a valid move tx to move alice's state to Bob
+ // According to the contract
+ val aliceMovedToBob = runMoveTx(aliceNode, aliceCreated.coreTransaction.outRef(0), bob)
+ val signers = aliceMovedToBob.sigs.map { it.by }
+ assertTrue {
+ notary.owningKey in signers &&
+ bob.owningKey in signers
+ }
+
+ // Alice: determine the stateref of the output state now owned by Bob.
+ // Alice can know this in a normal situation, because she created the move tx to move her state to Bob.
+ val bobsState = aliceMovedToBob.coreTransaction.outRef(0)
+
+ // Alice: execute another, malicious, tx to move Bob's stateRef state back to Alice.
+ // We handcraft a tx that we send directly to the notary, that transfers the state back to Alice.
+ // Charlie will not accept this, but the state will be "spent" because the notary *will* sign it and commit the
+ // input stateRef to its list of spent states.
+ val aliceMovedToAlice = runDenialOfStateMoveTx(aliceNode, bobsState, alice)
+ val signers2 = aliceMovedToAlice.sigs.map { it.by }
+ assertTrue { notary.owningKey in signers2 }
+
+ val aliceMaliciousState = aliceMovedToAlice.coreTransaction.outRef(0)
+
+ // Bob tries to spend it (use it as input) and it will fail
+ // Charlie will accept this, as it is a valid tx chain from his perspective, but
+ // the notary will not sign it, as it has already seen the input in Alice's malicious tx.
+ val ex = assertFailsWith {
+ runMoveTx(bobNode, bobsState, charlie)
+ }
+ assertThat(ex.error).isInstanceOf(NotaryError.Conflict::class.java)
+
+ // To show that the damage is limited to only the 'locking' of Bob's state in the notary,
+ // and to show that it does not include the ability for Alice to use the state for other purposes:
+ // Future tx counterparties of Alice will not accept the chain of txs leading to this state, because it
+ // never was a valid tx: Bob should have signed it and didn't.
+ // It is only the non-validating notary that does not check for that.
+ val charlieException = assertFailsWith {
+ runMoveTx(aliceNode, aliceMaliciousState, charlie)
+ }
+ assertEquals(
+ aliceMovedToAlice.id,
+ charlieException.txId,
+ "Expected Alice's malicious transaction to fail verification by Charlie"
+ )
+ }
+ private fun runDenialOfStateConsumeTx(
+ attackerNode: StartedMockNode,
+ stateRefToDeny: StateRef
+ ): SignedTransaction {
+ val attackerPubKey = attackerNode.info.singleIdentity().owningKey
+ val wireTx = SerializationFactory.defaultFactory.withCurrentContext(null) {
+ WireTransaction(
+ createComponentGroups(
+ inputs = listOf(stateRefToDeny),
+ outputs = emptyList(),
+ notary = notary,
+ attachments = listOf(SecureHash.zeroHash),
+ commands = listOf(Command(TestContract.Move(), attackerPubKey)),
+ networkParametersHash = attackerNode.services.networkParametersService.currentHash,
+ timeWindow = null,
+ references = emptyList()
+ ),
+ PrivacySalt()
+ )
+ }
+ val signatureMetadata = SignatureMetadata(
+ 5,
+ Crypto.findSignatureScheme(attackerPubKey).schemeNumberID
+ )
+ val signableData = SignableData(wireTx.id, signatureMetadata)
+ val sig = attackerNode.services.keyManagementService.sign(signableData, attackerPubKey)
+ val stx = SignedTransaction(wireTx, listOf(sig))
+
+ val notaryFuture = attackerNode.startFlow(NonTxCheckingNotaryClientFlow(stx))
+ mockNet.runNetwork()
+ return stx + notaryFuture.getOrThrow()
+ }
+
+ private fun runDenialOfStateMoveTx(
+ attackerNode: StartedMockNode,
+ inputOwnedBySomeoneElse: StateAndRef,
+ newOwner: Party
+ ): SignedTransaction {
+ val stx =
+ attackerNode.services.signInitialTransaction(buildMoveTxForDenialOfState(inputOwnedBySomeoneElse, newOwner))
+
+ // We skip collecting signatures from the counterparty, and directly notarise, because the non-validating does not check signatures anyway.
+ // Also, the counterparty (if it is not the attacker) would reject this, because they do resolve the tx chain, verify the contract and the signatures.
+ // That would fail, because the input state for the dos-transaction tx was not owned by us and the tx was not signed by the owner (bob).
+ val notaryFuture = attackerNode.startFlow(NonTxCheckingNotaryClientFlow(stx))
+ mockNet.runNetwork()
+ val notarySignedTx = stx + notaryFuture.getOrThrow()
+
+ // Alice needs to store the malicious tx to allow counterparties to fetch later when resolving the chain.
+ // A counterparty will then reject this tx, because it was not signed by the owner of the input state.
+ // But if we don't store it, the counterparty will fail even faster when trying to fetch the tx from us.
+ attackerNode.services.recordTransactions(notarySignedTx)
+
+ return notarySignedTx
+ }
+
+ private fun buildMoveTxForDenialOfState(
+ inputOwnedBySomeoneElse: StateAndRef,
+ attacker: Party
+ ): TransactionBuilder {
+ return TransactionBuilder(inputOwnedBySomeoneElse.state.notary)
+ .addInputState(inputOwnedBySomeoneElse)
+ .addOutputState(inputOwnedBySomeoneElse.state.data.copy(owner = attacker), PROGRAM_ID)
+ // Even though the contract and required sigs are not verified by the non-validating notary,
+ // we set only our key as required to prevent some annoying local exceptions during tx creation that
+ // are caused by us verifying our own tx during txbuilder->signedtx transition.
+ .addCommand(TestContract.Move(), attacker.owningKey)
+ }
+
+ private fun runMoveTx(
+ node: StartedMockNode,
+ input: StateAndRef,
+ newOwner: Party
+ ): SignedTransaction {
+ val tx = buildMoveTx(input, newOwner)
+ val stx = node.services.signInitialTransaction(tx)
+ val moveFuture = node.startFlow(MoveFlow(stx, newOwner, FinalityFlow::class))
+ mockNet.runNetwork()
+ return moveFuture.getOrThrow()
+ }
+
+ private fun buildMoveTx(input: StateAndRef, newOwner: Party): TransactionBuilder {
+ return TransactionBuilder(input.state.notary)
+ .addInputState(input)
+ .addOutputState(input.state.data.copy(owner = newOwner), PROGRAM_ID)
+ .addCommand(TestContract.Move(), input.state.data.owner.owningKey, newOwner.owningKey)
+ }
+
+ private fun runCreateTx(ownerNode: StartedMockNode, owner: Party): SignedTransaction {
+ val tx = buildCreateTx(owner)
+ val stx = ownerNode.services.signInitialTransaction(tx)
+ val future = ownerNode.startFlow(FinalityFlow(stx, emptyList()))
+ mockNet.runNetwork()
+ return future.getOrThrow()
+ }
+
+ private fun buildCreateTx(owner: Party): TransactionBuilder {
+ return TransactionBuilder(notary)
+ .addOutputState(TestContract.TestState(owner), PROGRAM_ID)
+ .addCommand(TestContract.Create(), owner.owningKey)
+ }
+}
diff --git a/notary/src/test/kotlin/com/ing/zknotary/flows/Util.kt b/notary/src/test/kotlin/com/ing/zknotary/flows/Util.kt
new file mode 100644
index 000000000..293834667
--- /dev/null
+++ b/notary/src/test/kotlin/com/ing/zknotary/flows/Util.kt
@@ -0,0 +1,151 @@
+package com.ing.zknotary.flows
+
+import co.paralleluniverse.fibers.Suspendable
+import com.ing.zknotary.client.flows.ZKFinalityFlow
+import com.ing.zknotary.client.flows.ZKNotaryFlow
+import com.ing.zknotary.common.zkp.DefaultZKConfig
+import com.ing.zknotary.common.zkp.ZKConfig
+import net.corda.core.contracts.ContractState
+import net.corda.core.crypto.TransactionSignature
+import net.corda.core.flows.CollectSignatureFlow
+import net.corda.core.flows.FlowLogic
+import net.corda.core.flows.FlowSession
+import net.corda.core.flows.InitiatedBy
+import net.corda.core.flows.InitiatingFlow
+import net.corda.core.flows.NotaryException
+import net.corda.core.flows.NotaryFlow
+import net.corda.core.flows.ReceiveFinalityFlow
+import net.corda.core.flows.SignTransactionFlow
+import net.corda.core.identity.Party
+import net.corda.core.transactions.SignedTransaction
+import kotlin.reflect.KClass
+import kotlin.reflect.KVisibility
+import kotlin.reflect.jvm.javaConstructor
+
+// This custom ZK notary client flow does not check the validity the transaction here as normal in NotaryFlow.Client,
+// because that would fail: the tx is invalid on purpose, so that we can confirm that the notary rejects or doesn't reject an invalid tx.
+// Other than that, it is an unmodified copy of NotaryFlow.Client.
+class ZKNonTxCheckingNotaryClientFlow(private val stx: SignedTransaction) : ZKNotaryFlow(stx) {
+ @Suspendable
+ @Throws(NotaryException::class)
+ override fun call(): List {
+ // We don't check the transaction here as normal in ZKNotaryFlow, because that would fail:
+ // the tx is invalid on purpose, so that we can confirm that the notary rejects or doesn't reject an invalid tx.
+ val notaryParty = stx.notary ?: throw IllegalStateException("Transaction does not specify a Notary")
+ val response = zkNotarise(notaryParty)
+ return validateResponse(response, notaryParty)
+ }
+}
+
+// This custom notary client flow does not check the validity the transaction here as normal in NotaryFlow.Client,
+// because that would fail: the tx is invalid on purpose, so that we can confirm that the notary rejects or doesn't reject an invalid tx.
+// Other than that, it is an unmodified copy of NotaryFlow.Client.
+class NonTxCheckingNotaryClientFlow(private val stx: SignedTransaction) : NotaryFlow.Client(stx) {
+ @Suspendable
+ @Throws(NotaryException::class)
+ override fun call(): List {
+ // We don't check the transaction here as normal in NotaryFlow.Client, because that would fail:
+ // the tx is invalid on purpose, so that we can confirm that the notary rejects or doesn't reject an invalid tx.
+ val notaryParty = stx.notary ?: throw IllegalStateException("Transaction does not specify a Notary")
+ val response = notarise(notaryParty)
+ return validateResponse(response, notaryParty)
+ }
+}
+
+@InitiatingFlow
+class ZKMoveFlow(
+ private val stx: SignedTransaction,
+ private val newOwner: Party,
+ private val zkConfig: ZKConfig = DefaultZKConfig
+) : FlowLogic() {
+
+ @Suspendable
+ override fun call(): SignedTransaction {
+ val newOwnerSession = initiateFlow(newOwner)
+ val allSignedTx =
+ stx + subFlow(CollectSignatureFlow(stx, newOwnerSession, newOwnerSession.counterparty.owningKey))
+ val flow = ZKFinalityFlow(
+ allSignedTx,
+ listOf(newOwnerSession),
+ zkConfig = zkConfig
+ )
+ return subFlow(flow)
+ }
+}
+
+@InitiatingFlow
+class MoveFlow>(
+ private val stx: SignedTransaction,
+ private val newOwner: Party,
+ finalityFlow: KClass
+) : FlowLogic() {
+
+ private val finalityFlowConstructor = finalityFlow.constructors.single {
+ it.visibility == KVisibility.PUBLIC &&
+ it.parameters.size == 3 &&
+ it.parameters[0].type.classifier == SignedTransaction::class &&
+ it.parameters[1].type.classifier == FlowSession::class &&
+ it.parameters[2].type.classifier == Array::class
+ }.javaConstructor!!
+
+ @Suspendable
+ override fun call(): SignedTransaction {
+ val newOwnerSession = initiateFlow(newOwner)
+ val allSignedTx =
+ stx + subFlow(CollectSignatureFlow(stx, newOwnerSession, newOwnerSession.counterparty.owningKey))
+ val flow = finalityFlowConstructor.newInstance(
+ allSignedTx,
+ newOwnerSession,
+ emptyList().toTypedArray()
+ )
+ return subFlow(flow)
+ }
+}
+
+@InitiatedBy(ZKMoveFlow::class)
+class ZKMoveReplyFlow(val otherSideSession: FlowSession) : FlowLogic() {
+ @Suspendable
+ override fun call(): SignedTransaction {
+ val signTransactionFlow = object : SignTransactionFlow(otherSideSession) {
+ override fun checkTransaction(stx: SignedTransaction) {
+ // Verify that we know who all the participants in the transaction are
+ val states: Iterable =
+ serviceHub.loadStates(stx.tx.inputs.toSet()).map { it.state.data } + stx.tx.outputs.map { it.data }
+ states.forEach { state ->
+ state.participants.forEach { anon ->
+ require(serviceHub.identityService.wellKnownPartyFromAnonymous(anon) != null) {
+ "Transaction state $state involves unknown participant $anon"
+ }
+ }
+ }
+ }
+ }
+
+ val txId = subFlow(signTransactionFlow).id
+ return subFlow(ReceiveFinalityFlow(otherSideSession, expectedTxId = txId))
+ }
+}
+
+@InitiatedBy(MoveFlow::class)
+class MoveReplyFlow(val otherSideSession: FlowSession) : FlowLogic() {
+ @Suspendable
+ override fun call(): SignedTransaction {
+ val signTransactionFlow = object : SignTransactionFlow(otherSideSession) {
+ override fun checkTransaction(stx: SignedTransaction) {
+ // Verify that we know who all the participants in the transaction are
+ val states: Iterable =
+ serviceHub.loadStates(stx.tx.inputs.toSet()).map { it.state.data } + stx.tx.outputs.map { it.data }
+ states.forEach { state ->
+ state.participants.forEach { anon ->
+ require(serviceHub.identityService.wellKnownPartyFromAnonymous(anon) != null) {
+ "Transaction state $state involves unknown participant $anon"
+ }
+ }
+ }
+ }
+ }
+
+ val txId = subFlow(signTransactionFlow).id
+ return subFlow(ReceiveFinalityFlow(otherSideSession, expectedTxId = txId))
+ }
+}
diff --git a/notary/src/test/kotlin/com/ing/zknotary/flows/ZKNotaryFlowTest.kt b/notary/src/test/kotlin/com/ing/zknotary/flows/ZKNotaryFlowTest.kt
new file mode 100644
index 000000000..c3337aa35
--- /dev/null
+++ b/notary/src/test/kotlin/com/ing/zknotary/flows/ZKNotaryFlowTest.kt
@@ -0,0 +1,174 @@
+package com.ing.zknotary.flows
+
+import com.ing.zknotary.client.flows.ZKFinalityFlow
+import com.ing.zknotary.common.contracts.TestContract
+import com.ing.zknotary.common.contracts.TestContract.Companion.PROGRAM_ID
+import net.corda.core.contracts.StateAndRef
+import net.corda.core.identity.CordaX500Name
+import net.corda.core.identity.Party
+import net.corda.core.transactions.SignedTransaction
+import net.corda.core.transactions.TransactionBuilder
+import net.corda.core.utilities.getOrThrow
+import net.corda.testing.common.internal.testNetworkParameters
+import net.corda.testing.core.ALICE_NAME
+import net.corda.testing.core.BOB_NAME
+import net.corda.testing.core.singleIdentity
+import net.corda.testing.node.MockNetwork
+import net.corda.testing.node.MockNetworkNotarySpec
+import net.corda.testing.node.MockNetworkParameters
+import net.corda.testing.node.StartedMockNode
+import net.corda.testing.node.internal.findCordapp
+import org.junit.After
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Test
+import java.time.Duration
+import java.time.Instant
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+
+class ZKNotaryFlowTest {
+ private lateinit var mockNet: MockNetwork
+ private lateinit var notaryNode: StartedMockNode
+ private lateinit var notary: Party
+ private lateinit var aliceNode: StartedMockNode
+ private lateinit var alice: Party
+ private lateinit var bobNode: StartedMockNode
+ private lateinit var bob: Party
+
+ @Before
+ fun setup() {
+ mockNet = MockNetwork(
+ MockNetworkParameters(
+ cordappsForAllNodes = listOf(
+ findCordapp("com.ing.zknotary.notary"),
+ findCordapp("com.ing.zknotary.common.contracts")
+ ),
+ notarySpecs = listOf(
+ MockNetworkNotarySpec(
+ name = CordaX500Name("Custom Notary", "Amsterdam", "NL"),
+ className = "com.ing.zknotary.notary.ZKNotaryService",
+ validating = false
+ )
+ ),
+ networkParameters = testNetworkParameters(minimumPlatformVersion = 5)
+ )
+ )
+ aliceNode = mockNet.createPartyNode(ALICE_NAME)
+ bobNode = mockNet.createPartyNode(BOB_NAME)
+ notaryNode = mockNet.defaultNotaryNode
+ notary = mockNet.defaultNotaryIdentity
+ alice = aliceNode.info.singleIdentity()
+ bob = bobNode.info.singleIdentity()
+
+ bobNode.registerInitiatedFlow(MoveReplyFlow::class.java)
+ bobNode.registerInitiatedFlow(ZKMoveReplyFlow::class.java)
+ }
+
+ @After
+ fun tearDown() {
+ mockNet.stopNodes()
+ }
+
+ @Test
+ fun `valid zk create tx is notarised and persisted by creator`() {
+ val stx = runCreateTx(aliceNode, alice)
+ assertTrue("custom notary should sign a valid tx") {
+ stx.sigs.any { it.by == notary.owningKey }
+ }
+ aliceNode.transaction {
+ assertEquals(stx, aliceNode.services.validatedTransactions.getTransaction(stx.id))
+ }
+ }
+
+ @Test
+ fun `valid zk move tx is notarised and persisted by all participants`() {
+ val createdStateAndRef = runCreateTx(aliceNode, alice).coreTransaction.outRef(0)
+
+ val stx = runMoveTx(aliceNode, buildValidMoveTx(createdStateAndRef, bob), bob)
+
+ val signers = stx.sigs.map { it.by }
+ assertTrue {
+ notary.owningKey in signers &&
+ bob.owningKey in signers
+ }
+
+ aliceNode.transaction {
+ assertEquals(stx, aliceNode.services.validatedTransactions.getTransaction(stx.id))
+ }
+ bobNode.transaction {
+ assertEquals(stx, bobNode.services.validatedTransactions.getTransaction(stx.id))
+ }
+ }
+
+ @Test
+ @Ignore("This tx is now successful because the non-validating notary does not validate the tx. This should fail when it verifies the ZK proof.")
+ fun `invalid zk move tx (contract violation) is rejected by the notary`() {
+ val createdStateAndRef = runCreateTx(aliceNode, alice).coreTransaction.outRef(0)
+ val stx = aliceNode.services.signInitialTransaction(buildContractViolatingMoveTx(createdStateAndRef, bob))
+
+ val notaryFuture = aliceNode.startFlow(ZKNonTxCheckingNotaryClientFlow(stx))
+ mockNet.runNetwork()
+ val notarySignedTx = notaryFuture.getOrThrow()
+ val signers = notarySignedTx.map { it.by }
+ assertTrue {
+ notary.owningKey in signers
+ }
+ }
+
+ private fun runMoveTx(
+ node: StartedMockNode,
+ tx: TransactionBuilder,
+ newOwner: Party
+ ): SignedTransaction {
+ val stx = node.services.signInitialTransaction(tx)
+ val moveFuture = node.startFlow(ZKMoveFlow(stx, newOwner))
+ mockNet.runNetwork()
+ return moveFuture.getOrThrow()
+ }
+
+ /**
+ * This tx violates the contract rule that the value of the input and output must be identical.
+ */
+ private fun buildContractViolatingMoveTx(
+ input: StateAndRef,
+ newOwner: Party
+ ): TransactionBuilder {
+ val oldValue = input.state.data.value
+ val newValue = if (oldValue == Int.MAX_VALUE) oldValue - 1 else oldValue + 1
+ return TransactionBuilder(input.state.notary)
+ .addInputState(input)
+ .addOutputState(input.state.data.copy(owner = newOwner, value = newValue), PROGRAM_ID)
+ .addCommand(TestContract.Move(), input.state.data.owner.owningKey, newOwner.owningKey)
+ }
+
+ private fun buildValidMoveTx(
+ input: StateAndRef,
+ newOwner: Party
+ ): TransactionBuilder {
+ return TransactionBuilder(input.state.notary)
+ .addInputState(input)
+ .addOutputState(input.state.data.copy(owner = newOwner), PROGRAM_ID)
+ .addCommand(TestContract.Move(), input.state.data.owner.owningKey, newOwner.owningKey)
+ }
+
+ private fun runCreateTx(ownerNode: StartedMockNode, owner: Party): SignedTransaction {
+ val tx = buildCreateTx(owner)
+ val stx = ownerNode.services.signInitialTransaction(tx)
+ val future = ownerNode.startFlow(
+ ZKFinalityFlow(
+ stx,
+ emptyList()
+ )
+ )
+ mockNet.runNetwork()
+ return future.getOrThrow()
+ }
+
+ private fun buildCreateTx(owner: Party): TransactionBuilder {
+ return TransactionBuilder(notary)
+ .addOutputState(TestContract.TestState(owner), PROGRAM_ID)
+ .addCommand(TestContract.Create(), owner.owningKey)
+ .setTimeWindow(Instant.now(), Duration.ofSeconds(30))
+ }
+}
diff --git a/notary/src/test/kotlin/com/ing/zknotary/notary/NotaryClientFlowRegistrationTest.kt b/notary/src/test/kotlin/com/ing/zknotary/notary/NotaryClientFlowRegistrationTest.kt
new file mode 100644
index 000000000..2884b27e4
--- /dev/null
+++ b/notary/src/test/kotlin/com/ing/zknotary/notary/NotaryClientFlowRegistrationTest.kt
@@ -0,0 +1,132 @@
+package com.ing.zknotary.notary
+
+import co.paralleluniverse.fibers.Suspendable
+import java.security.PublicKey
+import java.util.Random
+import kotlin.test.assertFailsWith
+import kotlin.test.assertTrue
+import net.corda.core.crypto.Crypto
+import net.corda.core.crypto.SecureHash
+import net.corda.core.crypto.SignableData
+import net.corda.core.crypto.SignatureMetadata
+import net.corda.core.crypto.TransactionSignature
+import net.corda.core.flows.FlowLogic
+import net.corda.core.flows.FlowSession
+import net.corda.core.flows.NotarisationResponse
+import net.corda.core.flows.NotaryError
+import net.corda.core.flows.NotaryException
+import net.corda.core.flows.NotaryFlow
+import net.corda.core.identity.CordaX500Name
+import net.corda.core.identity.Party
+import net.corda.core.internal.notary.NotaryService
+import net.corda.core.transactions.SignedTransaction
+import net.corda.core.utilities.getOrThrow
+import net.corda.core.utilities.unwrap
+import net.corda.node.services.api.ServiceHubInternal
+import net.corda.testing.contracts.DummyContract
+import net.corda.testing.core.ALICE_NAME
+import net.corda.testing.core.singleIdentity
+import net.corda.testing.node.MockNetwork
+import net.corda.testing.node.MockNetworkNotarySpec
+import net.corda.testing.node.MockNetworkParameters
+import net.corda.testing.node.StartedMockNode
+import net.corda.testing.node.internal.DUMMY_CONTRACTS_CORDAPP
+import net.corda.testing.node.internal.enclosedCordapp
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+
+class NotaryClientFlowRegistrationTest {
+ private lateinit var mockNet: MockNetwork
+ private lateinit var notaryNode: StartedMockNode
+ private lateinit var aliceNode: StartedMockNode
+ private lateinit var notary: Party
+ private lateinit var alice: Party
+
+ @Before
+ fun setup() {
+ mockNet = MockNetwork(
+ MockNetworkParameters(
+ cordappsForAllNodes = listOf(DUMMY_CONTRACTS_CORDAPP, enclosedCordapp()),
+ notarySpecs = listOf(
+ MockNetworkNotarySpec(
+ name = CordaX500Name("Custom Notary", "Amsterdam", "NL"),
+ className = "com.ing.zknotary.notary.NotaryClientFlowRegistrationTest\$CustomClientFlowNotaryService",
+ validating = false
+ )
+ )
+ )
+ )
+ aliceNode = mockNet.createPartyNode(ALICE_NAME)
+ notaryNode = mockNet.defaultNotaryNode
+ notary = mockNet.defaultNotaryIdentity
+ alice = aliceNode.info.singleIdentity()
+ }
+
+ @After
+ fun tearDown() {
+ mockNet.stopNodes()
+ }
+
+ @Test
+ fun `custom notary client flow with valid payload is successful`() {
+ val tx = DummyContract.generateInitial(Random().nextInt(), notary, alice.ref(0))
+ val stx = aliceNode.services.signInitialTransaction(tx)
+ val future = aliceNode.startFlow(CustomClientFlow("VALID", stx, notary))
+ mockNet.runNetwork()
+ val sigs = future.getOrThrow()
+ assertTrue("custom notary should sign a valid tx from a custom flow") { sigs.any { it.by == notary.owningKey } }
+ }
+
+ @Test
+ fun `custom notary client flow with invalid payload fails`() {
+ val tx = DummyContract.generateInitial(Random().nextInt(), notary, alice.ref(0))
+ val stx = aliceNode.services.signInitialTransaction(tx)
+ val future = aliceNode.startFlow(CustomClientFlow("NOT VALID", stx, notary))
+ mockNet.runNetwork()
+ val ex = assertFailsWith { future.getOrThrow() }
+ val notaryError = ex.error as NotaryError.TransactionInvalid
+ assertThat(notaryError.cause).hasMessageContaining("Payload should be 'VALID'")
+ }
+
+ class CustomClientFlowNotaryService(
+ override val services: ServiceHubInternal,
+ override val notaryIdentityKey: PublicKey
+ ) : NotaryService() {
+ override fun createServiceFlow(otherPartySession: FlowSession): FlowLogic =
+ object : FlowLogic() {
+ @Suspendable
+ override fun call(): Void? {
+ otherPartySession.receive().unwrap {
+ if (it != "VALID") {
+ throw NotaryException(NotaryError.TransactionInvalid(Exception("Payload should be 'VALID'")))
+ }
+ }
+
+ val signableData = SignableData(
+ SecureHash.zeroHash,
+ SignatureMetadata(
+ services.myInfo.platformVersion,
+ Crypto.findSignatureScheme(notaryIdentityKey).schemeNumberID
+ )
+ )
+ val signature = services.keyManagementService.sign(signableData, notaryIdentityKey)
+ otherPartySession.send(NotarisationResponse(listOf(signature)))
+ return null
+ }
+ }
+
+ override fun start() {}
+ override fun stop() {}
+ }
+
+ class CustomClientFlow(private val payload: Any, stx: SignedTransaction, private val notary: Party) : NotaryFlow.Client(stx) {
+ @Suspendable
+ override fun call(): List {
+ val session = initiateFlow(notary)
+ session.send(payload)
+ return session.receive().unwrap { it }.signatures
+ }
+ }
+}
diff --git a/notary/src/test/kotlin/com/ing/zknotary/notary/transactions/NooPSerializeProveVerifyTest.kt b/notary/src/test/kotlin/com/ing/zknotary/notary/transactions/NooPSerializeProveVerifyTest.kt
new file mode 100644
index 000000000..b7b5c3354
--- /dev/null
+++ b/notary/src/test/kotlin/com/ing/zknotary/notary/transactions/NooPSerializeProveVerifyTest.kt
@@ -0,0 +1,41 @@
+package com.ing.zknotary.notary.transactions
+
+import com.ing.zknotary.common.serializer.NoopZKInputSerializer
+import com.ing.zknotary.common.zkp.NoopProver
+import com.ing.zknotary.common.zkp.NoopVerifier
+import net.corda.core.crypto.sign
+import net.corda.testing.core.TestIdentity
+import net.corda.testing.node.MockServices
+import net.corda.testing.node.ledger
+import org.junit.Test
+
+class NooPSerializeProveVerifyTest {
+
+ private val alice = TestIdentity.fresh("alice")
+ private val bob = TestIdentity.fresh("bob")
+
+ private val services = MockServices(
+ listOf("com.ing.zknotary.common.contracts"),
+ alice
+ )
+
+ @Test
+ fun `Noop - prove and verify with valid tx is successful`() {
+ services.ledger {
+ val wtx = moveTestsState(createTestsState(owner = alice), newOwner = bob)
+ verifies()
+
+ val ltx = wtx.toLedgerTransaction(services)
+
+ val sigAlice = alice.keyPair.sign(wtx.id).bytes
+
+ val witness = NoopZKInputSerializer.serializeWitness(ltx, listOf(sigAlice))
+ val instance = NoopZKInputSerializer.serializeInstance(wtx.id)
+
+ val proof = NoopProver().prove(witness, instance)
+
+ NoopVerifier().verify(proof, instance)
+ }
+ }
+}
+
diff --git a/notary/src/test/kotlin/com/ing/zknotary/notary/transactions/Util.kt b/notary/src/test/kotlin/com/ing/zknotary/notary/transactions/Util.kt
new file mode 100644
index 000000000..8c77f141b
--- /dev/null
+++ b/notary/src/test/kotlin/com/ing/zknotary/notary/transactions/Util.kt
@@ -0,0 +1,34 @@
+package com.ing.zknotary.notary.transactions
+
+import com.ing.zknotary.common.contracts.TestContract
+import net.corda.core.contracts.StateAndRef
+import net.corda.core.transactions.WireTransaction
+import net.corda.testing.core.TestIdentity
+import net.corda.testing.dsl.LedgerDSL
+import net.corda.testing.dsl.TestLedgerDSLInterpreter
+import net.corda.testing.dsl.TestTransactionDSLInterpreter
+
+fun LedgerDSL.createTestsState(owner: TestIdentity): StateAndRef {
+ val createdState = TestContract.TestState(owner.party)
+ val wtx = unverifiedTransaction {
+ command(listOf(owner.publicKey), TestContract.Create())
+ output(TestContract.PROGRAM_ID, "Alice's asset", createdState)
+ }
+
+ return wtx.outRef(createdState)
+}
+
+fun LedgerDSL.moveTestsState(
+ input: StateAndRef,
+ newOwner: TestIdentity
+): WireTransaction {
+ val wtx = transaction {
+ input(input.ref)
+ output(TestContract.PROGRAM_ID, input.state.data.withNewOwner(newOwner.party).ownableState)
+ command(listOf(input.state.data.owner.owningKey), TestContract.Move())
+ verifies()
+ }
+
+ return wtx
+}
+
diff --git a/notary/src/test/kotlin/com/ing/zknotary/notary/transactions/VictorsSerializeProveVerifyTest.kt b/notary/src/test/kotlin/com/ing/zknotary/notary/transactions/VictorsSerializeProveVerifyTest.kt
new file mode 100644
index 000000000..0fe10c337
--- /dev/null
+++ b/notary/src/test/kotlin/com/ing/zknotary/notary/transactions/VictorsSerializeProveVerifyTest.kt
@@ -0,0 +1,42 @@
+package com.ing.zknotary.notary.transactions
+
+import com.ing.zknotary.common.serializer.VictorsZKInputSerializer
+import com.ing.zknotary.common.zkp.ZincProverCLI
+import com.ing.zknotary.common.zkp.ZincVerifierCLI
+import net.corda.core.crypto.sign
+import net.corda.testing.core.TestIdentity
+import net.corda.testing.node.MockServices
+import net.corda.testing.node.ledger
+import org.junit.Test
+
+class VictorsSerializeProveVerifyTest {
+
+ private val alice = TestIdentity.fresh("alice")
+ private val bob = TestIdentity.fresh("bob")
+
+ private val services = MockServices(
+ listOf("com.ing.zknotary.common.contracts"),
+ alice
+ )
+
+ @Test
+ fun `Victor - prove and verify with valid tx is successful`() {
+ services.ledger {
+ val wtx = moveTestsState(createTestsState(owner = alice), newOwner = bob)
+ verifies()
+
+ val ltx = wtx.toLedgerTransaction(services)
+ val sigAlice = alice.keyPair.sign(wtx.id).bytes
+
+ // Check out JsonZKInputSerializer for reference
+ val witness = VictorsZKInputSerializer.serializeWitness(ltx, listOf(sigAlice))
+ val instance = VictorsZKInputSerializer.serializeInstance(wtx.id)
+
+ val proof = ZincProverCLI("/path/to/prover/key").prove(witness, instance)
+
+ // No assertions required: this throws an exception on verification failure
+ ZincVerifierCLI("/path/to/verifier/key").verify(proof, instance)
+ }
+ }
+}
+
diff --git a/repositories.gradle b/repositories.gradle
new file mode 100644
index 000000000..de8e12412
--- /dev/null
+++ b/repositories.gradle
@@ -0,0 +1,8 @@
+repositories {
+ mavenLocal()
+ mavenCentral()
+ jcenter()
+ maven { url 'https://jitpack.io' }
+ maven { url 'https://ci-artifactory.corda.r3cev.com/artifactory/corda' }
+ maven { url 'https://repo.gradle.org/gradle/libs-releases' }
+}
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 000000000..16885d9bf
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1 @@
+include 'notary'
\ No newline at end of file