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