Merge remote-tracking branch 'open/master'

This commit is contained in:
Andrius Dagys 2017-05-11 13:59:02 +01:00
commit dfb63231e3
6891 changed files with 26188 additions and 581659 deletions

4
.gitignore vendored
View File

@ -14,7 +14,6 @@ local.properties
# General build files
**/build/*
!docs/build/*
lib/dokka.jar
@ -34,6 +33,9 @@ lib/dokka.jar
.idea/shelf
.idea/dataSources
# Include the -parameters compiler option by default in IntelliJ required for serialization.
!.idea/compiler.xml
# if you remove the above rule, at least ignore the following:
# User-specific stuff:

96
.idea/compiler.xml generated Normal file
View File

@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="1.8">
<module name="attachment-demo_integrationTest" target="1.8" />
<module name="attachment-demo_main" target="1.8" />
<module name="attachment-demo_test" target="1.8" />
<module name="bank-of-corda-demo_integrationTest" target="1.8" />
<module name="bank-of-corda-demo_main" target="1.8" />
<module name="bank-of-corda-demo_test" target="1.8" />
<module name="buildSrc_main" target="1.8" />
<module name="buildSrc_test" target="1.8" />
<module name="client_main" target="1.8" />
<module name="client_test" target="1.8" />
<module name="corda-project_main" target="1.8" />
<module name="corda-project_test" target="1.8" />
<module name="corda-webserver_integrationTest" target="1.8" />
<module name="corda-webserver_main" target="1.8" />
<module name="corda-webserver_test" target="1.8" />
<module name="core_main" target="1.8" />
<module name="core_test" target="1.8" />
<module name="demobench_main" target="1.8" />
<module name="demobench_test" target="1.8" />
<module name="docs_main" target="1.8" />
<module name="docs_source_example-code_integrationTest" target="1.8" />
<module name="docs_source_example-code_main" target="1.8" />
<module name="docs_source_example-code_test" target="1.8" />
<module name="docs_test" target="1.8" />
<module name="experimental_main" target="1.8" />
<module name="experimental_test" target="1.8" />
<module name="explorer-capsule_main" target="1.6" />
<module name="explorer-capsule_test" target="1.6" />
<module name="explorer_main" target="1.8" />
<module name="explorer_test" target="1.8" />
<module name="finance_main" target="1.8" />
<module name="finance_test" target="1.8" />
<module name="irs-demo_integrationTest" target="1.8" />
<module name="irs-demo_main" target="1.8" />
<module name="irs-demo_test" target="1.8" />
<module name="isolated_main" target="1.8" />
<module name="isolated_test" target="1.8" />
<module name="jackson_main" target="1.8" />
<module name="jackson_test" target="1.8" />
<module name="jfx_integrationTest" target="1.8" />
<module name="jfx_main" target="1.8" />
<module name="jfx_test" target="1.8" />
<module name="loadtest_main" target="1.8" />
<module name="loadtest_test" target="1.8" />
<module name="mock_main" target="1.8" />
<module name="mock_test" target="1.8" />
<module name="network-visualiser_main" target="1.8" />
<module name="network-visualiser_test" target="1.8" />
<module name="node-api_main" target="1.8" />
<module name="node-api_test" target="1.8" />
<module name="node-capsule_main" target="1.6" />
<module name="node-capsule_test" target="1.6" />
<module name="node-schemas_main" target="1.8" />
<module name="node-schemas_test" target="1.8" />
<module name="node_integrationTest" target="1.8" />
<module name="node_main" target="1.8" />
<module name="node_test" target="1.8" />
<module name="raft-notary-demo_main" target="1.8" />
<module name="raft-notary-demo_test" target="1.8" />
<module name="rpc_integrationTest" target="1.8" />
<module name="rpc_main" target="1.8" />
<module name="rpc_test" target="1.8" />
<module name="samples_main" target="1.8" />
<module name="samples_test" target="1.8" />
<module name="sandbox_main" target="1.8" />
<module name="sandbox_test" target="1.8" />
<module name="simm-valuation-demo_integrationTest" target="1.8" />
<module name="simm-valuation-demo_main" target="1.8" />
<module name="simm-valuation-demo_test" target="1.8" />
<module name="test-utils_main" target="1.8" />
<module name="test-utils_test" target="1.8" />
<module name="tools_main" target="1.8" />
<module name="tools_test" target="1.8" />
<module name="trader-demo_integrationTest" target="1.8" />
<module name="trader-demo_main" target="1.8" />
<module name="trader-demo_test" target="1.8" />
<module name="verifier_integrationTest" target="1.8" />
<module name="verifier_main" target="1.8" />
<module name="verifier_test" target="1.8" />
<module name="webcapsule_main" target="1.6" />
<module name="webcapsule_test" target="1.6" />
<module name="webserver-webcapsule_main" target="1.6" />
<module name="webserver-webcapsule_test" target="1.6" />
<module name="webserver_integrationTest" target="1.8" />
<module name="webserver_main" target="1.8" />
<module name="webserver_test" target="1.8" />
</bytecodeTargetLevel>
</component>
<component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_STRING" value="-parameters" />
</component>
</project>

View File

@ -1,15 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Attachment Demo: Run Nodes" type="JetRunConfigurationType" factoryName="Kotlin" singleton="true">
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
<option name="MAIN_CLASS_NAME" value="net.corda.attachmentdemo.MainKt" />
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " />
<option name="PROGRAM_PARAMETERS" value="" />
<option name="WORKING_DIRECTORY" value="" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" />
<option name="PASS_PARENT_ENVS" value="true" />
<module name="attachment-demo_main" />
<envs />
<method />
</configuration>
</component>

View File

@ -1,15 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Attachment Demo: Run Recipient" type="JetRunConfigurationType" factoryName="Kotlin" singleton="true">
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
<option name="MAIN_CLASS_NAME" value="net.corda.attachmentdemo.AttachmentDemoKt" />
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " />
<option name="PROGRAM_PARAMETERS" value="--role=RECIPIENT" />
<option name="WORKING_DIRECTORY" value="" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" />
<option name="PASS_PARENT_ENVS" value="true" />
<module name="attachment-demo_main" />
<envs />
<method />
</configuration>
</component>

View File

@ -1,15 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Attachment Demo: Run Sender" type="JetRunConfigurationType" factoryName="Kotlin" singleton="true">
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
<option name="MAIN_CLASS_NAME" value="net.corda.attachmentdemo.AttachmentDemoKt" />
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " />
<option name="PROGRAM_PARAMETERS" value="--role SENDER" />
<option name="WORKING_DIRECTORY" value="" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" />
<option name="PASS_PARENT_ENVS" value="true" />
<module name="attachment-demo_main" />
<envs />
<method />
</configuration>
</component>

View File

@ -1,15 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Bank of Corda Demo: Run Issuer" type="JetRunConfigurationType" factoryName="Kotlin" singleton="true">
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
<option name="MAIN_CLASS_NAME" value="net.corda.bank.BankOfCordaDriverKt" />
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " />
<option name="PROGRAM_PARAMETERS" value="--role ISSUER" />
<option name="WORKING_DIRECTORY" value="" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="true" />
<option name="ALTERNATIVE_JRE_PATH" value="1.8" />
<option name="PASS_PARENT_ENVS" value="true" />
<module name="bank-of-corda-demo_main" />
<envs />
<method />
</configuration>
</component>

View File

@ -1,15 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Bank of Corda Demo: Run RPC Cash Issue" type="JetRunConfigurationType" factoryName="Kotlin" singleton="true">
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
<option name="MAIN_CLASS_NAME" value="net.corda.bank.BankOfCordaDriverKt" />
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " />
<option name="PROGRAM_PARAMETERS" value="--role ISSUE_CASH_RPC --quantity 12345 --currency GBP" />
<option name="WORKING_DIRECTORY" value="" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" />
<option name="PASS_PARENT_ENVS" value="true" />
<module name="bank-of-corda-demo_main" />
<envs />
<method />
</configuration>
</component>

View File

@ -1,15 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Bank of Corda Demo: Run Web Cash Issue" type="JetRunConfigurationType" factoryName="Kotlin" singleton="true">
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
<option name="MAIN_CLASS_NAME" value="net.corda.bank.BankOfCordaDriverKt" />
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " />
<option name="PROGRAM_PARAMETERS" value="--role ISSUE_CASH_WEB --quantity 67890 --currency EUR" />
<option name="WORKING_DIRECTORY" value="" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" />
<option name="PASS_PARENT_ENVS" value="true" />
<module name="bank-of-corda-demo_main" />
<envs />
<method />
</configuration>
</component>

View File

@ -1,15 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="IRS Demo: Run Date Change" type="JetRunConfigurationType" factoryName="Kotlin" singleton="true">
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
<option name="MAIN_CLASS_NAME" value="net.corda.irs.IRSDemo" />
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " />
<option name="PROGRAM_PARAMETERS" value="--role Date 2018-01-01" />
<option name="WORKING_DIRECTORY" value="" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" />
<option name="PASS_PARENT_ENVS" value="true" />
<module name="irs-demo_main" />
<envs />
<method />
</configuration>
</component>

View File

@ -1,15 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="IRS Demo: Run Nodes" type="JetRunConfigurationType" factoryName="Kotlin" singleton="true">
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
<option name="MAIN_CLASS_NAME" value="net.corda.irs.MainKt" />
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " />
<option name="PROGRAM_PARAMETERS" value="" />
<option name="WORKING_DIRECTORY" value="" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" />
<option name="PASS_PARENT_ENVS" value="true" />
<module name="irs-demo_main" />
<envs />
<method />
</configuration>
</component>

View File

@ -1,15 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="IRS Demo: Run Trade" type="JetRunConfigurationType" factoryName="Kotlin" singleton="true">
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
<option name="MAIN_CLASS_NAME" value="net.corda.irs.IRSDemo" />
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " />
<option name="PROGRAM_PARAMETERS" value="--role Trade trade1" />
<option name="WORKING_DIRECTORY" value="" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" />
<option name="PASS_PARENT_ENVS" value="true" />
<module name="irs-demo_main" />
<envs />
<method />
</configuration>
</component>

View File

@ -1,15 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="IRS Demo: Upload Rates" type="JetRunConfigurationType" factoryName="Kotlin" singleton="true">
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
<option name="MAIN_CLASS_NAME" value="net.corda.irs.IRSDemo" />
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " />
<option name="PROGRAM_PARAMETERS" value="--role UploadRates" />
<option name="WORKING_DIRECTORY" value="" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" />
<option name="PASS_PARENT_ENVS" value="true" />
<module name="irs-demo_main" />
<envs />
<method />
</configuration>
</component>

View File

@ -1,15 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Raft Notary Demo: Run Nodes" type="JetRunConfigurationType" factoryName="Kotlin" singleton="true">
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
<option name="MAIN_CLASS_NAME" value="net.corda.notarydemo.MainKt" />
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " />
<option name="PROGRAM_PARAMETERS" value="" />
<option name="WORKING_DIRECTORY" value="" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" />
<option name="PASS_PARENT_ENVS" value="true" />
<module name="raft-notary-demo_main" />
<envs />
<method />
</configuration>
</component>

View File

@ -1,15 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Raft Notary Demo: Run Notarisation" type="JetRunConfigurationType" factoryName="Kotlin" singleton="true">
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
<option name="MAIN_CLASS_NAME" value="net.corda.notarydemo.NotaryDemoKt" />
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " />
<option name="PROGRAM_PARAMETERS" value="--certificates=&quot;build/notary-demo-nodes/Party/certificates&quot;" />
<option name="WORKING_DIRECTORY" value="" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" />
<option name="PASS_PARENT_ENVS" value="true" />
<module name="raft-notary-demo_main" />
<envs />
<method />
</configuration>
</component>

View File

@ -1,15 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="SIMM Valuation Demo" type="JetRunConfigurationType" factoryName="Kotlin" singleton="true">
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
<option name="MAIN_CLASS_NAME" value="net.corda.vega.MainKt" />
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " />
<option name="PROGRAM_PARAMETERS" value="" />
<option name="WORKING_DIRECTORY" value="" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" />
<option name="PASS_PARENT_ENVS" value="true" />
<module name="simm-valuation-demo_main" />
<envs />
<method />
</configuration>
</component>

View File

@ -1,15 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Trader Demo: Run Buyer" type="JetRunConfigurationType" factoryName="Kotlin" singleton="true">
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
<option name="MAIN_CLASS_NAME" value="net.corda.traderdemo.TraderDemoKt" />
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " />
<option name="PROGRAM_PARAMETERS" value="--role BUYER" />
<option name="WORKING_DIRECTORY" value="" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" />
<option name="PASS_PARENT_ENVS" value="true" />
<module name="trader-demo_main" />
<envs />
<method />
</configuration>
</component>

View File

@ -1,15 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Trader Demo: Run Nodes" type="JetRunConfigurationType" factoryName="Kotlin" singleton="true">
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
<option name="MAIN_CLASS_NAME" value="net.corda.traderdemo.MainKt" />
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " />
<option name="PROGRAM_PARAMETERS" value="" />
<option name="WORKING_DIRECTORY" value="" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" />
<option name="PASS_PARENT_ENVS" value="true" />
<module name="trader-demo_main" />
<envs />
<method />
</configuration>
</component>

View File

@ -1,15 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Trader Demo: Run Seller" type="JetRunConfigurationType" factoryName="Kotlin" singleton="true">
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
<option name="MAIN_CLASS_NAME" value="net.corda.traderdemo.TraderDemoKt" />
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " />
<option name="PROGRAM_PARAMETERS" value="--role SELLER" />
<option name="WORKING_DIRECTORY" value="" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" />
<option name="PASS_PARENT_ENVS" value="true" />
<module name="trader-demo_main" />
<envs />
<method />
</configuration>
</component>

View File

@ -61,8 +61,9 @@ To look at the Corda source and run some sample applications:
## Development State
Corda is currently in very early development and should not be used in production systems. Breaking
changes will happen on minor versions until 1.0. Experimentation with Corda is recommended.
Corda is under active development and is maturing rapidly. We are targeting
production-readiness in 2017. The API will continue to evolve throughout 2017;
backwards compatibility not assured until version 1.0.
Pull requests, experiments, and contributions are encouraged and welcomed.

View File

@ -1,16 +1,20 @@
buildscript {
// For sharing constants between builds
Properties props = new Properties()
file("publish.properties").withInputStream { props.load(it) }
Properties constants = new Properties()
file("$projectDir/constants.properties").withInputStream { constants.load(it) }
// Our version: bump this on release.
ext.corda_version = "0.10-SNAPSHOT"
ext.gradle_plugins_version = props.getProperty("gradlePluginsVersion")
ext.corda_release_version = "0.12-SNAPSHOT"
// Increment this on any release that changes public APIs anywhere in the Corda platform
// TODO This is going to be difficult until we have a clear separation throughout the code of what is public and what is internal
ext.corda_platform_version = 1
ext.gradle_plugins_version = constants.getProperty("gradlePluginsVersion")
// Dependency versions. Can run 'gradle dependencyUpdates' to find new versions of things.
//
// TODO: Sort this alphabetically.
ext.kotlin_version = '1.0.7'
ext.kotlin_version = constants.getProperty("kotlinVersion")
ext.quasar_version = '0.7.6' // TODO: Upgrade to 0.7.7+ when Quasar bug 238 is resolved.
ext.asm_version = '0.5.3'
ext.artemis_version = '1.5.3'
@ -19,24 +23,27 @@ buildscript {
ext.jersey_version = '2.25'
ext.jolokia_version = '2.0.0-M3'
ext.assertj_version = '3.6.1'
ext.slf4j_version = '1.7.24'
ext.slf4j_version = '1.7.25'
ext.log4j_version = '2.7'
ext.bouncycastle_version = '1.56'
ext.guava_version = '19.0'
ext.bouncycastle_version = constants.getProperty("bouncycastleVersion")
ext.guava_version = constants.getProperty("guavaVersion")
ext.quickcheck_version = '0.7'
ext.okhttp_version = '3.5.0'
ext.netty_version = '4.1.5.Final'
ext.typesafe_config_version = '1.3.1'
ext.typesafe_config_version = constants.getProperty("typesafeConfigVersion")
ext.fileupload_version = '1.3.2'
ext.junit_version = '4.12'
ext.mockito_version = '1.10.19'
ext.jopt_simple_version = '5.0.2'
ext.jansi_version = '1.14'
ext.hibernate_version = '5.2.6.Final'
ext.h2_version = '1.4.194'
ext.rxjava_version = '1.2.4'
ext.requery_version = '1.1.1'
ext.requery_version = '1.2.1'
ext.dokka_version = '0.9.13'
ext.crash_version = '1.3.2'
// Update 121 is required for ObjectInputFilter and at time of writing 131 was latest:
ext.java8_minUpdateVersion = '131'
repositories {
mavenLocal()
@ -66,13 +73,10 @@ ext {
corda_revision = org.ajoberstar.grgit.Grgit.open(file('.')).head().id
}
apply plugin: 'kotlin'
apply plugin: 'project-report'
apply plugin: 'com.github.ben-manes.versions'
apply plugin: 'net.corda.plugins.publish-utils'
apply plugin: 'net.corda.plugins.quasar-utils'
apply plugin: 'net.corda.plugins.cordformation'
apply plugin: 'org.jetbrains.dokka'
// We need the following three lines even though they're inside an allprojects {} block below because otherwise
// IntelliJ gets confused when importing the project and ends up erasing and recreating the .idea directory, along
@ -84,22 +88,65 @@ targetCompatibility = 1.8
allprojects {
apply plugin: 'kotlin'
apply plugin: 'java'
apply plugin: 'jacoco'
sourceCompatibility = 1.8
targetCompatibility = 1.8
// Use manual resource copying of log4j2.xml rather than source sets.
// This prevents problems in IntelliJ with regard to duplicate source roots.
processTestResources {
from file("$rootDir/config/test/log4j2.xml")
}
tasks.withType(JavaCompile) {
options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" << "-Xlint:-options"
options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" << "-Xlint:-options" << "-parameters"
}
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
kotlinOptions {
languageVersion = "1.1"
apiVersion = "1.1"
jvmTarget = "1.8"
javaParameters = true // Useful for reflection.
}
}
tasks.withType(Jar) { // Includes War and Ear
manifest {
attributes('Corda-Release-Version': corda_release_version)
attributes('Corda-Platform-Version': corda_platform_version)
attributes('Corda-Revision': corda_revision)
attributes('Corda-Vendor': 'Corda Open Source')
}
}
tasks.withType(Test) {
// Prevent the project from creating temporary files outside of the build directory.
systemProperties['java.io.tmpdir'] = buildDir
}
group 'net.corda'
version "$corda_version"
version "$corda_release_version"
repositories {
mavenLocal()
mavenCentral()
jcenter()
// TODO: remove this once we eliminate Exposed
maven {
url 'https://dl.bintray.com/kotlin/exposed'
}
maven { url 'https://jitpack.io' }
}
configurations.compile {
// We want to use SLF4J's version of these bindings: jcl-over-slf4j
// Remove any transitive dependency on Apache's version.
exclude group: 'commons-logging', module: 'commons-logging'
}
}
// Check that we are running on a Java 8 JDK. The source/targetCompatibility values above aren't sufficient to
@ -108,7 +155,7 @@ allprojects {
// We recommend a specific minor version (unfortunately, not checkable directly) because JavaFX adds APIs in
// minor releases, so we can't work with just any Java 8, it has to be a recent one.
if (!JavaVersion.current().java8Compatible)
throw new GradleException("Corda requires Java 8, please upgrade to at least 1.8.0_112")
throw new GradleException("Corda requires Java 8, please upgrade to at least 1.8.0_$java8_minUpdateVersion")
repositories {
mavenCentral()
@ -123,8 +170,9 @@ dependencies {
compile project(':node')
compile "com.google.guava:guava:$guava_version"
runtime project(path: ":node:capsule", configuration: 'runtimeArtifacts')
runtime project(path: ":node:webserver:webcapsule", configuration: 'runtimeArtifacts')
// Set to compile to ensure it exists now deploy nodes no longer relies on build
compile project(path: ":node:capsule", configuration: 'runtimeArtifacts')
compile project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts')
// For the buildCordappDependenciesJar task
runtime project(':client:jfx')
@ -132,7 +180,7 @@ dependencies {
runtime project(':client:rpc')
runtime project(':core')
runtime project(':finance')
runtime project(':node:webserver')
runtime project(':webserver')
testCompile project(':test-utils')
}
@ -161,18 +209,18 @@ tasks.withType(Test) {
reports.html.destination = file("${reporting.baseDir}/${name}")
}
task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['build']) {
task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
directory "./build/nodes"
networkMap "Controller"
networkMap "CN=Controller,O=R3,OU=corda,L=London,C=UK"
node {
name "Controller"
name "CN=Controller,O=R3,OU=corda,L=London,C=UK"
nearestCity "London"
advertisedServices = ["corda.notary.validating"]
p2pPort 10002
cordapps = []
}
node {
name "Bank A"
name "CN=Bank A,O=R3,OU=corda,L=London,C=UK"
nearestCity "London"
advertisedServices = []
p2pPort 10012
@ -181,7 +229,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['build']) {
cordapps = []
}
node {
name "Bank B"
name "CN=Bank B,O=R3,OU=corda,L=London,C=UK"
nearestCity "New York"
advertisedServices = []
p2pPort 10007
@ -201,7 +249,7 @@ bintrayConfig {
projectUrl = 'https://github.com/corda/corda'
gpgSign = true
gpgPassphrase = System.getenv('CORDA_BINTRAY_GPG_PASSPHRASE')
publications = ['jfx', 'mock', 'rpc', 'core', 'corda', 'corda-webserver', 'finance', 'node', 'node-api', 'node-schemas', 'test-utils', 'jackson', 'webserver']
publications = ['jfx', 'mock', 'rpc', 'core', 'corda', 'corda-webserver', 'finance', 'node', 'node-api', 'node-schemas', 'test-utils', 'jackson', 'verifier', 'webserver']
license {
name = 'Apache-2.0'
url = 'https://www.apache.org/licenses/LICENSE-2.0'
@ -214,25 +262,6 @@ bintrayConfig {
}
}
// API docs
dokka {
moduleName = 'corda'
outputDirectory = 'docs/build/html/api/kotlin'
processConfigurations = ['compile']
sourceDirs = files('core/src/main/kotlin', 'client/jfx/src/main/kotlin', 'client/mock/src/main/kotlin', 'client/rpc/src/main/kotlin', 'node/src/main/kotlin', 'finance/src/main/kotlin', 'client/jackson/src/main/kotlin')
}
task dokkaJavadoc(type: org.jetbrains.dokka.gradle.DokkaTask) {
moduleName = 'corda'
outputFormat = "javadoc"
outputDirectory = 'docs/build/html/api/javadoc'
processConfigurations = ['compile']
sourceDirs = files('core/src/main/kotlin', 'client/jfx/src/main/kotlin', 'client/mock/src/main/kotlin', 'client/rpc/src/main/kotlin', 'node/src/main/kotlin', 'finance/src/main/kotlin', 'client/jackson/src/main/kotlin')
}
task apidocs(dependsOn: ['dokka', 'dokkaJavadoc'])
// Build a ZIP of all JARs required to compile the Cordapp template
// Note: corda.jar is used at runtime so no runtime ZIP is necessary.
// Resulting ZIP can be found in "build/distributions"

View File

@ -1,3 +1,10 @@
buildscript {
Properties constants = new Properties()
file("../constants.properties").withInputStream { constants.load(it) }
ext.guava_version = constants.getProperty("guavaVersion")
}
apply plugin: 'maven'
repositories {
@ -5,6 +12,5 @@ repositories {
}
dependencies {
// Cannot use ext.guava_version here :(
compile "com.google.guava:guava:20.0"
compile "com.google.guava:guava:$guava_version"
}

View File

@ -2,18 +2,9 @@ apply plugin: 'java'
apply plugin: 'kotlin'
apply plugin: 'net.corda.plugins.publish-utils'
repositories {
mavenLocal()
mavenCentral()
jcenter()
maven {
url 'https://dl.bintray.com/kotlin/exposed'
}
}
dependencies {
compile project(':core')
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
// Jackson and its plugins: parsing to/from JSON and other textual formats.

View File

@ -10,6 +10,8 @@ import com.fasterxml.jackson.module.kotlin.KotlinModule
import net.corda.core.contracts.Amount
import net.corda.core.contracts.BusinessCalendar
import net.corda.core.crypto.*
import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.Party
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.node.NodeInfo
import net.corda.core.node.services.IdentityService
@ -17,7 +19,10 @@ import net.corda.core.serialization.OpaqueBytes
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.i2p.crypto.eddsa.EdDSAPublicKey
import org.bouncycastle.asn1.ASN1InputStream
import org.bouncycastle.asn1.x500.X500Name
import java.math.BigDecimal
import java.security.PublicKey
import java.util.*
/**
@ -31,21 +36,28 @@ object JacksonSupport {
// If you change this API please update the docs in the docsite (json.rst)
interface PartyObjectMapper {
@Deprecated("Use partyFromX500Name instead")
fun partyFromName(partyName: String): Party?
fun partyFromKey(owningKey: CompositeKey): Party?
fun partyFromPrincipal(principal: X500Name): Party?
fun partyFromKey(owningKey: PublicKey): Party?
}
class RpcObjectMapper(val rpc: CordaRPCOps, factory: JsonFactory) : PartyObjectMapper, ObjectMapper(factory) {
override fun partyFromName(partyName: String): Party? = rpc.partyFromName(partyName)
override fun partyFromKey(owningKey: CompositeKey): Party? = rpc.partyFromKey(owningKey)
override fun partyFromPrincipal(principal: X500Name): Party? = rpc.partyFromX500Name(principal)
override fun partyFromKey(owningKey: PublicKey): Party? = rpc.partyFromKey(owningKey)
}
class IdentityObjectMapper(val identityService: IdentityService, factory: JsonFactory) : PartyObjectMapper, ObjectMapper(factory) {
override fun partyFromName(partyName: String): Party? = identityService.partyFromName(partyName)
override fun partyFromKey(owningKey: CompositeKey): Party? = identityService.partyFromKey(owningKey)
override fun partyFromPrincipal(principal: X500Name): Party? = identityService.partyFromX500Name(principal)
override fun partyFromKey(owningKey: PublicKey): Party? = identityService.partyFromKey(owningKey)
}
class NoPartyObjectMapper(factory: JsonFactory): PartyObjectMapper, ObjectMapper(factory) {
class NoPartyObjectMapper(factory: JsonFactory) : PartyObjectMapper, ObjectMapper(factory) {
override fun partyFromName(partyName: String): Party? = throw UnsupportedOperationException()
override fun partyFromKey(owningKey: CompositeKey): Party? = throw UnsupportedOperationException()
override fun partyFromPrincipal(principal: X500Name): Party? = throw UnsupportedOperationException()
override fun partyFromKey(owningKey: PublicKey): Party? = throw UnsupportedOperationException()
}
val cordaModule: Module by lazy {
@ -82,6 +94,10 @@ object JacksonSupport {
// For OpaqueBytes
addDeserializer(OpaqueBytes::class.java, OpaqueBytesDeserializer)
addSerializer(OpaqueBytes::class.java, OpaqueBytesSerializer)
// For X.500 distinguished names
addDeserializer(X500Name::class.java, X500NameDeserializer)
addSerializer(X500Name::class.java, X500NameSerializer)
}
}
@ -126,14 +142,14 @@ object JacksonSupport {
}
// TODO this needs to use some industry identifier(s) instead of these keys
val key = CompositeKey.parseFromBase58(parser.text)
val key = parsePublicKeyBase58(parser.text)
return AnonymousParty(key)
}
}
object PartySerializer : JsonSerializer<Party>() {
override fun serialize(obj: Party, generator: JsonGenerator, provider: SerializerProvider) {
generator.writeString(obj.name)
generator.writeString(obj.name.toString())
}
}
@ -144,8 +160,28 @@ object JacksonSupport {
}
val mapper = parser.codec as PartyObjectMapper
// TODO this needs to use some industry identifier(s) not just these human readable names
return mapper.partyFromName(parser.text) ?: throw JsonParseException(parser, "Could not find a Party with name ${parser.text}")
val principal = X500Name(parser.text)
return mapper.partyFromPrincipal(principal) ?: throw JsonParseException(parser, "Could not find a Party with name ${principal}")
}
}
object X500NameSerializer : JsonSerializer<X500Name>() {
override fun serialize(obj: X500Name, generator: JsonGenerator, provider: SerializerProvider) {
generator.writeString(obj.toString())
}
}
object X500NameDeserializer : JsonDeserializer<X500Name>() {
override fun deserialize(parser: JsonParser, context: DeserializationContext): X500Name {
if (parser.currentToken == JsonToken.FIELD_NAME) {
parser.nextToken()
}
return try {
X500Name(parser.text)
} catch(ex: IllegalArgumentException) {
throw JsonParseException(parser, "Invalid X.500 name ${parser.text}: ${ex.message}", ex)
}
}
}
@ -204,7 +240,7 @@ object JacksonSupport {
object PublicKeySerializer : JsonSerializer<EdDSAPublicKey>() {
override fun serialize(obj: EdDSAPublicKey, generator: JsonGenerator, provider: SerializerProvider) {
check(obj.params == ed25519Curve)
check(obj.params == Crypto.EDDSA_ED25519_SHA512.algSpec)
generator.writeString(obj.toBase58String())
}
}
@ -212,7 +248,7 @@ object JacksonSupport {
object PublicKeyDeserializer : JsonDeserializer<EdDSAPublicKey>() {
override fun deserialize(parser: JsonParser, context: DeserializationContext): EdDSAPublicKey {
return try {
parsePublicKeyBase58(parser.text)
parsePublicKeyBase58(parser.text) as EdDSAPublicKey
} catch (e: Exception) {
throw JsonParseException(parser, "Invalid public key ${parser.text}: ${e.message}")
}
@ -228,7 +264,7 @@ object JacksonSupport {
object CompositeKeyDeserializer : JsonDeserializer<CompositeKey>() {
override fun deserialize(parser: JsonParser, context: DeserializationContext): CompositeKey {
return try {
CompositeKey.parseFromBase58(parser.text)
parsePublicKeyBase58(parser.text) as CompositeKey
} catch (e: Exception) {
throw JsonParseException(parser, "Invalid composite key ${parser.text}: ${e.message}")
}

View File

@ -5,17 +5,15 @@ import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
import com.google.common.collect.HashMultimap
import com.google.common.collect.Multimap
import com.google.common.collect.MultimapBuilder
import net.corda.jackson.StringToMethodCallParser.ParsedMethodCall
import org.slf4j.LoggerFactory
import java.lang.reflect.Constructor
import java.lang.reflect.Method
import java.util.*
import java.util.concurrent.Callable
import javax.annotation.concurrent.ThreadSafe
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.KotlinReflectionInternalError
import kotlin.reflect.jvm.internal.KotlinReflectionInternalError
import kotlin.reflect.jvm.kotlinFunction
/**
@ -75,14 +73,14 @@ import kotlin.reflect.jvm.kotlinFunction
@ThreadSafe
open class StringToMethodCallParser<in T : Any> @JvmOverloads constructor(
targetType: Class<out T>,
private val om: ObjectMapper = JacksonSupport.createNonRpcMapper(YAMLFactory()))
{
private val om: ObjectMapper = JacksonSupport.createNonRpcMapper(YAMLFactory())) {
/** Same as the regular constructor but takes a Kotlin reflection [KClass] instead of a Java [Class]. */
constructor(targetType: KClass<out T>) : this(targetType.java)
companion object {
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
private val ignoredNames = Object::class.java.methods.map { it.name }
private fun methodsFromType(clazz: Class<*>): Multimap<String, Method> {
val result = HashMultimap.create<String, Method>()
for ((key, value) in clazz.methods.filterNot { it.isSynthetic && it.name !in ignoredNames }.map { it.name to it }) {
@ -90,6 +88,7 @@ open class StringToMethodCallParser<in T : Any> @JvmOverloads constructor(
}
return result
}
private val log = LoggerFactory.getLogger(StringToMethodCallParser::class.java)!!
}
@ -126,7 +125,7 @@ open class StringToMethodCallParser<in T : Any> @JvmOverloads constructor(
return method.parameters.mapIndexed { index, param ->
when {
param.isNamePresent -> param.name
// index + 1 because the first Kotlin reflection param is 'this', but that doesn't match Java reflection.
// index + 1 because the first Kotlin reflection param is 'this', but that doesn't match Java reflection.
kf != null -> kf.parameters[index + 1].name ?: throw UnparseableCallException.ReflectionDataMissing(method.name, index)
else -> throw UnparseableCallException.ReflectionDataMissing(method.name, index)
}
@ -147,11 +146,12 @@ open class StringToMethodCallParser<in T : Any> @JvmOverloads constructor(
}
}
open class UnparseableCallException(command: String) : Exception("Could not parse as a command: $command") {
open class UnparseableCallException(command: String, cause: Throwable? = null) : Exception("Could not parse as a command: $command", cause) {
class UnknownMethod(val methodName: String) : UnparseableCallException("Unknown command name: $methodName")
class MissingParameter(methodName: String, val paramName: String, command: String) : UnparseableCallException("Parameter $paramName missing from attempt to invoke $methodName in command: $command")
class TooManyParameters(methodName: String, command: String) : UnparseableCallException("Too many parameters provided for $methodName: $command")
class ReflectionDataMissing(methodName: String, argIndex: Int) : UnparseableCallException("Method $methodName missing parameter name at index $argIndex")
class FailedParse(e: Exception) : UnparseableCallException(e.message ?: e.toString(), e)
}
/**
@ -193,10 +193,14 @@ open class StringToMethodCallParser<in T : Any> @JvmOverloads constructor(
val parameterString = "{ $args }"
val tree: JsonNode = om.readTree(parameterString) ?: throw UnparseableCallException(args)
if (tree.size() > parameters.size) throw UnparseableCallException.TooManyParameters(methodNameHint, args)
val inOrderParams: List<Any?> = parameters.mapIndexed { index, param ->
val inOrderParams: List<Any?> = parameters.mapIndexed { _, param ->
val (argName, argType) = param
val entry = tree[argName] ?: throw UnparseableCallException.MissingParameter(methodNameHint, argName, args)
om.readValue(entry.traverse(om), argType)
try {
om.readValue(entry.traverse(om), argType)
} catch(e: Exception) {
throw UnparseableCallException.FailedParse(e)
}
}
if (log.isDebugEnabled) {
inOrderParams.forEachIndexed { i, param ->

View File

@ -10,7 +10,7 @@ class StringToMethodCallParserTest {
fun simple() = "simple"
fun string(note: String) = note
fun twoStrings(a: String, b: String) = a + b
fun simpleObject(hash: SecureHash.SHA256) = hash.toString()!!
fun simpleObject(hash: SecureHash.SHA256) = hash.toString()
fun complexObject(pair: Pair<Int, String>) = pair
fun overload(a: String) = a

View File

@ -4,18 +4,6 @@ apply plugin: 'net.corda.plugins.publish-utils'
description 'Corda client JavaFX modules'
repositories {
mavenLocal()
mavenCentral()
maven {
url 'http://oss.sonatype.org/content/repositories/snapshots'
}
jcenter()
maven {
url 'https://dl.bintray.com/kotlin/exposed'
}
}
//noinspection GroovyAssignabilityCheck
configurations {
// we don't want isolated.jar in classPath, since we want to test jar being dynamically loaded as an attachment
@ -33,11 +21,6 @@ sourceSets {
srcDir file('src/integration-test/kotlin')
}
}
test {
resources {
srcDir "../../config/test"
}
}
}
// To find potential version conflicts, run "gradle htmlDependencyReport" and then look in
@ -48,6 +31,7 @@ dependencies {
compile project(':finance')
compile project(':client:rpc')
compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
compile "com.google.guava:guava:$guava_version"
// ReactFX: Functional reactive UI programming.

View File

@ -1,118 +0,0 @@
package net.corda.client.jfx
import net.corda.client.rpc.CordaRPCClient
import net.corda.core.contracts.DOLLARS
import net.corda.core.flows.FlowException
import net.corda.core.getOrThrow
import net.corda.core.messaging.startFlow
import net.corda.core.node.services.ServiceInfo
import net.corda.core.random63BitValue
import net.corda.core.serialization.OpaqueBytes
import net.corda.flows.CashIssueFlow
import net.corda.flows.CashPaymentFlow
import net.corda.node.internal.Node
import net.corda.node.services.startFlowPermission
import net.corda.node.services.transactions.ValidatingNotaryService
import net.corda.nodeapi.User
import net.corda.testing.node.NodeBasedTest
import org.apache.activemq.artemis.api.core.ActiveMQSecurityException
import org.assertj.core.api.Assertions.assertThatExceptionOfType
import org.junit.After
import org.junit.Before
import org.junit.Test
import java.util.*
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class CordaRPCClientTest : NodeBasedTest() {
private val rpcUser = User("user1", "test", permissions = setOf(
startFlowPermission<CashIssueFlow>(),
startFlowPermission<CashPaymentFlow>()
))
private lateinit var node: Node
private lateinit var client: CordaRPCClient
@Before
fun setUp() {
node = startNode("Alice", rpcUsers = listOf(rpcUser), advertisedServices = setOf(ServiceInfo(ValidatingNotaryService.type))).getOrThrow()
client = CordaRPCClient(node.configuration.rpcAddress!!)
}
@After
fun done() {
client.close()
}
@Test
fun `log in with valid username and password`() {
client.start(rpcUser.username, rpcUser.password)
}
@Test
fun `log in with unknown user`() {
assertThatExceptionOfType(ActiveMQSecurityException::class.java).isThrownBy {
client.start(random63BitValue().toString(), rpcUser.password)
}
}
@Test
fun `log in with incorrect password`() {
assertThatExceptionOfType(ActiveMQSecurityException::class.java).isThrownBy {
client.start(rpcUser.username, random63BitValue().toString())
}
}
@Test
fun `close-send deadlock and premature shutdown on empty observable`() {
println("Starting client")
client.start(rpcUser.username, rpcUser.password)
println("Creating proxy")
val proxy = client.proxy()
println("Starting flow")
val flowHandle = proxy.startFlow(
::CashIssueFlow,
20.DOLLARS, OpaqueBytes.of(0), node.info.legalIdentity, node.info.legalIdentity)
println("Started flow, waiting on result")
flowHandle.progress.subscribe {
println("PROGRESS $it")
}
println("Result: ${flowHandle.returnValue.getOrThrow()}")
}
@Test
fun `FlowException thrown by flow`() {
client.start(rpcUser.username, rpcUser.password)
val proxy = client.proxy()
val handle = proxy.startFlow(::CashPaymentFlow, 100.DOLLARS, node.info.legalIdentity)
// TODO Restrict this to CashException once RPC serialisation has been fixed
assertThatExceptionOfType(FlowException::class.java).isThrownBy {
handle.returnValue.getOrThrow()
}
}
@Test
fun `get cash balances`() {
println("Starting client")
client.start(rpcUser.username, rpcUser.password)
println("Creating proxy")
val proxy = client.proxy()
val startCash = proxy.getCashBalances()
assertTrue(startCash.isEmpty(), "Should not start with any cash")
val flowHandle = proxy.startFlow(::CashIssueFlow,
123.DOLLARS, OpaqueBytes.of(0),
node.info.legalIdentity, node.info.legalIdentity
)
println("Started issuing cash, waiting on result")
flowHandle.progress.subscribe {
println("CashIssue PROGRESS $it")
}
val finishCash = proxy.getCashBalances()
println("Cash Balances: $finishCash")
assertEquals(1, finishCash.size)
assertEquals(123.DOLLARS, finishCash.get(Currency.getInstance("USD")))
}
}

View File

@ -6,6 +6,9 @@ import net.corda.core.bufferUntilSubscribed
import net.corda.core.contracts.Amount
import net.corda.core.contracts.DOLLARS
import net.corda.core.contracts.USD
import net.corda.core.crypto.isFulfilledBy
import net.corda.core.crypto.keys
import net.corda.core.flows.FlowInitiator
import net.corda.core.flows.StateMachineRunId
import net.corda.core.getOrThrow
import net.corda.core.messaging.CordaRPCOps
@ -18,6 +21,10 @@ import net.corda.core.node.services.StateMachineTransactionMapping
import net.corda.core.node.services.Vault
import net.corda.core.serialization.OpaqueBytes
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.ALICE
import net.corda.core.utilities.BOB
import net.corda.core.utilities.CHARLIE
import net.corda.core.utilities.DUMMY_NOTARY
import net.corda.flows.CashExitFlow
import net.corda.flows.CashIssueFlow
import net.corda.flows.CashPaymentFlow
@ -30,21 +37,25 @@ import net.corda.testing.expect
import net.corda.testing.expectEvents
import net.corda.testing.node.DriverBasedTest
import net.corda.testing.sequence
import org.bouncycastle.asn1.x500.X500Name
import org.junit.Test
import rx.Observable
class NodeMonitorModelTest : DriverBasedTest() {
lateinit var aliceNode: NodeInfo
lateinit var bobNode: NodeInfo
lateinit var notaryNode: NodeInfo
lateinit var rpc: CordaRPCOps
lateinit var rpcBob: CordaRPCOps
lateinit var stateMachineTransactionMapping: Observable<StateMachineTransactionMapping>
lateinit var stateMachineUpdates: Observable<StateMachineUpdate>
lateinit var stateMachineUpdatesBob: Observable<StateMachineUpdate>
lateinit var progressTracking: Observable<ProgressTrackingEvent>
lateinit var transactions: Observable<SignedTransaction>
lateinit var vaultUpdates: Observable<Vault.Update>
lateinit var networkMapUpdates: Observable<NetworkMapCache.MapChange>
lateinit var newNode: (String) -> NodeInfo
lateinit var newNode: (X500Name) -> NodeInfo
override fun setup() = driver {
val cashUser = User("user1", "test", permissions = setOf(
@ -52,15 +63,14 @@ class NodeMonitorModelTest : DriverBasedTest() {
startFlowPermission<CashPaymentFlow>(),
startFlowPermission<CashExitFlow>())
)
val aliceNodeFuture = startNode("Alice", rpcUsers = listOf(cashUser))
val notaryNodeFuture = startNode("Notary", advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)))
val aliceNodeFuture = startNode(ALICE.name, rpcUsers = listOf(cashUser))
val notaryNodeFuture = startNode(DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)))
val aliceNodeHandle = aliceNodeFuture.getOrThrow()
val notaryNodeHandle = notaryNodeFuture.getOrThrow()
aliceNode = aliceNodeHandle.nodeInfo
notaryNode = notaryNodeHandle.nodeInfo
newNode = { nodeName -> startNode(nodeName).getOrThrow().nodeInfo }
val monitor = NodeMonitorModel()
stateMachineTransactionMapping = monitor.stateMachineTransactionMapping.bufferUntilSubscribed()
stateMachineUpdates = monitor.stateMachineUpdates.bufferUntilSubscribed()
progressTracking = monitor.progressTracking.bufferUntilSubscribed()
@ -70,26 +80,32 @@ class NodeMonitorModelTest : DriverBasedTest() {
monitor.register(aliceNodeHandle.configuration.rpcAddress!!, cashUser.username, cashUser.password)
rpc = monitor.proxyObservable.value!!
val bobNodeHandle = startNode(BOB.name, rpcUsers = listOf(cashUser)).getOrThrow()
bobNode = bobNodeHandle.nodeInfo
val monitorBob = NodeMonitorModel()
stateMachineUpdatesBob = monitorBob.stateMachineUpdates.bufferUntilSubscribed()
monitorBob.register(bobNodeHandle.configuration.rpcAddress!!, cashUser.username, cashUser.password)
rpcBob = monitorBob.proxyObservable.value!!
runTest()
}
@Test
fun `network map update`() {
newNode("Bob")
newNode("Charlie")
newNode(CHARLIE.name)
networkMapUpdates.filter { !it.node.advertisedServices.any { it.info.type.isNotary() } }
.filter { !it.node.advertisedServices.any { it.info.type == NetworkMapService.type } }
.expectEvents(isStrict = false) {
sequence(
// TODO : Add test for remove when driver DSL support individual node shutdown.
expect { output: NetworkMapCache.MapChange ->
require(output.node.legalIdentity.name == "Alice") { "Expecting : Alice, Actual : ${output.node.legalIdentity.name}" }
require(output.node.legalIdentity.name == ALICE.name) { "Expecting : ${ALICE.name}, Actual : ${output.node.legalIdentity.name}" }
},
expect { output: NetworkMapCache.MapChange ->
require(output.node.legalIdentity.name == "Bob") { "Expecting : Bob, Actual : ${output.node.legalIdentity.name}" }
require(output.node.legalIdentity.name == BOB.name) { "Expecting : ${BOB.name}, Actual : ${output.node.legalIdentity.name}" }
},
expect { output: NetworkMapCache.MapChange ->
require(output.node.legalIdentity.name == "Charlie") { "Expecting : Charlie, Actual : ${output.node.legalIdentity.name}" }
require(output.node.legalIdentity.name == CHARLIE.name) { "Expecting : ${CHARLIE.name}, Actual : ${output.node.legalIdentity.name}" }
}
)
}
@ -108,12 +124,12 @@ class NodeMonitorModelTest : DriverBasedTest() {
sequence(
// SNAPSHOT
expect { output: Vault.Update ->
require(output.consumed.size == 0) { output.consumed.size }
require(output.produced.size == 0) { output.produced.size }
require(output.consumed.isEmpty()) { output.consumed.size }
require(output.produced.isEmpty()) { output.produced.size }
},
// ISSUE
expect { output: Vault.Update ->
require(output.consumed.size == 0) { output.consumed.size }
require(output.consumed.isEmpty()) { output.consumed.size }
require(output.produced.size == 1) { output.produced.size }
}
)
@ -123,7 +139,7 @@ class NodeMonitorModelTest : DriverBasedTest() {
@Test
fun `cash issue and move`() {
rpc.startFlow(::CashIssueFlow, 100.DOLLARS, OpaqueBytes.of(1), aliceNode.legalIdentity, notaryNode.notaryIdentity).returnValue.getOrThrow()
rpc.startFlow(::CashPaymentFlow, 100.DOLLARS, aliceNode.legalIdentity).returnValue.getOrThrow()
rpc.startFlow(::CashPaymentFlow, 100.DOLLARS, bobNode.legalIdentity).returnValue.getOrThrow()
var issueSmId: StateMachineRunId? = null
var moveSmId: StateMachineRunId? = null
@ -134,6 +150,8 @@ class NodeMonitorModelTest : DriverBasedTest() {
// ISSUE
expect { add: StateMachineUpdate.Added ->
issueSmId = add.id
val initiator = add.stateMachineInfo.initiator
require(initiator is FlowInitiator.RPC && initiator.username == "user1")
},
expect { remove: StateMachineUpdate.Removed ->
require(remove.id == issueSmId)
@ -141,6 +159,8 @@ class NodeMonitorModelTest : DriverBasedTest() {
// MOVE
expect { add: StateMachineUpdate.Added ->
moveSmId = add.id
val initiator = add.stateMachineInfo.initiator
require(initiator is FlowInitiator.RPC && initiator.username == "user1")
},
expect { remove: StateMachineUpdate.Removed ->
require(remove.id == moveSmId)
@ -148,28 +168,38 @@ class NodeMonitorModelTest : DriverBasedTest() {
)
}
stateMachineUpdatesBob.expectEvents {
sequence(
// MOVE
expect { add: StateMachineUpdate.Added ->
val initiator = add.stateMachineInfo.initiator
require(initiator is FlowInitiator.Peer && initiator.party.name == aliceNode.legalIdentity.name)
}
)
}
transactions.expectEvents {
sequence(
// ISSUE
expect { tx ->
require(tx.tx.inputs.isEmpty())
require(tx.tx.outputs.size == 1)
val signaturePubKeys = tx.sigs.map { it.by }.toSet()
expect { stx ->
require(stx.tx.inputs.isEmpty())
require(stx.tx.outputs.size == 1)
val signaturePubKeys = stx.sigs.map { it.by }.toSet()
// Only Alice signed
val aliceKey = aliceNode.legalIdentity.owningKey
require(signaturePubKeys.size <= aliceKey.keys.size)
require(aliceKey.isFulfilledBy(signaturePubKeys))
issueTx = tx
issueTx = stx
},
// MOVE
expect { tx ->
require(tx.tx.inputs.size == 1)
require(tx.tx.outputs.size == 1)
val signaturePubKeys = tx.sigs.map { it.by }.toSet()
expect { stx ->
require(stx.tx.inputs.size == 1)
require(stx.tx.outputs.size == 1)
val signaturePubKeys = stx.sigs.map { it.by }.toSet()
// Alice and Notary signed
require(aliceNode.legalIdentity.owningKey.isFulfilledBy(signaturePubKeys))
require(notaryNode.notaryIdentity.owningKey.isFulfilledBy(signaturePubKeys))
moveTx = tx
moveTx = stx
}
)
}
@ -178,18 +208,18 @@ class NodeMonitorModelTest : DriverBasedTest() {
sequence(
// SNAPSHOT
expect { output: Vault.Update ->
require(output.consumed.size == 0) { output.consumed.size }
require(output.produced.size == 0) { output.produced.size }
require(output.consumed.isEmpty()) { output.consumed.size }
require(output.produced.isEmpty()) { output.produced.size }
},
// ISSUE
expect { update ->
require(update.consumed.size == 0) { update.consumed.size }
require(update.consumed.isEmpty()) { update.consumed.size }
require(update.produced.size == 1) { update.produced.size }
},
// MOVE
expect { update ->
require(update.consumed.size == 1) { update.consumed.size }
require(update.produced.size == 1) { update.produced.size }
require(update.produced.isEmpty()) { update.produced.size }
}
)
}

View File

@ -2,7 +2,6 @@ package net.corda.client.jfx.model
import javafx.collections.FXCollections
import javafx.collections.ObservableList
import kotlinx.support.jdk8.collections.removeIf
import net.corda.client.jfx.utils.fold
import net.corda.client.jfx.utils.map
import net.corda.contracts.asset.Cash
@ -28,7 +27,7 @@ class ContractStateModel {
private val cashStatesDiff: Observable<Diff<Cash.State>> = contractStatesDiff.map {
Diff(it.added.filterCashStateAndRefs(), it.removed.filterCashStateAndRefs())
}
val cashStates: ObservableList<StateAndRef<Cash.State>> = cashStatesDiff.fold(FXCollections.observableArrayList()) { list, statesDiff ->
val cashStates: ObservableList<StateAndRef<Cash.State>> = cashStatesDiff.fold(FXCollections.observableArrayList()) { list: MutableList<StateAndRef<Cash.State>>, statesDiff ->
list.removeIf { it in statesDiff.removed }
list.addAll(statesDiff.added)
}

View File

@ -3,12 +3,11 @@ package net.corda.client.jfx.model
import javafx.beans.value.ObservableValue
import javafx.collections.FXCollections
import javafx.collections.ObservableList
import kotlinx.support.jdk8.collections.removeIf
import net.corda.client.jfx.utils.firstOrDefault
import net.corda.client.jfx.utils.firstOrNullObservable
import net.corda.client.jfx.utils.fold
import net.corda.client.jfx.utils.map
import net.corda.core.crypto.CompositeKey
import net.corda.core.crypto.keys
import net.corda.core.node.NodeInfo
import net.corda.core.node.services.NetworkMapCache.MapChange
import java.security.PublicKey
@ -40,10 +39,6 @@ class NetworkIdentityModel {
}
// TODO: Use Identity Service in service hub instead?
fun lookup(compositeKey: CompositeKey): ObservableValue<NodeInfo?> = parties.firstOrDefault(notaries.firstOrNullObservable { it.notaryIdentity.owningKey == compositeKey }) {
it.legalIdentity.owningKey == compositeKey
}
fun lookup(publicKey: PublicKey): ObservableValue<NodeInfo?> = parties.firstOrDefault(notaries.firstOrNullObservable { it.notaryIdentity.owningKey.keys.any { it == publicKey } }) {
it.legalIdentity.owningKey.keys.any { it == publicKey }
}

View File

@ -3,6 +3,7 @@ package net.corda.client.jfx.model
import com.google.common.net.HostAndPort
import javafx.beans.property.SimpleObjectProperty
import net.corda.client.rpc.CordaRPCClient
import net.corda.client.rpc.CordaRPCClientConfiguration
import net.corda.core.flows.StateMachineRunId
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.StateMachineInfo
@ -52,11 +53,14 @@ class NodeMonitorModel {
* TODO provide an unsubscribe mechanism
*/
fun register(nodeHostAndPort: HostAndPort, username: String, password: String) {
val client = CordaRPCClient(nodeHostAndPort){
maxRetryInterval = 10.seconds.toMillis()
}
client.start(username, password)
val proxy = client.proxy()
val client = CordaRPCClient(
hostAndPort = nodeHostAndPort,
configuration = CordaRPCClientConfiguration.default.copy(
connectionMaxRetryInterval = 10.seconds
)
)
val connection = client.start(username, password)
val proxy = connection.proxy
val (stateMachines, stateMachineUpdates) = proxy.stateMachinesAndUpdates()
// Extract the flow tracking stream

View File

@ -30,9 +30,13 @@ data class PartiallyResolvedTransaction(
val inputs: List<ObservableValue<InputResolution>>) {
val id = transaction.id
sealed class InputResolution(val stateRef: StateRef) {
class Unresolved(stateRef: StateRef) : InputResolution(stateRef)
class Resolved(val stateAndRef: StateAndRef<ContractState>) : InputResolution(stateAndRef.ref)
sealed class InputResolution {
abstract val stateRef: StateRef
data class Unresolved(override val stateRef: StateRef) : InputResolution()
data class Resolved(val stateAndRef: StateAndRef<ContractState>) : InputResolution() {
override val stateRef: StateRef get() = stateAndRef.ref
}
}
companion object {
@ -54,22 +58,13 @@ data class PartiallyResolvedTransaction(
}
}
sealed class TransactionCreateStatus(val message: String?) {
class Started(message: String?) : TransactionCreateStatus(message)
class Failed(message: String?) : TransactionCreateStatus(message)
data class FlowStatus(val status: String)
override fun toString(): String = message ?: javaClass.simpleName
}
sealed class StateMachineStatus {
abstract val stateMachineName: String
data class FlowStatus(
val status: String
)
sealed class StateMachineStatus(val stateMachineName: String) {
class Added(stateMachineName: String) : StateMachineStatus(stateMachineName)
class Removed(stateMachineName: String) : StateMachineStatus(stateMachineName)
override fun toString(): String = "${javaClass.simpleName}($stateMachineName)"
data class Added(override val stateMachineName: String) : StateMachineStatus()
data class Removed(override val stateMachineName: String) : StateMachineStatus()
}
data class StateMachineData(

View File

@ -4,7 +4,6 @@ import javafx.collections.FXCollections
import javafx.collections.ListChangeListener
import javafx.collections.ObservableList
import javafx.collections.transformation.TransformationList
import kotlin.comparisons.compareValues
/**
* Given an [ObservableList]<[E]> and a grouping key [K], [AggregatedList] groups the elements by the key into a fresh

View File

@ -3,7 +3,6 @@ package net.corda.client.jfx.utils
import javafx.beans.binding.Bindings
import javafx.beans.value.ObservableValue
import javafx.collections.ObservableList
import kotlinx.support.jdk8.collections.stream
import net.corda.client.jfx.model.ExchangeRate
import net.corda.core.contracts.Amount
import org.fxmisc.easybind.EasyBind
@ -14,7 +13,7 @@ import java.util.stream.Collectors
* Utility bindings for the [Amount] type, similar in spirit to [Bindings]
*/
object AmountBindings {
fun <T> sum(amounts: ObservableList<Amount<T>>, token: T) = EasyBind.map(
fun <T : Any> sum(amounts: ObservableList<Amount<T>>, token: T) = EasyBind.map(
Bindings.createLongBinding({
amounts.stream().collect(Collectors.summingLong {
require(it.token == token)

View File

@ -31,7 +31,7 @@ class ChosenList<E>(
}
init {
chosenListObservable.addListener { observable: Observable -> rechoose() }
chosenListObservable.addListener { _: Observable -> rechoose() }
currentList.addListener(listener)
beginChange()
nextAdd(0, currentList.size)

View File

@ -5,7 +5,6 @@ import javafx.collections.ListChangeListener
import javafx.collections.ObservableList
import javafx.collections.transformation.TransformationList
import java.util.*
import kotlin.comparisons.compareValues
/**
* [ConcatenatedList] takes a list of lists and concatenates them. Any change to the underlying lists or the outer list

View File

@ -38,7 +38,7 @@ class FlattenedList<A>(val sourceList: ObservableList<out ObservableValue<out A>
}
private fun createListener(wrapped: WrappedObservableValue<out A>): ChangeListener<A> {
val listener = ChangeListener<A> { _observableValue, oldValue, newValue ->
val listener = ChangeListener<A> { _, oldValue, _ ->
val currentIndex = indexMap[wrapped]!!.first
beginChange()
nextReplace(currentIndex, currentIndex + 1, listOf(oldValue))
@ -55,7 +55,7 @@ class FlattenedList<A>(val sourceList: ObservableList<out ObservableValue<out A>
val from = c.from
val to = c.to
val permutation = IntArray(to, { c.getPermutation(it) })
indexMap.replaceAll { _observableValue, pair -> Pair(permutation[pair.first], pair.second) }
indexMap.replaceAll { _, pair -> Pair(permutation[pair.first], pair.second) }
nextPermutation(from, to, permutation)
} else if (c.wasUpdated()) {
throw UnsupportedOperationException("FlattenedList doesn't support Update changes")

View File

@ -4,7 +4,6 @@ import javafx.collections.FXCollections
import javafx.collections.MapChangeListener
import javafx.collections.ObservableList
import javafx.collections.ObservableMap
import kotlin.comparisons.compareValues
/**
* [MapValuesList] takes an [ObservableMap] and returns its values as an [ObservableList].

View File

@ -67,7 +67,7 @@ fun <A> Observable<A>.recordInSequence(): ObservableList<A> {
* @param toKey Function retrieving the key to associate with.
* @param merge The function to be called if there is an existing element at the key.
*/
fun <A, K> Observable<A>.recordAsAssociation(toKey: (A) -> K, merge: (K, oldValue: A, newValue: A) -> A = { _key, _oldValue, newValue -> newValue }): ObservableMap<K, A> {
fun <A, K> Observable<A>.recordAsAssociation(toKey: (A) -> K, merge: (K, oldValue: A, newValue: A) -> A = { _, _, newValue -> newValue }): ObservableMap<K, A> {
return fold(FXCollections.observableHashMap<K, A>()) { map, item ->
val key = toKey(item)
map[key] = map[key]?.let { merge(key, it, item) } ?: item

View File

@ -111,8 +111,14 @@ fun <A> ObservableList<out A>.filter(predicate: ObservableValue<(A) -> Boolean>)
* val owners: ObservableList<Person> = dogs.map(Dog::owner).filterNotNull()
*/
fun <A> ObservableList<out A?>.filterNotNull(): ObservableList<A> {
//TODO This is a tactical work round for an issue with SAM conversion (https://youtrack.jetbrains.com/issue/ALL-1552) so that the M10 explorer works.
@Suppress("UNCHECKED_CAST")
return filtered { it != null } as ObservableList<A>
return (this as ObservableList<A?>).filtered(object : Predicate<A?> {
override fun test(t: A?): Boolean {
return t != null
}
}) as ObservableList<A>
}
/**
@ -158,7 +164,7 @@ fun <K, A, B> ObservableList<out A>.associateBy(toKey: (A) -> K, assemble: (K, A
* val nameToPerson: ObservableMap<String, Person> = people.associateBy(Person::name)
*/
fun <K, A> ObservableList<out A>.associateBy(toKey: (A) -> K): ObservableMap<K, A> {
return associateBy(toKey) { key, value -> value }
return associateBy(toKey) { _, value -> value }
}
/**
@ -176,7 +182,7 @@ fun <K : Any, A : Any, B> ObservableList<out A>.associateByAggregation(toKey: (A
* val heightToPeople: ObservableMap<Long, ObservableList<Person>> = people.associateByAggregation(Person::height)
*/
fun <K : Any, A : Any> ObservableList<out A>.associateByAggregation(toKey: (A) -> K): ObservableMap<K, ObservableList<A>> {
return associateByAggregation(toKey) { key, value -> value }
return associateByAggregation(toKey) { _, value -> value }
}
/**
@ -260,7 +266,7 @@ fun <A : Any, B : Any, K : Any> ObservableList<A>.leftOuterJoin(
val leftTableMap = associateByAggregation(leftToJoinKey)
val rightTableMap = rightTable.associateByAggregation(rightToJoinKey)
val joinedMap: ObservableMap<K, Pair<ObservableList<A>, ObservableList<B>>> =
LeftOuterJoinedMap(leftTableMap, rightTableMap) { _key, left, rightValue ->
LeftOuterJoinedMap(leftTableMap, rightTableMap) { _, left, rightValue ->
Pair(left, ChosenList(rightValue.map { it ?: FXCollections.emptyObservableList() }))
}
return joinedMap
@ -285,7 +291,7 @@ fun <A> ObservableList<A>.last(): ObservableValue<A?> {
}
fun <T : Any> ObservableList<T>.unique(): ObservableList<T> {
return AggregatedList(this, { it }, { key, _list -> key })
return AggregatedList(this, { it }, { key, _ -> key })
}
fun ObservableValue<*>.isNotNull(): BooleanBinding {

View File

@ -16,7 +16,7 @@ class AssociatedListTest {
@Before
fun setup() {
sourceList = FXCollections.observableArrayList(0)
associatedList = AssociatedList(sourceList, { it % 3 }) { mod3, number -> number }
associatedList = AssociatedList(sourceList, { it % 3 }) { _, number -> number }
replayedMap = ReplayedMap(associatedList)
}

View File

@ -4,32 +4,12 @@ apply plugin: 'net.corda.plugins.publish-utils'
description 'Corda client mock modules'
repositories {
mavenLocal()
mavenCentral()
maven {
url 'http://oss.sonatype.org/content/repositories/snapshots'
}
jcenter()
maven {
url 'https://dl.bintray.com/kotlin/exposed'
}
}
//noinspection GroovyAssignabilityCheck
configurations {
// we don't want isolated.jar in classPath, since we want to test jar being dynamically loaded as an attachment
runtime.exclude module: 'isolated'
}
sourceSets {
test {
resources {
srcDir "../../config/test"
}
}
}
// To find potential version conflicts, run "gradle htmlDependencyReport" and then look in
// build/reports/project/dependencies/index.html for green highlighted parts of the tree.

View File

@ -1,10 +1,8 @@
package net.corda.client.mock
import net.corda.contracts.asset.Cash
import net.corda.core.contracts.*
import net.corda.core.crypto.Party
import net.corda.core.contracts.Amount
import net.corda.core.identity.Party
import net.corda.core.serialization.OpaqueBytes
import net.corda.core.transactions.TransactionBuilder
import net.corda.flows.CashFlowCommand
import java.util.*
@ -12,91 +10,27 @@ import java.util.*
* [Generator]s for incoming/outgoing events to/from the [WalletMonitorService]. Internally it keeps track of owned
* state/ref pairs, but it doesn't necessarily generate "correct" events!
*/
class EventGenerator(
val parties: List<Party>,
val notary: Party,
val currencies: List<Currency> = listOf(USD, GBP, CHF),
val issuers: List<Party> = parties
) {
private var vault = listOf<StateAndRef<Cash.State>>()
val issuerGenerator =
Generator.pickOne(issuers).combine(Generator.intRange(0, 1)) { party, ref -> party.ref(ref.toByte()) }
class EventGenerator(val parties: List<Party>, val currencies: List<Currency>, val notary: Party) {
private val partyGenerator = Generator.pickOne(parties)
private val issueRefGenerator = Generator.intRange(0, 1).map { number -> OpaqueBytes(ByteArray(1, { number.toByte() })) }
private val amountGenerator = Generator.longRange(10000, 1000000)
private val currencyGenerator = Generator.pickOne(currencies)
val currencyGenerator = Generator.pickOne(currencies)
val issuedGenerator = issuerGenerator.combine(currencyGenerator) { issuer, currency -> Issued(issuer, currency) }
val amountIssuedGenerator = generateAmount(1, 10000, issuedGenerator)
val publicKeyGenerator = Generator.pickOne(parties.map { it.owningKey })
val partyGenerator = Generator.pickOne(parties)
val cashStateGenerator = amountIssuedGenerator.combine(publicKeyGenerator) { amount, from ->
val builder = TransactionBuilder(notary = notary)
builder.addOutputState(Cash.State(amount, from))
builder.addCommand(Command(Cash.Commands.Issue(), amount.token.issuer.party.owningKey))
builder.toWireTransaction().outRef<Cash.State>(0)
private val issueCashGenerator = amountGenerator.combine(partyGenerator, issueRefGenerator, currencyGenerator) { amount, to, issueRef, ccy ->
CashFlowCommand.IssueCash(Amount(amount, ccy), issueRef, to, notary)
}
val consumedGenerator: Generator<Set<StateRef>> = Generator.frequency(
0.7 to Generator.pure(setOf()),
0.3 to Generator.impure { vault }.bind { states ->
Generator.sampleBernoulli(states, 0.2).map { someStates ->
val consumedSet = someStates.map { it.ref }.toSet()
vault = vault.filter { it.ref !in consumedSet }
consumedSet
}
}
)
val producedGenerator: Generator<Set<StateAndRef<ContractState>>> = Generator.frequency(
// 0.1 to Generator.pure(setOf())
0.9 to Generator.impure { vault }.bind { states ->
Generator.replicate(2, cashStateGenerator).map {
vault = states + it
it.toSet()
}
}
)
private val exitCashGenerator = amountGenerator.combine(issueRefGenerator, currencyGenerator) { amount, issueRef, ccy ->
CashFlowCommand.ExitCash(Amount(amount, ccy), issueRef)
}
val issueRefGenerator = Generator.intRange(0, 1).map { number -> OpaqueBytes(ByteArray(1, { number.toByte() })) }
val moveCashGenerator = amountGenerator.combine(partyGenerator, currencyGenerator) { amountIssued, recipient, currency ->
CashFlowCommand.PayCash(Amount(amountIssued, currency), recipient)
}
val amountGenerator = Generator.intRange(0, 10000).combine(currencyGenerator) { quantity, currency -> Amount(quantity.toLong(), currency) }
val issueCashGenerator =
amountGenerator.combine(partyGenerator, issueRefGenerator) { amount, to, issueRef ->
CashFlowCommand.IssueCash(
amount,
issueRef,
to,
notary
)
}
val moveCashGenerator =
amountIssuedGenerator.combine(partyGenerator) { amountIssued, recipient ->
CashFlowCommand.PayCash(
amount = amountIssued.withoutIssuer(),
recipient = recipient
)
}
val exitCashGenerator =
amountIssuedGenerator.map {
CashFlowCommand.ExitCash(
it.withoutIssuer(),
it.token.issuer.reference
)
}
val clientCommandGenerator = Generator.frequency(
1.0 to moveCashGenerator
)
val bankOfCordaExitGenerator = Generator.frequency(
0.4 to exitCashGenerator
)
val bankOfCordaIssueGenerator = Generator.frequency(
0.6 to issueCashGenerator
)
val issuerGenerator = Generator.frequency(listOf(
0.1 to exitCashGenerator,
0.9 to issueCashGenerator
))
}

View File

@ -144,6 +144,23 @@ fun Generator.Companion.doubleRange(from: Double, to: Double): Generator<Double>
from + it.nextDouble() * (to - from)
}
fun Generator.Companion.char() = Generator {
val codePoint = Math.abs(it.nextInt()) % (17 * (1 shl 16))
if (Character.isValidCodePoint(codePoint)) {
return@Generator ErrorOr(codePoint.toChar())
} else {
ErrorOr.of(IllegalStateException("Could not generate valid codepoint"))
}
}
fun Generator.Companion.string(meanSize: Double = 16.0) = replicatePoisson(meanSize, char()).map {
val builder = StringBuilder()
it.forEach {
builder.append(it)
}
builder.toString()
}
fun <A> Generator.Companion.replicate(number: Int, generator: Generator<A>): Generator<List<A>> {
val generators = mutableListOf<Generator<A>>()
for (i in 1..number) {

View File

@ -4,18 +4,6 @@ apply plugin: 'net.corda.plugins.publish-utils'
description 'Corda client RPC modules'
repositories {
mavenLocal()
mavenCentral()
maven {
url 'http://oss.sonatype.org/content/repositories/snapshots'
}
jcenter()
maven {
url 'https://dl.bintray.com/kotlin/exposed'
}
}
//noinspection GroovyAssignabilityCheck
configurations {
// we don't want isolated.jar in classPath, since we want to test jar being dynamically loaded as an attachment
@ -33,11 +21,6 @@ sourceSets {
srcDir file('src/integration-test/kotlin')
}
}
test {
resources {
srcDir "../../config/test"
}
}
}
// To find potential version conflicts, run "gradle htmlDependencyReport" and then look in
@ -53,6 +36,7 @@ dependencies {
testCompile "org.assertj:assertj-core:${assertj_version}"
testCompile project(':test-utils')
testCompile project(':client:mock')
// Integration test helpers
integrationTestCompile "junit:junit:$junit_version"

View File

@ -0,0 +1,154 @@
package net.corda.client.rpc
import net.corda.core.contracts.DOLLARS
import net.corda.core.flows.FlowInitiator
import net.corda.core.getOrThrow
import net.corda.core.messaging.*
import net.corda.core.node.services.ServiceInfo
import net.corda.core.random63BitValue
import net.corda.core.serialization.OpaqueBytes
import net.corda.core.utilities.ALICE
import net.corda.flows.CashException
import net.corda.flows.CashIssueFlow
import net.corda.flows.CashPaymentFlow
import net.corda.node.internal.Node
import net.corda.node.services.startFlowPermission
import net.corda.node.services.transactions.ValidatingNotaryService
import net.corda.nodeapi.User
import net.corda.testing.node.NodeBasedTest
import org.apache.activemq.artemis.api.core.ActiveMQSecurityException
import org.assertj.core.api.Assertions.assertThatExceptionOfType
import org.junit.After
import org.junit.Before
import org.junit.Test
import java.util.*
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class CordaRPCClientTest : NodeBasedTest() {
private val rpcUser = User("user1", "test", permissions = setOf(
startFlowPermission<CashIssueFlow>(),
startFlowPermission<CashPaymentFlow>()
))
private lateinit var node: Node
private lateinit var client: CordaRPCClient
private var connection: CordaRPCConnection? = null
private fun login(username: String, password: String) {
connection = client.start(username, password)
}
@Before
fun setUp() {
node = startNode(ALICE.name, rpcUsers = listOf(rpcUser), advertisedServices = setOf(ServiceInfo(ValidatingNotaryService.type))).getOrThrow()
client = CordaRPCClient(node.configuration.rpcAddress!!)
}
@After
fun done() {
connection?.close()
}
@Test
fun `log in with valid username and password`() {
login(rpcUser.username, rpcUser.password)
}
@Test
fun `log in with unknown user`() {
assertThatExceptionOfType(ActiveMQSecurityException::class.java).isThrownBy {
login(random63BitValue().toString(), rpcUser.password)
}
}
@Test
fun `log in with incorrect password`() {
assertThatExceptionOfType(ActiveMQSecurityException::class.java).isThrownBy {
login(rpcUser.username, random63BitValue().toString())
}
}
@Test
fun `close-send deadlock and premature shutdown on empty observable`() {
println("Starting client")
login(rpcUser.username, rpcUser.password)
println("Creating proxy")
println("Starting flow")
val flowHandle = connection!!.proxy.startTrackedFlow(
::CashIssueFlow,
20.DOLLARS, OpaqueBytes.of(0), node.info.legalIdentity, node.info.legalIdentity)
println("Started flow, waiting on result")
flowHandle.progress.subscribe {
println("PROGRESS $it")
}
println("Result: ${flowHandle.returnValue.getOrThrow()}")
}
@Test
fun `sub-type of FlowException thrown by flow`() {
login(rpcUser.username, rpcUser.password)
val handle = connection!!.proxy.startFlow(::CashPaymentFlow, 100.DOLLARS, node.info.legalIdentity)
assertThatExceptionOfType(CashException::class.java).isThrownBy {
handle.returnValue.getOrThrow()
}
}
@Test
fun `check basic flow has no progress`() {
login(rpcUser.username, rpcUser.password)
connection!!.proxy.startFlow(::CashPaymentFlow, 100.DOLLARS, node.info.legalIdentity).use {
assertFalse(it is FlowProgressHandle<*>)
assertTrue(it is FlowHandle<*>)
}
}
@Test
fun `get cash balances`() {
login(rpcUser.username, rpcUser.password)
val proxy = connection!!.proxy
val startCash = proxy.getCashBalances()
assertTrue(startCash.isEmpty(), "Should not start with any cash")
val flowHandle = proxy.startFlow(::CashIssueFlow,
123.DOLLARS, OpaqueBytes.of(0),
node.info.legalIdentity, node.info.legalIdentity
)
println("Started issuing cash, waiting on result")
flowHandle.returnValue.get()
val finishCash = proxy.getCashBalances()
println("Cash Balances: $finishCash")
assertEquals(1, finishCash.size)
assertEquals(123.DOLLARS, finishCash.get(Currency.getInstance("USD")))
}
@Test
fun `flow initiator via RPC`() {
login(rpcUser.username, rpcUser.password)
val proxy = connection!!.proxy
val smUpdates = proxy.stateMachinesAndUpdates()
var countRpcFlows = 0
var countShellFlows = 0
smUpdates.second.subscribe {
if (it is StateMachineUpdate.Added) {
val initiator = it.stateMachineInfo.initiator
if (initiator is FlowInitiator.RPC)
countRpcFlows++
if (initiator is FlowInitiator.Shell)
countShellFlows++
}
}
val nodeIdentity = node.info.legalIdentity
node.services.startFlow(CashIssueFlow(2000.DOLLARS, OpaqueBytes.of(0), nodeIdentity, nodeIdentity), FlowInitiator.Shell).resultFuture.getOrThrow()
proxy.startFlow(::CashIssueFlow,
123.DOLLARS, OpaqueBytes.of(0),
nodeIdentity, nodeIdentity
).returnValue.getOrThrow()
proxy.startFlowDynamic(CashIssueFlow::class.java,
1000.DOLLARS, OpaqueBytes.of(0),
nodeIdentity, nodeIdentity).returnValue.getOrThrow()
assertEquals(2, countRpcFlows)
assertEquals(1, countShellFlows)
}
}

View File

@ -0,0 +1,169 @@
package net.corda.client.rpc
import com.esotericsoftware.kryo.Kryo
import com.esotericsoftware.kryo.Serializer
import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output
import com.esotericsoftware.kryo.pool.KryoPool
import com.google.common.util.concurrent.Futures
import net.corda.core.messaging.RPCOps
import net.corda.core.millis
import net.corda.core.random63BitValue
import net.corda.node.services.messaging.RPCServerConfiguration
import net.corda.nodeapi.RPCApi
import net.corda.nodeapi.RPCKryo
import net.corda.testing.*
import org.apache.activemq.artemis.api.core.SimpleString
import org.junit.Test
import rx.Observable
import rx.subjects.PublishSubject
import rx.subjects.UnicastSubject
import java.time.Duration
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
class RPCStabilityTests {
interface LeakObservableOps: RPCOps {
fun leakObservable(): Observable<Nothing>
}
@Test
fun `client cleans up leaked observables`() {
rpcDriver {
val leakObservableOpsImpl = object : LeakObservableOps {
val leakedUnsubscribedCount = AtomicInteger(0)
override val protocolVersion = 0
override fun leakObservable(): Observable<Nothing> {
return PublishSubject.create<Nothing>().doOnUnsubscribe {
leakedUnsubscribedCount.incrementAndGet()
}
}
}
val server = startRpcServer<LeakObservableOps>(ops = leakObservableOpsImpl)
val proxy = startRpcClient<LeakObservableOps>(server.get().hostAndPort).get()
// Leak many observables
val N = 200
(1..N).toList().parallelStream().forEach {
proxy.leakObservable()
}
// In a loop force GC and check whether the server is notified
while (true) {
System.gc()
if (leakObservableOpsImpl.leakedUnsubscribedCount.get() == N) break
Thread.sleep(100)
}
}
}
interface TrackSubscriberOps : RPCOps {
fun subscribe(): Observable<Unit>
}
/**
* In this test we create a number of out of process RPC clients that call [TrackSubscriberOps.subscribe] in a loop.
*/
@Test
fun `server cleans up queues after disconnected clients`() {
rpcDriver {
val trackSubscriberOpsImpl = object : TrackSubscriberOps {
override val protocolVersion = 0
val subscriberCount = AtomicInteger(0)
val trackSubscriberCountObservable = UnicastSubject.create<Unit>().share().
doOnSubscribe { subscriberCount.incrementAndGet() }.
doOnUnsubscribe { subscriberCount.decrementAndGet() }
override fun subscribe(): Observable<Unit> {
return trackSubscriberCountObservable
}
}
val server = startRpcServer<TrackSubscriberOps>(
configuration = RPCServerConfiguration.default.copy(
reapInterval = 100.millis
),
ops = trackSubscriberOpsImpl
).get()
val numberOfClients = 4
val clients = Futures.allAsList((1 .. numberOfClients).map {
startRandomRpcClient<TrackSubscriberOps>(server.hostAndPort)
}).get()
// Poll until all clients connect
pollUntilClientNumber(server, numberOfClients)
pollUntilTrue("number of times subscribe() has been called") { trackSubscriberOpsImpl.subscriberCount.get() >= 100 }.get()
// Kill one client
clients[0].destroyForcibly()
pollUntilClientNumber(server, numberOfClients - 1)
// Kill the rest
(1 .. numberOfClients - 1).forEach {
clients[it].destroyForcibly()
}
pollUntilClientNumber(server, 0)
// Now poll until the server detects the disconnects and unsubscribes from all obserables.
pollUntilTrue("number of times subscribe() has been called") { trackSubscriberOpsImpl.subscriberCount.get() == 0 }.get()
}
}
interface SlowConsumerRPCOps : RPCOps {
fun streamAtInterval(interval: Duration, size: Int): Observable<ByteArray>
}
class SlowConsumerRPCOpsImpl : SlowConsumerRPCOps {
override val protocolVersion = 0
override fun streamAtInterval(interval: Duration, size: Int): Observable<ByteArray> {
val chunk = ByteArray(size)
return Observable.interval(interval.toMillis(), TimeUnit.MILLISECONDS).map { chunk }
}
}
val dummyObservableSerialiser = object : Serializer<Observable<Any>>() {
override fun write(kryo: Kryo?, output: Output?, `object`: Observable<Any>?) {
}
override fun read(kryo: Kryo?, input: Input?, type: Class<Observable<Any>>?): Observable<Any> {
return Observable.empty()
}
}
@Test
fun `slow consumers are kicked`() {
val kryoPool = KryoPool.Builder { RPCKryo(dummyObservableSerialiser) }.build()
rpcDriver {
val server = startRpcServer(maxBufferedBytesPerClient = 10 * 1024 * 1024, ops = SlowConsumerRPCOpsImpl()).get()
// Construct an RPC session manually so that we can hang in the message handler
val myQueue = "${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.test.${random63BitValue()}"
val session = startArtemisSession(server.hostAndPort)
session.createTemporaryQueue(myQueue, myQueue)
val consumer = session.createConsumer(myQueue, null, -1, -1, false)
consumer.setMessageHandler {
Thread.sleep(50) // 5x slower than the server producer
it.acknowledge()
}
val producer = session.createProducer(RPCApi.RPC_SERVER_QUEUE_NAME)
session.start()
pollUntilClientNumber(server, 1)
val message = session.createMessage(false)
val request = RPCApi.ClientToServer.RpcRequest(
clientAddress = SimpleString(myQueue),
id = RPCApi.RpcRequestId(random63BitValue()),
methodName = SlowConsumerRPCOps::streamAtInterval.name,
arguments = listOf(10.millis, 123456)
)
request.writeToClientMessage(kryoPool, message)
producer.send(message)
session.commit()
// We are consuming slower than the server is producing, so we should be kicked after a while
pollUntilClientNumber(server, 0)
}
}
}
fun RPCDriverExposedDSLInterface.pollUntilClientNumber(server: RpcServerHandle, expected: Int) {
pollUntilTrue("number of RPC clients to become $expected") {
val clientAddresses = server.serverControl.addressNames.filter { it.startsWith(RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX) }
clientAddresses.size == expected
}.get()
}

View File

@ -1,164 +1,49 @@
package net.corda.client.rpc
import com.google.common.net.HostAndPort
import net.corda.nodeapi.config.SSLConfiguration
import net.corda.core.ThreadBox
import net.corda.core.logElapsedTime
import net.corda.client.rpc.internal.RPCClient
import net.corda.client.rpc.internal.RPCClientConfiguration
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.minutes
import net.corda.core.seconds
import net.corda.core.utilities.loggerFor
import net.corda.nodeapi.ArtemisMessagingComponent
import net.corda.nodeapi.ArtemisTcpTransport.Companion.tcpTransport
import net.corda.nodeapi.ConnectionDirection
import net.corda.nodeapi.RPCException
import net.corda.nodeapi.rpcLog
import org.apache.activemq.artemis.api.core.ActiveMQException
import org.apache.activemq.artemis.api.core.client.ActiveMQClient
import org.apache.activemq.artemis.api.core.client.ClientSession
import org.apache.activemq.artemis.api.core.client.ClientSessionFactory
import org.apache.activemq.artemis.api.core.client.ServerLocator
import rx.Observable
import java.io.Closeable
import net.corda.nodeapi.config.SSLConfiguration
import java.time.Duration
import javax.annotation.concurrent.ThreadSafe
/**
* An RPC client connects to the specified server and allows you to make calls to the server that perform various
* useful tasks. See the documentation for [proxy] or review the docsite to learn more about how this API works.
*
* @param host The hostname and messaging port of the node.
* @param config If specified, the SSL configuration to use. If not specified, SSL will be disabled and the node will only be authenticated on non-SSL RPC port, the RPC traffic with not be encrypted when SSL is disabled.
*/
@ThreadSafe
class CordaRPCClient(val host: HostAndPort, override val config: SSLConfiguration? = null, val serviceConfigurationOverride: (ServerLocator.() -> Unit)? = null) : Closeable, ArtemisMessagingComponent() {
private companion object {
val log = loggerFor<CordaRPCClient>()
class CordaRPCConnection internal constructor(
connection: RPCClient.RPCConnection<CordaRPCOps>
) : RPCClient.RPCConnection<CordaRPCOps> by connection
data class CordaRPCClientConfiguration(
val connectionMaxRetryInterval: Duration
) {
internal fun toRpcClientConfiguration(): RPCClientConfiguration {
return RPCClientConfiguration.default.copy(
connectionMaxRetryInterval = connectionMaxRetryInterval
)
}
companion object {
@JvmStatic
val default = CordaRPCClientConfiguration(
connectionMaxRetryInterval = RPCClientConfiguration.default.connectionMaxRetryInterval
)
}
}
class CordaRPCClient(
hostAndPort: HostAndPort,
sslConfiguration: SSLConfiguration? = null,
configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.default
) {
private val rpcClient = RPCClient<CordaRPCOps>(
tcpTransport(ConnectionDirection.Outbound(), hostAndPort, sslConfiguration),
configuration.toRpcClientConfiguration()
)
fun start(username: String, password: String): CordaRPCConnection {
return CordaRPCConnection(rpcClient.start(CordaRPCOps::class.java, username, password))
}
// TODO: Certificate handling for clients needs more work.
private inner class State {
var running = false
lateinit var sessionFactory: ClientSessionFactory
lateinit var session: ClientSession
lateinit var clientImpl: CordaRPCClientImpl
}
private val state = ThreadBox(State())
/**
* Opens the connection to the server with the given username and password, then returns itself.
* Registers a JVM shutdown hook to cleanly disconnect.
*/
@Throws(ActiveMQException::class)
fun start(username: String, password: String): CordaRPCClient {
state.locked {
check(!running)
log.logElapsedTime("Startup") {
checkStorePasswords()
val serverLocator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport(ConnectionDirection.Outbound(), host, config, enableSSL = config != null)).apply {
// TODO: Put these in config file or make it user configurable?
threadPoolMaxSize = 1
confirmationWindowSize = 100000 // a guess
retryInterval = 5.seconds.toMillis()
retryIntervalMultiplier = 1.5 // Exponential backoff
maxRetryInterval = 3.minutes.toMillis()
serviceConfigurationOverride?.invoke(this)
}
sessionFactory = serverLocator.createSessionFactory()
session = sessionFactory.createSession(username, password, false, true, true, serverLocator.isPreAcknowledge, serverLocator.ackBatchSize)
session.start()
clientImpl = CordaRPCClientImpl(session, state.lock, username)
running = true
}
}
Runtime.getRuntime().addShutdownHook(Thread {
close()
})
return this
}
/**
* A convenience function that opens a connection with the given credentials, executes the given code block with all
* available RPCs in scope and shuts down the RPC connection again. It's meant for quick prototyping and demos. For
* more control you probably want to control the lifecycle of the client and proxies independently, as well as
* configuring a timeout and other such features via the [proxy] method.
*
* After this method returns the client is closed and can't be restarted.
*/
@Throws(ActiveMQException::class)
fun <T> use(username: String, password: String, block: CordaRPCOps.() -> T): T {
require(!state.locked { running })
start(username, password)
(this as Closeable).use {
return proxy().block()
}
}
/** Shuts down the client and lets the server know it can free the used resources (in a nice way). */
override fun close() {
state.locked {
if (!running) return
session.close()
sessionFactory.close()
running = false
}
}
/**
* Returns a fresh proxy that lets you invoke RPCs on the server. Calls on it block, and if the server throws an
* exception then it will be rethrown on the client. Proxies are thread safe but only one RPC can be in flight at
* once. If you'd like to perform multiple RPCs in parallel, use this function multiple times to get multiple
* proxies.
*
* Creation of a proxy is a somewhat expensive operation that involves calls to the server, so if you want to do
* calls from many threads at once you should cache one proxy per thread and reuse them. This function itself is
* thread safe though so requires no extra synchronisation.
*
* RPC sends and receives are logged on the net.corda.rpc logger.
*
* By default there are no timeouts on calls. This is deliberate, RPCs without timeouts can survive restarts,
* maintenance downtime and moves of the server. RPCs can survive temporary losses or changes in client connectivity,
* like switching between wifi networks. You can specify a timeout on the level of a proxy. If a call times
* out it will throw [RPCException.Deadline].
*
* The [CordaRPCOps] defines what client RPCs are available. If an RPC returns an [Observable] anywhere in the
* object graph returned then the server-side observable is transparently linked to a messaging queue, and that
* queue linked to another observable on the client side here. *You are expected to use it*. The server will begin
* buffering messages immediately that it will expect you to drain by subscribing to the returned observer. You can
* opt-out of this by simply casting the [Observable] to [Closeable] or [AutoCloseable] and then calling the close
* method on it. You don't have to explicitly close the observable if you actually subscribe to it: it will close
* itself and free up the server-side resources either when the client or JVM itself is shutdown, or when there are
* no more subscribers to it. Once all the subscribers to a returned observable are unsubscribed, the observable is
* closed and you can't then re-subscribe again: you'll have to re-request a fresh observable with another RPC.
*
* The proxy and linked observables consume some small amount of resources on the server. It's OK to just exit your
* process and let the server clean up, but in a long running process where you only need something for a short
* amount of time it is polite to cast the objects to [Closeable] or [AutoCloseable] and close it when you are done.
* Finalizers are in place to warn you if you lose a reference to an unclosed proxy or observable.
*
* @throws RPCException if the server version is too low or if the server isn't reachable within the given time.
*/
@JvmOverloads
@Throws(RPCException::class)
fun proxy(timeout: Duration? = null, minVersion: Int = 0): CordaRPCOps {
return state.locked {
check(running) { "Client must have been started first" }
log.logElapsedTime("Proxy build") {
clientImpl.proxyFor(CordaRPCOps::class.java, timeout, minVersion)
}
}
}
@Suppress("UNUSED")
private fun finalize() {
state.locked {
if (running) {
rpcLog.warn("A CordaMQClient is being finalised whilst still running, did you forget to call close?")
close()
}
}
inline fun <A> use(username: String, password: String, block: (CordaRPCConnection) -> A): A {
return start(username, password).use(block)
}
}

View File

@ -1,402 +0,0 @@
package net.corda.client.rpc
import com.esotericsoftware.kryo.Kryo
import com.esotericsoftware.kryo.KryoException
import com.esotericsoftware.kryo.Serializer
import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output
import com.esotericsoftware.kryo.pool.KryoPool
import com.google.common.cache.CacheBuilder
import net.corda.core.ErrorOr
import net.corda.core.bufferUntilSubscribed
import net.corda.core.messaging.RPCOps
import net.corda.core.messaging.RPCReturnsObservables
import net.corda.core.random63BitValue
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.corda.core.utilities.debug
import net.corda.nodeapi.*
import org.apache.activemq.artemis.api.core.ActiveMQObjectClosedException
import org.apache.activemq.artemis.api.core.Message.HDR_DUPLICATE_DETECTION_ID
import org.apache.activemq.artemis.api.core.SimpleString
import org.apache.activemq.artemis.api.core.client.ClientConsumer
import org.apache.activemq.artemis.api.core.client.ClientMessage
import org.apache.activemq.artemis.api.core.client.ClientProducer
import org.apache.activemq.artemis.api.core.client.ClientSession
import rx.Observable
import rx.subjects.PublishSubject
import java.io.Closeable
import java.lang.ref.WeakReference
import java.lang.reflect.InvocationHandler
import java.lang.reflect.Method
import java.lang.reflect.Proxy
import java.time.Duration
import java.util.*
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.ReentrantLock
import javax.annotation.concurrent.GuardedBy
import javax.annotation.concurrent.ThreadSafe
import kotlin.concurrent.withLock
import kotlin.reflect.jvm.javaMethod
/**
* Core RPC engine implementation, to learn how to use RPC you should be looking at [CordaRPCClient].
*
* # Design notes
*
* The way RPCs are handled is fairly standard except for the handling of observables. When an RPC might return
* an [Observable] it is specially tagged. This causes the client to create a new transient queue for the
* receiving of observables and their observations with a random ID in the name. This ID is sent to the server in
* a message header. All observations are sent via this single queue.
*
* The reason for doing it this way and not the more obvious approach of one-queue-per-observable is that we want
* the queues to be *transient*, meaning their lifetime in the broker is tied to the session that created them.
* A server side observable and its associated queue is not a cost-free thing, let alone the memory and resources
* needed to actually generate the observations themselves, therefore we want to ensure these cannot leak. A
* transient queue will be deleted automatically if the client session terminates, which by default happens on
* disconnect but can also be configured to happen after a short delay (this allows clients to e.g. switch IP
* address). On the server the deletion of the observations queue triggers unsubscription from the associated
* observables, which in turn may then be garbage collected.
*
* Creating a transient queue requires a roundtrip to the broker and thus doing an RPC that could return
* observables takes two server roundtrips instead of one. That's why we require RPCs to be marked with
* [RPCReturnsObservables] as needing this special treatment instead of always doing it.
*
* If the Artemis/JMS APIs allowed us to create transient queues assigned to someone else then we could
* potentially use a different design in which the node creates new transient queues (one per observable) on the
* fly. The client would then have to watch out for this and start consuming those queues as they were created.
*
* We use one queue per RPC because we don't know ahead of time how many observables the server might return and
* often the server doesn't know either, which pushes towards a single queue design, but at the same time the
* processing of observations returned by an RPC might be striped across multiple threads and we'd like
* backpressure management to not be scoped per client process but with more granularity. So we end up with
* a compromise where the unit of backpressure management is the response to a single RPC.
*
* TODO: Backpressure isn't propagated all the way through the MQ broker at the moment.
*/
class CordaRPCClientImpl(private val session: ClientSession,
private val sessionLock: ReentrantLock,
private val username: String) {
companion object {
private val closeableCloseMethod = Closeable::close.javaMethod
private val autocloseableCloseMethod = AutoCloseable::close.javaMethod
}
/**
* Builds a proxy for the given type, which must descend from [RPCOps].
*
* @see CordaRPCClient.proxy for more information about how to use the proxies.
*/
fun <T : RPCOps> proxyFor(rpcInterface: Class<T>, timeout: Duration? = null, minVersion: Int = 0): T {
sessionLock.withLock {
if (producer == null)
producer = session.createProducer()
}
val proxyImpl = RPCProxyHandler(timeout)
@Suppress("UNCHECKED_CAST")
val proxy = Proxy.newProxyInstance(rpcInterface.classLoader, arrayOf(rpcInterface, Closeable::class.java), proxyImpl) as T
proxyImpl.serverProtocolVersion = proxy.protocolVersion
if (minVersion > proxyImpl.serverProtocolVersion)
throw RPCException("Requested minimum protocol version $minVersion is higher than the server's supported protocol version (${proxyImpl.serverProtocolVersion})")
return proxy
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
//region RPC engine
//
// You can find docs on all this in the api doc for the proxyFor method, and in the docsite.
// Utility to quickly suck out the contents of an Artemis message. There's probably a more efficient way to
// do this.
private fun <T : Any> ClientMessage.deserialize(kryo: Kryo): T = ByteArray(bodySize).apply { bodyBuffer.readBytes(this) }.deserialize(kryo)
// We by default use a weak reference so GC can happen, otherwise they persist for the life of the client.
@GuardedBy("sessionLock")
private val addressToQueuedObservables = CacheBuilder.newBuilder().weakValues().build<String, QueuedObservable>()
// This is used to hold a reference counted hard reference when we know there are subscribers.
private val hardReferencesToQueuedObservables = Collections.synchronizedSet(mutableSetOf<QueuedObservable>())
private var producer: ClientProducer? = null
class ObservableDeserializer() : Serializer<Observable<Any>>() {
override fun read(kryo: Kryo, input: Input, type: Class<Observable<Any>>): Observable<Any> {
val qName = kryo.context[RPCKryoQNameKey] as String
val rpcName = kryo.context[RPCKryoMethodNameKey] as String
val rpcLocation = kryo.context[RPCKryoLocationKey] as Throwable
val rpcClient = kryo.context[RPCKryoClientKey] as CordaRPCClientImpl
val handle = input.readInt(true)
val ob = rpcClient.sessionLock.withLock {
rpcClient.addressToQueuedObservables.getIfPresent(qName) ?: rpcClient.QueuedObservable(qName, rpcName, rpcLocation).apply {
rpcClient.addressToQueuedObservables.put(qName, this)
}
}
val result = ob.getForHandle(handle)
rpcLog.debug { "Deserializing and connecting a new observable for $rpcName on $qName: $result" }
return result
}
override fun write(kryo: Kryo, output: Output, `object`: Observable<Any>) {
throw UnsupportedOperationException("not implemented")
}
}
/**
* The proxy class returned to the client is auto-generated on the fly by the java.lang.reflect Proxy
* infrastructure. The JDK Proxy class writes bytecode into memory for a class that implements the requested
* interfaces and then routes all method calls to the invoke method below in a conveniently reified form.
* We can then easily take the data about the method call and turn it into an RPC. This avoids the need
* for the compile-time code generation which is so common in RPC systems.
*/
@ThreadSafe
private inner class RPCProxyHandler(private val timeout: Duration?) : InvocationHandler, Closeable {
private val proxyId = random63BitValue()
private val consumer: ClientConsumer
var serverProtocolVersion = 0
init {
val proxyAddress = constructAddress(proxyId)
consumer = sessionLock.withLock {
session.createTemporaryQueue(proxyAddress, proxyAddress)
session.createConsumer(proxyAddress)
}
}
private fun constructAddress(addressId: Long) = "${ArtemisMessagingComponent.CLIENTS_PREFIX}$username.rpc.$addressId"
@Synchronized
override fun invoke(proxy: Any, method: Method, args: Array<out Any>?): Any? {
if (isCloseInvocation(method)) {
close()
return null
}
if (method.name == "toString" && args == null)
return "Client RPC proxy"
if (consumer.isClosed)
throw RPCException("RPC Proxy is closed")
// All invoked methods on the proxy end up here.
val location = Throwable()
rpcLog.debug {
val argStr = args?.joinToString() ?: ""
"-> RPC -> ${method.name}($argStr): ${method.returnType}"
}
checkMethodVersion(method)
val msg: ClientMessage = createMessage(method)
// We could of course also check the return type of the method to see if it's Observable, but I'd
// rather haved the annotation be used consistently.
val returnsObservables = method.isAnnotationPresent(RPCReturnsObservables::class.java)
val kryo = if (returnsObservables) maybePrepareForObservables(location, method, msg) else createRPCKryoForDeserialization(this@CordaRPCClientImpl)
val next: ErrorOr<*> = try {
sendRequest(args, msg)
receiveResponse(kryo, method, timeout)
} finally {
releaseRPCKryoForDeserialization(kryo)
}
rpcLog.debug { "<- RPC <- ${method.name} = $next" }
return unwrapOrThrow(next)
}
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
private fun unwrapOrThrow(next: ErrorOr<*>): Any? {
val ex = next.error
if (ex != null) {
// Replace the stack trace because that's an implementation detail of the server that isn't so
// helpful to the user who wants to see where the error was on their side, and serialising stack
// frame objects is a bit annoying. We slice it here to avoid the invoke() machinery being exposed.
// The resulting exception looks like it was thrown from inside the called method.
(ex as java.lang.Throwable).stackTrace = java.lang.Throwable().stackTrace.let { it.sliceArray(1..it.size - 1) }
throw ex
} else {
return next.value
}
}
private fun receiveResponse(kryo: Kryo, method: Method, timeout: Duration?): ErrorOr<*> {
val artemisMessage: ClientMessage =
if (timeout == null)
consumer.receive() ?: throw ActiveMQObjectClosedException()
else
consumer.receive(timeout.toMillis()) ?: throw RPCException.DeadlineExceeded(method.name)
artemisMessage.acknowledge()
val next = artemisMessage.deserialize<ErrorOr<*>>(kryo)
return next
}
private fun sendRequest(args: Array<out Any>?, msg: ClientMessage) {
sessionLock.withLock {
val argsKryo = createRPCKryoForDeserialization(this@CordaRPCClientImpl)
val serializedArgs = try {
(args ?: emptyArray<Any?>()).serialize(argsKryo)
} catch (e: KryoException) {
throw RPCException("Could not serialize RPC arguments", e)
} finally {
releaseRPCKryoForDeserialization(argsKryo)
}
msg.writeBodyBufferBytes(serializedArgs.bytes)
producer!!.send(ArtemisMessagingComponent.RPC_REQUESTS_QUEUE, msg)
}
}
private fun maybePrepareForObservables(location: Throwable, method: Method, msg: ClientMessage): Kryo {
// Create a temporary queue just for the emissions on any observables that are returned.
val observationsId = random63BitValue()
val observationsQueueName = constructAddress(observationsId)
session.createTemporaryQueue(observationsQueueName, observationsQueueName)
msg.putLongProperty(ClientRPCRequestMessage.OBSERVATIONS_TO, observationsId)
// And make sure that we deserialise observable handles so that they're linked to the right
// queue. Also record a bit of metadata for debugging purposes.
return createRPCKryoForDeserialization(this@CordaRPCClientImpl, observationsQueueName, method.name, location)
}
private fun createMessage(method: Method): ClientMessage {
return session.createMessage(false).apply {
putStringProperty(ClientRPCRequestMessage.METHOD_NAME, method.name)
putLongProperty(ClientRPCRequestMessage.REPLY_TO, proxyId)
// Use the magic deduplication property built into Artemis as our message identity too
putStringProperty(HDR_DUPLICATE_DETECTION_ID, SimpleString(UUID.randomUUID().toString()))
}
}
private fun checkMethodVersion(method: Method) {
val methodVersion = method.getAnnotation(RPCSinceVersion::class.java)?.version ?: 0
if (methodVersion > serverProtocolVersion)
throw UnsupportedOperationException("Method ${method.name} was added in RPC protocol version $methodVersion but the server is running $serverProtocolVersion")
}
private fun isCloseInvocation(method: Method) = method == closeableCloseMethod || method == autocloseableCloseMethod
override fun close() {
consumer.close()
sessionLock.withLock { session.deleteQueue(constructAddress(proxyId)) }
}
override fun toString() = "Corda RPC Proxy listening on queue ${constructAddress(proxyId)}"
}
/**
* When subscribed to, starts consuming from the given queue name and demultiplexing the observables being
* sent to it. The server queue is moved into in-memory buffers (one per attached server-side observable)
* until drained through a subscription. When the subscriptions are all gone, the server-side queue is deleted.
*/
@ThreadSafe
private inner class QueuedObservable(private val qName: String,
private val rpcName: String,
private val rpcLocation: Throwable) {
private val root = PublishSubject.create<MarshalledObservation>()
private val rootShared = root.doOnUnsubscribe { close() }.share()
// This could be made more efficient by using a specialised IntMap
// When handling this map we don't synchronise on [this], otherwise there is a race condition between close() and deliver()
private val observables = Collections.synchronizedMap(HashMap<Int, Observable<Any>>())
private var consumer: ClientConsumer? = null
private val referenceCount = AtomicInteger(0)
// We have to create a weak reference, otherwise we cannot be GC'd.
init {
val weakThis = WeakReference<QueuedObservable>(this)
consumer = sessionLock.withLock { session.createConsumer(qName) }.setMessageHandler { weakThis.get()?.deliver(it) }
}
/**
* We have to reference count subscriptions to the returned [Observable]s to prevent early GC because we are
* weak referenced.
*
* Derived [Observables] (e.g. filtered etc) hold a strong reference to the original, but for example, if
* the pattern as follows is used, the original passes out of scope and the direction of reference is from the
* original to the [Observer]. We use the reference counting to allow for this pattern.
*
* val observationsSubject = PublishSubject.create<Observation>()
* originalObservable.subscribe(observationsSubject)
* return observationsSubject
*/
private fun refCountUp() {
if(referenceCount.andIncrement == 0) {
hardReferencesToQueuedObservables.add(this)
}
}
private fun refCountDown() {
if(referenceCount.decrementAndGet() == 0) {
hardReferencesToQueuedObservables.remove(this)
}
}
fun getForHandle(handle: Int): Observable<Any> {
synchronized(observables) {
return observables.getOrPut(handle) {
/**
* Note that the order of bufferUntilSubscribed() -> dematerialize() is very important here.
*
* In particular doing it the other way around may result in the following edge case:
* The RPC returns two (or more) Observables. The first Observable unsubscribes *during serialisation*,
* before the second one is hit, causing the [rootShared] to unsubscribe and consequently closing
* the underlying artemis queue, even though the second Observable was not even registered.
*
* The buffer -> dematerialize order ensures that the Observable may not unsubscribe until the caller
* subscribes, which must be after full deserialisation and registering of all top level Observables.
*
* In addition, when subscribe and unsubscribe is called on the [Observable] returned here, we
* reference count a hard reference to this [QueuedObservable] to prevent premature GC.
*/
rootShared.filter { it.forHandle == handle }.map { it.what }.bufferUntilSubscribed().dematerialize<Any>().doOnSubscribe { refCountUp() }.doOnUnsubscribe { refCountDown() }.share()
}
}
}
private fun deliver(msg: ClientMessage) {
msg.acknowledge()
val kryo = createRPCKryoForDeserialization(this@CordaRPCClientImpl, qName, rpcName, rpcLocation)
val received: MarshalledObservation = try { msg.deserialize(kryo) } finally {
releaseRPCKryoForDeserialization(kryo)
}
rpcLog.debug { "<- Observable [$rpcName] <- Received $received" }
synchronized(observables) {
// Force creation of the buffer if it doesn't already exist.
getForHandle(received.forHandle)
root.onNext(received)
}
}
@Synchronized
fun close() {
rpcLog.debug("Closing queue observable for call to $rpcName : $qName")
consumer?.close()
consumer = null
sessionLock.withLock { session.deleteQueue(qName) }
}
@Suppress("UNUSED")
fun finalize() {
val c = synchronized(this) { consumer }
if (c != null) {
rpcLog.warn("A hot observable returned from an RPC ($rpcName) was never subscribed to. " +
"This wastes server-side resources because it was queueing observations for retrieval. " +
"It is being closed now, but please adjust your code to subscribe and unsubscribe from the observable to close it explicitly.", rpcLocation)
c.close()
}
}
}
//endregion
}
private val rpcDesKryoPool = KryoPool.Builder { RPCKryo(CordaRPCClientImpl.ObservableDeserializer()) }.build()
fun createRPCKryoForDeserialization(rpcClient: CordaRPCClientImpl, qName: String? = null, rpcName: String? = null, rpcLocation: Throwable? = null): Kryo {
val kryo = rpcDesKryoPool.borrow()
kryo.context.put(RPCKryoClientKey, rpcClient)
kryo.context.put(RPCKryoQNameKey, qName)
kryo.context.put(RPCKryoMethodNameKey, rpcName)
kryo.context.put(RPCKryoLocationKey, rpcLocation)
return kryo
}
fun releaseRPCKryoForDeserialization(kryo: Kryo) {
rpcDesKryoPool.release(kryo)
}

View File

@ -0,0 +1,19 @@
package net.corda.client.rpc
import rx.Observable
/**
* This function should be invoked on any unwanted Observables returned from RPC to release the server resources.
*
* subscribe({}, {}) was used instead of simply calling subscribe()
* because if an {@code onError} emission arrives (eg. due to an non-correct transaction, such as 'Not sufficient funds')
* then {@link OnErrorNotImplementedException} is thrown. As we won't handle exceptions from unused Observables,
* empty inputs are used to subscribe({}, {}).
*/
fun <T> Observable<T>.notUsed() {
try {
this.subscribe({}, {}).unsubscribe()
} catch (e: Exception) {
// Swallow any other exceptions as well.
}
}

View File

@ -0,0 +1,169 @@
package net.corda.client.rpc.internal
import com.google.common.net.HostAndPort
import net.corda.core.logElapsedTime
import net.corda.core.messaging.RPCOps
import net.corda.core.minutes
import net.corda.core.random63BitValue
import net.corda.core.seconds
import net.corda.core.utilities.loggerFor
import net.corda.nodeapi.ArtemisTcpTransport.Companion.tcpTransport
import net.corda.nodeapi.ConnectionDirection
import net.corda.nodeapi.RPCApi
import net.corda.nodeapi.RPCException
import net.corda.nodeapi.config.SSLConfiguration
import org.apache.activemq.artemis.api.core.SimpleString
import org.apache.activemq.artemis.api.core.TransportConfiguration
import org.apache.activemq.artemis.api.core.client.ActiveMQClient
import java.io.Closeable
import java.lang.reflect.Proxy
import java.time.Duration
/**
* This configuration may be used to tweak the internals of the RPC client.
*/
data class RPCClientConfiguration(
/** The minimum protocol version required from the server */
val minimumServerProtocolVersion: Int,
/**
* If set to true the client will track RPC call sites. If an error occurs subsequently during the RPC or in a
* returned Observable stream the stack trace of the originating RPC will be shown as well. Note that
* constructing call stacks is a moderately expensive operation.
*/
val trackRpcCallSites: Boolean,
/**
* The interval of unused observable reaping. Leaked Observables (unused ones) are detected using weak references
* and are cleaned up in batches in this interval. If set too large it will waste server side resources for this
* duration. If set too low it wastes client side cycles.
*/
val reapInterval: Duration,
/** The number of threads to use for observations (for executing [Observable.onNext]) */
val observationExecutorPoolSize: Int,
/** The maximum number of producers to create to handle outgoing messages */
val producerPoolBound: Int,
/**
* Determines the concurrency level of the Observable Cache. This is exposed because it implicitly determines
* the limit on the number of leaked observables reaped because of garbage collection per reaping.
* See the implementation of [com.google.common.cache.LocalCache] for details.
*/
val cacheConcurrencyLevel: Int,
/** The retry interval of artemis connections in milliseconds */
val connectionRetryInterval: Duration,
/** The retry interval multiplier for exponential backoff */
val connectionRetryIntervalMultiplier: Double,
/** Maximum retry interval */
val connectionMaxRetryInterval: Duration,
/** Maximum file size */
val maxFileSize: Int
) {
companion object {
@JvmStatic
val default = RPCClientConfiguration(
minimumServerProtocolVersion = 0,
trackRpcCallSites = false,
reapInterval = 1.seconds,
observationExecutorPoolSize = 4,
producerPoolBound = 1,
cacheConcurrencyLevel = 8,
connectionRetryInterval = 5.seconds,
connectionRetryIntervalMultiplier = 1.5,
connectionMaxRetryInterval = 3.minutes,
/** 10 MiB maximum allowed file size for attachments, including message headers. TODO: acquire this value from Network Map when supported. */
maxFileSize = 10485760
)
}
}
/**
* An RPC client that may be used to create connections to an RPC server.
*
* @param transport The Artemis transport to use to connect to the server.
* @param rpcConfiguration Configuration used to tweak client behaviour.
*/
class RPCClient<I : RPCOps>(
val transport: TransportConfiguration,
val rpcConfiguration: RPCClientConfiguration = RPCClientConfiguration.default
) {
constructor(
hostAndPort: HostAndPort,
sslConfiguration: SSLConfiguration? = null,
configuration: RPCClientConfiguration = RPCClientConfiguration.default
) : this(tcpTransport(ConnectionDirection.Outbound(), hostAndPort, sslConfiguration), configuration)
companion object {
private val log = loggerFor<RPCClient<*>>()
}
/**
* Holds a proxy object implementing [I] that forwards requests to the RPC server.
*
* [Closeable.close] may be used to shut down the connection and release associated resources.
*/
interface RPCConnection<out I : RPCOps> : Closeable {
val proxy: I
/** The RPC protocol version reported by the server */
val serverProtocolVersion: Int
}
/**
* Returns an [RPCConnection] containing a proxy that lets you invoke RPCs on the server. Calls on it block, and if
* the server throws an exception then it will be rethrown on the client. Proxies are thread safe and may be used to
* invoke multiple RPCs in parallel.
*
* RPC sends and receives are logged on the net.corda.rpc logger.
*
* The [RPCOps] defines what client RPCs are available. If an RPC returns an [Observable] anywhere in the object
* graph returned then the server-side observable is transparently forwarded to the client side here.
* *You are expected to use it*. The server will begin buffering messages immediately that it will expect you to
* drain by subscribing to the returned observer. You can opt-out of this by simply calling the
* [net.corda.client.rpc.notUsed] method on it. You don't have to explicitly close the observable if you actually
* subscribe to it: it will close itself and free up the server-side resources either when the client or JVM itself
* is shutdown, or when there are no more subscribers to it. Once all the subscribers to a returned observable are
* unsubscribed or the observable completes successfully or with an error, the observable is closed and you can't
* then re-subscribe again: you'll have to re-request a fresh observable with another RPC.
*
* @param rpcOpsClass The [Class] of the RPC interface.
* @param username The username to authenticate with.
* @param password The password to authenticate with.
* @throws RPCException if the server version is too low or if the server isn't reachable within the given time.
*/
fun start(
rpcOpsClass: Class<I>,
username: String,
password: String
): RPCConnection<I> {
return log.logElapsedTime("Startup") {
val clientAddress = SimpleString("${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.$username.${random63BitValue()}")
val serverLocator = ActiveMQClient.createServerLocatorWithoutHA(transport).apply {
retryInterval = rpcConfiguration.connectionRetryInterval.toMillis()
retryIntervalMultiplier = rpcConfiguration.connectionRetryIntervalMultiplier
maxRetryInterval = rpcConfiguration.connectionMaxRetryInterval.toMillis()
minLargeMessageSize = rpcConfiguration.maxFileSize
}
val proxyHandler = RPCClientProxyHandler(rpcConfiguration, username, password, serverLocator, clientAddress, rpcOpsClass)
proxyHandler.start()
@Suppress("UNCHECKED_CAST")
val ops = Proxy.newProxyInstance(rpcOpsClass.classLoader, arrayOf(rpcOpsClass), proxyHandler) as I
val serverProtocolVersion = ops.protocolVersion
if (serverProtocolVersion < rpcConfiguration.minimumServerProtocolVersion) {
throw RPCException("Requested minimum protocol version (${rpcConfiguration.minimumServerProtocolVersion}) is higher" +
" than the server's supported protocol version ($serverProtocolVersion)")
}
proxyHandler.setServerProtocolVersion(serverProtocolVersion)
log.debug("RPC connected, returning proxy")
object : RPCConnection<I> {
override val proxy = ops
override val serverProtocolVersion = serverProtocolVersion
override fun close() {
proxyHandler.close()
serverLocator.close()
}
}
}
}
}

View File

@ -0,0 +1,422 @@
package net.corda.client.rpc.internal
import com.esotericsoftware.kryo.Kryo
import com.esotericsoftware.kryo.Serializer
import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output
import com.esotericsoftware.kryo.pool.KryoPool
import com.google.common.cache.Cache
import com.google.common.cache.CacheBuilder
import com.google.common.cache.RemovalCause
import com.google.common.cache.RemovalListener
import com.google.common.util.concurrent.SettableFuture
import com.google.common.util.concurrent.ThreadFactoryBuilder
import net.corda.core.ThreadBox
import net.corda.core.getOrThrow
import net.corda.core.messaging.RPCOps
import net.corda.core.random63BitValue
import net.corda.core.serialization.KryoPoolWithContext
import net.corda.core.utilities.*
import net.corda.nodeapi.*
import org.apache.activemq.artemis.api.core.SimpleString
import org.apache.activemq.artemis.api.core.client.ActiveMQClient.DEFAULT_ACK_BATCH_SIZE
import org.apache.activemq.artemis.api.core.client.ClientMessage
import org.apache.activemq.artemis.api.core.client.ServerLocator
import rx.Notification
import rx.Observable
import rx.subjects.UnicastSubject
import sun.reflect.CallerSensitive
import java.lang.reflect.InvocationHandler
import java.lang.reflect.Method
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
import kotlin.collections.ArrayList
import kotlin.reflect.jvm.javaMethod
/**
* This class provides a proxy implementation of an RPC interface for RPC clients. It translates API calls to lower-level
* RPC protocol messages. For this protocol see [RPCApi].
*
* When a method is called on the interface the arguments are serialised and the request is forwarded to the server. The
* server then executes the code that implements the RPC and sends a reply.
*
* An RPC reply may contain [Observable]s, which are serialised simply as unique IDs. On the client side we create a
* [UnicastSubject] for each such ID. Subsequently the server may send observations attached to this ID, which are
* forwarded to the [UnicastSubject]. Note that the observations themselves may contain further [Observable]s, which are
* handled in the same way.
*
* To do the above we take advantage of Kryo's datastructure traversal. When the client is deserialising a message from
* the server that may contain Observables it is supplied with an [ObservableContext] that exposes the map used to demux
* the observations. When an [Observable] is encountered during traversal a new [UnicastSubject] is added to the map and
* we carry on. Each observation later contains the corresponding Observable ID, and we just forward that to the
* associated [UnicastSubject].
*
* The client may signal that it no longer consumes a particular [Observable]. This may be done explicitly by
* unsubscribing from the [Observable], or if the [Observable] is garbage collected the client will eventually
* automatically signal the server. This is done using a cache that holds weak references to the [UnicastSubject]s.
* The cleanup happens in batches using a dedicated reaper, scheduled on [reaperExecutor].
*/
class RPCClientProxyHandler(
private val rpcConfiguration: RPCClientConfiguration,
private val rpcUsername: String,
private val rpcPassword: String,
private val serverLocator: ServerLocator,
private val clientAddress: SimpleString,
private val rpcOpsClass: Class<out RPCOps>
) : InvocationHandler {
private enum class State {
UNSTARTED,
SERVER_VERSION_NOT_SET,
STARTED,
FINISHED
}
private val lifeCycle = LifeCycle(State.UNSTARTED)
private companion object {
val log = loggerFor<RPCClientProxyHandler>()
// Note that this KryoPool is not yet capable of deserialising Observables, it requires Proxy-specific context
// to do that. However it may still be used for serialisation of RPC requests and related messages.
val kryoPool = KryoPool.Builder { RPCKryo(RpcClientObservableSerializer) }.build()
// To check whether toString() is being invoked
val toStringMethod: Method = Object::toString.javaMethod!!
}
// Used for reaping
private val reaperExecutor = Executors.newScheduledThreadPool(
1,
ThreadFactoryBuilder().setNameFormat("rpc-client-reaper-%d").build()
)
// A sticky pool for running Observable.onNext()s. We need the stickiness to preserve the observation ordering.
private val observationExecutorThreadFactory = ThreadFactoryBuilder().setNameFormat("rpc-client-observation-pool-%d").build()
private val observationExecutorPool = LazyStickyPool(rpcConfiguration.observationExecutorPoolSize) {
Executors.newFixedThreadPool(1, observationExecutorThreadFactory)
}
// Holds the RPC reply futures.
private val rpcReplyMap = RpcReplyMap()
// Optionally holds RPC call site stack traces to be shown on errors/warnings.
private val callSiteMap = if (rpcConfiguration.trackRpcCallSites) CallSiteMap() else null
// Holds the Observables and a reference store to keep Observables alive when subscribed to.
private val observableContext = ObservableContext(
callSiteMap = callSiteMap,
observableMap = createRpcObservableMap(),
hardReferenceStore = Collections.synchronizedSet(mutableSetOf<Observable<*>>())
)
// Holds a reference to the scheduled reaper.
private lateinit var reaperScheduledFuture: ScheduledFuture<*>
// The protocol version of the server, to be initialised to the value of [RPCOps.protocolVersion]
private var serverProtocolVersion: Int? = null
// Stores the Observable IDs that are already removed from the map but are not yet sent to the server.
private val observablesToReap = ThreadBox(object {
var observables = ArrayList<RPCApi.ObservableId>()
})
// A Kryo pool that automatically adds the observable context when an instance is requested.
private val kryoPoolWithObservableContext = RpcClientObservableSerializer.createPoolWithContext(kryoPool, observableContext)
private fun createRpcObservableMap(): RpcObservableMap {
val onObservableRemove = RemovalListener<RPCApi.ObservableId, UnicastSubject<Notification<Any>>> {
val rpcCallSite = callSiteMap?.remove(it.key.toLong)
if (it.cause == RemovalCause.COLLECTED) {
log.warn(listOf(
"A hot observable returned from an RPC was never subscribed to.",
"This wastes server-side resources because it was queueing observations for retrieval.",
"It is being closed now, but please adjust your code to call .notUsed() on the observable",
"to close it explicitly. (Java users: subscribe to it then unsubscribe). This warning",
"will appear less frequently in future versions of the platform and you can ignore it",
"if you want to.").joinToString(" "), rpcCallSite)
}
observablesToReap.locked { observables.add(it.key) }
}
return CacheBuilder.newBuilder().
weakValues().
removalListener(onObservableRemove).
concurrencyLevel(rpcConfiguration.cacheConcurrencyLevel).
build()
}
// We cannot pool consumers as we need to preserve the original muxed message order.
// TODO We may need to pool these somehow anyway, otherwise if the server sends many big messages in parallel a
// single consumer may be starved for flow control credits. Recheck this once Artemis's large message streaming is
// integrated properly.
private lateinit var sessionAndConsumer: ArtemisConsumer
// Pool producers to reduce contention on the client side.
private val sessionAndProducerPool = LazyPool(bound = rpcConfiguration.producerPoolBound) {
// Note how we create new sessions *and* session factories per producer.
// We cannot simply pool producers on one session because sessions are single threaded.
// We cannot simply pool sessions on one session factory because flow control credits are tied to factories, so
// sessions tend to starve each other when used concurrently.
val sessionFactory = serverLocator.createSessionFactory()
val session = sessionFactory.createSession(rpcUsername, rpcPassword, false, true, true, false, DEFAULT_ACK_BATCH_SIZE)
session.start()
ArtemisProducer(sessionFactory, session, session.createProducer(RPCApi.RPC_SERVER_QUEUE_NAME))
}
/**
* Start the client. This creates the per-client queue, starts the consumer session and the reaper.
*/
fun start() {
reaperScheduledFuture = reaperExecutor.scheduleAtFixedRate(
this::reapObservables,
rpcConfiguration.reapInterval.toMillis(),
rpcConfiguration.reapInterval.toMillis(),
TimeUnit.MILLISECONDS
)
sessionAndProducerPool.run {
it.session.createTemporaryQueue(clientAddress, clientAddress)
}
val sessionFactory = serverLocator.createSessionFactory()
val session = sessionFactory.createSession(rpcUsername, rpcPassword, false, true, true, false, DEFAULT_ACK_BATCH_SIZE)
val consumer = session.createConsumer(clientAddress)
consumer.setMessageHandler(this@RPCClientProxyHandler::artemisMessageHandler)
sessionAndConsumer = ArtemisConsumer(sessionFactory, session, consumer)
lifeCycle.transition(State.UNSTARTED, State.SERVER_VERSION_NOT_SET)
session.start()
}
// This is the general function that transforms a client side RPC to internal Artemis messages.
override fun invoke(proxy: Any, method: Method, arguments: Array<out Any?>?): Any? {
lifeCycle.requireState { it == State.STARTED || it == State.SERVER_VERSION_NOT_SET }
checkProtocolVersion(method)
if (method == toStringMethod) {
return "Client RPC proxy for $rpcOpsClass"
}
if (sessionAndConsumer.session.isClosed) {
throw RPCException("RPC Proxy is closed")
}
val rpcId = RPCApi.RpcRequestId(random63BitValue())
callSiteMap?.set(rpcId.toLong, Throwable("<Call site of root RPC '${method.name}'>"))
try {
val request = RPCApi.ClientToServer.RpcRequest(clientAddress, rpcId, method.name, arguments?.toList() ?: emptyList())
val replyFuture = SettableFuture.create<Any>()
sessionAndProducerPool.run {
val message = it.session.createMessage(false)
request.writeToClientMessage(kryoPool, message)
log.debug {
val argumentsString = arguments?.joinToString() ?: ""
"-> RPC($rpcId) -> ${method.name}($argumentsString): ${method.returnType}"
}
require(rpcReplyMap.put(rpcId, replyFuture) == null) {
"Generated several RPC requests with same ID $rpcId"
}
it.producer.send(message)
it.session.commit()
}
return replyFuture.getOrThrow()
} finally {
callSiteMap?.remove(rpcId.toLong)
}
}
// The handler for Artemis messages.
private fun artemisMessageHandler(message: ClientMessage) {
val serverToClient = RPCApi.ServerToClient.fromClientMessage(kryoPoolWithObservableContext, message)
log.debug { "Got message from RPC server $serverToClient" }
when (serverToClient) {
is RPCApi.ServerToClient.RpcReply -> {
val replyFuture = rpcReplyMap.remove(serverToClient.id)
if (replyFuture == null) {
log.error("RPC reply arrived to unknown RPC ID ${serverToClient.id}, this indicates an internal RPC error.")
} else {
val rpcCallSite = callSiteMap?.get(serverToClient.id.toLong)
serverToClient.result.match(
onError = {
if (rpcCallSite != null) addRpcCallSiteToThrowable(it, rpcCallSite)
replyFuture.setException(it)
},
onValue = { replyFuture.set(it) }
)
}
}
is RPCApi.ServerToClient.Observation -> {
val observable = observableContext.observableMap.getIfPresent(serverToClient.id)
if (observable == null) {
log.debug("Observation ${serverToClient.content} arrived to unknown Observable with ID ${serverToClient.id}. " +
"This may be due to an observation arriving before the server was " +
"notified of observable shutdown")
} else {
// We schedule the onNext() on an executor sticky-pooled based on the Observable ID.
observationExecutorPool.run(serverToClient.id) { executor ->
executor.submit {
val content = serverToClient.content
if (content.isOnCompleted || content.isOnError) {
observableContext.observableMap.invalidate(serverToClient.id)
}
// Add call site information on error
if (content.isOnError) {
val rpcCallSite = callSiteMap?.get(serverToClient.id.toLong)
if (rpcCallSite != null) addRpcCallSiteToThrowable(content.throwable, rpcCallSite)
}
observable.onNext(content)
}
}
}
}
}
message.acknowledge()
}
/**
* Closes the RPC proxy. Reaps all observables, shuts down the reaper, closes all sessions and executors.
*/
fun close() {
sessionAndConsumer.consumer.close()
sessionAndConsumer.session.close()
sessionAndConsumer.sessionFactory.close()
reaperScheduledFuture.cancel(false)
observableContext.observableMap.invalidateAll()
reapObservables()
reaperExecutor.shutdownNow()
sessionAndProducerPool.close().forEach {
it.producer.close()
it.session.close()
it.sessionFactory.close()
}
// Note the ordering is important, we shut down the consumer *before* the observation executor, otherwise we may
// leak borrowed executors.
val observationExecutors = observationExecutorPool.close()
observationExecutors.forEach { it.shutdownNow() }
observationExecutors.forEach { it.awaitTermination(100, TimeUnit.MILLISECONDS) }
lifeCycle.transition(State.STARTED, State.FINISHED)
}
/**
* Check the [RPCSinceVersion] of the passed in [calledMethod] against the server's protocol version.
*/
private fun checkProtocolVersion(calledMethod: Method) {
val serverProtocolVersion = serverProtocolVersion
if (serverProtocolVersion == null) {
lifeCycle.requireState(State.SERVER_VERSION_NOT_SET)
} else {
lifeCycle.requireState(State.STARTED)
val sinceVersion = calledMethod.getAnnotation(RPCSinceVersion::class.java)?.version ?: 0
if (sinceVersion > serverProtocolVersion) {
throw UnsupportedOperationException("Method $calledMethod was added in RPC protocol version $sinceVersion but the server is running $serverProtocolVersion")
}
}
}
/**
* Set the server's protocol version. Note that before doing so the client is not considered fully started, although
* RPCs already may be called with it.
*/
internal fun setServerProtocolVersion(version: Int) {
if (serverProtocolVersion == null) {
serverProtocolVersion = version
} else {
throw IllegalStateException("setServerProtocolVersion called, but the protocol version was already set!")
}
lifeCycle.transition(State.SERVER_VERSION_NOT_SET, State.STARTED)
}
private fun reapObservables() {
observableContext.observableMap.cleanUp()
val observableIds = observablesToReap.locked {
if (observables.isNotEmpty()) {
val temporary = observables
observables = ArrayList()
temporary
} else {
null
}
}
if (observableIds != null) {
log.debug { "Reaping ${observableIds.size} observables" }
sessionAndProducerPool.run {
val message = it.session.createMessage(false)
RPCApi.ClientToServer.ObservablesClosed(observableIds).writeToClientMessage(message)
it.producer.send(message)
}
}
}
}
private typealias RpcObservableMap = Cache<RPCApi.ObservableId, UnicastSubject<Notification<Any>>>
private typealias RpcReplyMap = ConcurrentHashMap<RPCApi.RpcRequestId, SettableFuture<Any?>>
private typealias CallSiteMap = ConcurrentHashMap<Long, Throwable?>
/**
* Holds a context available during Kryo deserialisation of messages that are expected to contain Observables.
*
* @param observableMap holds the Observables that are ultimately exposed to the user.
* @param hardReferenceStore holds references to Observables we want to keep alive while they are subscribed to.
*/
private data class ObservableContext(
val callSiteMap: CallSiteMap?,
val observableMap: RpcObservableMap,
val hardReferenceStore: MutableSet<Observable<*>>
)
/**
* A [Serializer] to deserialise Observables once the corresponding Kryo instance has been provided with an [ObservableContext].
*/
private object RpcClientObservableSerializer : Serializer<Observable<Any>>() {
private object RpcObservableContextKey
fun createPoolWithContext(kryoPool: KryoPool, observableContext: ObservableContext): KryoPool {
return KryoPoolWithContext(kryoPool, RpcObservableContextKey, observableContext)
}
override fun read(kryo: Kryo, input: Input, type: Class<Observable<Any>>): Observable<Any> {
@Suppress("UNCHECKED_CAST")
val observableContext = kryo.context[RpcObservableContextKey] as ObservableContext
val observableId = RPCApi.ObservableId(input.readLong(true))
val observable = UnicastSubject.create<Notification<Any>>()
require(observableContext.observableMap.getIfPresent(observableId) == null) {
"Multiple Observables arrived with the same ID $observableId"
}
val rpcCallSite = getRpcCallSite(kryo, observableContext)
observableContext.observableMap.put(observableId, observable)
observableContext.callSiteMap?.put(observableId.toLong, rpcCallSite)
// We pin all Observables into a hard reference store (rooted in the RPC proxy) on subscription so that users
// don't need to store a reference to the Observables themselves.
return observable.pinInSubscriptions(observableContext.hardReferenceStore).doOnUnsubscribe {
// This causes Future completions to give warnings because the corresponding OnComplete sent from the server
// will arrive after the client unsubscribes from the observable and consequently invalidates the mapping.
// The unsubscribe is due to [ObservableToFuture]'s use of first().
observableContext.observableMap.invalidate(observableId)
}.dematerialize()
}
override fun write(kryo: Kryo, output: Output, observable: Observable<Any>) {
throw UnsupportedOperationException("Cannot serialise Observables on the client side")
}
private fun getRpcCallSite(kryo: Kryo, observableContext: ObservableContext): Throwable? {
val rpcRequestOrObservableId = kryo.context[RPCApi.RpcRequestOrObservableIdKey] as Long
return observableContext.callSiteMap?.get(rpcRequestOrObservableId)
}
}
private fun addRpcCallSiteToThrowable(throwable: Throwable, callSite: Throwable) {
var currentThrowable = throwable
while (true) {
val cause = currentThrowable.cause
if (cause == null) {
currentThrowable.initCause(callSite)
break
} else {
currentThrowable = cause
}
}
}
private fun <T> Observable<T>.pinInSubscriptions(hardReferenceStore: MutableSet<Observable<*>>): Observable<T> {
val refCount = AtomicInteger(0)
return this.doOnSubscribe {
if (refCount.getAndIncrement() == 0) {
require(hardReferenceStore.add(this)) { "Reference store already contained reference $this on add" }
}
}.doOnUnsubscribe {
if (refCount.decrementAndGet() == 0) {
require(hardReferenceStore.remove(this)) { "Reference store did not contain reference $this on remove" }
}
}
}

View File

@ -0,0 +1,56 @@
package net.corda.client.rpc
import net.corda.client.rpc.internal.RPCClientConfiguration
import net.corda.core.flatMap
import net.corda.core.map
import net.corda.core.messaging.RPCOps
import net.corda.node.services.messaging.RPCServerConfiguration
import net.corda.nodeapi.User
import net.corda.testing.RPCDriverExposedDSLInterface
import net.corda.testing.rpcTestUser
import net.corda.testing.startInVmRpcClient
import net.corda.testing.startRpcClient
import org.apache.activemq.artemis.api.core.client.ClientSession
import org.junit.runners.Parameterized
open class AbstractRPCTest {
enum class RPCTestMode {
InVm,
Netty
}
companion object {
@JvmStatic @Parameterized.Parameters(name = "Mode = {0}")
fun defaultModes() = modes(RPCTestMode.InVm, RPCTestMode.Netty)
fun modes(vararg modes: RPCTestMode) = listOf(*modes).map { arrayOf(it) }
}
@Parameterized.Parameter
lateinit var mode: RPCTestMode
data class TestProxy<out I : RPCOps>(
val ops: I,
val createSession: () -> ClientSession
)
inline fun <reified I : RPCOps> RPCDriverExposedDSLInterface.testProxy(
ops: I,
rpcUser: User = rpcTestUser,
clientConfiguration: RPCClientConfiguration = RPCClientConfiguration.default,
serverConfiguration: RPCServerConfiguration = RPCServerConfiguration.default
): TestProxy<I> {
return when (mode) {
RPCTestMode.InVm ->
startInVmRpcServer(ops = ops, rpcUser = rpcUser, configuration = serverConfiguration).flatMap {
startInVmRpcClient<I>(rpcUser.username, rpcUser.password, clientConfiguration).map {
TestProxy(it, { startInVmArtemisSession(rpcUser.username, rpcUser.password) })
}
}.get()
RPCTestMode.Netty ->
startRpcServer(ops = ops, rpcUser = rpcUser, configuration = serverConfiguration).flatMap { server ->
startRpcClient<I>(server.hostAndPort, rpcUser.username, rpcUser.password, clientConfiguration).map {
TestProxy(it, { startArtemisSession(server.hostAndPort, rpcUser.username, rpcUser.password) })
}
}.get()
}
}
}

View File

@ -0,0 +1,192 @@
package net.corda.client.rpc
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.SettableFuture
import net.corda.core.getOrThrow
import net.corda.core.messaging.RPCOps
import net.corda.core.success
import net.corda.node.services.messaging.getRpcContext
import net.corda.nodeapi.RPCSinceVersion
import net.corda.testing.RPCDriverExposedDSLInterface
import net.corda.testing.rpcDriver
import net.corda.testing.rpcTestUser
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import rx.Observable
import rx.subjects.PublishSubject
import java.util.concurrent.CountDownLatch
import java.util.concurrent.LinkedBlockingQueue
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
@RunWith(Parameterized::class)
class ClientRPCInfrastructureTests : AbstractRPCTest() {
// TODO: Test that timeouts work
private fun RPCDriverExposedDSLInterface.testProxy() = testProxy<TestOps>(TestOpsImpl()).ops
interface TestOps : RPCOps {
@Throws(IllegalArgumentException::class)
fun barf()
fun void()
fun someCalculation(str: String, num: Int): String
fun makeObservable(): Observable<Int>
fun makeComplicatedObservable(): Observable<Pair<String, Observable<String>>>
fun makeListenableFuture(): ListenableFuture<Int>
fun makeComplicatedListenableFuture(): ListenableFuture<Pair<String, ListenableFuture<String>>>
@RPCSinceVersion(2)
fun addedLater()
fun captureUser(): String
}
private lateinit var complicatedObservable: Observable<Pair<String, Observable<String>>>
private lateinit var complicatedListenableFuturee: ListenableFuture<Pair<String, ListenableFuture<String>>>
inner class TestOpsImpl : TestOps {
override val protocolVersion = 1
override fun barf(): Unit = throw IllegalArgumentException("Barf!")
override fun void() {}
override fun someCalculation(str: String, num: Int) = "$str $num"
override fun makeObservable(): Observable<Int> = Observable.just(1, 2, 3, 4)
override fun makeListenableFuture(): ListenableFuture<Int> = Futures.immediateFuture(1)
override fun makeComplicatedObservable() = complicatedObservable
override fun makeComplicatedListenableFuture(): ListenableFuture<Pair<String, ListenableFuture<String>>> = complicatedListenableFuturee
override fun addedLater(): Unit = throw IllegalStateException()
override fun captureUser(): String = getRpcContext().currentUser.username
}
@Test
fun `simple RPCs`() {
rpcDriver {
val proxy = testProxy()
// Does nothing, doesn't throw.
proxy.void()
assertEquals("Barf!", assertFailsWith<IllegalArgumentException> {
proxy.barf()
}.message)
assertEquals("hi 5", proxy.someCalculation("hi", 5))
}
}
@Test
fun `simple observable`() {
rpcDriver {
val proxy = testProxy()
// This tests that the observations are transmitted correctly, also completion is transmitted.
val observations = proxy.makeObservable().toBlocking().toIterable().toList()
assertEquals(listOf(1, 2, 3, 4), observations)
}
}
@Test
fun `complex observables`() {
rpcDriver {
val proxy = testProxy()
// This checks that we can return an object graph with complex usage of observables, like an observable
// that emits objects that contain more observables.
val serverQuotes = PublishSubject.create<Pair<String, Observable<String>>>()
val unsubscribeLatch = CountDownLatch(1)
complicatedObservable = serverQuotes.asObservable().doOnUnsubscribe { unsubscribeLatch.countDown() }
val twainQuotes = "Mark Twain" to Observable.just(
"I have never let my schooling interfere with my education.",
"Clothes make the man. Naked people have little or no influence on society."
)
val wildeQuotes = "Oscar Wilde" to Observable.just(
"I can resist everything except temptation.",
"Always forgive your enemies - nothing annoys them so much."
)
val clientQuotes = LinkedBlockingQueue<String>()
val clientObs = proxy.makeComplicatedObservable()
val subscription = clientObs.subscribe {
val name = it.first
it.second.subscribe {
clientQuotes += "Quote by $name: $it"
}
}
assertThat(clientQuotes).isEmpty()
serverQuotes.onNext(twainQuotes)
assertEquals("Quote by Mark Twain: I have never let my schooling interfere with my education.", clientQuotes.take())
assertEquals("Quote by Mark Twain: Clothes make the man. Naked people have little or no influence on society.", clientQuotes.take())
serverQuotes.onNext(wildeQuotes)
assertEquals("Quote by Oscar Wilde: I can resist everything except temptation.", clientQuotes.take())
assertEquals("Quote by Oscar Wilde: Always forgive your enemies - nothing annoys them so much.", clientQuotes.take())
assertTrue(serverQuotes.hasObservers())
subscription.unsubscribe()
unsubscribeLatch.await()
}
}
@Test
fun `simple ListenableFuture`() {
rpcDriver {
val proxy = testProxy()
val value = proxy.makeListenableFuture().getOrThrow()
assertThat(value).isEqualTo(1)
}
}
@Test
fun `complex ListenableFuture`() {
rpcDriver {
val proxy = testProxy()
val serverQuote = SettableFuture.create<Pair<String, ListenableFuture<String>>>()
complicatedListenableFuturee = serverQuote
val twainQuote = "Mark Twain" to Futures.immediateFuture("I have never let my schooling interfere with my education.")
val clientQuotes = LinkedBlockingQueue<String>()
val clientFuture = proxy.makeComplicatedListenableFuture()
clientFuture.success {
val name = it.first
it.second.success {
clientQuotes += "Quote by $name: $it"
}
}
assertThat(clientQuotes).isEmpty()
serverQuote.set(twainQuote)
assertThat(clientQuotes.take()).isEqualTo("Quote by Mark Twain: I have never let my schooling interfere with my education.")
// TODO This final assert sometimes fails because the relevant queue hasn't been removed yet
}
}
@Test
fun versioning() {
rpcDriver {
val proxy = testProxy()
assertFailsWith<UnsupportedOperationException> { proxy.addedLater() }
}
}
@Test
fun `authenticated user is available to RPC`() {
rpcDriver {
val proxy = testProxy()
assertThat(proxy.captureUser()).isEqualTo(rpcTestUser.username)
}
}
}

View File

@ -0,0 +1,182 @@
package net.corda.client.rpc
import net.corda.client.rpc.internal.RPCClientConfiguration
import net.corda.core.future
import net.corda.core.messaging.RPCOps
import net.corda.core.millis
import net.corda.core.random63BitValue
import net.corda.core.serialization.CordaSerializable
import net.corda.node.services.messaging.RPCServerConfiguration
import net.corda.testing.RPCDriverExposedDSLInterface
import net.corda.testing.rpcDriver
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import rx.Observable
import rx.subjects.UnicastSubject
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CountDownLatch
@RunWith(Parameterized::class)
class RPCConcurrencyTests : AbstractRPCTest() {
/**
* Holds a "rose"-tree of [Observable]s which allows us to test arbitrary [Observable] nesting in RPC replies.
*/
@CordaSerializable
data class ObservableRose<out A>(val value: A, val branches: Observable<out ObservableRose<A>>)
private interface TestOps : RPCOps {
fun newLatch(numberOfDowns: Int): Long
fun waitLatch(id: Long)
fun downLatch(id: Long)
fun getImmediateObservableTree(depth: Int, branchingFactor: Int): ObservableRose<Int>
fun getParallelObservableTree(depth: Int, branchingFactor: Int): ObservableRose<Int>
}
class TestOpsImpl : TestOps {
private val latches = ConcurrentHashMap<Long, CountDownLatch>()
override val protocolVersion = 0
override fun newLatch(numberOfDowns: Int): Long {
val id = random63BitValue()
val latch = CountDownLatch(numberOfDowns)
latches.put(id, latch)
return id
}
override fun waitLatch(id: Long) {
latches[id]!!.await()
}
override fun downLatch(id: Long) {
latches[id]!!.countDown()
}
override fun getImmediateObservableTree(depth: Int, branchingFactor: Int): ObservableRose<Int> {
val branches = if (depth == 0) {
Observable.empty<ObservableRose<Int>>()
} else {
Observable.just(getImmediateObservableTree(depth - 1, branchingFactor)).repeat(branchingFactor.toLong())
}
return ObservableRose(depth, branches)
}
override fun getParallelObservableTree(depth: Int, branchingFactor: Int): ObservableRose<Int> {
val branches = if (depth == 0) {
Observable.empty<ObservableRose<Int>>()
} else {
val publish = UnicastSubject.create<ObservableRose<Int>>()
future {
(1..branchingFactor).toList().parallelStream().forEach {
publish.onNext(getParallelObservableTree(depth - 1, branchingFactor))
}
publish.onCompleted()
}
publish
}
return ObservableRose(depth, branches)
}
}
private lateinit var testOpsImpl: TestOpsImpl
private fun RPCDriverExposedDSLInterface.testProxy(): TestProxy<TestOps> {
testOpsImpl = TestOpsImpl()
return testProxy<TestOps>(
testOpsImpl,
clientConfiguration = RPCClientConfiguration.default.copy(
reapInterval = 100.millis,
cacheConcurrencyLevel = 16
),
serverConfiguration = RPCServerConfiguration.default.copy(
rpcThreadPoolSize = 4
)
)
}
@Test
fun `call multiple RPCs in parallel`() {
rpcDriver {
val proxy = testProxy()
val numberOfBlockedCalls = 2
val numberOfDownsRequired = 100
val id = proxy.ops.newLatch(numberOfDownsRequired)
val done = CountDownLatch(numberOfBlockedCalls)
// Start a couple of blocking RPC calls
(1..numberOfBlockedCalls).forEach {
future {
proxy.ops.waitLatch(id)
done.countDown()
}
}
// Down the latch that the others are waiting for concurrently
(1..numberOfDownsRequired).toList().parallelStream().forEach {
proxy.ops.downLatch(id)
}
done.await()
}
}
private fun intPower(base: Int, power: Int): Int {
return when (power) {
0 -> 1
1 -> base
else -> {
val a = intPower(base, power / 2)
if (power and 1 == 0) {
a * a
} else {
a * a * base
}
}
}
}
@Test
fun `nested immediate observables sequence correctly`() {
rpcDriver {
// We construct a rose tree of immediate Observables and check that parent observations arrive before children.
val proxy = testProxy()
val treeDepth = 6
val treeBranchingFactor = 3
val remainingLatch = CountDownLatch((intPower(treeBranchingFactor, treeDepth + 1) - 1) / (treeBranchingFactor - 1))
val depthsSeen = Collections.synchronizedSet(HashSet<Int>())
fun ObservableRose<Int>.subscribeToAll() {
remainingLatch.countDown()
this.branches.subscribe { tree ->
(tree.value + 1..treeDepth - 1).forEach {
require(it in depthsSeen) { "Got ${tree.value} before $it" }
}
depthsSeen.add(tree.value)
tree.subscribeToAll()
}
}
proxy.ops.getImmediateObservableTree(treeDepth, treeBranchingFactor).subscribeToAll()
remainingLatch.await()
}
}
@Test
fun `parallel nested observables`() {
rpcDriver {
val proxy = testProxy()
val treeDepth = 2
val treeBranchingFactor = 10
val remainingLatch = CountDownLatch((intPower(treeBranchingFactor, treeDepth + 1) - 1) / (treeBranchingFactor - 1))
val depthsSeen = Collections.synchronizedSet(HashSet<Int>())
fun ObservableRose<Int>.subscribeToAll() {
remainingLatch.countDown()
branches.subscribe { tree ->
(tree.value + 1..treeDepth - 1).forEach {
require(it in depthsSeen) { "Got ${tree.value} before $it" }
}
depthsSeen.add(tree.value)
tree.subscribeToAll()
}
}
proxy.ops.getParallelObservableTree(treeDepth, treeBranchingFactor).subscribeToAll()
remainingLatch.await()
}
}
}

View File

@ -0,0 +1,315 @@
package net.corda.client.rpc
import com.codahale.metrics.ConsoleReporter
import com.codahale.metrics.Gauge
import com.codahale.metrics.JmxReporter
import com.codahale.metrics.MetricRegistry
import com.google.common.base.Stopwatch
import net.corda.client.rpc.internal.RPCClientConfiguration
import net.corda.core.messaging.RPCOps
import net.corda.core.minutes
import net.corda.core.seconds
import net.corda.core.utilities.Rate
import net.corda.core.utilities.div
import net.corda.node.driver.ShutdownManager
import net.corda.node.services.messaging.RPCServerConfiguration
import net.corda.testing.RPCDriverExposedDSLInterface
import net.corda.testing.measure
import net.corda.testing.rpcDriver
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import java.time.Duration
import java.util.*
import java.util.concurrent.*
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.ReentrantLock
import javax.management.ObjectName
import kotlin.concurrent.thread
import kotlin.concurrent.withLock
@Ignore("Only use this locally for profiling")
@RunWith(Parameterized::class)
class RPCPerformanceTests : AbstractRPCTest() {
companion object {
@JvmStatic @Parameterized.Parameters(name = "Mode = {0}")
fun modes() = modes(RPCTestMode.Netty)
}
private interface TestOps : RPCOps {
fun simpleReply(input: ByteArray, sizeOfReply: Int): ByteArray
}
class TestOpsImpl : TestOps {
override val protocolVersion = 0
override fun simpleReply(input: ByteArray, sizeOfReply: Int): ByteArray {
return ByteArray(sizeOfReply)
}
}
private fun RPCDriverExposedDSLInterface.testProxy(
clientConfiguration: RPCClientConfiguration,
serverConfiguration: RPCServerConfiguration
): TestProxy<TestOps> {
return testProxy<TestOps>(
TestOpsImpl(),
clientConfiguration = clientConfiguration,
serverConfiguration = serverConfiguration
)
}
private fun warmup() {
rpcDriver {
val proxy = testProxy(
RPCClientConfiguration.default,
RPCServerConfiguration.default
)
val executor = Executors.newFixedThreadPool(4)
val N = 10000
val latch = CountDownLatch(N)
for (i in 1 .. N) {
executor.submit {
proxy.ops.simpleReply(ByteArray(1024), 1024)
latch.countDown()
}
}
latch.await()
}
}
data class SimpleRPCResult(
val requestPerSecond: Double,
val averageIndividualMs: Double,
val Mbps: Double
)
@Test
fun `measure Megabytes per second for simple RPCs`() {
warmup()
val inputOutputSizes = listOf(1024, 4096, 100 * 1024)
val overallTraffic = 512 * 1024 * 1024L
measure(inputOutputSizes, (1..5)) { inputOutputSize, N ->
rpcDriver {
val proxy = testProxy(
RPCClientConfiguration.default.copy(
cacheConcurrencyLevel = 16,
observationExecutorPoolSize = 2,
producerPoolBound = 2
),
RPCServerConfiguration.default.copy(
rpcThreadPoolSize = 8,
consumerPoolSize = 2,
producerPoolBound = 8
)
)
val numberOfRequests = overallTraffic / (2 * inputOutputSize)
val timings = Collections.synchronizedList(ArrayList<Long>())
val executor = Executors.newFixedThreadPool(8)
val totalElapsed = Stopwatch.createStarted().apply {
startInjectorWithBoundedQueue(
executor = executor,
numberOfInjections = numberOfRequests.toInt(),
queueBound = 100
) {
val elapsed = Stopwatch.createStarted().apply {
proxy.ops.simpleReply(ByteArray(inputOutputSize), inputOutputSize)
}.stop().elapsed(TimeUnit.MICROSECONDS)
timings.add(elapsed)
}
}.stop().elapsed(TimeUnit.MICROSECONDS)
executor.shutdownNow()
SimpleRPCResult(
requestPerSecond = 1000000.0 * numberOfRequests.toDouble() / totalElapsed.toDouble(),
averageIndividualMs = timings.average() / 1000.0,
Mbps = (overallTraffic.toDouble() / totalElapsed.toDouble()) * (1000000.0 / (1024.0 * 1024.0))
)
}
}.forEach(::println)
}
/**
* Runs 20k RPCs per second for two minutes and publishes relevant stats to JMX.
*/
@Test
fun `consumption rate`() {
rpcDriver {
val metricRegistry = startReporter()
val proxy = testProxy(
RPCClientConfiguration.default.copy(
reapInterval = 1.seconds,
cacheConcurrencyLevel = 16,
producerPoolBound = 8
),
RPCServerConfiguration.default.copy(
rpcThreadPoolSize = 8,
consumerPoolSize = 1,
producerPoolBound = 8
)
)
measurePerformancePublishMetrics(
metricRegistry = metricRegistry,
parallelism = 8,
overallDuration = 5.minutes,
injectionRate = 20000L / TimeUnit.SECONDS,
queueSizeMetricName = "$mode.QueueSize",
workDurationMetricName = "$mode.WorkDuration",
shutdownManager = this.shutdownManager,
work = {
proxy.ops.simpleReply(ByteArray(4096), 4096)
}
)
}
}
data class BigMessagesResult(
val Mbps: Double
)
@Test
fun `big messages`() {
warmup()
measure(listOf(1)) { clientParallelism -> // TODO this hangs with more parallelism
rpcDriver {
val proxy = testProxy(
RPCClientConfiguration.default,
RPCServerConfiguration.default.copy(
consumerPoolSize = 1
)
)
val executor = Executors.newFixedThreadPool(clientParallelism)
val numberOfMessages = 1000
val bigSize = 10_000_000
val elapsed = Stopwatch.createStarted().apply {
startInjectorWithBoundedQueue(
executor = executor,
numberOfInjections = numberOfMessages,
queueBound = 4
) {
proxy.ops.simpleReply(ByteArray(bigSize), 0)
}
}.stop().elapsed(TimeUnit.MICROSECONDS)
executor.shutdownNow()
BigMessagesResult(
Mbps = bigSize.toDouble() * numberOfMessages.toDouble() / elapsed * (1000000.0 / (1024.0 * 1024.0))
)
}
}.forEach(::println)
}
}
fun measurePerformancePublishMetrics(
metricRegistry: MetricRegistry,
parallelism: Int,
overallDuration: Duration,
injectionRate: Rate,
queueSizeMetricName: String,
workDurationMetricName: String,
shutdownManager: ShutdownManager,
work: () -> Unit
) {
val workSemaphore = Semaphore(0)
metricRegistry.register(queueSizeMetricName, Gauge { workSemaphore.availablePermits() })
val workDurationTimer = metricRegistry.timer(workDurationMetricName)
val executor = Executors.newSingleThreadScheduledExecutor()
val workExecutor = Executors.newFixedThreadPool(parallelism)
val timings = Collections.synchronizedList(ArrayList<Long>())
for (i in 1 .. parallelism) {
workExecutor.submit {
try {
while (true) {
workSemaphore.acquire()
workDurationTimer.time {
timings.add(
Stopwatch.createStarted().apply {
work()
}.stop().elapsed(TimeUnit.MICROSECONDS)
)
}
}
} catch (throwable: Throwable) {
throwable.printStackTrace()
}
}
}
val injector = executor.scheduleAtFixedRate(
{
workSemaphore.release((injectionRate * TimeUnit.SECONDS).toInt())
},
0,
1,
TimeUnit.SECONDS
)
shutdownManager.registerShutdown {
injector.cancel(true)
workExecutor.shutdownNow()
executor.shutdownNow()
workExecutor.awaitTermination(1, TimeUnit.SECONDS)
executor.awaitTermination(1, TimeUnit.SECONDS)
}
Thread.sleep(overallDuration.toMillis())
}
fun startInjectorWithBoundedQueue(
executor: ExecutorService,
numberOfInjections: Int,
queueBound: Int,
work: () -> Unit
) {
val remainingLatch = CountDownLatch(numberOfInjections)
val queuedCount = AtomicInteger(0)
val lock = ReentrantLock()
val canQueueAgain = lock.newCondition()
val injectorShutdown = AtomicBoolean(false)
val injector = thread(name = "injector") {
while (true) {
if (injectorShutdown.get()) break
executor.submit {
work()
if (queuedCount.decrementAndGet() < queueBound / 2) {
lock.withLock {
canQueueAgain.signal()
}
}
remainingLatch.countDown()
}
if (queuedCount.incrementAndGet() > queueBound) {
lock.withLock {
canQueueAgain.await()
}
}
}
}
remainingLatch.await()
injectorShutdown.set(true)
injector.join()
}
fun RPCDriverExposedDSLInterface.startReporter(): MetricRegistry {
val metricRegistry = MetricRegistry()
val jmxReporter = thread {
JmxReporter.
forRegistry(metricRegistry).
inDomain("net.corda").
createsObjectNamesWith { _, domain, name ->
// Make the JMX hierarchy a bit better organised.
val category = name.substringBefore('.')
val subName = name.substringAfter('.', "")
if (subName == "")
ObjectName("$domain:name=$category")
else
ObjectName("$domain:type=$category,name=$subName")
}.
build().
start()
}
val consoleReporter = thread {
ConsoleReporter.forRegistry(metricRegistry).build().start(1, TimeUnit.SECONDS)
}
shutdownManager.registerShutdown {
jmxReporter.interrupt()
consoleReporter.interrupt()
jmxReporter.join()
consoleReporter.join()
}
return metricRegistry
}

View File

@ -0,0 +1,93 @@
package net.corda.client.rpc
import net.corda.core.messaging.RPCOps
import net.corda.node.services.messaging.requirePermission
import net.corda.node.services.messaging.getRpcContext
import net.corda.nodeapi.PermissionException
import net.corda.nodeapi.User
import net.corda.testing.RPCDriverExposedDSLInterface
import net.corda.testing.rpcDriver
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import kotlin.test.assertFailsWith
@RunWith(Parameterized::class)
class RPCPermissionsTests : AbstractRPCTest() {
companion object {
const val DUMMY_FLOW = "StartFlow.net.corda.flows.DummyFlow"
const val OTHER_FLOW = "StartFlow.net.corda.flows.OtherFlow"
const val ALL_ALLOWED = "ALL"
}
/*
* RPC operation.
*/
interface TestOps : RPCOps {
fun validatePermission(str: String)
}
class TestOpsImpl : TestOps {
override val protocolVersion = 1
override fun validatePermission(str: String) = getRpcContext().requirePermission(str)
}
/**
* Create an RPC proxy for the given user.
*/
private fun RPCDriverExposedDSLInterface.testProxyFor(rpcUser: User) = testProxy<TestOps>(TestOpsImpl(), rpcUser).ops
private fun userOf(name: String, permissions: Set<String>) = User(name, "password", permissions)
@Test
fun `empty user cannot use any flows`() {
rpcDriver {
val emptyUser = userOf("empty", emptySet())
val proxy = testProxyFor(emptyUser)
assertFailsWith(PermissionException::class,
"User ${emptyUser.username} should not be allowed to use $DUMMY_FLOW.",
{ proxy.validatePermission(DUMMY_FLOW) })
}
}
@Test
fun `admin user can use any flow`() {
rpcDriver {
val adminUser = userOf("admin", setOf(ALL_ALLOWED))
val proxy = testProxyFor(adminUser)
proxy.validatePermission(DUMMY_FLOW)
}
}
@Test
fun `joe user is allowed to use DummyFlow`() {
rpcDriver {
val joeUser = userOf("joe", setOf(DUMMY_FLOW))
val proxy = testProxyFor(joeUser)
proxy.validatePermission(DUMMY_FLOW)
}
}
@Test
fun `joe user is not allowed to use OtherFlow`() {
rpcDriver {
val joeUser = userOf("joe", setOf(DUMMY_FLOW))
val proxy = testProxyFor(joeUser)
assertFailsWith(PermissionException::class,
"User ${joeUser.username} should not be allowed to use $OTHER_FLOW",
{ proxy.validatePermission(OTHER_FLOW) })
}
}
@Test
fun `check ALL is implemented the correct way round` () {
rpcDriver {
val joeUser = userOf("joe", setOf(DUMMY_FLOW))
val proxy = testProxyFor(joeUser)
assertFailsWith(PermissionException::class,
"Permission $ALL_ALLOWED should not do anything for User ${joeUser.username}",
{ proxy.validatePermission(ALL_ALLOWED) })
}
}
}

View File

@ -0,0 +1,25 @@
package net.corda.client.rpc
import java.io.InputStream
class RepeatingBytesInputStream(val bytesToRepeat: ByteArray, val numberOfBytes: Int) : InputStream() {
private var bytesLeft = numberOfBytes
override fun available() = bytesLeft
override fun read(): Int {
if (bytesLeft == 0) {
return -1
} else {
bytesLeft--
return bytesToRepeat[(numberOfBytes - bytesLeft) % bytesToRepeat.size].toInt()
}
}
override fun read(byteArray: ByteArray, offset: Int, length: Int): Int {
val until = Math.min(Math.min(offset + length, byteArray.size), offset + bytesLeft)
for (i in offset .. until - 1) {
byteArray[i] = bytesToRepeat[(numberOfBytes - bytesLeft + i - offset) % bytesToRepeat.size]
}
val bytesRead = until - offset
bytesLeft -= bytesRead
return if (bytesRead == 0 && bytesLeft == 0) -1 else bytesRead
}
}

View File

@ -1,97 +0,0 @@
package net.corda.client.rpc
import net.corda.core.messaging.RPCOps
import net.corda.core.serialization.SerializedBytes
import net.corda.core.utilities.LogHelper
import net.corda.node.services.RPCUserService
import net.corda.node.services.User
import net.corda.node.services.messaging.ArtemisMessagingComponent
import net.corda.node.services.messaging.RPCDispatcher
import net.corda.node.utilities.AffinityExecutor
import org.apache.activemq.artemis.api.core.Message
import org.apache.activemq.artemis.api.core.SimpleString
import org.apache.activemq.artemis.api.core.TransportConfiguration
import org.apache.activemq.artemis.api.core.client.ActiveMQClient
import org.apache.activemq.artemis.api.core.client.ClientMessage
import org.apache.activemq.artemis.api.core.client.ClientProducer
import org.apache.activemq.artemis.api.core.client.ClientSession
import org.apache.activemq.artemis.core.config.impl.ConfigurationImpl
import org.apache.activemq.artemis.core.remoting.impl.invm.InVMAcceptorFactory
import org.apache.activemq.artemis.core.remoting.impl.invm.InVMConnectorFactory
import org.apache.activemq.artemis.core.server.embedded.EmbeddedActiveMQ
import org.junit.After
import org.junit.Before
import java.util.*
import java.util.concurrent.locks.ReentrantLock
abstract class AbstractClientRPC {
lateinit var artemis: EmbeddedActiveMQ
lateinit var serverSession: ClientSession
lateinit var clientSession: ClientSession
lateinit var producer: ClientProducer
lateinit var serverThread: AffinityExecutor.ServiceAffinityExecutor
@Before
fun rpcSetup() {
// Set up an in-memory Artemis with an RPC requests queue.
artemis = EmbeddedActiveMQ()
artemis.setConfiguration(ConfigurationImpl().apply {
acceptorConfigurations = setOf(TransportConfiguration(InVMAcceptorFactory::class.java.name))
isSecurityEnabled = false
isPersistenceEnabled = false
})
artemis.start()
val serverLocator = ActiveMQClient.createServerLocatorWithoutHA(TransportConfiguration(InVMConnectorFactory::class.java.name))
val sessionFactory = serverLocator.createSessionFactory()
serverSession = sessionFactory.createSession()
serverSession.start()
serverSession.createTemporaryQueue(ArtemisMessagingComponent.RPC_REQUESTS_QUEUE, ArtemisMessagingComponent.RPC_REQUESTS_QUEUE)
producer = serverSession.createProducer()
serverThread = AffinityExecutor.ServiceAffinityExecutor("unit-tests-rpc-dispatch-thread", 1)
serverSession.createTemporaryQueue("activemq.notifications", "rpc.qremovals", "_AMQ_NotifType = 'BINDING_REMOVED'")
clientSession = sessionFactory.createSession()
clientSession.start()
LogHelper.setLevel("+net.corda.rpc")
}
@After
fun rpcShutdown() {
safeClose(producer)
clientSession.stop()
serverSession.stop()
artemis.stop()
serverThread.shutdownNow()
}
fun <T: RPCOps> rpcProxyFor(rpcUser: User, rpcImpl: T, type: Class<T>): T {
val userService = object : RPCUserService {
override fun getUser(username: String): User? = if (username == rpcUser.username) rpcUser else null
override val users: List<User> get() = listOf(rpcUser)
}
val dispatcher = object : RPCDispatcher(rpcImpl, userService, "SomeName") {
override fun send(data: SerializedBytes<*>, toAddress: String) {
val msg = serverSession.createMessage(false).apply {
writeBodyBufferBytes(data.bytes)
// Use the magic deduplication property built into Artemis as our message identity too
putStringProperty(Message.HDR_DUPLICATE_DETECTION_ID, SimpleString(UUID.randomUUID().toString()))
}
producer.send(toAddress, msg)
}
override fun getUser(message: ClientMessage): User = rpcUser
}
val serverNotifConsumer = serverSession.createConsumer("rpc.qremovals")
val serverConsumer = serverSession.createConsumer(ArtemisMessagingComponent.RPC_REQUESTS_QUEUE)
dispatcher.start(serverConsumer, serverNotifConsumer, serverThread)
return CordaRPCClientImpl(clientSession, ReentrantLock(), rpcUser.username).proxyFor(type)
}
fun safeClose(obj: Any) = try { (obj as AutoCloseable).close() } catch (e: Exception) {}
}

View File

@ -1,194 +0,0 @@
package net.corda.client.rpc
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.SettableFuture
import net.corda.core.getOrThrow
import net.corda.core.messaging.RPCOps
import net.corda.core.messaging.RPCReturnsObservables
import net.corda.core.success
import net.corda.node.services.User
import net.corda.node.services.messaging.CURRENT_RPC_USER
import net.corda.node.services.messaging.RPCSinceVersion
import org.apache.activemq.artemis.api.core.SimpleString
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Test
import rx.Observable
import rx.subjects.PublishSubject
import java.util.concurrent.CountDownLatch
import java.util.concurrent.LinkedBlockingQueue
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
class ClientRPCInfrastructureTests : AbstractClientRPC() {
// TODO: Test that timeouts work
lateinit var proxy: TestOps
private val authenticatedUser = User("test", "password", permissions = setOf())
@Before
fun setup() {
proxy = rpcProxyFor(authenticatedUser, TestOpsImpl(), TestOps::class.java)
}
@After
fun shutdown() {
safeClose(proxy)
}
interface TestOps : RPCOps {
@Throws(IllegalArgumentException::class)
fun barf()
fun void()
fun someCalculation(str: String, num: Int): String
@RPCReturnsObservables
fun makeObservable(): Observable<Int>
@RPCReturnsObservables
fun makeComplicatedObservable(): Observable<Pair<String, Observable<String>>>
@RPCReturnsObservables
fun makeListenableFuture(): ListenableFuture<Int>
@RPCReturnsObservables
fun makeComplicatedListenableFuture(): ListenableFuture<Pair<String, ListenableFuture<String>>>
@RPCSinceVersion(2)
fun addedLater()
fun captureUser(): String
}
private lateinit var complicatedObservable: Observable<Pair<String, Observable<String>>>
private lateinit var complicatedListenableFuturee: ListenableFuture<Pair<String, ListenableFuture<String>>>
inner class TestOpsImpl : TestOps {
override val protocolVersion = 1
override fun barf(): Unit = throw IllegalArgumentException("Barf!")
override fun void() {}
override fun someCalculation(str: String, num: Int) = "$str $num"
override fun makeObservable(): Observable<Int> = Observable.just(1, 2, 3, 4)
override fun makeListenableFuture(): ListenableFuture<Int> = Futures.immediateFuture(1)
override fun makeComplicatedObservable() = complicatedObservable
override fun makeComplicatedListenableFuture(): ListenableFuture<Pair<String, ListenableFuture<String>>> = complicatedListenableFuturee
override fun addedLater(): Unit = throw UnsupportedOperationException("not implemented")
override fun captureUser(): String = CURRENT_RPC_USER.get().username
}
@Test
fun `simple RPCs`() {
// Does nothing, doesn't throw.
proxy.void()
assertEquals("Barf!", assertFailsWith<IllegalArgumentException> {
proxy.barf()
}.message)
assertEquals("hi 5", proxy.someCalculation("hi", 5))
}
@Test
fun `simple observable`() {
// This tests that the observations are transmitted correctly, also completion is transmitted.
val observations = proxy.makeObservable().toBlocking().toIterable().toList()
assertEquals(listOf(1, 2, 3, 4), observations)
}
@Test
fun `complex observables`() {
// This checks that we can return an object graph with complex usage of observables, like an observable
// that emits objects that contain more observables.
val serverQuotes = PublishSubject.create<Pair<String, Observable<String>>>()
val unsubscribeLatch = CountDownLatch(1)
complicatedObservable = serverQuotes.asObservable().doOnUnsubscribe { unsubscribeLatch.countDown() }
val twainQuotes = "Mark Twain" to Observable.just(
"I have never let my schooling interfere with my education.",
"Clothes make the man. Naked people have little or no influence on society."
)
val wildeQuotes = "Oscar Wilde" to Observable.just(
"I can resist everything except temptation.",
"Always forgive your enemies - nothing annoys them so much."
)
val clientQuotes = LinkedBlockingQueue<String>()
val clientObs = proxy.makeComplicatedObservable()
val subscription = clientObs.subscribe {
val name = it.first
it.second.subscribe {
clientQuotes += "Quote by $name: $it"
}
}
val rpcQueuesQuery = SimpleString("clients.${authenticatedUser.username}.rpc.*")
assertEquals(2, clientSession.addressQuery(rpcQueuesQuery).queueNames.size)
assertThat(clientQuotes).isEmpty()
serverQuotes.onNext(twainQuotes)
assertEquals("Quote by Mark Twain: I have never let my schooling interfere with my education.", clientQuotes.take())
assertEquals("Quote by Mark Twain: Clothes make the man. Naked people have little or no influence on society.", clientQuotes.take())
serverQuotes.onNext(wildeQuotes)
assertEquals("Quote by Oscar Wilde: I can resist everything except temptation.", clientQuotes.take())
assertEquals("Quote by Oscar Wilde: Always forgive your enemies - nothing annoys them so much.", clientQuotes.take())
assertTrue(serverQuotes.hasObservers())
subscription.unsubscribe()
unsubscribeLatch.await()
assertEquals(1, clientSession.addressQuery(rpcQueuesQuery).queueNames.size)
}
@Test
fun `simple ListenableFuture`() {
val value = proxy.makeListenableFuture().getOrThrow()
assertThat(value).isEqualTo(1)
}
@Test
fun `complex ListenableFuture`() {
val serverQuote = SettableFuture.create<Pair<String, ListenableFuture<String>>>()
complicatedListenableFuturee = serverQuote
val twainQuote = "Mark Twain" to Futures.immediateFuture("I have never let my schooling interfere with my education.")
val clientQuotes = LinkedBlockingQueue<String>()
val clientFuture = proxy.makeComplicatedListenableFuture()
clientFuture.success {
val name = it.first
it.second.success {
clientQuotes += "Quote by $name: $it"
}
}
val rpcQueuesQuery = SimpleString("clients.${authenticatedUser.username}.rpc.*")
assertEquals(2, clientSession.addressQuery(rpcQueuesQuery).queueNames.size)
assertThat(clientQuotes).isEmpty()
serverQuote.set(twainQuote)
assertThat(clientQuotes.take()).isEqualTo("Quote by Mark Twain: I have never let my schooling interfere with my education.")
// TODO This final assert sometimes fails because the relevant queue hasn't been removed yet
// assertEquals(1, clientSession.addressQuery(rpcQueuesQuery).queueNames.size)
}
@Test
fun versioning() {
assertFailsWith<UnsupportedOperationException> { proxy.addedLater() }
}
@Test
fun `authenticated user is available to RPC`() {
assertThat(proxy.captureUser()).isEqualTo(authenticatedUser.username)
}
}

View File

@ -1,84 +0,0 @@
package net.corda.client.rpc
import net.corda.core.messaging.RPCOps
import net.corda.node.services.User
import net.corda.node.services.messaging.*
import org.junit.After
import org.junit.Test
import kotlin.test.*
class RPCPermissionsTest : AbstractClientRPC() {
companion object {
const val DUMMY_FLOW = "StartFlow.net.corda.flows.DummyFlow"
const val OTHER_FLOW = "StartFlow.net.corda.flows.OtherFlow"
const val ALL_ALLOWED = "ALL"
}
lateinit var proxy: TestOps
@After
fun shutdown() {
safeClose(proxy)
}
/*
* RPC operation.
*/
interface TestOps : RPCOps {
fun validatePermission(str: String)
}
class TestOpsImpl : TestOps {
override val protocolVersion = 1
override fun validatePermission(str: String) = requirePermission(str)
}
/**
* Create an RPC proxy for the given user.
*/
private fun proxyFor(rpcUser: User): TestOps = rpcProxyFor(rpcUser, TestOpsImpl(), TestOps::class.java)
private fun userOf(name: String, permissions: Set<String>) = User(name, "password", permissions)
@Test
fun `empty user cannot use any flows`() {
val emptyUser = userOf("empty", emptySet())
proxy = proxyFor(emptyUser)
assertFailsWith(PermissionException::class,
"User ${emptyUser.username} should not be allowed to use $DUMMY_FLOW.",
{ proxy.validatePermission(DUMMY_FLOW) })
}
@Test
fun `admin user can use any flow`() {
val adminUser = userOf("admin", setOf(ALL_ALLOWED))
proxy = proxyFor(adminUser)
proxy.validatePermission(DUMMY_FLOW)
}
@Test
fun `joe user is allowed to use DummyFlow`() {
val joeUser = userOf("joe", setOf(DUMMY_FLOW))
proxy = proxyFor(joeUser)
proxy.validatePermission(DUMMY_FLOW)
}
@Test
fun `joe user is not allowed to use OtherFlow`() {
val joeUser = userOf("joe", setOf(DUMMY_FLOW))
proxy = proxyFor(joeUser)
assertFailsWith(PermissionException::class,
"User ${joeUser.username} should not be allowed to use $OTHER_FLOW",
{ proxy.validatePermission(OTHER_FLOW) })
}
@Test
fun `check ALL is implemented the correct way round` () {
val joeUser = userOf("joe", setOf(DUMMY_FLOW))
proxy = proxyFor(joeUser)
assertFailsWith(PermissionException::class,
"Permission $ALL_ALLOWED should not do anything for User ${joeUser.username}",
{ proxy.validatePermission(ALL_ALLOWED) })
}
}

View File

@ -1,4 +1,4 @@
myLegalName : "Bank A"
myLegalName : "CN=Bank A,O=Bank A,L=London,C=UK"
nearestCity : "London"
keyStorePassword : "cordacadevpass"
trustStorePassword : "trustpass"
@ -8,6 +8,6 @@ webAddress : "localhost:10004"
extraAdvertisedServiceIds : [ "corda.interest_rates" ]
networkMapService : {
address : "localhost:10000"
legalName : "Network Map Service"
legalName : "CN=Network Map Service,O=R3,OU=corda,L=London,C=UK"
}
useHTTPS : false

View File

@ -1,4 +1,4 @@
myLegalName : "Bank B"
myLegalName : "CN=Bank B,O=Bank A,L=London,C=UK"
nearestCity : "London"
keyStorePassword : "cordacadevpass"
trustStorePassword : "trustpass"
@ -8,6 +8,6 @@ webAddress : "localhost:10007"
extraAdvertisedServiceIds : [ "corda.interest_rates" ]
networkMapService : {
address : "localhost:10000"
legalName : "Network Map Service"
legalName : "CN=Network Map Service,O=R3,OU=corda,L=London,C=UK"
}
useHTTPS : false

View File

@ -57,5 +57,8 @@
<AppenderRef ref="Console-Appender-Println"/>
<AppenderRef ref="RollingFile-Appender" />
</Logger>
<Logger name="org.apache.activemq.artemis.core.server" level="error" additivity="false">
<AppenderRef ref="RollingFile-Appender"/>
</Logger>
</Loggers>
</Configuration>
</Configuration>

View File

@ -1,4 +1,4 @@
myLegalName : "Notary Service"
myLegalName : "CN=Notary Service,O=R3,OU=corda,L=London,C=UK"
nearestCity : "London"
keyStorePassword : "cordacadevpass"
trustStorePassword : "trustpass"

5
constants.properties Normal file
View File

@ -0,0 +1,5 @@
gradlePluginsVersion=0.12.0
kotlinVersion=1.1.2
guavaVersion=21.0
bouncycastleVersion=1.56
typesafeConfigVersion=1.3.1

View File

@ -10,26 +10,6 @@ buildscript {
}
}
repositories {
mavenLocal()
mavenCentral()
jcenter()
maven {
url 'http://oss.sonatype.org/content/repositories/snapshots'
}
maven {
url 'https://dl.bintray.com/kotlin/exposed'
}
}
sourceSets {
test {
resources {
srcDir "../config/test"
}
}
}
dependencies {
testCompile "junit:junit:$junit_version"
@ -42,9 +22,8 @@ dependencies {
testCompile project(":node")
testCompile project(":test-utils")
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
compile "org.jetbrains.kotlinx:kotlinx-support-jdk8:0.3"
testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
// Thread safety annotations
@ -84,7 +63,7 @@ dependencies {
compile "com.fasterxml.jackson.core:jackson-databind:${jackson_version}"
// Java ed25519 implementation. See https://github.com/str4d/ed25519-java/
compile 'net.i2p.crypto:eddsa:0.1.0'
compile 'net.i2p.crypto:eddsa:0.2.0'
// Bouncy castle support needed for X509 certificate manipulation
compile "org.bouncycastle:bcprov-jdk15on:${bouncycastle_version}"
@ -93,20 +72,14 @@ dependencies {
// JPA 2.1 annotations.
compile "org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final"
// RS API: Response type and codes for ApiUtils.
compile "javax.ws.rs:javax.ws.rs-api:2.0.1"
// Requery: SQL based query & persistence for Kotlin
compile "io.requery:requery-kotlin:$requery_version"
// For AMQP serialisation.
compile "org.apache.qpid:proton-j:0.18.0"
}
configurations {
compile {
// We want to use SLF4J's version of these binding: jcl-over-slf4j
// Remove any transitive dependency on Apache's version.
exclude group: 'commons-logging', module: 'commons-logging'
}
testArtifacts.extendsFrom testRuntime
}

View File

@ -3,21 +3,19 @@
package net.corda.core
import com.google.common.base.Function
import com.google.common.base.Throwables
import com.google.common.io.ByteStreams
import com.google.common.util.concurrent.*
import kotlinx.support.jdk7.use
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.newSecureRandom
import net.corda.core.crypto.sha256
import net.corda.core.serialization.CordaSerializable
import org.slf4j.Logger
import rx.Observable
import rx.Observer
import rx.subjects.PublishSubject
import rx.subjects.UnicastSubject
import java.io.BufferedInputStream
import java.io.InputStream
import java.io.OutputStream
import java.io.*
import java.math.BigDecimal
import java.nio.charset.Charset
import java.nio.charset.StandardCharsets.UTF_8
@ -25,11 +23,22 @@ import java.nio.file.*
import java.nio.file.attribute.FileAttribute
import java.time.Duration
import java.time.temporal.Temporal
import java.util.HashMap
import java.util.concurrent.*
import java.util.concurrent.locks.ReentrantLock
import java.util.function.BiConsumer
import java.util.stream.Stream
import java.util.zip.Deflater
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
import kotlin.collections.Iterable
import kotlin.collections.LinkedHashMap
import kotlin.collections.List
import kotlin.collections.filter
import kotlin.collections.firstOrNull
import kotlin.collections.fold
import kotlin.collections.forEach
import kotlin.concurrent.withLock
import kotlin.reflect.KProperty
@ -75,7 +84,7 @@ fun <T> future(block: () -> T): ListenableFuture<T> = CompletableToListenable(Co
private class CompletableToListenable<T>(private val base: CompletableFuture<T>) : Future<T> by base, ListenableFuture<T> {
override fun addListener(listener: Runnable, executor: Executor) {
base.whenCompleteAsync(BiConsumer { result, exception -> listener.run() }, executor)
base.whenCompleteAsync(BiConsumer { _, _ -> listener.run() }, executor)
}
}
@ -102,7 +111,9 @@ fun <T> ListenableFuture<T>.failure(executor: Executor, body: (Throwable) -> Uni
infix fun <T> ListenableFuture<T>.then(body: () -> Unit): ListenableFuture<T> = apply { then(RunOnCallerThread, body) }
infix fun <T> ListenableFuture<T>.success(body: (T) -> Unit): ListenableFuture<T> = apply { success(RunOnCallerThread, body) }
infix fun <T> ListenableFuture<T>.failure(body: (Throwable) -> Unit): ListenableFuture<T> = apply { failure(RunOnCallerThread, body) }
infix fun <F, T> ListenableFuture<F>.map(mapper: (F) -> T): ListenableFuture<T> = Futures.transform(this, Function { mapper(it!!) })
@Suppress("UNCHECKED_CAST") // We need the awkward cast because otherwise F cannot be nullable, even though it's safe.
infix fun <F, T> ListenableFuture<F>.map(mapper: (F) -> T): ListenableFuture<T> = Futures.transform(this, { (mapper as (F?) -> T)(it) })
infix fun <F, T> ListenableFuture<F>.flatMap(mapper: (F) -> ListenableFuture<T>): ListenableFuture<T> = Futures.transformAsync(this) { mapper(it!!) }
/** Executes the given block and sets the future to either the result, or any exception that was thrown. */
inline fun <T> SettableFuture<T>.catch(block: () -> T) {
@ -152,7 +163,7 @@ fun Path.writeLines(lines: Iterable<CharSequence>, charset: Charset = UTF_8, var
fun InputStream.copyTo(target: Path, vararg options: CopyOption): Long = Files.copy(this, target, *options)
// Simple infix function to add back null safety that the JDK lacks: timeA until timeB
infix fun Temporal.until(endExclusive: Temporal) = Duration.between(this, endExclusive)
infix fun Temporal.until(endExclusive: Temporal): Duration = Duration.between(this, endExclusive)
/** Returns the index of the given item or throws [IllegalArgumentException] if not found. */
fun <T> List<T>.indexOfOrThrow(item: T): Int {
@ -271,11 +282,7 @@ class TransientProperty<out T>(private val initializer: () -> T) {
@Transient private var v: T? = null
@Synchronized
operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
if (v == null)
v = initializer()
return v!!
}
operator fun getValue(thisRef: Any?, property: KProperty<*>) = v ?: initializer().also { v = it }
}
/**
@ -308,11 +315,44 @@ fun extractZipFile(inputStream: InputStream, toDirectory: Path) {
}
}
/**
* Get a valid InputStream from an in-memory zip as required for tests.
* Note that a slightly bigger than numOfExpectedBytes size is expected.
*/
@Throws(IllegalArgumentException::class)
fun sizedInputStreamAndHash(numOfExpectedBytes: Int): InputStreamAndHash {
if (numOfExpectedBytes <= 0) throw IllegalArgumentException("A positive number of numOfExpectedBytes is required.")
val baos = ByteArrayOutputStream()
ZipOutputStream(baos).use({ zos ->
val arraySize = 1024
val bytes = ByteArray(arraySize)
val n = (numOfExpectedBytes - 1) / arraySize + 1 // same as Math.ceil(numOfExpectedBytes/arraySize).
zos.setLevel(Deflater.NO_COMPRESSION)
zos.putNextEntry(ZipEntry("z"))
for (i in 0 until n) {
zos.write(bytes, 0, arraySize)
}
zos.closeEntry()
})
return getInputStreamAndHashFromOutputStream(baos)
}
/** Convert a [ByteArrayOutputStream] to [InputStreamAndHash]. */
fun getInputStreamAndHashFromOutputStream(baos: ByteArrayOutputStream): InputStreamAndHash {
// TODO: Consider converting OutputStream to InputStream without creating a ByteArray, probably using piped streams.
val bytes = baos.toByteArray()
// TODO: Consider calculating sha256 on the fly using a DigestInputStream.
return InputStreamAndHash(ByteArrayInputStream(bytes), bytes.sha256())
}
data class InputStreamAndHash(val inputStream: InputStream, val sha256: SecureHash.SHA256)
// TODO: Generic csv printing utility for clases.
val Throwable.rootCause: Throwable get() = Throwables.getRootCause(this)
/** Representation of an operation that may have thrown an error. */
@Suppress("DataClassPrivateConstructor")
@CordaSerializable
data class ErrorOr<out A> private constructor(val value: A?, val error: Throwable?) {
// The ErrorOr holds a value iff error == null
@ -364,6 +404,8 @@ data class ErrorOr<out A> private constructor(val value: A?, val error: Throwabl
ErrorOr.of(error)
}
}
fun mapError(function: (Throwable) -> Throwable) = ErrorOr(value, error?.let(function))
}
/**
@ -419,3 +461,9 @@ fun codePointsString(vararg codePoints: Int): String {
codePoints.forEach { builder.append(Character.toChars(it)) }
return builder.toString()
}
fun <T> Class<T>.checkNotUnorderedHashMap() {
if (HashMap::class.java.isAssignableFrom(this) && !LinkedHashMap::class.java.isAssignableFrom(this)) {
throw NotSerializableException("Map type $this is unstable under iteration. Suggested fix: use LinkedHashMap instead.")
}
}

View File

@ -2,8 +2,9 @@
package net.corda.core.contracts
import net.corda.core.crypto.CompositeKey
import net.corda.core.crypto.Party
import net.corda.core.identity.Party
import java.security.PublicKey
import java.math.BigDecimal
import java.util.*
/**
@ -31,11 +32,13 @@ fun commodity(code: String) = Commodity.getInstance(code)!!
@JvmField val RUB = currency("RUB")
@JvmField val FCOJ = commodity("FCOJ") // Frozen concentrated orange juice, yum!
fun DOLLARS(amount: Int): Amount<Currency> = Amount(amount.toLong() * 100, USD)
fun DOLLARS(amount: Double): Amount<Currency> = Amount((amount * 100).toLong(), USD)
fun POUNDS(amount: Int): Amount<Currency> = Amount(amount.toLong() * 100, GBP)
fun SWISS_FRANCS(amount: Int): Amount<Currency> = Amount(amount.toLong() * 100, CHF)
fun FCOJ(amount: Int): Amount<Commodity> = Amount(amount.toLong() * 100, FCOJ)
fun <T : Any> AMOUNT(amount: Int, token: T): Amount<T> = Amount.fromDecimal(BigDecimal.valueOf(amount.toLong()), token)
fun <T : Any> AMOUNT(amount: Double, token: T): Amount<T> = Amount.fromDecimal(BigDecimal.valueOf(amount), token)
fun DOLLARS(amount: Int): Amount<Currency> = AMOUNT(amount, USD)
fun DOLLARS(amount: Double): Amount<Currency> = AMOUNT(amount, USD)
fun POUNDS(amount: Int): Amount<Currency> = AMOUNT(amount, GBP)
fun SWISS_FRANCS(amount: Int): Amount<Currency> = AMOUNT(amount, CHF)
fun FCOJ(amount: Int): Amount<Commodity> = AMOUNT(amount, FCOJ)
val Int.DOLLARS: Amount<Currency> get() = DOLLARS(this)
val Double.DOLLARS: Amount<Currency> get() = DOLLARS(this)
@ -48,15 +51,22 @@ infix fun Commodity.`issued by`(deposit: PartyAndReference) = issuedBy(deposit)
infix fun Amount<Currency>.`issued by`(deposit: PartyAndReference) = issuedBy(deposit)
infix fun Currency.issuedBy(deposit: PartyAndReference) = Issued(deposit, this)
infix fun Commodity.issuedBy(deposit: PartyAndReference) = Issued(deposit, this)
infix fun Amount<Currency>.issuedBy(deposit: PartyAndReference) = Amount(quantity, token.issuedBy(deposit))
infix fun Amount<Currency>.issuedBy(deposit: PartyAndReference) = Amount(quantity, displayTokenSize, token.issuedBy(deposit))
//// Requirements /////////////////////////////////////////////////////////////////////////////////////////////////////
object Requirements {
@Suppress("NOTHING_TO_INLINE") // Inlining this takes it out of our committed ABI.
infix inline fun String.by(expr: Boolean) {
infix inline fun String.using(expr: Boolean) {
if (!expr) throw IllegalArgumentException("Failed requirement: $this")
}
// Avoid overloading Kotlin keywords
@Deprecated("This function is deprecated, use 'using' instead",
ReplaceWith("using (expr)", "net.corda.core.contracts.Requirements.using"))
@Suppress("NOTHING_TO_INLINE") // Inlining this takes it out of our committed ABI.
infix inline fun String.by(expr: Boolean) {
using(expr)
}
}
inline fun <R> requireThat(body: Requirements.() -> R) = Requirements.body()
@ -66,7 +76,7 @@ inline fun <R> requireThat(body: Requirements.() -> R) = Requirements.body()
// TODO: Provide a version of select that interops with Java
/** Filters the command list by type, party and public key all at once. */
inline fun <reified T : CommandData> Collection<AuthenticatedObject<CommandData>>.select(signer: CompositeKey? = null,
inline fun <reified T : CommandData> Collection<AuthenticatedObject<CommandData>>.select(signer: PublicKey? = null,
party: Party? = null) =
filter { it.value is T }.
filter { if (signer == null) true else signer in it.signers }.
@ -76,7 +86,7 @@ inline fun <reified T : CommandData> Collection<AuthenticatedObject<CommandData>
// TODO: Provide a version of select that interops with Java
/** Filters the command list by type, parties and public keys all at once. */
inline fun <reified T : CommandData> Collection<AuthenticatedObject<CommandData>>.select(signers: Collection<CompositeKey>?,
inline fun <reified T : CommandData> Collection<AuthenticatedObject<CommandData>>.select(signers: Collection<PublicKey>?,
parties: Collection<Party>?) =
filter { it.value is T }.
filter { if (signers == null) true else it.signers.containsAll(signers) }.
@ -93,18 +103,6 @@ inline fun <reified T : CommandData> Collection<AuthenticatedObject<CommandData>
fun <C : CommandData> Collection<AuthenticatedObject<CommandData>>.requireSingleCommand(klass: Class<C>) =
mapNotNull { @Suppress("UNCHECKED_CAST") if (klass.isInstance(it.value)) it as AuthenticatedObject<C> else null }.single()
/**
* Simple functionality for verifying a move command. Verifies that each input has a signature from its owning key.
*
* @param T the type of the move command.
*/
@Throws(IllegalArgumentException::class)
// TODO: Can we have a common Move command for all contracts and avoid the reified type parameter here?
inline fun <reified T : MoveCommand> verifyMoveCommand(inputs: List<OwnableState>,
tx: TransactionForContract)
: MoveCommand
= verifyMoveCommand<T>(inputs, tx.commands)
/**
* Simple functionality for verifying a move command. Verifies that each input has a signature from its owning key.
*
@ -121,7 +119,7 @@ inline fun <reified T : MoveCommand> verifyMoveCommand(inputs: List<OwnableState
val command = commands.requireSingleCommand<T>()
val keysThatSigned = command.signers.toSet()
requireThat {
"the owning keys are a subset of the signing keys" by keysThatSigned.containsAll(owningPubKeys)
"the owning keys are a subset of the signing keys" using keysThatSigned.containsAll(owningPubKeys)
}
return command.value
}

View File

@ -1,9 +1,9 @@
package net.corda.core.contracts
import net.corda.core.crypto.CompositeKey
import net.corda.core.crypto.Party
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.Party
import net.corda.core.transactions.TransactionBuilder
import java.security.PublicKey
// The dummy contract doesn't do anything useful. It exists for testing purposes.
@ -14,12 +14,12 @@ data class DummyContract(override val legalContractReference: SecureHash = Secur
val magicNumber: Int
}
data class SingleOwnerState(override val magicNumber: Int = 0, override val owner: CompositeKey) : OwnableState, State {
data class SingleOwnerState(override val magicNumber: Int = 0, override val owner: PublicKey) : OwnableState, State {
override val contract = DUMMY_PROGRAM_ID
override val participants: List<CompositeKey>
override val participants: List<PublicKey>
get() = listOf(owner)
override fun withNewOwner(newOwner: CompositeKey) = Pair(Commands.Move(), copy(owner = newOwner))
override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner))
}
/**
@ -28,9 +28,9 @@ data class DummyContract(override val legalContractReference: SecureHash = Secur
* in a different field, however this is a good example of a contract with multiple states.
*/
data class MultiOwnerState(override val magicNumber: Int = 0,
val owners: List<CompositeKey>) : ContractState, State {
val owners: List<PublicKey>) : ContractState, State {
override val contract = DUMMY_PROGRAM_ID
override val participants: List<CompositeKey> get() = owners
override val participants: List<PublicKey> get() = owners
}
interface Commands : CommandData {
@ -55,8 +55,8 @@ data class DummyContract(override val legalContractReference: SecureHash = Secur
}
}
fun move(prior: StateAndRef<DummyContract.SingleOwnerState>, newOwner: CompositeKey) = move(listOf(prior), newOwner)
fun move(priors: List<StateAndRef<DummyContract.SingleOwnerState>>, newOwner: CompositeKey): TransactionBuilder {
fun move(prior: StateAndRef<DummyContract.SingleOwnerState>, newOwner: PublicKey) = move(listOf(prior), newOwner)
fun move(priors: List<StateAndRef<DummyContract.SingleOwnerState>>, newOwner: PublicKey): TransactionBuilder {
require(priors.isNotEmpty())
val priorState = priors[0].state.data
val (cmd, state) = priorState.withNewOwner(newOwner)

View File

@ -1,9 +1,9 @@
package net.corda.core.contracts
import net.corda.core.crypto.CompositeKey
import net.corda.core.crypto.SecureHash
import net.corda.core.transactions.WireTransaction
import net.corda.flows.ContractUpgradeFlow
import java.security.PublicKey
// The dummy contract doesn't do anything useful. It exists for testing purposes.
val DUMMY_V2_PROGRAM_ID = DummyContractV2()
@ -11,12 +11,13 @@ val DUMMY_V2_PROGRAM_ID = DummyContractV2()
/**
* Dummy contract state for testing of the upgrade process.
*/
// DOCSTART 1
class DummyContractV2 : UpgradedContract<DummyContract.State, DummyContractV2.State> {
override val legacyContract = DummyContract::class.java
data class State(val magicNumber: Int = 0, val owners: List<CompositeKey>) : ContractState {
data class State(val magicNumber: Int = 0, val owners: List<PublicKey>) : ContractState {
override val contract = DUMMY_V2_PROGRAM_ID
override val participants: List<CompositeKey> = owners
override val participants: List<PublicKey> = owners
}
interface Commands : CommandData {
@ -35,7 +36,7 @@ class DummyContractV2 : UpgradedContract<DummyContract.State, DummyContractV2.St
// The "empty contract"
override val legalContractReference: SecureHash = SecureHash.sha256("")
// DOCEND 1
/**
* Generate an upgrade transaction from [DummyContract].
*
@ -43,7 +44,7 @@ class DummyContractV2 : UpgradedContract<DummyContract.State, DummyContractV2.St
*
* @return a pair of wire transaction, and a set of those who should sign the transaction for it to be valid.
*/
fun generateUpgradeFromV1(vararg states: StateAndRef<DummyContract.State>): Pair<WireTransaction, Set<CompositeKey>> {
fun generateUpgradeFromV1(vararg states: StateAndRef<DummyContract.State>): Pair<WireTransaction, Set<PublicKey>> {
val notary = states.map { it.state.notary }.single()
require(states.isNotEmpty())

View File

@ -1,12 +1,12 @@
package net.corda.core.contracts
import net.corda.core.crypto.CompositeKey
import java.security.PublicKey
/**
* Dummy state for use in testing. Not part of any contract, not even the [DummyContract].
*/
data class DummyState(val magicNumber: Int = 0) : ContractState {
override val contract = DUMMY_PROGRAM_ID
override val participants: List<CompositeKey>
override val participants: List<PublicKey>
get() = emptyList()
}

View File

@ -11,16 +11,25 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize
import com.google.common.annotations.VisibleForTesting
import net.corda.core.serialization.CordaSerializable
import java.math.BigDecimal
import java.math.BigInteger
import java.math.RoundingMode
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.util.*
/**
* This interface is used by [Amount] to determine the conversion ratio from
* indicative/displayed asset amounts in [BigDecimal] to fungible tokens represented by Amount objects.
*/
interface TokenizableAssetInfo {
val displayTokenSize: BigDecimal
}
/**
* Amount represents a positive quantity of some token (currency, asset, etc.), measured in quantity of the smallest
* representable units. Note that quantity is not necessarily 1/100ths of a currency unit, but are the actual smallest
* amount used in whatever underlying thing the amount represents.
* representable units. The nominal quantity represented by each individual token is equal to the [displayTokenSize].
* The scale property of the [displayTokenSize] should correctly reflect the displayed decimal places and is used
* when rounding conversions from indicative/displayed amounts in [BigDecimal] to Amount occur via the Amount.fromDecimal method.
*
* Amounts of different tokens *do not mix* and attempting to add or subtract two amounts of different currencies
* will throw [IllegalArgumentException]. Amounts may not be negative. Amounts are represented internally using a signed
@ -28,25 +37,62 @@ import java.util.*
* multiplication are overflow checked and will throw [ArithmeticException] if the operation would have caused integer
* overflow.
*
* TODO: It may make sense to replace this with convenience extensions over the JSR 354 MonetaryAmount interface,
* in particular for use during calculations. This may also resolve...
* TODO: Think about how positive-only vs positive-or-negative amounts can be represented in the type system.
* TODO: Add either a scaling factor, or a variant for use in calculations.
*
* @param quantity the number of tokens as a Long value.
* @param displayTokenSize the nominal display unit size of a single token,
* potentially with trailing decimal display places if the scale parameter is non-zero.
* @param T the type of the token, for example [Currency].
* T should implement TokenizableAssetInfo if automatic conversion to/from a display format is required.
*
* TODO Proper lookup of currencies in a locale and context sensitive fashion is not supported and is left to the application.
*/
@CordaSerializable
data class Amount<T>(val quantity: Long, val token: T) : Comparable<Amount<T>> {
data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal, val token: T) : Comparable<Amount<T>> {
companion object {
/**
* Build a currency amount from a decimal representation. For example, with an input of "12.34" GBP,
* returns an amount with a quantity of "1234".
* Build an Amount from a decimal representation. For example, with an input of "12.34 GBP",
* returns an amount with a quantity of "1234" tokens. The displayTokenSize as determined via
* getDisplayTokenSize is used to determine the conversion scaling.
* e.g. Bonds might be in nominal amounts of 100, currencies in 0.01 penny units.
*
* @see Amount<Currency>.toDecimal
* @throws ArithmeticException if the intermediate calculations cannot be converted to an unsigned 63-bit token amount.
*/
fun fromDecimal(quantity: BigDecimal, currency: Currency) : Amount<Currency> {
val longQuantity = quantity.movePointRight(currency.defaultFractionDigits).toLong()
return Amount(longQuantity, currency)
@JvmStatic
@JvmOverloads
fun <T : Any> fromDecimal(displayQuantity: BigDecimal, token: T, rounding: RoundingMode = RoundingMode.FLOOR): Amount<T> {
val tokenSize = getDisplayTokenSize(token)
val tokenCount = displayQuantity.divide(tokenSize).setScale(0, rounding).longValueExact()
return Amount(tokenCount, tokenSize, token)
}
/**
* For a particular token returns a zero sized Amount<T>
*/
@JvmStatic
fun <T : Any> zero(token: T): Amount<T> {
val tokenSize = getDisplayTokenSize(token)
return Amount(0L, tokenSize, token)
}
/**
* Determines the representation of one Token quantity in BigDecimal. For Currency and Issued<Currency>
* the definitions is taken from Currency defaultFractionDigits property e.g. 2 for USD, or 0 for JPY
* so that the automatic token size is the conventional minimum penny amount.
* For other possible token types the asset token should implement TokenizableAssetInfo to
* correctly report the designed nominal amount.
*/
fun getDisplayTokenSize(token: Any): BigDecimal {
if (token is TokenizableAssetInfo) {
return token.displayTokenSize
}
if (token is Currency) {
return BigDecimal.ONE.scaleByPowerOfTen(-token.defaultFractionDigits)
}
if (token is Issued<*>) {
return getDisplayTokenSize(token.product)
}
return BigDecimal.ONE
}
private val currencySymbols: Map<String, Currency> = mapOf(
@ -111,44 +157,93 @@ data class Amount<T>(val quantity: Long, val token: T) : Comparable<Amount<T>> {
}
init {
// Negative amounts are of course a vital part of any ledger, but negative values are only valid in certain
// contexts: you cannot send a negative amount of cash, but you can (sometimes) have a negative balance.
// If you want to express a negative amount, for now, use a long.
// Amount represents a static balance of physical assets as managed by the distributed ledger and is not allowed
// to become negative a rule further maintained by the Contract verify method.
// N.B. If concepts such as an account overdraft are required this should be modelled separately via Obligations,
// or similar second order smart contract concepts.
require(quantity >= 0) { "Negative amounts are not allowed: $quantity" }
}
/**
* Construct the amount using the given decimal value as quantity. Any fractional part
* is discarded. To convert and use the fractional part, see [fromDecimal].
* Automatic conversion constructor from number of tokens to an Amount using getDisplayTokenSize to determine
* the displayTokenSize.
*
* @param tokenQuantity the number of tokens represented.
* @param token the type of the token, for example a [Currency] object.
*/
constructor(quantity: BigDecimal, token: T) : this(quantity.toLong(), token)
constructor(quantity: BigInteger, token: T) : this(quantity.toLong(), token)
constructor(tokenQuantity: Long, token: T) : this(tokenQuantity, getDisplayTokenSize(token), token)
/**
* A checked addition operator is supported to simplify aggregation of Amounts.
* @throws ArithmeticException if there is overflow of Amount tokens during the summation
* Mixing non-identical token types will throw [IllegalArgumentException]
*/
operator fun plus(other: Amount<T>): Amount<T> {
checkToken(other)
return Amount(Math.addExact(quantity, other.quantity), token)
return Amount(Math.addExact(quantity, other.quantity), displayTokenSize, token)
}
/**
* A checked addition operator is supported to simplify netting of Amounts.
* If this leads to the Amount going negative this will throw [IllegalArgumentException].
* @throws ArithmeticException if there is Numeric underflow
* Mixing non-identical token types will throw [IllegalArgumentException]
*/
operator fun minus(other: Amount<T>): Amount<T> {
checkToken(other)
return Amount(Math.subtractExact(quantity, other.quantity), token)
return Amount(Math.subtractExact(quantity, other.quantity), displayTokenSize, token)
}
private fun checkToken(other: Amount<T>) {
require(other.token == token) { "Token mismatch: ${other.token} vs $token" }
require(other.displayTokenSize == displayTokenSize) { "Token size mismatch: ${other.displayTokenSize} vs $displayTokenSize" }
}
operator fun div(other: Long): Amount<T> = Amount(quantity / other, token)
operator fun times(other: Long): Amount<T> = Amount(Math.multiplyExact(quantity, other), token)
operator fun div(other: Int): Amount<T> = Amount(quantity / other, token)
operator fun times(other: Int): Amount<T> = Amount(Math.multiplyExact(quantity, other.toLong()), token)
/**
* The multiplication operator is supported to allow easy calculation for multiples of a primitive Amount.
* Note this is not a conserving operation, so it may not always be correct modelling of proper token behaviour.
* N.B. Division is not supported as fractional tokens are not representable by an Amount.
*/
operator fun times(other: Long): Amount<T> = Amount(Math.multiplyExact(quantity, other), displayTokenSize, token)
operator fun times(other: Int): Amount<T> = Amount(Math.multiplyExact(quantity, other.toLong()), displayTokenSize, token)
/**
* This method provides a token conserving divide mechanism.
* @param partitions the number of amounts to divide the current quantity into.
* @result Returns [partitions] separate Amount objects which sum to the same quantity as this Amount
* and differ by no more than a single token in size.
*/
fun splitEvenly(partitions: Int): List<Amount<T>> {
require(partitions >= 1) { "Must split amount into one, or more pieces" }
val commonTokensPerPartition = quantity.div(partitions)
val residualTokens = quantity - (commonTokensPerPartition * partitions)
val splitAmount = Amount(commonTokensPerPartition, displayTokenSize, token)
val splitAmountPlusOne = Amount(commonTokensPerPartition + 1L, displayTokenSize, token)
return (0..partitions - 1).map { if (it < residualTokens) splitAmountPlusOne else splitAmount }.toList()
}
/**
* Convert a currency [Amount] to a decimal representation. For example, with an amount with a quantity
* of "1234" GBP, returns "12.34". The precise representation is controlled by the displayTokenSize,
* which determines the size of a single token and controls the trailing decimal places via it's scale property.
*
* @see Amount.Companion.fromDecimal
*/
fun toDecimal(): BigDecimal = BigDecimal.valueOf(quantity, 0) * displayTokenSize
/**
* Convert a currency [Amount] to a display string representation.
*
* For example, with an amount with a quantity of "1234" GBP, returns "12.34 GBP".
* The result of fromDecimal is used to control the numerical formatting and
* the token specifier appended is taken from token.toString.
*
* @see Amount.Companion.fromDecimal
*/
override fun toString(): String {
val bd = if (token is Currency)
BigDecimal(quantity).movePointLeft(token.defaultFractionDigits)
else
BigDecimal(quantity)
return bd.toPlainString() + " " + token
return toDecimal().toPlainString() + " " + token
}
override fun compareTo(other: Amount<T>): Int {
@ -157,17 +252,206 @@ data class Amount<T>(val quantity: Long, val token: T) : Comparable<Amount<T>> {
}
}
/**
* Convert a currency [Amount] to a decimal representation. For example, with an amount with a quantity
* of "1234" GBP, returns "12.34".
*
* @see Amount.Companion.fromDecimal
*/
fun Amount<Currency>.toDecimal() : BigDecimal = BigDecimal(quantity).movePointLeft(token.defaultFractionDigits)
fun <T> Iterable<Amount<T>>.sumOrNull() = if (!iterator().hasNext()) null else sumOrThrow()
fun <T> Iterable<Amount<T>>.sumOrThrow() = reduce { left, right -> left + right }
fun <T> Iterable<Amount<T>>.sumOrZero(currency: T) = if (iterator().hasNext()) sumOrThrow() else Amount(0, currency)
fun <T : Any> Iterable<Amount<T>>.sumOrNull() = if (!iterator().hasNext()) null else sumOrThrow()
fun <T : Any> Iterable<Amount<T>>.sumOrThrow() = reduce { left, right -> left + right }
fun <T : Any> Iterable<Amount<T>>.sumOrZero(token: T) = if (iterator().hasNext()) sumOrThrow() else Amount.zero(token)
/**
* Simple data class to associate the origin, owner, or holder of a particular Amount object.
* @param source the holder of the Amount.
* @param amount the Amount of asset available.
* @param ref is an optional field used for housekeeping in the caller.
* e.g. to point back at the original Vault state objects.
* @see SourceAndAmount.apply which processes a list of SourceAndAmount objects
* and calculates the resulting Amount distribution as a new list of SourceAndAmount objects.
*/
data class SourceAndAmount<T : Any, out P : Any>(val source: P, val amount: Amount<T>, val ref: Any? = null)
/**
* This class represents a possibly negative transfer of tokens from one vault state to another, possibly at a future date.
*
* @param quantityDelta is a signed Long value representing the exchanged number of tokens. If positive then
* it represents the movement of Math.abs(quantityDelta) tokens away from source and receipt of Math.abs(quantityDelta)
* at the destination. If the quantityDelta is negative then the source will receive Math.abs(quantityDelta) tokens
* and the destination will lose Math.abs(quantityDelta) tokens.
* Where possible the source and destination should be coded to ensure a positive quantityDelta,
* but in various scenarios it may be more consistent to allow positive and negative values.
* For example it is common for a bank to code asset flows as gains and losses from its perspective i.e. always the destination.
* @param token represents the type of asset token as would be used to construct Amount<T> objects.
* @param source is the [Party], [Account], [CompositeKey], or other identifier of the token source if quantityDelta is positive,
* or the token sink if quantityDelta is negative. The type P should support value equality.
* @param destination is the [Party], [Account], [CompositeKey], or other identifier of the token sink if quantityDelta is positive,
* or the token source if quantityDelta is negative. The type P should support value equality.
*/
@CordaSerializable
class AmountTransfer<T : Any, P : Any>(val quantityDelta: Long,
val token: T,
val source: P,
val destination: P) {
companion object {
/**
* Construct an AmountTransfer object from an indicative/displayable BigDecimal source, applying rounding as specified.
* The token size is determined from the token type and is the same as for [Amount] of the same token.
* @param displayQuantityDelta is the signed amount to transfer between source and destination in displayable units.
* Positive values mean transfers from source to destination. Negative values mean transfers from destination to source.
* @param token defines the asset being represented in the transfer. The token should implement [TokenizableAssetInfo] if custom
* conversion logic is required.
* @param source The payer of the transfer if displayQuantityDelta is positive, the payee if displayQuantityDelta is negative
* @param destination The payee of the transfer if displayQuantityDelta is positive, the payer if displayQuantityDelta is negative
* @param rounding The mode of rounding to apply after scaling to integer token units.
*/
@JvmStatic
@JvmOverloads
fun <T : Any, P : Any> fromDecimal(displayQuantityDelta: BigDecimal,
token: T,
source: P,
destination: P,
rounding: RoundingMode = RoundingMode.DOWN): AmountTransfer<T, P> {
val tokenSize = Amount.getDisplayTokenSize(token)
val deltaTokenCount = displayQuantityDelta.divide(tokenSize).setScale(0, rounding).longValueExact()
return AmountTransfer(deltaTokenCount, token, source, destination)
}
/**
* Helper to make a zero size AmountTransfer
*/
@JvmStatic
fun <T : Any, P : Any> zero(token: T,
source: P,
destination: P): AmountTransfer<T, P> = AmountTransfer(0L, token, source, destination)
}
init {
require(source != destination) { "The source and destination cannot be the same ($source)" }
}
/**
* Add together two [AmountTransfer] objects to produce the single equivalent net flow.
* The addition only applies to AmountTransfer objects with the same token type.
* Also the pair of parties must be aligned, although source destination may be
* swapped in the second item.
* @throws ArithmeticException if there is underflow, or overflow in the summations.
*/
operator fun plus(other: AmountTransfer<T, P>): AmountTransfer<T, P> {
require(other.token == token) { "Token mismatch: ${other.token} vs $token" }
require((other.source == source && other.destination == destination)
|| (other.source == destination && other.destination == source)) {
"Only AmountTransfer between the same two parties can be aggregated/netted"
}
return if (other.source == source) {
AmountTransfer(Math.addExact(quantityDelta, other.quantityDelta), token, source, destination)
} else {
AmountTransfer(Math.subtractExact(quantityDelta, other.quantityDelta), token, source, destination)
}
}
/**
* Convert the quantityDelta to a displayable format BigDecimal value. The conversion ratio is the same as for
* [Amount] of the same token type.
*/
fun toDecimal(): BigDecimal = BigDecimal.valueOf(quantityDelta, 0) * Amount.getDisplayTokenSize(token)
fun copy(quantityDelta: Long = this.quantityDelta,
token: T = this.token,
source: P = this.source,
destination: P = this.destination): AmountTransfer<T, P> = AmountTransfer(quantityDelta, token, source, destination)
/**
* Checks value equality of AmountTransfer objects, but also matches the reversed source and destination equivalent.
*/
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other?.javaClass != javaClass) return false
other as AmountTransfer<*, *>
if (token != other.token) return false
if (source == other.source) {
if (destination != other.destination) return false
if (quantityDelta != other.quantityDelta) return false
return true
} else if (source == other.destination) {
if (destination != other.source) return false
if (quantityDelta != -other.quantityDelta) return false
return true
}
return false
}
/**
* HashCode ensures that reversed source and destination equivalents will hash to the same value.
*/
override fun hashCode(): Int {
var result = Math.abs(quantityDelta).hashCode() // ignore polarity reversed values
result = 31 * result + token.hashCode()
result = 31 * result + (source.hashCode() xor destination.hashCode()) // XOR to ensure the same hash for swapped source and destination
return result
}
override fun toString(): String {
return "Transfer from $source to $destination of ${this.toDecimal().toPlainString()} $token"
}
/**
* Novation is a common financial operation in which a bilateral exchange is modified so that the same
* relative asset exchange happens, but with each party exchanging versus a central counterparty, or clearing house.
*
* @param centralParty The central party to face the exchange against.
* @return Returns two new AmountTransfers each between one of the original parties and the centralParty.
* The net total exchange is the same as in the original input.
*/
fun novate(centralParty: P): Pair<AmountTransfer<T, P>, AmountTransfer<T, P>> = Pair(copy(destination = centralParty), copy(source = centralParty))
/**
* Applies this AmountTransfer to a list of [SourceAndAmount] objects representing balances.
* The list can be heterogeneous in terms of token types and parties, so long as there is sufficient balance
* of the correct token type held with the party paying for the transfer.
* @param balances The source list of [SourceAndAmount] objects containing the funds to satisfy the exchange.
* @param newRef An optional marker object which is attached to any new [SourceAndAmount] objects created in the output.
* i.e. To the new payment destination entry and to any residual change output.
* @return The returned list is a copy of the original list, except that funds needed to cover the exchange
* will have been removed and a new output and possibly residual amount entry will be added at the end of the list.
* @throws ArithmeticException if there is underflow in the summations.
*/
fun apply(balances: List<SourceAndAmount<T, P>>, newRef: Any? = null): List<SourceAndAmount<T, P>> {
val (payer, payee) = if (quantityDelta >= 0L) Pair(source, destination) else Pair(destination, source)
val transfer = Math.abs(quantityDelta)
var residual = transfer
val outputs = mutableListOf<SourceAndAmount<T, P>>()
var remaining: SourceAndAmount<T, P>? = null
var newAmount: SourceAndAmount<T, P>? = null
for (balance in balances) {
if (balance.source != payer
|| balance.amount.token != token
|| residual == 0L) {
// Just copy across unmodified.
outputs += balance
} else if (balance.amount.quantity < residual) {
// Consume the payers amount and do not copy across.
residual -= balance.amount.quantity
} else {
// Calculate any residual spend left on the payers balance.
if (balance.amount.quantity > residual) {
remaining = SourceAndAmount(payer, balance.amount.copy(quantity = Math.subtractExact(balance.amount.quantity, residual)), newRef)
}
// Build the new output payment to the payee.
newAmount = SourceAndAmount(payee, balance.amount.copy(quantity = transfer), newRef)
// Clear the residual.
residual = 0L
}
}
require(residual == 0L) { "Insufficient funds. Unable to process $this" }
if (remaining != null) {
outputs += remaining
}
outputs += newAmount!!
return outputs
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
@ -232,7 +516,7 @@ data class Tenor(val name: String) {
val adjustedMaturityDate = calendar.applyRollConvention(maturityDate, DateRollConvention.ModifiedFollowing)
val daysToMaturity = calculateDaysBetween(startDate, adjustedMaturityDate, DayCountBasisYear.Y360, DayCountBasisDay.DActual)
return daysToMaturity.toInt()
return daysToMaturity
}
override fun toString(): String = name
@ -265,43 +549,25 @@ enum class DateRollDirection(val value: Long) { FORWARD(1), BACKWARD(-1) }
* There are some additional rules which are explained in the individual cases below.
*/
@CordaSerializable
enum class DateRollConvention {
enum class DateRollConvention(val direction: () -> DateRollDirection, val isModified: Boolean) {
// direction() cannot be a val due to the throw in the Actual instance
/** Don't roll the date, use the one supplied. */
Actual {
override fun direction(): DateRollDirection = throw UnsupportedOperationException("Direction is not relevant for convention Actual")
override val isModified: Boolean = false
},
Actual({ throw UnsupportedOperationException("Direction is not relevant for convention Actual") }, false),
/** Following is the next business date from this one. */
Following {
override fun direction(): DateRollDirection = DateRollDirection.FORWARD
override val isModified: Boolean = false
},
Following({ DateRollDirection.FORWARD }, false),
/**
* "Modified following" is the next business date, unless it's in the next month, in which case use the preceeding
* business date.
*/
ModifiedFollowing {
override fun direction(): DateRollDirection = DateRollDirection.FORWARD
override val isModified: Boolean = true
},
ModifiedFollowing({ DateRollDirection.FORWARD }, true),
/** Previous is the previous business date from this one. */
Previous {
override fun direction(): DateRollDirection = DateRollDirection.BACKWARD
override val isModified: Boolean = false
},
Previous({ DateRollDirection.BACKWARD }, false),
/**
* Modified previous is the previous business date, unless it's in the previous month, in which case use the next
* business date.
*/
ModifiedPrevious {
override fun direction(): DateRollDirection = DateRollDirection.BACKWARD
override val isModified: Boolean = true
};
abstract fun direction(): DateRollDirection
abstract val isModified: Boolean
ModifiedPrevious({ DateRollDirection.BACKWARD }, true);
}
@ -345,31 +611,14 @@ enum class PaymentRule {
*/
@Suppress("unused") // TODO: Revisit post-Vega and see if annualCompoundCount is still needed.
@CordaSerializable
enum class Frequency(val annualCompoundCount: Int) {
Annual(1) {
override fun offset(d: LocalDate, n: Long) = d.plusYears(1 * n)
},
SemiAnnual(2) {
override fun offset(d: LocalDate, n: Long) = d.plusMonths(6 * n)
},
Quarterly(4) {
override fun offset(d: LocalDate, n: Long) = d.plusMonths(3 * n)
},
Monthly(12) {
override fun offset(d: LocalDate, n: Long) = d.plusMonths(1 * n)
},
Weekly(52) {
override fun offset(d: LocalDate, n: Long) = d.plusWeeks(1 * n)
},
BiWeekly(26) {
override fun offset(d: LocalDate, n: Long) = d.plusWeeks(2 * n)
},
Daily(365) {
override fun offset(d: LocalDate, n: Long) = d.plusDays(1 * n)
};
abstract fun offset(d: LocalDate, n: Long = 1): LocalDate
// Daily() // Let's not worry about this for now.
enum class Frequency(val annualCompoundCount: Int, val offset: LocalDate.(Long) -> LocalDate) {
Annual(1, { plusYears(1 * it) }),
SemiAnnual(2, { plusMonths(6 * it) }),
Quarterly(4, { plusMonths(3 * it) }),
Monthly(12, { plusMonths(1 * it) }),
Weekly(52, { plusWeeks(1 * it) }),
BiWeekly(26, { plusWeeks(2 * it) }),
Daily(365, { plusDays(1 * it) });
}
@ -396,7 +645,7 @@ open class BusinessCalendar private constructor(val holidayDates: List<LocalDate
}.toMap()
/** Parses a date of the form YYYY-MM-DD, like 2016-01-10 for 10th Jan. */
fun parseDateFromString(it: String) = LocalDate.parse(it, DateTimeFormatter.ISO_LOCAL_DATE)
fun parseDateFromString(it: String): LocalDate = LocalDate.parse(it, DateTimeFormatter.ISO_LOCAL_DATE)
/** Returns a business calendar that combines all the named holiday calendars into one list of holiday dates. */
fun getInstance(vararg calname: String) = BusinessCalendar(
@ -546,7 +795,10 @@ enum class NetType {
@CordaSerializable
data class Commodity(val commodityCode: String,
val displayName: String,
val defaultFractionDigits: Int = 0) {
val defaultFractionDigits: Int = 0) : TokenizableAssetInfo {
override val displayTokenSize: BigDecimal
get() = BigDecimal.ONE.scaleByPowerOfTen(-defaultFractionDigits)
companion object {
private val registry = mapOf(
// Simple example commodity, as in http://www.investopedia.com/university/commodities/commodities14.asp

View File

@ -1,7 +1,12 @@
package net.corda.core.contracts
import net.corda.core.crypto.CompositeKey
import net.corda.core.identity.Party
import net.corda.core.flows.FlowException
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.loggerFor
import net.corda.core.utilities.trace
import java.security.PublicKey
import java.util.*
class InsufficientBalanceException(val amountMissing: Amount<*>) : FlowException("Insufficient balance, missing $amountMissing")
@ -19,17 +24,17 @@ class InsufficientBalanceException(val amountMissing: Amount<*>) : FlowException
* @param T a type that represents the asset in question. This should describe the basic type of the asset
* (GBP, USD, oil, shares in company <X>, etc.) and any additional metadata (issuer, grade, class, etc.).
*/
interface FungibleAsset<T> : OwnableState {
interface FungibleAsset<T : Any> : OwnableState {
val amount: Amount<Issued<T>>
/**
* There must be an ExitCommand signed by these keys to destroy the amount. While all states require their
* owner to sign, some (i.e. cash) also require the issuer.
*/
val exitKeys: Collection<CompositeKey>
val exitKeys: Collection<PublicKey>
/** There must be a MoveCommand signed by this key to claim the amount */
override val owner: CompositeKey
override val owner: PublicKey
fun move(newAmount: Amount<Issued<T>>, newOwner: CompositeKey): FungibleAsset<T>
fun move(newAmount: Amount<Issued<T>>, newOwner: PublicKey): FungibleAsset<T>
// Just for grouping
interface Commands : CommandData {
@ -45,7 +50,7 @@ interface FungibleAsset<T> : OwnableState {
* A command stating that money has been withdrawn from the shared ledger and is now accounted for
* in some other way.
*/
interface Exit<T> : Commands {
interface Exit<T : Any> : Commands {
val amount: Amount<Issued<T>>
}
}
@ -54,8 +59,8 @@ interface FungibleAsset<T> : OwnableState {
// Small DSL extensions.
/** Sums the asset states in the list, returning null if there are none. */
fun <T> Iterable<ContractState>.sumFungibleOrNull() = filterIsInstance<FungibleAsset<T>>().map { it.amount }.sumOrNull()
fun <T : Any> Iterable<ContractState>.sumFungibleOrNull() = filterIsInstance<FungibleAsset<T>>().map { it.amount }.sumOrNull()
/** Sums the asset states in the list, returning zero of the given token if there are none. */
fun <T> Iterable<ContractState>.sumFungibleOrZero(token: Issued<T>) = filterIsInstance<FungibleAsset<T>>().map { it.amount }.sumOrZero(token)
fun <T : Any> Iterable<ContractState>.sumFungibleOrZero(token: Issued<T>) = filterIsInstance<FungibleAsset<T>>().map { it.amount }.sumOrZero(token)

View File

@ -1,18 +1,16 @@
package net.corda.core.contracts
import net.corda.core.contracts.clauses.Clause
import net.corda.core.crypto.AnonymousParty
import net.corda.core.crypto.CompositeKey
import net.corda.core.crypto.Party
import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowLogicRef
import net.corda.core.flows.FlowLogicRefFactory
import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.Party
import net.corda.core.node.services.ServiceType
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.OpaqueBytes
import net.corda.core.serialization.serialize
import net.corda.core.serialization.*
import net.corda.core.transactions.TransactionBuilder
import java.io.FileNotFoundException
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.security.PublicKey
@ -53,7 +51,7 @@ interface MultilateralNettableState<out T : Any> {
val multilateralNetState: T
}
interface NettableState<N : BilateralNettableState<N>, T : Any> : BilateralNettableState<N>,
interface NettableState<N : BilateralNettableState<N>, out T : Any> : BilateralNettableState<N>,
MultilateralNettableState<T>
/**
@ -116,7 +114,7 @@ interface ContractState {
* The participants list should normally be derived from the contents of the state. E.g. for [Cash] the participants
* list should just contain the owner.
*/
val participants: List<CompositeKey>
val participants: List<PublicKey>
}
/**
@ -146,26 +144,13 @@ data class TransactionState<out T : ContractState> @JvmOverloads constructor(
* Note that an encumbered state that is being consumed must have its encumbrance consumed in the same transaction,
* otherwise the transaction is not valid.
*/
val encumbrance: Int? = null) {
/**
* Copies the underlying state, replacing the notary field with the new value.
* To replace the notary, we need an approval (signature) from _all_ participants of the [ContractState].
*/
fun withNotary(newNotary: Party) = TransactionState(this.data, newNotary, encumbrance)
}
val encumbrance: Int? = null)
/** Wraps the [ContractState] in a [TransactionState] object */
infix fun <T : ContractState> T.`with notary`(newNotary: Party) = withNotary(newNotary)
infix fun <T : ContractState> T.withNotary(newNotary: Party) = TransactionState(this, newNotary)
/**
* Marker interface for data classes that represent the issuance state for a contract. These are intended as templates
* from which the state object is initialised.
*/
interface IssuanceDefinition
/**
* Definition for an issued product, which can be cash, a cash-like thing, assets, or generally anything else that's
* quantifiable with integer quantities.
@ -173,7 +158,7 @@ interface IssuanceDefinition
* @param P the type of product underlying the definition, for example [Currency].
*/
@CordaSerializable
data class Issued<out P>(val issuer: PartyAndReference, val product: P) {
data class Issued<out P : Any>(val issuer: PartyAndReference, val product: P) {
override fun toString() = "$product issued by $issuer"
}
@ -182,17 +167,17 @@ data class Issued<out P>(val issuer: PartyAndReference, val product: P) {
* cares about specific issuers with code that will accept any, or which is imposing issuer constraints via some
* other mechanism and the additional type safety is not wanted.
*/
fun <T> Amount<Issued<T>>.withoutIssuer(): Amount<T> = Amount(quantity, token.product)
fun <T : Any> Amount<Issued<T>>.withoutIssuer(): Amount<T> = Amount(quantity, token.product)
/**
* A contract state that can have a single owner.
*/
interface OwnableState : ContractState {
/** There must be a MoveCommand signed by this key to claim the amount */
val owner: CompositeKey
val owner: PublicKey
/** Copies the underlying data structure, replacing the owner field with this new value and leaving the rest alone */
fun withNewOwner(newOwner: CompositeKey): Pair<CommandData, OwnableState>
fun withNewOwner(newOwner: PublicKey): Pair<CommandData, OwnableState>
}
/** Something which is scheduled to happen at a point in time */
@ -237,14 +222,14 @@ interface LinearState : ContractState {
/**
* True if this should be tracked by our vault(s).
* */
*/
fun isRelevant(ourKeys: Set<PublicKey>): Boolean
/**
* Standard clause to verify the LinearState safety properties.
*/
@CordaSerializable
class ClauseVerifier<S : LinearState, C : CommandData>() : Clause<S, C, Unit>() {
class ClauseVerifier<in S : LinearState, C : CommandData> : Clause<S, C, Unit>() {
override fun verify(tx: TransactionForContract,
inputs: List<S>,
outputs: List<S>,
@ -253,8 +238,8 @@ interface LinearState : ContractState {
val inputIds = inputs.map { it.linearId }.distinct()
val outputIds = outputs.map { it.linearId }.distinct()
requireThat {
"LinearStates are not merged" by (inputIds.count() == inputs.count())
"LinearStates are not split" by (outputIds.count() == outputs.count())
"LinearStates are not merged" using (inputIds.count() == inputs.count())
"LinearStates are not split" using (outputIds.count() == outputs.count())
}
return emptySet()
}
@ -360,7 +345,8 @@ inline fun <reified T : ContractState> Iterable<StateAndRef<ContractState>>.filt
@CordaSerializable
data class PartyAndReference(val party: AnonymousParty, val reference: OpaqueBytes) {
constructor(party: Party, reference: OpaqueBytes) : this(party.toAnonymous(), reference)
override fun toString() = "${party}$reference"
override fun toString() = "$party$reference"
}
/** Marker interface for classes that represent commands */
@ -375,12 +361,12 @@ abstract class TypeOnlyCommandData : CommandData {
/** Command data/content plus pubkey pair: the signature is stored at the end of the serialized bytes */
@CordaSerializable
data class Command(val value: CommandData, val signers: List<CompositeKey>) {
data class Command(val value: CommandData, val signers: List<PublicKey>) {
init {
require(signers.isNotEmpty())
}
constructor(data: CommandData, key: CompositeKey) : this(data, listOf(key))
constructor(data: CommandData, key: PublicKey) : this(data, listOf(key))
private fun commandDataToString() = value.toString().let { if (it.contains("@")) it.replace('$', '.').split("@")[0] else it }
override fun toString() = "${commandDataToString()} with pubkeys ${signers.joinToString()}"
@ -414,7 +400,7 @@ data class UpgradeCommand(val upgradedContractClass: Class<out UpgradedContract<
/** Wraps an object that was signed by a public key, which may be a well known/recognised institutional key. */
@CordaSerializable
data class AuthenticatedObject<out T : Any>(
val signers: List<CompositeKey>,
val signers: List<PublicKey>,
/** If any public keys were recognised, the looked up institutions are available here */
val signingParties: List<Party>,
val value: T
@ -494,26 +480,54 @@ interface UpgradedContract<in OldState : ContractState, out NewState : ContractS
* - Facts generated by oracles which might be reused a lot
*/
interface Attachment : NamedByHash {
fun open(): InputStream
fun openAsJAR() = JarInputStream(open())
fun openAsJAR(): JarInputStream {
val stream = open()
try {
return JarInputStream(stream)
} catch (t: Throwable) {
stream.use { throw t }
}
}
/**
* Finds the named file case insensitively and copies it to the output stream.
*
* @throws FileNotFoundException if the given path doesn't exist in the attachment.
*/
fun extractFile(path: String, outputTo: OutputStream) {
val p = path.toLowerCase().split('\\', '/')
openAsJAR().use { jar ->
while (true) {
val e = jar.nextJarEntry ?: break
if (e.name.toLowerCase().split('\\', '/') == p) {
jar.copyTo(outputTo)
return
}
jar.closeEntry()
fun extractFile(path: String, outputTo: OutputStream) = openAsJAR().use { it.extractFile(path, outputTo) }
}
abstract class AbstractAttachment(dataLoader: () -> ByteArray) : Attachment {
companion object {
fun SerializeAsTokenContext.attachmentDataLoader(id: SecureHash): () -> ByteArray {
val storage = serviceHub.storageService.attachments
return {
val a = storage.openAttachment(id) ?: throw MissingAttachmentsException(listOf(id))
if (a is AbstractAttachment) a.attachmentData else a.open().use { it.readBytes() }
}
}
throw FileNotFoundException()
}
protected val attachmentData: ByteArray by lazy(dataLoader)
override fun open(): InputStream = attachmentData.inputStream()
override fun equals(other: Any?) = other === this || other is Attachment && other.id == this.id
override fun hashCode() = id.hashCode()
override fun toString() = "${javaClass.simpleName}(id=$id)"
}
@Throws(IOException::class)
fun JarInputStream.extractFile(path: String, outputTo: OutputStream) {
val p = path.toLowerCase().split('\\', '/')
while (true) {
val e = nextJarEntry ?: break
if (!e.isDirectory && e.name.toLowerCase().split('\\', '/') == p) {
copyTo(outputTo)
return
}
closeEntry()
}
throw FileNotFoundException(path)
}

View File

@ -1,17 +1,14 @@
package net.corda.core.contracts
import net.corda.core.crypto.CompositeKey
import net.corda.core.crypto.Party
import net.corda.core.identity.Party
import net.corda.core.serialization.CordaSerializable
import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.TransactionBuilder
import java.security.PublicKey
/** Defines transaction build & validation logic for a specific transaction type */
@CordaSerializable
sealed class TransactionType {
override fun equals(other: Any?) = other?.javaClass == javaClass
override fun hashCode() = javaClass.name.hashCode()
/**
* Check that the transaction is valid based on:
* - General platform rules
@ -23,16 +20,16 @@ sealed class TransactionType {
fun verify(tx: LedgerTransaction) {
require(tx.notary != null || tx.timestamp == null) { "Transactions with timestamps must be notarised." }
val duplicates = detectDuplicateInputs(tx)
if (duplicates.isNotEmpty()) throw TransactionVerificationException.DuplicateInputStates(tx, duplicates)
if (duplicates.isNotEmpty()) throw TransactionVerificationException.DuplicateInputStates(tx.id, duplicates)
val missing = verifySigners(tx)
if (missing.isNotEmpty()) throw TransactionVerificationException.SignersMissing(tx, missing.toList())
if (missing.isNotEmpty()) throw TransactionVerificationException.SignersMissing(tx.id, missing.toList())
verifyTransaction(tx)
}
/** Check that the list of signers includes all the necessary keys */
fun verifySigners(tx: LedgerTransaction): Set<CompositeKey> {
fun verifySigners(tx: LedgerTransaction): Set<PublicKey> {
val notaryKey = tx.inputs.map { it.state.notary.owningKey }.toSet()
if (notaryKey.size > 1) throw TransactionVerificationException.MoreThanOneNotary(tx)
if (notaryKey.size > 1) throw TransactionVerificationException.MoreThanOneNotary(tx.id)
val requiredKeys = getRequiredSigners(tx) + notaryKey
val missing = requiredKeys - tx.mustSign
@ -57,15 +54,15 @@ sealed class TransactionType {
* Return the list of public keys that that require signatures for the transaction type.
* Note: the notary key is checked separately for all transactions and need not be included.
*/
abstract fun getRequiredSigners(tx: LedgerTransaction): Set<CompositeKey>
abstract fun getRequiredSigners(tx: LedgerTransaction): Set<PublicKey>
/** Implement type specific transaction validation logic */
abstract fun verifyTransaction(tx: LedgerTransaction)
/** A general transaction type where transaction validity is determined by custom contract code */
class General : TransactionType() {
object General : TransactionType() {
/** Just uses the default [TransactionBuilder] with no special logic */
class Builder(notary: Party?) : TransactionBuilder(General(), notary) {}
class Builder(notary: Party?) : TransactionBuilder(General, notary)
override fun verifyTransaction(tx: LedgerTransaction) {
verifyNoNotaryChange(tx)
@ -84,7 +81,7 @@ sealed class TransactionType {
if (tx.notary != null && tx.inputs.isNotEmpty()) {
tx.outputs.forEach {
if (it.notary != tx.notary) {
throw TransactionVerificationException.NotaryChangeInWrongTransactionType(tx, it.notary)
throw TransactionVerificationException.NotaryChangeInWrongTransactionType(tx.id, tx.notary, it.notary)
}
}
}
@ -93,13 +90,14 @@ sealed class TransactionType {
private fun verifyEncumbrances(tx: LedgerTransaction) {
// Validate that all encumbrances exist within the set of input states.
val encumberedInputs = tx.inputs.filter { it.state.encumbrance != null }
encumberedInputs.forEach { encumberedInput ->
encumberedInputs.forEach { (state, ref) ->
val encumbranceStateExists = tx.inputs.any {
it.ref.txhash == encumberedInput.ref.txhash && it.ref.index == encumberedInput.state.encumbrance
it.ref.txhash == ref.txhash && it.ref.index == state.encumbrance
}
if (!encumbranceStateExists) {
throw TransactionVerificationException.TransactionMissingEncumbranceException(
tx, encumberedInput.state.encumbrance!!,
tx.id,
state.encumbrance!!,
TransactionVerificationException.Direction.INPUT
)
}
@ -111,7 +109,8 @@ sealed class TransactionType {
val encumbranceIndex = output.encumbrance ?: continue
if (encumbranceIndex == i || encumbranceIndex >= tx.outputs.size) {
throw TransactionVerificationException.TransactionMissingEncumbranceException(
tx, encumbranceIndex,
tx.id,
encumbranceIndex,
TransactionVerificationException.Direction.OUTPUT)
}
}
@ -129,7 +128,7 @@ sealed class TransactionType {
try {
contract.verify(ctx)
} catch(e: Throwable) {
throw TransactionVerificationException.ContractRejection(tx, contract, e)
throw TransactionVerificationException.ContractRejection(tx.id, contract, e)
}
}
}
@ -141,12 +140,12 @@ sealed class TransactionType {
* A special transaction type for reassigning a notary for a state. Validation does not involve running
* any contract code, it just checks that the states are unmodified apart from the notary field.
*/
class NotaryChange : TransactionType() {
object NotaryChange : TransactionType() {
/**
* A transaction builder that automatically sets the transaction type to [NotaryChange]
* and adds the list of participants to the signers set for every input state.
*/
class Builder(notary: Party) : TransactionBuilder(NotaryChange(), notary) {
class Builder(notary: Party) : TransactionBuilder(NotaryChange, notary) {
override fun addInputState(stateAndRef: StateAndRef<*>) {
signers.addAll(stateAndRef.state.data.participants)
super.addInputState(stateAndRef)
@ -167,7 +166,7 @@ sealed class TransactionType {
}
check(tx.commands.isEmpty())
} catch (e: IllegalStateException) {
throw TransactionVerificationException.InvalidNotaryChange(tx)
throw TransactionVerificationException.InvalidNotaryChange(tx.id)
}
}

View File

@ -1,11 +1,10 @@
package net.corda.core.contracts
import net.corda.core.crypto.CompositeKey
import net.corda.core.crypto.Party
import net.corda.core.identity.Party
import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowException
import net.corda.core.serialization.CordaSerializable
import net.corda.core.transactions.LedgerTransaction
import java.security.PublicKey
import java.util.*
// TODO: Consider moving this out of the core module and providing a different way for unit tests to test contracts.
@ -91,31 +90,29 @@ class TransactionResolutionException(val hash: SecureHash) : FlowException() {
override fun toString(): String = "Transaction resolution failure for $hash"
}
class AttachmentResolutionException(val hash : SecureHash) : FlowException() {
class AttachmentResolutionException(val hash: SecureHash) : FlowException() {
override fun toString(): String = "Attachment resolution failure for $hash"
}
@CordaSerializable
class TransactionConflictException(val conflictRef: StateRef, val tx1: LedgerTransaction, val tx2: LedgerTransaction) : Exception()
sealed class TransactionVerificationException(val tx: LedgerTransaction, cause: Throwable?) : FlowException(cause) {
class ContractRejection(tx: LedgerTransaction, val contract: Contract, cause: Throwable?) : TransactionVerificationException(tx, cause)
class MoreThanOneNotary(tx: LedgerTransaction) : TransactionVerificationException(tx, null)
class SignersMissing(tx: LedgerTransaction, val missing: List<CompositeKey>) : TransactionVerificationException(tx, null) {
sealed class TransactionVerificationException(val txId: SecureHash, cause: Throwable?) : FlowException(cause) {
class ContractRejection(txId: SecureHash, val contract: Contract, cause: Throwable?) : TransactionVerificationException(txId, cause)
class MoreThanOneNotary(txId: SecureHash) : TransactionVerificationException(txId, null)
class SignersMissing(txId: SecureHash, val missing: List<PublicKey>) : TransactionVerificationException(txId, null) {
override fun toString(): String = "Signers missing: ${missing.joinToString()}"
}
class DuplicateInputStates(tx: LedgerTransaction, val duplicates: Set<StateRef>) : TransactionVerificationException(tx, null) {
class DuplicateInputStates(txId: SecureHash, val duplicates: Set<StateRef>) : TransactionVerificationException(txId, null) {
override fun toString(): String = "Duplicate inputs: ${duplicates.joinToString()}"
}
class InvalidNotaryChange(tx: LedgerTransaction) : TransactionVerificationException(tx, null)
class NotaryChangeInWrongTransactionType(tx: LedgerTransaction, val outputNotary: Party) : TransactionVerificationException(tx, null) {
class InvalidNotaryChange(txId: SecureHash) : TransactionVerificationException(txId, null)
class NotaryChangeInWrongTransactionType(txId: SecureHash, val txNotary: Party, val outputNotary: Party) : TransactionVerificationException(txId, null) {
override fun toString(): String {
return "Found unexpected notary change in transaction. Tx notary: ${tx.notary}, found: $outputNotary"
return "Found unexpected notary change in transaction. Tx notary: $txNotary, found: $outputNotary"
}
}
class TransactionMissingEncumbranceException(tx: LedgerTransaction, val missing: Int, val inOut: Direction) : TransactionVerificationException(tx, null) {
class TransactionMissingEncumbranceException(txId: SecureHash, val missing: Int, val inOut: Direction) : TransactionVerificationException(txId, null) {
override val message: String get() = "Missing required encumbrance $missing in $inOut"
}

View File

@ -9,7 +9,7 @@ import java.util.*
/**
* Compose a number of clauses, such that all of the clauses must run for verification to pass.
*/
open class AllOf<S : ContractState, C : CommandData, K : Any>(firstClause: Clause<S, C, K>, vararg remainingClauses: Clause<S, C, K>) : CompositeClause<S, C, K>() {
open class AllOf<S : ContractState, C : CommandData, K : Any>(firstClause: Clause<S, C, K>, vararg remainingClauses: Clause<S, C, K>) : CompositeClause<S, C, K>() {
override val clauses = ArrayList<Clause<S, C, K>>()
init {
@ -19,7 +19,7 @@ open class AllOf<S : ContractState, C : CommandData, K : Any>(firstClause: Claus
override fun matchedClauses(commands: List<AuthenticatedObject<C>>): List<Clause<S, C, K>> {
clauses.forEach { clause ->
check(clause.matches(commands)) { "Failed to match clause ${clause}" }
check(clause.matches(commands)) { "Failed to match clause $clause" }
}
return clauses
}

View File

@ -20,7 +20,7 @@ fun <C : CommandData> verifyClause(tx: TransactionForContract,
commands: List<AuthenticatedObject<C>>) {
if (Clause.log.isTraceEnabled) {
clause.getExecutionPath(commands).forEach {
Clause.log.trace("Tx ${tx.origHash} clause: ${clause}")
Clause.log.trace("Tx ${tx.origHash} clause: $clause")
}
}
val matchedCommands = clause.verify(tx, tx.inputs, tx.outputs, commands, null)

View File

@ -8,8 +8,8 @@ import net.corda.core.contracts.TransactionForContract
/**
* Filter the states that are passed through to the wrapped clause, to restrict them to a specific type.
*/
class FilterOn<S : ContractState, C : CommandData, K : Any>(val clause: Clause<S, C, K>,
val filterStates: (List<ContractState>) -> List<S>) : Clause<ContractState, C, K>() {
class FilterOn<S : ContractState, C : CommandData, in K : Any>(val clause: Clause<S, C, K>,
val filterStates: (List<ContractState>) -> List<S>) : Clause<ContractState, C, K>() {
override val requiredCommands: Set<Class<out CommandData>>
= clause.requiredCommands

View File

@ -4,18 +4,13 @@ import net.corda.core.contracts.AuthenticatedObject
import net.corda.core.contracts.CommandData
import net.corda.core.contracts.ContractState
import net.corda.core.contracts.TransactionForContract
import net.corda.core.utilities.loggerFor
import java.util.*
/**
* Compose a number of clauses, such that the first match is run, and it errors if none is run.
*/
@Deprecated("Use FirstOf instead")
class FirstComposition<S : ContractState, C : CommandData, K : Any>(val firstClause: Clause<S, C, K>, vararg remainingClauses: Clause<S, C, K>) : CompositeClause<S, C, K>() {
companion object {
val logger = loggerFor<FirstComposition<*, *, *>>()
}
class FirstComposition<S : ContractState, C : CommandData, K : Any>(firstClause: Clause<S, C, K>, vararg remainingClauses: Clause<S, C, K>) : CompositeClause<S, C, K>() {
override val clauses = ArrayList<Clause<S, C, K>>()
override fun matchedClauses(commands: List<AuthenticatedObject<C>>): List<Clause<S, C, K>> = listOf(clauses.first { it.matches(commands) })

View File

@ -10,7 +10,7 @@ import java.util.*
/**
* Compose a number of clauses, such that the first match is run, and it errors if none is run.
*/
class FirstOf<S : ContractState, C : CommandData, K : Any>(val firstClause: Clause<S, C, K>, vararg remainingClauses: Clause<S, C, K>) : CompositeClause<S, C, K>() {
class FirstOf<S : ContractState, C : CommandData, K : Any>(firstClause: Clause<S, C, K>, vararg remainingClauses: Clause<S, C, K>) : CompositeClause<S, C, K>() {
companion object {
val logger = loggerFor<FirstOf<*, *, *>>()
}

View File

@ -1,25 +0,0 @@
package net.corda.core.crypto
import net.corda.core.contracts.PartyAndReference
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.OpaqueBytes
import java.security.PublicKey
/**
* An [AbstractParty] contains the common elements of [Party] and [AnonymousParty], specifically the owning key of
* the party. In most cases [Party] or [AnonymousParty] should be used, depending on use-case.
*/
@CordaSerializable
abstract class AbstractParty(val owningKey: CompositeKey) {
/** A helper constructor that converts the given [PublicKey] in to a [CompositeKey] with a single node */
constructor(owningKey: PublicKey) : this(owningKey.composite)
/** Anonymised parties do not include any detail apart from owning key, so equality is dependent solely on the key */
override fun equals(other: Any?): Boolean = other is AbstractParty && this.owningKey == other.owningKey
override fun hashCode(): Int = owningKey.hashCode()
abstract fun toAnonymous() : AnonymousParty
abstract fun nameOrNull() : String?
abstract fun ref(bytes: OpaqueBytes): PartyAndReference
fun ref(vararg bytes: Byte) = ref(OpaqueBytes.of(*bytes))
}

View File

@ -1,23 +0,0 @@
package net.corda.core.crypto
import net.corda.core.contracts.PartyAndReference
import net.corda.core.serialization.OpaqueBytes
import java.security.PublicKey
/**
* The [AnonymousParty] class contains enough information to uniquely identify a [Party] while excluding private
* information such as name. It is intended to represent a party on the distributed ledger.
*/
class AnonymousParty(owningKey: CompositeKey) : AbstractParty(owningKey) {
/** A helper constructor that converts the given [PublicKey] in to a [CompositeKey] with a single node */
constructor(owningKey: PublicKey) : this(owningKey.composite)
// Use the key as the bulk of the toString(), but include a human readable identifier as well, so that [Party]
// can put in the key and actual name
override fun toString() = "${owningKey.toBase58String()} <Anonymous>"
override fun nameOrNull(): String? = null
override fun ref(bytes: OpaqueBytes): PartyAndReference = PartyAndReference(this, bytes)
override fun toAnonymous() = this
}

View File

@ -1,151 +1,146 @@
package net.corda.core.crypto
import net.corda.core.crypto.CompositeKey.Leaf
import net.corda.core.crypto.CompositeKey.Node
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import java.security.PublicKey
/**
* A tree data structure that enables the representation of composite public keys.
* Notice that with that implementation CompositeKey extends PublicKey. Leaves are represented by single public keys.
*
* In the simplest case it may just contain a single node encapsulating a [PublicKey] a [Leaf].
*
* For more complex scenarios, such as *"Both Alice and Bob need to sign to consume a state S"*, we can represent
* the requirement by creating a tree with a root [Node], and Alice and Bob as children [Leaf]s.
* For complex scenarios, such as *"Both Alice and Bob need to sign to consume a state S"*, we can represent
* the requirement by creating a tree with a root [CompositeKey], and Alice and Bob as children.
* The root node would specify *weights* for each of its children and a *threshold* the minimum total weight required
* (e.g. the minimum number of child signatures required) to satisfy the tree signature requirement.
*
* Using these constructs we can express e.g. 1 of N (OR) or N of N (AND) signature requirements. By nesting we can
* create multi-level requirements such as *"either the CEO or 3 of 5 of his assistants need to sign"*.
*
* [CompositeKey] maintains a list of [NodeAndWeight]s which holds child subtree with associated weight carried by child node signatures.
*
* The [threshold] specifies the minimum total weight required (in the simple case the minimum number of child
* signatures required) to satisfy the sub-tree rooted at this node.
*/
@CordaSerializable
sealed class CompositeKey {
/** Checks whether [keys] match a sufficient amount of leaf nodes */
abstract fun isFulfilledBy(keys: Iterable<PublicKey>): Boolean
fun isFulfilledBy(key: PublicKey) = isFulfilledBy(setOf(key))
/** Returns all [PublicKey]s contained within the tree leaves */
abstract val keys: Set<PublicKey>
/** Checks whether any of the given [keys] matches a leaf on the tree */
fun containsAny(otherKeys: Iterable<PublicKey>) = keys.intersect(otherKeys).isNotEmpty()
class CompositeKey private constructor (val threshold: Int,
children: List<NodeAndWeight>) : PublicKey {
val children = children.sorted()
init {
require (children.size == children.toSet().size) { "Trying to construct CompositeKey with duplicated child nodes." }
// If we want PublicKey we only keep one key, otherwise it will lead to semantically equivalent trees but having different structures.
require(children.size > 1) { "Cannot construct CompositeKey with only one child node." }
}
/**
* This is generated by serializing the composite key with Kryo, and encoding the resulting bytes in base58.
* A custom serialization format is being used.
*
* TODO: follow the crypto-conditions ASN.1 spec, some changes are needed to be compatible with the condition
* structure, e.g. mapping a PublicKey to a condition with the specific feature (ED25519).
* Holds node - weight pairs for a CompositeKey. Ordered first by weight, then by node's hashCode.
*/
fun toBase58String(): String = Base58.encode(this.serialize().bytes)
@CordaSerializable
data class NodeAndWeight(val node: PublicKey, val weight: Int): Comparable<NodeAndWeight> {
override fun compareTo(other: NodeAndWeight): Int {
if (weight == other.weight) {
return node.hashCode().compareTo(other.node.hashCode())
}
else return weight.compareTo(other.weight)
}
}
companion object {
fun parseFromBase58(encoded: String) = Base58.decode(encoded).deserialize<CompositeKey>()
}
/** The leaf node of the tree a wrapper around a [PublicKey] primitive */
class Leaf(val publicKey: PublicKey) : CompositeKey() {
override fun isFulfilledBy(keys: Iterable<PublicKey>) = publicKey in keys
override val keys: Set<PublicKey>
get() = setOf(publicKey)
// TODO: remove once data class inheritance is enabled
override fun equals(other: Any?): Boolean {
return this === other || other is Leaf && other.publicKey == this.publicKey
}
override fun hashCode() = publicKey.hashCode()
override fun toString() = publicKey.toStringShort()
// TODO: Get the design standardised and from there define a recognised name
val ALGORITHM = "X-Corda-CompositeKey"
// TODO: We should be using a well defined format.
val FORMAT = "X-Corda-Kryo"
}
/**
* Represents a node in the key tree. It maintains a list of child nodes sub-trees, and associated
* [weights] carried by child node signatures.
*
* The [threshold] specifies the minimum total weight required (in the simple case the minimum number of child
* signatures required) to satisfy the sub-tree rooted at this node.
* Takes single PublicKey and checks if CompositeKey requirements hold for that key.
*/
class Node(val threshold: Int,
val children: List<CompositeKey>,
val weights: List<Int>) : CompositeKey() {
fun isFulfilledBy(key: PublicKey) = isFulfilledBy(setOf(key))
override fun isFulfilledBy(keys: Iterable<PublicKey>): Boolean {
val totalWeight = children.mapIndexed { i, childNode ->
if (childNode.isFulfilledBy(keys)) weights[i] else 0
}.sum()
override fun getAlgorithm() = ALGORITHM
override fun getEncoded(): ByteArray = this.serialize().bytes
override fun getFormat() = FORMAT
return totalWeight >= threshold
}
override val keys: Set<PublicKey>
get() = children.flatMap { it.keys }.toSet()
// Auto-generated. TODO: remove once data class inheritance is enabled
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other?.javaClass != javaClass) return false
other as Node
if (threshold != other.threshold) return false
if (weights != other.weights) return false
if (children != other.children) return false
return true
}
override fun hashCode(): Int {
var result = threshold
result = 31 * result + weights.hashCode()
result = 31 * result + children.hashCode()
return result
}
override fun toString() = "(${children.joinToString()})"
/**
* Function checks if the public keys corresponding to the signatures are matched against the leaves of the composite
* key tree in question, and the total combined weight of all children is calculated for every intermediary node.
* If all thresholds are satisfied, the composite key requirement is considered to be met.
*/
fun isFulfilledBy(keysToCheck: Iterable<PublicKey>): Boolean {
if (keysToCheck.any { it is CompositeKey } ) return false
val totalWeight = children.map { (node, weight) ->
if (node is CompositeKey) {
if (node.isFulfilledBy(keysToCheck)) weight else 0
} else {
if (keysToCheck.contains(node)) weight else 0
}
}.sum()
return totalWeight >= threshold
}
/** A helper class for building a [CompositeKey.Node]. */
class Builder() {
private val children: MutableList<CompositeKey> = mutableListOf()
private val weights: MutableList<Int> = mutableListOf()
/**
* Set of all leaf keys of that CompositeKey.
*/
val leafKeys: Set<PublicKey>
get() = children.flatMap { it.node.keys }.toSet() // Uses PublicKey.keys extension.
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is CompositeKey) return false
if (threshold != other.threshold) return false
if (children != other.children) return false
return true
}
override fun hashCode(): Int {
var result = threshold
result = 31 * result + children.hashCode()
return result
}
override fun toString() = "(${children.joinToString()})"
/** A helper class for building a [CompositeKey]. */
class Builder {
private val children: MutableList<NodeAndWeight> = mutableListOf()
/** Adds a child [CompositeKey] node. Specifying a [weight] for the child is optional and will default to 1. */
fun addKey(key: CompositeKey, weight: Int = 1): Builder {
children.add(key)
weights.add(weight)
fun addKey(key: PublicKey, weight: Int = 1): Builder {
children.add(NodeAndWeight(key, weight))
return this
}
fun addKeys(vararg keys: CompositeKey): Builder {
fun addKeys(vararg keys: PublicKey): Builder {
keys.forEach { addKey(it) }
return this
}
fun addKeys(keys: List<CompositeKey>): Builder = addKeys(*keys.toTypedArray())
fun addKeys(keys: List<PublicKey>): Builder = addKeys(*keys.toTypedArray())
/**
* Builds the [CompositeKey.Node]. If [threshold] is not specified, it will default to
* Builds the [CompositeKey]. If [threshold] is not specified, it will default to
* the size of the children, effectively generating an "N of N" requirement.
* During process removes single keys wrapped in [CompositeKey] and enforces ordering on child nodes.
*/
fun build(threshold: Int? = null): CompositeKey.Node {
return Node(threshold ?: children.size, children.toList(), weights.toList())
@Throws(IllegalArgumentException::class)
fun build(threshold: Int? = null): PublicKey {
val n = children.size
if (n > 1)
return CompositeKey(threshold ?: n, children)
else if (n == 1) {
require(threshold == null || threshold == children.first().weight)
{ "Trying to build invalid CompositeKey, threshold value different than weight of single child node." }
return children.first().node // We can assume that this node is a correct CompositeKey.
}
else throw IllegalArgumentException("Trying to build CompositeKey without child nodes.")
}
}
/**
* Returns the enclosed [PublicKey] for a [CompositeKey] with a single leaf node
*
* @throws IllegalArgumentException if the [CompositeKey] contains more than one node
*/
val singleKey: PublicKey
get() = keys.singleOrNull() ?: throw IllegalStateException("The key is composed of more than one PublicKey primitive")
}
/** Returns the set of all [PublicKey]s contained in the leaves of the [CompositeKey]s */
val Iterable<CompositeKey>.keys: Set<PublicKey>
/**
* Expands all [CompositeKey]s present in PublicKey iterable to set of single [PublicKey]s.
* If an element of the set is a single PublicKey it gives just that key, if it is a [CompositeKey] it returns all leaf
* keys for that composite element.
*/
val Iterable<PublicKey>.expandedCompositeKeys: Set<PublicKey>
get() = flatMap { it.keys }.toSet()

View File

@ -0,0 +1,84 @@
package net.corda.core.crypto
import net.corda.core.serialization.deserialize
import java.io.ByteArrayOutputStream
import java.security.*
import java.security.spec.AlgorithmParameterSpec
/**
* Dedicated class for storing a set of signatures that comprise [CompositeKey].
*/
class CompositeSignature : Signature(ALGORITHM) {
companion object {
val ALGORITHM = "X-Corda-CompositeSig"
}
private var signatureState: State? = null
/**
* Check that the signature state has been initialised, then return it.
*/
@Throws(SignatureException::class)
private fun assertInitialised(): State {
if (signatureState == null)
throw SignatureException("Engine has not been initialised")
return signatureState!!
}
@Throws(InvalidAlgorithmParameterException::class)
override fun engineGetParameter(param: String?): Any {
throw InvalidAlgorithmParameterException("Composite signatures do not support any parameters")
}
@Throws(InvalidKeyException::class)
override fun engineInitSign(privateKey: PrivateKey?) {
throw InvalidKeyException("Composite signatures must be assembled independently from signatures provided by the component private keys")
}
@Throws(InvalidKeyException::class)
override fun engineInitVerify(publicKey: PublicKey?) {
if (publicKey is CompositeKey) {
signatureState = State(ByteArrayOutputStream(1024), publicKey)
} else {
throw InvalidKeyException("Key to verify must be a composite key")
}
}
@Throws(InvalidAlgorithmParameterException::class)
override fun engineSetParameter(param: String?, value: Any?) {
throw InvalidAlgorithmParameterException("Composite signatures do not support any parameters")
}
@Throws(InvalidAlgorithmParameterException::class)
override fun engineSetParameter(params: AlgorithmParameterSpec) {
throw InvalidAlgorithmParameterException("Composite signatures do not support any parameters")
}
@Throws(SignatureException::class)
override fun engineSign(): ByteArray {
throw SignatureException("Composite signatures must be assembled independently from signatures provided by the component private keys")
}
override fun engineUpdate(b: Byte) {
assertInitialised().buffer.write(b.toInt())
}
override fun engineUpdate(b: ByteArray, off: Int, len: Int) {
assertInitialised().buffer.write(b, off, len)
}
@Throws(SignatureException::class)
override fun engineVerify(sigBytes: ByteArray): Boolean = assertInitialised().engineVerify(sigBytes)
data class State(val buffer: ByteArrayOutputStream, val verifyKey: CompositeKey) {
fun engineVerify(sigBytes: ByteArray): Boolean {
val sig = sigBytes.deserialize<CompositeSignaturesWithKeys>()
return if (verifyKey.isFulfilledBy(sig.sigs.map { it.by })) {
val clearData = buffer.toByteArray()
sig.sigs.all { it.isValid(clearData) }
} else {
false
}
}
}
}

View File

@ -0,0 +1,14 @@
package net.corda.core.crypto
import net.corda.core.serialization.CordaSerializable
/**
* Custom class for holding signature data. This exists for later extension work to provide a standardised cross-platform
* serialization format (i.e. not Kryo).
*/
@CordaSerializable
data class CompositeSignaturesWithKeys(val sigs: List<DigitalSignature.WithKey>) {
companion object {
val EMPTY = CompositeSignaturesWithKeys(emptyList())
}
}

View File

@ -0,0 +1,39 @@
package net.corda.core.crypto
import org.bouncycastle.asn1.x509.AlgorithmIdentifier
import org.bouncycastle.operator.ContentSigner
import java.io.OutputStream
import java.security.PrivateKey
import java.security.Provider
import java.security.SecureRandom
import java.security.Signature
/**
* Provide extra OID look up for signature algorithm not supported by bouncy castle.
* This builder will use bouncy castle's JcaContentSignerBuilder as fallback for unknown algorithm.
*/
object ContentSignerBuilder {
fun build(signatureScheme: SignatureScheme, privateKey: PrivateKey, provider: Provider?, random: SecureRandom? = null): ContentSigner {
val sigAlgId = AlgorithmIdentifier(signatureScheme.signatureOID)
val sig = Signature.getInstance(signatureScheme.signatureName, provider).apply {
if (random != null) {
initSign(privateKey, random)
} else {
initSign(privateKey)
}
}
return object : ContentSigner {
private val stream = SignatureOutputStream(sig)
override fun getAlgorithmIdentifier(): AlgorithmIdentifier = sigAlgId
override fun getOutputStream(): OutputStream = stream
override fun getSignature(): ByteArray = stream.signature
}
}
private class SignatureOutputStream(private val sig: Signature) : OutputStream() {
internal val signature: ByteArray get() = sig.sign()
override fun write(bytes: ByteArray, off: Int, len: Int) = sig.update(bytes, off, len)
override fun write(bytes: ByteArray) = sig.update(bytes)
override fun write(b: Int) = sig.update(b.toByte())
}
}

View File

@ -1,15 +1,42 @@
package net.corda.core.crypto
import net.i2p.crypto.eddsa.EdDSAEngine
import net.i2p.crypto.eddsa.EdDSAKey
import net.corda.core.random63BitValue
import net.i2p.crypto.eddsa.*
import net.i2p.crypto.eddsa.math.GroupElement
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable
import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec
import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec
import org.bouncycastle.asn1.ASN1EncodableVector
import org.bouncycastle.asn1.ASN1ObjectIdentifier
import org.bouncycastle.asn1.DERSequence
import org.bouncycastle.asn1.bc.BCObjectIdentifiers
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x509.*
import org.bouncycastle.asn1.x9.X9ObjectIdentifiers
import org.bouncycastle.cert.bc.BcX509ExtensionUtils
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey
import org.bouncycastle.jcajce.provider.util.AsymmetricKeyInfoConverter
import org.bouncycastle.jce.ECNamedCurveTable
import org.bouncycastle.jce.interfaces.ECKey
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.pkcs.PKCS10CertificationRequest
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder
import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider
import org.bouncycastle.pqc.jcajce.spec.SPHINCS256KeyGenParameterSpec
import java.math.BigInteger
import java.security.*
import java.security.KeyFactory
import java.security.KeyPairGenerator
import java.security.cert.X509Certificate
import java.security.spec.InvalidKeySpecException
import java.security.spec.PKCS8EncodedKeySpec
import java.security.spec.X509EncodedKeySpec
import java.util.*
/**
* This object controls and provides the available and supported signature schemes for Corda.
@ -25,58 +52,58 @@ import java.security.spec.X509EncodedKeySpec
* </ul>
*/
object Crypto {
/**
* RSA_SHA256 signature scheme using SHA256 as hash algorithm and MGF1 (with SHA256) as mask generation function.
* Note: Recommended key size >= 3072 bits.
*/
private val RSA_SHA256 = SignatureScheme(
val RSA_SHA256 = SignatureScheme(
1,
"RSA_SHA256",
PKCSObjectIdentifiers.id_RSASSA_PSS,
BouncyCastleProvider.PROVIDER_NAME,
"RSA",
Signature.getInstance("SHA256WITHRSAANDMGF1", "BC"),
KeyFactory.getInstance("RSA", "BC"),
KeyPairGenerator.getInstance("RSA", "BC"),
"SHA256WITHRSAANDMGF1",
null,
3072,
"RSA_SHA256 signature scheme using SHA256 as hash algorithm and MGF1 (with SHA256) as mask generation function."
)
/** ECDSA signature scheme using the secp256k1 Koblitz curve. */
private val ECDSA_SECP256K1_SHA256 = SignatureScheme(
val ECDSA_SECP256K1_SHA256 = SignatureScheme(
2,
"ECDSA_SECP256K1_SHA256",
X9ObjectIdentifiers.ecdsa_with_SHA256,
BouncyCastleProvider.PROVIDER_NAME,
"ECDSA",
Signature.getInstance("SHA256withECDSA", "BC"),
KeyFactory.getInstance("ECDSA", "BC"),
KeyPairGenerator.getInstance("ECDSA", "BC"),
"SHA256withECDSA",
ECNamedCurveTable.getParameterSpec("secp256k1"),
256,
"ECDSA signature scheme using the secp256k1 Koblitz curve."
)
/** ECDSA signature scheme using the secp256r1 (NIST P-256) curve. */
private val ECDSA_SECP256R1_SHA256 = SignatureScheme(
val ECDSA_SECP256R1_SHA256 = SignatureScheme(
3,
"ECDSA_SECP256R1_SHA256",
X9ObjectIdentifiers.ecdsa_with_SHA256,
BouncyCastleProvider.PROVIDER_NAME,
"ECDSA",
Signature.getInstance("SHA256withECDSA", "BC"),
KeyFactory.getInstance("ECDSA", "BC"),
KeyPairGenerator.getInstance("ECDSA", "BC"),
"SHA256withECDSA",
ECNamedCurveTable.getParameterSpec("secp256r1"),
256,
"ECDSA signature scheme using the secp256r1 (NIST P-256) curve."
)
/** EdDSA signature scheme using the ed255519 twisted Edwards curve. */
private val EDDSA_ED25519_SHA512 = SignatureScheme(
val EDDSA_ED25519_SHA512 = SignatureScheme(
4,
"EDDSA_ED25519_SHA512",
"EdDSA",
EdDSAEngine(),
EdDSAKeyFactory(),
net.i2p.crypto.eddsa.KeyPairGenerator(), // EdDSA engine uses a custom KeyPairGenerator Vs BouncyCastle.
EdDSANamedCurveTable.getByName("ed25519-sha-512"),
ASN1ObjectIdentifier("1.3.101.112"),
// We added EdDSA to bouncy castle for certificate signing.
BouncyCastleProvider.PROVIDER_NAME,
EdDSAKey.KEY_ALGORITHM,
EdDSAEngine.SIGNATURE_ALGORITHM,
EdDSANamedCurveTable.getByName("ED25519"),
256,
"EdDSA signature scheme using the ed25519 twisted Edwards curve."
)
@ -85,13 +112,13 @@ object Crypto {
* SPHINCS-256 hash-based signature scheme. It provides 128bit security against post-quantum attackers
* at the cost of larger key sizes and loss of compatibility.
*/
private val SPHINCS256_SHA256 = SignatureScheme(
val SPHINCS256_SHA256 = SignatureScheme(
5,
"SPHINCS-256_SHA512",
"SPHINCS-256",
Signature.getInstance("SHA512WITHSPHINCS256", "BCPQC"),
KeyFactory.getInstance("SPHINCS256", "BCPQC"),
KeyPairGenerator.getInstance("SPHINCS256", "BCPQC"),
BCObjectIdentifiers.sphincs256_with_SHA512,
"BCPQC",
"SPHINCS256",
"SHA512WITHSPHINCS256",
SPHINCS256KeyGenParameterSpec(SPHINCS256KeyGenParameterSpec.SHA512_256),
256,
"SPHINCS-256 hash-based signature scheme. It provides 128bit security against post-quantum attackers " +
@ -99,20 +126,38 @@ object Crypto {
)
/** Our default signature scheme if no algorithm is specified (e.g. for key generation). */
private val DEFAULT_SIGNATURE_SCHEME = EDDSA_ED25519_SHA512
val DEFAULT_SIGNATURE_SCHEME = EDDSA_ED25519_SHA512
/**
* Supported digital signature schemes.
* Note: Only the schemes added in this map will be supported (see [Crypto]).
* Do not forget to add the DEFAULT_SIGNATURE_SCHEME as well.
*/
private val supportedSignatureSchemes = mapOf(
RSA_SHA256.schemeCodeName to RSA_SHA256,
ECDSA_SECP256K1_SHA256.schemeCodeName to ECDSA_SECP256K1_SHA256,
ECDSA_SECP256R1_SHA256.schemeCodeName to ECDSA_SECP256R1_SHA256,
EDDSA_ED25519_SHA512.schemeCodeName to EDDSA_ED25519_SHA512,
SPHINCS256_SHA256.schemeCodeName to SPHINCS256_SHA256
)
val supportedSignatureSchemes = listOf(
RSA_SHA256,
ECDSA_SECP256K1_SHA256,
ECDSA_SECP256R1_SHA256,
EDDSA_ED25519_SHA512,
SPHINCS256_SHA256
).associateBy { it.schemeCodeName }
// This map is required to defend against users that forcibly call Security.addProvider / Security.removeProvider
// that could cause unexpected and suspicious behaviour.
// i.e. if someone removes a Provider and then he/she adds a new one with the same name.
// The val is private to avoid any harmful state changes.
private val providerMap: Map<String, Provider> = mapOf(
BouncyCastleProvider.PROVIDER_NAME to getBouncyCastleProvider(),
"BCPQC" to BouncyCastlePQCProvider()) // unfortunately, provider's name is not final in BouncyCastlePQCProvider, so we explicitly set it.
private fun getBouncyCastleProvider() = BouncyCastleProvider().apply {
putAll(EdDSASecurityProvider())
addKeyInfoConverter(EDDSA_ED25519_SHA512.signatureOID, KeyInfoConverter(EDDSA_ED25519_SHA512))
}
init {
// This registration is needed for reading back EdDSA key from java keystore.
// TODO: Find a way to make JKS work with bouncy castle provider or implement our own provide so we don't have to register bouncy castle provider.
Security.addProvider(getBouncyCastleProvider())
}
/**
* Factory pattern to retrieve the corresponding [SignatureScheme] based on the type of the [String] input.
@ -122,17 +167,7 @@ object Crypto {
* @return a currently supported SignatureScheme.
* @throws IllegalArgumentException if the requested signature scheme is not supported.
*/
private fun findSignatureScheme(schemeCodeName: String): SignatureScheme = supportedSignatureSchemes[schemeCodeName] ?: throw IllegalArgumentException("Unsupported key/algorithm for metadata schemeCodeName: ${schemeCodeName}")
/**
* Retrieve the corresponding [SignatureScheme] based on the type of the input [KeyPair].
* Note that only the Corda platform standard schemes are supported (see [Crypto]).
* This function is usually called when requiring to sign signatures.
* @param keyPair a cryptographic [KeyPair].
* @return a currently supported SignatureScheme or null.
* @throws IllegalArgumentException if the requested signature scheme is not supported.
*/
private fun findSignatureScheme(keyPair: KeyPair): SignatureScheme = findSignatureScheme(keyPair.private)
fun findSignatureScheme(schemeCodeName: String): SignatureScheme = supportedSignatureSchemes[schemeCodeName] ?: throw IllegalArgumentException("Unsupported key/algorithm for metadata schemeCodeName: $schemeCodeName")
/**
* Retrieve the corresponding [SignatureScheme] based on the type of the input [Key].
@ -144,46 +179,39 @@ object Crypto {
* @return a currently supported SignatureScheme.
* @throws IllegalArgumentException if the requested key type is not supported.
*/
private fun findSignatureScheme(key: Key): SignatureScheme {
fun findSignatureScheme(key: Key): SignatureScheme {
for (sig in supportedSignatureSchemes.values) {
val algorithm = key.algorithm
var algorithm = key.algorithm
if (algorithm == "EC") algorithm = "ECDSA" // required to read ECC keys from Keystore, because encoding may change algorithm name from ECDSA to EC.
if (algorithm == "SPHINCS-256") algorithm = "SPHINCS256" // because encoding may change algorithm name from SPHINCS256 to SPHINCS-256.
if (algorithm == sig.algorithmName) {
// If more than one ECDSA schemes are supported, we should distinguish between them by checking their curve parameters.
// TODO: change 'continue' to 'break' if only one EdDSA curve will be used.
if (algorithm == "EdDSA") {
if ((key as EdDSAKey).params == sig.algSpec) {
if ((key is EdDSAPublicKey && publicKeyOnCurve(sig, key)) || (key is EdDSAPrivateKey && key.params == sig.algSpec)) {
return sig
} else continue
} else break // use continue if in the future we support more than one Edwards curves.
} else if (algorithm == "ECDSA") {
if ((key as ECKey).parameters == sig.algSpec) {
if ((key is BCECPublicKey && publicKeyOnCurve(sig, key)) || (key is BCECPrivateKey && key.parameters == sig.algSpec)) {
return sig
} else continue
} else return sig // it's either RSA_SHA256 or SPHINCS-256.
}
}
throw IllegalArgumentException("Unsupported key/algorithm for the private key: ${key.encoded.toBase58()}")
throw IllegalArgumentException("Unsupported key/algorithm for the key: ${key.encoded.toBase58()}")
}
/**
* Retrieve the corresponding signature scheme code name based on the type of the input [Key].
* See [Crypto] for the supported scheme code names.
* @param key either private or public.
* @return signatureSchemeCodeName for a [Key].
* @throws IllegalArgumentException if the requested key type is not supported.
*/
fun findSignatureSchemeCodeName(key: Key): String = findSignatureScheme(key).schemeCodeName
/**
* Decode a PKCS8 encoded key to its [PrivateKey] object.
* Use this method if the key type is a-priori unknown.
* @param encodedKey a PKCS8 encoded private key.
* @throws IllegalArgumentException on not supported scheme or if the given key specification
* is inappropriate for this key factory to produce a private key.
*/
@Throws(IllegalArgumentException::class)
fun decodePrivateKey(encodedKey: ByteArray): PrivateKey {
for (sig in supportedSignatureSchemes.values) {
for ((_, _, _, providerName, algorithmName) in supportedSignatureSchemes.values) {
try {
return sig.keyFactory.generatePrivate(PKCS8EncodedKeySpec(encodedKey))
return KeyFactory.getInstance(algorithmName, providerMap[providerName]).generatePrivate(PKCS8EncodedKeySpec(encodedKey))
} catch (ikse: InvalidKeySpecException) {
// ignore it - only used to bypass the scheme that causes an exception.
}
@ -193,17 +221,27 @@ object Crypto {
/**
* Decode a PKCS8 encoded key to its [PrivateKey] object based on the input scheme code name.
* This will be used by Kryo deserialisation.
* @param encodedKey a PKCS8 encoded private key.
* This should be used when the type key is known, e.g. during Kryo deserialisation or with key caches or key managers.
* @param schemeCodeName a [String] that should match a key in supportedSignatureSchemes map (e.g. ECDSA_SECP256K1_SHA256).
* @param encodedKey a PKCS8 encoded private key.
* @throws IllegalArgumentException on not supported scheme or if the given key specification
* is inappropriate for this key factory to produce a private key.
*/
@Throws(IllegalArgumentException::class, InvalidKeySpecException::class)
fun decodePrivateKey(encodedKey: ByteArray, schemeCodeName: String): PrivateKey {
val sig = findSignatureScheme(schemeCodeName)
fun decodePrivateKey(schemeCodeName: String, encodedKey: ByteArray): PrivateKey = decodePrivateKey(findSignatureScheme(schemeCodeName), encodedKey)
/**
* Decode a PKCS8 encoded key to its [PrivateKey] object based on the input scheme code name.
* This should be used when the type key is known, e.g. during Kryo deserialisation or with key caches or key managers.
* @param signatureScheme a signature scheme (e.g. ECDSA_SECP256K1_SHA256).
* @param encodedKey a PKCS8 encoded private key.
* @throws IllegalArgumentException on not supported scheme or if the given key specification
* is inappropriate for this key factory to produce a private key.
*/
@Throws(IllegalArgumentException::class, InvalidKeySpecException::class)
fun decodePrivateKey(signatureScheme: SignatureScheme, encodedKey: ByteArray): PrivateKey {
try {
return sig.keyFactory.generatePrivate(PKCS8EncodedKeySpec(encodedKey))
return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePrivate(PKCS8EncodedKeySpec(encodedKey))
} catch (ikse: InvalidKeySpecException) {
throw InvalidKeySpecException("This private key cannot be decoded, please ensure it is PKCS8 encoded and that it corresponds to the input scheme's code name.", ikse)
}
@ -211,16 +249,16 @@ object Crypto {
/**
* Decode an X509 encoded key to its [PublicKey] object.
* Use this method if the key type is a-priori unknown.
* @param encodedKey an X509 encoded public key.
* @throws UnsupportedSchemeException on not supported scheme.
* @throws IllegalArgumentException on not supported scheme or if the given key specification
* is inappropriate for this key factory to produce a private key.
*/
@Throws(IllegalArgumentException::class)
fun decodePublicKey(encodedKey: ByteArray): PublicKey {
for (sig in supportedSignatureSchemes.values) {
for ((_, _, _, providerName, algorithmName) in supportedSignatureSchemes.values) {
try {
return sig.keyFactory.generatePublic(X509EncodedKeySpec(encodedKey))
return KeyFactory.getInstance(algorithmName, providerMap[providerName]).generatePublic(X509EncodedKeySpec(encodedKey))
} catch (ikse: InvalidKeySpecException) {
// ignore it - only used to bypass the scheme that causes an exception.
}
@ -230,39 +268,34 @@ object Crypto {
/**
* Decode an X509 encoded key to its [PrivateKey] object based on the input scheme code name.
* This will be used by Kryo deserialisation.
* @param encodedKey an X509 encoded public key.
* This should be used when the type key is known, e.g. during Kryo deserialisation or with key caches or key managers.
* @param schemeCodeName a [String] that should match a key in supportedSignatureSchemes map (e.g. ECDSA_SECP256K1_SHA256).
* @param encodedKey an X509 encoded public key.
* @throws IllegalArgumentException if the requested scheme is not supported
* @throws InvalidKeySpecException if the given key specification
* is inappropriate for this key factory to produce a public key.
*/
@Throws(IllegalArgumentException::class, InvalidKeySpecException::class)
fun decodePublicKey(encodedKey: ByteArray, schemeCodeName: String): PublicKey {
val sig = findSignatureScheme(schemeCodeName)
fun decodePublicKey(schemeCodeName: String, encodedKey: ByteArray): PublicKey = decodePublicKey(findSignatureScheme(schemeCodeName), encodedKey)
/**
* Decode an X509 encoded key to its [PrivateKey] object based on the input scheme code name.
* This should be used when the type key is known, e.g. during Kryo deserialisation or with key caches or key managers.
* @param signatureScheme a signature scheme (e.g. ECDSA_SECP256K1_SHA256).
* @param encodedKey an X509 encoded public key.
* @throws IllegalArgumentException if the requested scheme is not supported
* @throws InvalidKeySpecException if the given key specification
* is inappropriate for this key factory to produce a public key.
*/
@Throws(IllegalArgumentException::class, InvalidKeySpecException::class)
fun decodePublicKey(signatureScheme: SignatureScheme, encodedKey: ByteArray): PublicKey {
try {
return sig.keyFactory.generatePublic(X509EncodedKeySpec(encodedKey))
return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePublic(X509EncodedKeySpec(encodedKey))
} catch (ikse: InvalidKeySpecException) {
throw throw InvalidKeySpecException("This public key cannot be decoded, please ensure it is X509 encoded and that it corresponds to the input scheme's code name.", ikse)
}
}
/**
* Utility to simplify the act of generating keys.
* Normally, we don't expect other errors here, assuming that key generation parameters for every supported signature scheme have been unit-tested.
* @param schemeCodeName a signature scheme's code name (e.g. ECDSA_SECP256K1_SHA256).
* @return a KeyPair for the requested scheme.
* @throws IllegalArgumentException if the requested signature scheme is not supported.
*/
@Throws(IllegalArgumentException::class)
fun generateKeyPair(schemeCodeName: String): KeyPair = findSignatureScheme(schemeCodeName).keyPairGenerator.generateKeyPair()
/**
* Generate a KeyPair using the default signature scheme.
* @return a new KeyPair.
*/
fun generateKeyPair(): KeyPair = DEFAULT_SIGNATURE_SCHEME.keyPairGenerator.generateKeyPair()
/**
* Generic way to sign [ByteArray] data with a [PrivateKey]. Strategy on on identifying the actual signing scheme is based
* on the [PrivateKey] type, but if the schemeCodeName is known, then better use doSign(signatureScheme: String, privateKey: PrivateKey, clearData: ByteArray).
@ -274,7 +307,7 @@ object Crypto {
* @throws SignatureException if signing is not possible due to malformed data or private key.
*/
@Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class)
fun doSign(privateKey: PrivateKey, clearData: ByteArray) = doSign(findSignatureScheme(privateKey).sig, privateKey, clearData)
fun doSign(privateKey: PrivateKey, clearData: ByteArray) = doSign(findSignatureScheme(privateKey), privateKey, clearData)
/**
* Generic way to sign [ByteArray] data with a [PrivateKey] and a known schemeCodeName [String].
@ -287,11 +320,11 @@ object Crypto {
* @throws SignatureException if signing is not possible due to malformed data or private key.
*/
@Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class)
fun doSign(schemeCodeName: String, privateKey: PrivateKey, clearData: ByteArray) = doSign(findSignatureScheme(schemeCodeName).sig, privateKey, clearData)
fun doSign(schemeCodeName: String, privateKey: PrivateKey, clearData: ByteArray) = doSign(findSignatureScheme(schemeCodeName), privateKey, clearData)
/**
* Generic way to sign [ByteArray] data with a [PrivateKey] and a known [Signature].
* @param signature a [Signature] object, retrieved from supported signature schemes, see [Crypto].
* @param signatureScheme a [SignatureScheme] object, retrieved from supported signature schemes, see [Crypto].
* @param privateKey the signer's [PrivateKey].
* @param clearData the data/message to be signed in [ByteArray] form (usually the Merkle root).
* @return the digital signature (in [ByteArray]) on the input message.
@ -300,7 +333,10 @@ object Crypto {
* @throws SignatureException if signing is not possible due to malformed data or private key.
*/
@Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class)
private fun doSign(signature: Signature, privateKey: PrivateKey, clearData: ByteArray): ByteArray {
fun doSign(signatureScheme: SignatureScheme, privateKey: PrivateKey, clearData: ByteArray): ByteArray {
if (!supportedSignatureSchemes.containsKey(signatureScheme.schemeCodeName))
throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $signatureScheme.schemeCodeName")
val signature = Signature.getInstance(signatureScheme.signatureName, providerMap[signatureScheme.providerName])
if (clearData.isEmpty()) throw Exception("Signing of an empty array is not permitted!")
signature.initSign(privateKey)
signature.update(clearData)
@ -330,6 +366,7 @@ object Crypto {
/**
* Utility to simplify the act of verifying a digital signature.
* It returns true if it succeeds, but it always throws an exception if verification fails.
* @param schemeCodeName a signature scheme's code name (e.g. ECDSA_SECP256K1_SHA256).
* @param publicKey the signer's [PublicKey].
* @param signatureData the signatureData on a message.
* @param clearData the clear data/message that was signed (usually the Merkle root).
@ -341,7 +378,7 @@ object Crypto {
* @throws IllegalArgumentException if the signature scheme is not supported or if any of the clear or signature data is empty.
*/
@Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class)
fun doVerify(schemeCodeName: String, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray) = doVerify(findSignatureScheme(schemeCodeName).sig, publicKey, signatureData, clearData)
fun doVerify(schemeCodeName: String, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray) = doVerify(findSignatureScheme(schemeCodeName), publicKey, signatureData, clearData)
/**
* Utility to simplify the act of verifying a digital signature by identifying the signature scheme used from the input public key's type.
@ -359,12 +396,12 @@ object Crypto {
* @throws IllegalArgumentException if the signature scheme is not supported or if any of the clear or signature data is empty.
*/
@Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class)
fun doVerify(publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray) = doVerify(findSignatureScheme(publicKey).sig, publicKey, signatureData, clearData)
fun doVerify(publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray) = doVerify(findSignatureScheme(publicKey), publicKey, signatureData, clearData)
/**
* Method to verify a digital signature.
* It returns true if it succeeds, but it always throws an exception if verification fails.
* @param signature a [Signature] object, retrieved from supported signature schemes, see [Crypto].
* @param signatureScheme a [SignatureScheme] object, retrieved from supported signature schemes, see [Crypto].
* @param publicKey the signer's [PublicKey].
* @param signatureData the signatureData on a message.
* @param clearData the clear data/message that was signed (usually the Merkle root).
@ -373,14 +410,15 @@ object Crypto {
* @throws SignatureException if this signatureData object is not initialized properly,
* the passed-in signatureData is improperly encoded or of the wrong type,
* if this signatureData scheme is unable to process the input data provided, if the verification is not possible.
* @throws IllegalArgumentException if any of the clear or signature data is empty.
* @throws IllegalArgumentException if the signature scheme is not supported or if any of the clear or signature data is empty.
*/
private fun doVerify(signature: Signature, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray): Boolean {
@Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class)
fun doVerify(signatureScheme: SignatureScheme, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray): Boolean {
if (!supportedSignatureSchemes.containsKey(signatureScheme.schemeCodeName))
throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $signatureScheme.schemeCodeName")
if (signatureData.isEmpty()) throw IllegalArgumentException("Signature data is empty!")
if (clearData.isEmpty()) throw IllegalArgumentException("Clear data is empty, nothing to verify!")
signature.initVerify(publicKey)
signature.update(clearData)
val verificationResult = signature.verify(signatureData)
val verificationResult = isValid(signatureScheme, publicKey, signatureData, clearData)
if (verificationResult) {
return true
} else {
@ -407,15 +445,181 @@ object Crypto {
}
/**
* Check if the requested signature scheme is supported by the system.
* @param schemeCodeName a signature scheme's code name (e.g. ECDSA_SECP256K1_SHA256).
* @return true if the signature scheme is supported.
* Utility to simplify the act of verifying a digital signature by identifying the signature scheme used from the input public key's type.
* It returns true if it succeeds and false if not. In comparison to [doVerify] if the key and signature
* do not match it returns false rather than throwing an exception. Normally you should use the function which throws,
* as it avoids the risk of failing to test the result.
* Use this method if the signature scheme is not a-priori known.
* @param publicKey the signer's [PublicKey].
* @param signatureData the signatureData on a message.
* @param clearData the clear data/message that was signed (usually the Merkle root).
* @return true if verification passes or false if verification fails.
* @throws SignatureException if this signatureData object is not initialized properly,
* the passed-in signatureData is improperly encoded or of the wrong type,
* if this signatureData scheme is unable to process the input data provided, if the verification is not possible.
*/
@Throws(SignatureException::class)
fun isValid(publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray) = isValid(findSignatureScheme(publicKey), publicKey, signatureData, clearData)
/**
* Method to verify a digital signature. In comparison to [doVerify] if the key and signature
* do not match it returns false rather than throwing an exception.
* Use this method if the signature scheme type is a-priori unknown.
* @param signatureScheme a [SignatureScheme] object, retrieved from supported signature schemes, see [Crypto].
* @param publicKey the signer's [PublicKey].
* @param signatureData the signatureData on a message.
* @param clearData the clear data/message that was signed (usually the Merkle root).
* @return true if verification passes or false if verification fails.
* @throws SignatureException if this signatureData object is not initialized properly,
* the passed-in signatureData is improperly encoded or of the wrong type,
* if this signatureData scheme is unable to process the input data provided, if the verification is not possible.
* @throws IllegalArgumentException if the requested signature scheme is not supported.
*/
@Throws(SignatureException::class, IllegalArgumentException::class)
fun isValid(signatureScheme: SignatureScheme, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray): Boolean {
if (!supportedSignatureSchemes.containsKey(signatureScheme.schemeCodeName))
throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $signatureScheme.schemeCodeName")
val signature = Signature.getInstance(signatureScheme.signatureName, providerMap[signatureScheme.providerName])
signature.initVerify(publicKey)
signature.update(clearData)
return signature.verify(signatureData)
}
/**
* Utility to simplify the act of generating keys.
* Normally, we don't expect other errors here, assuming that key generation parameters for every supported signature scheme have been unit-tested.
* @param schemeCodeName a signature scheme's code name (e.g. ECDSA_SECP256K1_SHA256).
* @return a KeyPair for the requested signature scheme code name.
* @throws IllegalArgumentException if the requested signature scheme is not supported.
*/
@Throws(IllegalArgumentException::class)
fun generateKeyPair(schemeCodeName: String): KeyPair = generateKeyPair(findSignatureScheme(schemeCodeName))
/**
* Generate a [KeyPair] for the selected [SignatureScheme].
* Note that RSA is the sole algorithm initialized specifically by its supported keySize.
* @param signatureScheme a supported [SignatureScheme], see [Crypto], default to [DEFAULT_SIGNATURE_SCHEME] if not provided.
* @return a new [KeyPair] for the requested [SignatureScheme].
* @throws IllegalArgumentException if the requested signature scheme is not supported.
*/
@Throws(IllegalArgumentException::class)
@JvmOverloads
fun generateKeyPair(signatureScheme: SignatureScheme = DEFAULT_SIGNATURE_SCHEME): KeyPair {
if (!supportedSignatureSchemes.containsKey(signatureScheme.schemeCodeName))
throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $signatureScheme.schemeCodeName")
val keyPairGenerator = KeyPairGenerator.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName])
if (signatureScheme.algSpec != null)
keyPairGenerator.initialize(signatureScheme.algSpec, newSecureRandom())
else
keyPairGenerator.initialize(signatureScheme.keySize, newSecureRandom())
return keyPairGenerator.generateKeyPair()
}
/**
* Returns a key pair derived from the given [BigInteger] entropy. This is useful for unit tests
* and other cases where you want hard-coded private keys.
* Currently, [EDDSA_ED25519_SHA512] is the sole scheme supported for this operation.
* @param signatureScheme a supported [SignatureScheme], see [Crypto].
* @param entropy a [BigInteger] value.
* @return a new [KeyPair] from an entropy input.
* @throws IllegalArgumentException if the requested signature scheme is not supported for KeyPair generation using an entropy input.
*/
fun generateKeyPairFromEntropy(signatureScheme: SignatureScheme, entropy: BigInteger): KeyPair {
when (signatureScheme) {
EDDSA_ED25519_SHA512 -> return generateEdDSAKeyPairFromEntropy(entropy)
}
throw IllegalArgumentException("Unsupported signature scheme for fixed entropy-based key pair generation: $signatureScheme.schemeCodeName")
}
/**
* Returns a [DEFAULT_SIGNATURE_SCHEME] key pair derived from the given [BigInteger] entropy.
* @param entropy a [BigInteger] value.
* @return a new [KeyPair] from an entropy input.
*/
fun generateKeyPairFromEntropy(entropy: BigInteger): KeyPair = generateKeyPairFromEntropy(DEFAULT_SIGNATURE_SCHEME, entropy)
// custom key pair generator from entropy.
private fun generateEdDSAKeyPairFromEntropy(entropy: BigInteger): KeyPair {
val params = EDDSA_ED25519_SHA512.algSpec as EdDSANamedCurveSpec
val bytes = entropy.toByteArray().copyOf(params.curve.field.getb() / 8) // need to pad the entropy to the valid seed length.
val priv = EdDSAPrivateKeySpec(bytes, params)
val pub = EdDSAPublicKeySpec(priv.a, params)
return KeyPair(EdDSAPublicKey(pub), EdDSAPrivateKey(priv))
}
/** Check if the requested signature scheme is supported by the system. */
fun isSupportedSignatureScheme(schemeCodeName: String): Boolean = schemeCodeName in supportedSignatureSchemes
/** @return the default signature scheme's code name. */
fun getDefaultSignatureSchemeCodeName(): String = DEFAULT_SIGNATURE_SCHEME.schemeCodeName
fun isSupportedSignatureScheme(signatureScheme: SignatureScheme): Boolean = signatureScheme.schemeCodeName in supportedSignatureSchemes
/** @return a [List] of Strings with the scheme code names defined in [SignatureScheme] for all of our supported signature schemes, see [Crypto]. */
fun listSupportedSignatureSchemes(): List<String> = supportedSignatureSchemes.keys.toList()
/**
* Use bouncy castle utilities to sign completed X509 certificate with CA cert private key
*/
fun createCertificate(issuer: X500Name, issuerKeyPair: KeyPair,
subject: X500Name, subjectPublicKey: PublicKey,
keyUsage: KeyUsage, purposes: List<KeyPurposeId>,
signatureScheme: SignatureScheme, validityWindow: Pair<Date, Date>,
pathLength: Int? = null, subjectAlternativeName: List<GeneralName>? = null): X509Certificate {
val provider = providerMap[signatureScheme.providerName]
val serial = BigInteger.valueOf(random63BitValue())
val keyPurposes = DERSequence(ASN1EncodableVector().apply { purposes.forEach { add(it) } })
val builder = JcaX509v3CertificateBuilder(issuer, serial, validityWindow.first, validityWindow.second, subject, subjectPublicKey)
.addExtension(Extension.subjectKeyIdentifier, false, BcX509ExtensionUtils().createSubjectKeyIdentifier(SubjectPublicKeyInfo.getInstance(subjectPublicKey.encoded)))
.addExtension(Extension.basicConstraints, pathLength != null, if (pathLength == null) BasicConstraints(false) else BasicConstraints(pathLength))
.addExtension(Extension.keyUsage, false, keyUsage)
.addExtension(Extension.extendedKeyUsage, false, keyPurposes)
if (subjectAlternativeName != null && subjectAlternativeName.isNotEmpty()) {
builder.addExtension(Extension.subjectAlternativeName, false, DERSequence(subjectAlternativeName.toTypedArray()))
}
val signer = ContentSignerBuilder.build(signatureScheme, issuerKeyPair.private, provider)
return JcaX509CertificateConverter().setProvider(provider).getCertificate(builder.build(signer)).apply {
checkValidity(Date())
verify(issuerKeyPair.public, provider)
}
}
/**
* Create certificate signing request using provided information.
*/
fun createCertificateSigningRequest(subject: X500Name, keyPair: KeyPair, signatureScheme: SignatureScheme): PKCS10CertificationRequest {
val signer = ContentSignerBuilder.build(signatureScheme, keyPair.private, providerMap[signatureScheme.providerName])
return JcaPKCS10CertificationRequestBuilder(subject, keyPair.public).build(signer)
}
private class KeyInfoConverter(val signatureScheme: SignatureScheme) : AsymmetricKeyInfoConverter {
override fun generatePublic(keyInfo: SubjectPublicKeyInfo?): PublicKey? = keyInfo?.let { decodePublicKey(signatureScheme, it.encoded) }
override fun generatePrivate(keyInfo: PrivateKeyInfo?): PrivateKey? = keyInfo?.let { decodePrivateKey(signatureScheme, it.encoded) }
}
/**
* Check if a point's coordinates are on the expected curve to avoid certain types of ECC attacks.
* Point-at-infinity is not permitted as well.
* @see <a href="https://safecurves.cr.yp.to/twist.html">Small subgroup and invalid-curve attacks</a> for a more descriptive explanation on such attacks.
* We use this function on [findSignatureScheme] for a [PublicKey]; currently used for signature verification only.
* Thus, as these attacks are mostly not relevant to signature verification, we should note that
* we're doing it out of an abundance of caution and specifically to proactively protect developers
* against using these points as part of a DH key agreement or for use cases as yet unimagined.
* This method currently applies to BouncyCastle's ECDSA (both R1 and K1 curves) and I2P's EdDSA (ed25519 curve).
* @param publicKey a [PublicKey], usually used to validate a signer's public key in on the Curve.
* @param signatureScheme a [SignatureScheme] object, retrieved from supported signature schemes, see [Crypto].
* @return true if the point lies on the curve or false if it doesn't.
* @throws IllegalArgumentException if the requested signature scheme or the key type is not supported.
*/
@Throws(IllegalArgumentException::class)
fun publicKeyOnCurve(signatureScheme: SignatureScheme, publicKey: PublicKey): Boolean {
if (!isSupportedSignatureScheme(signatureScheme))
throw IllegalArgumentException("Unsupported signature scheme: $signatureScheme.schemeCodeName")
when (publicKey) {
is BCECPublicKey -> return (publicKey.parameters == signatureScheme.algSpec && !publicKey.q.isInfinity && publicKey.q.isValid)
is EdDSAPublicKey -> return (publicKey.params == signatureScheme.algSpec && !isEdDSAPointAtInfinity(publicKey) && publicKey.a.isOnCurve)
else -> throw IllegalArgumentException("Unsupported key type: ${publicKey::class}")
}
}
// return true if EdDSA publicKey is point at infinity.
// For EdDSA a custom function is required as it is not supported by the I2P implementation.
private fun isEdDSAPointAtInfinity(publicKey: EdDSAPublicKey) = publicKey.a.toP3() == (EDDSA_ED25519_SHA512.algSpec as EdDSANamedCurveSpec).curve.getZero(GroupElement.Representation.P3)
}

View File

@ -1,125 +0,0 @@
@file:JvmName("CryptoUtilities")
package net.corda.core.crypto
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.OpaqueBytes
import net.i2p.crypto.eddsa.EdDSAEngine
import net.i2p.crypto.eddsa.EdDSAPrivateKey
import net.i2p.crypto.eddsa.EdDSAPublicKey
import net.i2p.crypto.eddsa.KeyPairGenerator
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable
import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec
import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec
import java.math.BigInteger
import java.security.*
/** A wrapper around a digital signature. */
@CordaSerializable
open class DigitalSignature(bits: ByteArray) : OpaqueBytes(bits) {
/** A digital signature that identifies who the public key is owned by. */
open class WithKey(val by: PublicKey, bits: ByteArray) : DigitalSignature(bits) {
fun verifyWithECDSA(content: ByteArray) = by.verifyWithECDSA(content, this)
fun verifyWithECDSA(content: OpaqueBytes) = by.verifyWithECDSA(content.bytes, this)
}
// TODO: consider removing this as whoever needs to identify the signer should be able to derive it from the public key
class LegallyIdentifiable(val signer: Party, bits: ByteArray) : WithKey(signer.owningKey.singleKey, bits)
}
@CordaSerializable
object NullPublicKey : PublicKey, Comparable<PublicKey> {
override fun getAlgorithm() = "NULL"
override fun getEncoded() = byteArrayOf(0)
override fun getFormat() = "NULL"
override fun compareTo(other: PublicKey): Int = if (other == NullPublicKey) 0 else -1
override fun toString() = "NULL_KEY"
}
val NullCompositeKey = NullPublicKey.composite
// TODO: Clean up this duplication between Null and Dummy public key
@CordaSerializable
class DummyPublicKey(val s: String) : PublicKey, Comparable<PublicKey> {
override fun getAlgorithm() = "DUMMY"
override fun getEncoded() = s.toByteArray()
override fun getFormat() = "ASN.1"
override fun compareTo(other: PublicKey): Int = BigInteger(encoded).compareTo(BigInteger(other.encoded))
override fun equals(other: Any?) = other is DummyPublicKey && other.s == s
override fun hashCode(): Int = s.hashCode()
override fun toString() = "PUBKEY[$s]"
}
/** A signature with a key and value of zero. Useful when you want a signature object that you know won't ever be used. */
@CordaSerializable
object NullSignature : DigitalSignature.WithKey(NullPublicKey, ByteArray(32))
/** Utility to simplify the act of signing a byte array */
fun PrivateKey.signWithECDSA(bytes: ByteArray): DigitalSignature {
val signer = EdDSAEngine()
signer.initSign(this)
signer.update(bytes)
val sig = signer.sign()
return DigitalSignature(sig)
}
fun PrivateKey.signWithECDSA(bytesToSign: ByteArray, publicKey: PublicKey): DigitalSignature.WithKey {
return DigitalSignature.WithKey(publicKey, signWithECDSA(bytesToSign).bytes)
}
val ed25519Curve = EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.CURVE_ED25519_SHA512)
fun parsePublicKeyBase58(base58String: String) = EdDSAPublicKey(EdDSAPublicKeySpec(Base58.decode(base58String), ed25519Curve))
fun PublicKey.toBase58String() = Base58.encode((this as EdDSAPublicKey).abyte)
fun KeyPair.signWithECDSA(bytesToSign: ByteArray) = private.signWithECDSA(bytesToSign, public)
fun KeyPair.signWithECDSA(bytesToSign: OpaqueBytes) = private.signWithECDSA(bytesToSign.bytes, public)
fun KeyPair.signWithECDSA(bytesToSign: OpaqueBytes, party: Party) = signWithECDSA(bytesToSign.bytes, party)
fun KeyPair.signWithECDSA(bytesToSign: ByteArray, party: Party): DigitalSignature.LegallyIdentifiable {
check(public in party.owningKey.keys)
val sig = signWithECDSA(bytesToSign)
return DigitalSignature.LegallyIdentifiable(party, sig.bytes)
}
/** Utility to simplify the act of verifying a signature */
fun PublicKey.verifyWithECDSA(content: ByteArray, signature: DigitalSignature) {
val verifier = EdDSAEngine()
verifier.initVerify(this)
verifier.update(content)
if (verifier.verify(signature.bytes) == false)
throw SignatureException("Signature did not match")
}
/** Render a public key to a string, using a short form if it's an elliptic curve public key */
fun PublicKey.toStringShort(): String {
return (this as? EdDSAPublicKey)?.let { key ->
"DL" + Base58.encode(key.abyte) // DL -> Distributed Ledger
} ?: toString()
}
/** Creates a [CompositeKey] with a single leaf node containing the public key */
val PublicKey.composite: CompositeKey get() = CompositeKey.Leaf(this)
/** Returns the set of all [PublicKey]s of the signatures */
fun Iterable<DigitalSignature.WithKey>.byKeys() = map { it.by }.toSet()
// Allow Kotlin destructuring: val (private, public) = keyPair
operator fun KeyPair.component1() = this.private
operator fun KeyPair.component2() = this.public
/** A simple wrapper that will make it easier to swap out the EC algorithm we use in future */
fun generateKeyPair(): KeyPair = KeyPairGenerator().generateKeyPair()
/**
* Returns a key pair derived from the given private key entropy. This is useful for unit tests and other cases where
* you want hard-coded private keys.
*/
fun entropyToKeyPair(entropy: BigInteger): KeyPair {
val params = EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.CURVE_ED25519_SHA512)
val bytes = entropy.toByteArray().copyOf(params.curve.field.getb() / 8)
val priv = EdDSAPrivateKeySpec(bytes, params)
val pub = EdDSAPublicKeySpec(priv.a, params)
val key = KeyPair(EdDSAPublicKey(pub), EdDSAPrivateKey(priv))
return key
}

View File

@ -1,23 +1,155 @@
@file:JvmName("CryptoUtils")
package net.corda.core.crypto
import net.corda.core.identity.Party
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.OpaqueBytes
import net.i2p.crypto.eddsa.EdDSAPublicKey
import java.math.BigInteger
import net.corda.core.utilities.SgxSupport
import java.security.*
@CordaSerializable
object NullPublicKey : PublicKey, Comparable<PublicKey> {
override fun getAlgorithm() = "NULL"
override fun getEncoded() = byteArrayOf(0)
override fun getFormat() = "NULL"
override fun compareTo(other: PublicKey): Int = if (other == NullPublicKey) 0 else -1
override fun toString() = "NULL_KEY"
}
// TODO: Clean up this duplication between Null and Dummy public key
@CordaSerializable
class DummyPublicKey(val s: String) : PublicKey, Comparable<PublicKey> {
override fun getAlgorithm() = "DUMMY"
override fun getEncoded() = s.toByteArray()
override fun getFormat() = "ASN.1"
override fun compareTo(other: PublicKey): Int = BigInteger(encoded).compareTo(BigInteger(other.encoded))
override fun equals(other: Any?) = other is DummyPublicKey && other.s == s
override fun hashCode(): Int = s.hashCode()
override fun toString() = "PUBKEY[$s]"
}
/** A signature with a key and value of zero. Useful when you want a signature object that you know won't ever be used. */
@CordaSerializable
object NullSignature : DigitalSignature.WithKey(NullPublicKey, ByteArray(32))
/**
* Helper function for signing.
* @param clearData the data/message to be signed in [ByteArray] form (usually the Merkle root).
* Utility to simplify the act of signing a byte array.
* @param bytesToSign the data/message to be signed in [ByteArray] form (usually the Merkle root).
* @return the [DigitalSignature] object on the input message.
* @throws IllegalArgumentException if the signature scheme is not supported for this private key.
* @throws InvalidKeyException if the private key is invalid.
* @throws SignatureException if signing is not possible due to malformed data or private key.
*/
@Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class)
fun PrivateKey.sign(bytesToSign: ByteArray): DigitalSignature {
return DigitalSignature(Crypto.doSign(this, bytesToSign))
}
fun PrivateKey.sign(bytesToSign: ByteArray, publicKey: PublicKey): DigitalSignature.WithKey {
return DigitalSignature.WithKey(publicKey, this.sign(bytesToSign).bytes)
}
/**
* Helper function to sign with a key pair.
* @param bytesToSign the data/message to be signed in [ByteArray] form (usually the Merkle root).
* @return the digital signature (in [ByteArray]) on the input message.
* @throws IllegalArgumentException if the signature scheme is not supported for this private key.
* @throws InvalidKeyException if the private key is invalid.
* @throws SignatureException if signing is not possible due to malformed data or private key.
*/
@Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class)
fun PrivateKey.sign(clearData: ByteArray): ByteArray = Crypto.doSign(this, clearData)
fun KeyPair.sign(bytesToSign: ByteArray) = private.sign(bytesToSign, public)
fun KeyPair.sign(bytesToSign: OpaqueBytes) = private.sign(bytesToSign.bytes, public)
fun KeyPair.sign(bytesToSign: OpaqueBytes, party: Party) = sign(bytesToSign.bytes, party)
// TODO This case will need more careful thinking, as party owningKey can be a CompositeKey. One way of doing that is
// implementation of CompositeSignature.
@Throws(InvalidKeyException::class)
fun KeyPair.sign(bytesToSign: ByteArray, party: Party): DigitalSignature.LegallyIdentifiable {
val sig = sign(bytesToSign)
val sigKey = when (party.owningKey) { // Quick workaround when we have CompositeKey as Party owningKey.
is CompositeKey -> throw InvalidKeyException("Signing for parties with CompositeKey not supported.")
else -> party.owningKey
}
return DigitalSignature.LegallyIdentifiable(party, sig.bytes)
}
/**
* Utility to simplify the act of verifying a signature.
*
* @throws InvalidKeyException if the key to verify the signature with is not valid (i.e. wrong key type for the
* signature).
* @throws SignatureException if the signature is invalid (i.e. damaged), or does not match the key (incorrect).
* @throws IllegalArgumentException if the signature scheme is not supported or if any of the clear or signature data is empty.
*/
// TODO: SignatureException should be used only for a damaged signature, as per `java.security.Signature.verify()`,
@Throws(SignatureException::class, IllegalArgumentException::class, InvalidKeyException::class)
fun PublicKey.verify(content: ByteArray, signature: DigitalSignature) = Crypto.doVerify(this, signature.bytes, content)
/**
* Utility to simplify the act of verifying a signature. In comparison to [verify] if the key and signature
* do not match it returns false rather than throwing an exception. Normally you should use the function which throws,
* as it avoids the risk of failing to test the result, but this is for uses such as [java.security.Signature.verify]
* implementations.
*
* @throws InvalidKeyException if the key to verify the signature with is not valid (i.e. wrong key type for the
* signature).
* @throws SignatureException if the signature is invalid (i.e. damaged).
* @throws IllegalArgumentException if the signature scheme is not supported or if any of the clear or signature data is empty.
* @return whether the signature is correct for this key.
*/
@Throws(IllegalStateException::class, SignatureException::class, IllegalArgumentException::class)
fun PublicKey.isValid(content: ByteArray, signature: DigitalSignature) : Boolean {
if (this is CompositeKey)
throw IllegalStateException("Verification of CompositeKey signatures currently not supported.") // TODO CompositeSignature verification.
return Crypto.isValid(this, signature.bytes, content)
}
/** Render a public key to its hash (in Base58) of its serialised form using the DL prefix. */
fun PublicKey.toStringShort(): String = "DL" + this.toSHA256Bytes().toBase58()
val PublicKey.keys: Set<PublicKey> get() {
return if (this is CompositeKey) this.leafKeys
else setOf(this)
}
fun PublicKey.isFulfilledBy(otherKey: PublicKey): Boolean = isFulfilledBy(setOf(otherKey))
fun PublicKey.isFulfilledBy(otherKeys: Iterable<PublicKey>): Boolean {
return if (this is CompositeKey) this.isFulfilledBy(otherKeys)
else this in otherKeys
}
/** Checks whether any of the given [keys] matches a leaf on the CompositeKey tree or a single PublicKey */
fun PublicKey.containsAny(otherKeys: Iterable<PublicKey>): Boolean {
return if (this is CompositeKey) keys.intersect(otherKeys).isNotEmpty()
else this in otherKeys
}
/** Returns the set of all [PublicKey]s of the signatures */
fun Iterable<DigitalSignature.WithKey>.byKeys() = map { it.by }.toSet()
// Allow Kotlin destructuring: val (private, public) = keyPair
operator fun KeyPair.component1(): PrivateKey = this.private
operator fun KeyPair.component2(): PublicKey = this.public
/** A simple wrapper that will make it easier to swap out the EC algorithm we use in future */
fun generateKeyPair(): KeyPair = Crypto.generateKeyPair()
/**
* Returns a key pair derived from the given private key entropy. This is useful for unit tests and other cases where
* you want hard-coded private keys.
* This currently works for the default signature scheme EdDSA ed25519 only.
*/
fun entropyToKeyPair(entropy: BigInteger): KeyPair = Crypto.generateKeyPairFromEntropy(entropy)
/**
* Helper function for signing.
* @param metaDataFull tha attached MetaData object.
* @return a [DSWithMetaDataFull] object.
* @param metaData tha attached MetaData object.
* @return a [TransactionSignature] object.
* @throws IllegalArgumentException if the signature scheme is not supported for this private key.
* @throws InvalidKeyException if the private key is invalid.
* @throws SignatureException if signing is not possible due to malformed data or private key.
@ -25,17 +157,6 @@ fun PrivateKey.sign(clearData: ByteArray): ByteArray = Crypto.doSign(this, clear
@Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class)
fun PrivateKey.sign(metaData: MetaData): TransactionSignature = Crypto.doSign(this, metaData)
/**
* Helper function to sign with a key pair.
* @param clearData the data/message to be signed in [ByteArray] form (usually the Merkle root).
* @return the digital signature (in [ByteArray]) on the input message.
* @throws IllegalArgumentException if the signature scheme is not supported for this private key.
* @throws InvalidKeyException if the private key is invalid.
* @throws SignatureException if signing is not possible due to malformed data or private key.
*/
@Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class)
fun KeyPair.sign(clearData: ByteArray): ByteArray = Crypto.doSign(this.private, clearData)
/**
* Helper function to verify a signature.
* @param signatureData the signature on a message.
@ -66,7 +187,7 @@ fun PublicKey.verify(transactionSignature: TransactionSignature): Boolean {
/**
* Helper function for the signers to verify their own signature.
* @param signature the signature on a message.
* @param signatureData the signature on a message.
* @param clearData the clear data/message that was signed (usually the Merkle root).
* @throws InvalidKeyException if the key is invalid.
* @throws SignatureException if this signatureData object is not initialized properly,
@ -86,7 +207,7 @@ fun KeyPair.verify(signatureData: ByteArray, clearData: ByteArray): Boolean = Cr
* which should never happen and suggests an unusual JVM or non-standard Java library.
*/
@Throws(NoSuchAlgorithmException::class)
fun safeRandomBytes(numOfBytes: Int): ByteArray {
fun secureRandomBytes(numOfBytes: Int): ByteArray {
return newSecureRandom().generateSeed(numOfBytes)
}

View File

@ -0,0 +1,52 @@
package net.corda.core.crypto
import net.corda.core.identity.Party
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.OpaqueBytes
import java.security.InvalidKeyException
import java.security.PublicKey
import java.security.SignatureException
// TODO: Is there a use-case for bare [DigitalSignature], or is everything a [DigitalSignature.WithKey]? If there's no
// actual use-case, we should merge the with key version into the parent class. In that case [CompositeSignatureWithKeys]
// should be renamed to match.
/** A wrapper around a digital signature. */
@CordaSerializable
open class DigitalSignature(bits: ByteArray) : OpaqueBytes(bits) {
/** A digital signature that identifies who the public key is owned by. */
open class WithKey(val by: PublicKey, bits: ByteArray) : DigitalSignature(bits) {
/**
* Utility to simplify the act of verifying a signature.
*
* @throws InvalidKeyException if the key to verify the signature with is not valid (i.e. wrong key type for the
* signature).
* @throws SignatureException if the signature is invalid (i.e. damaged), or does not match the key (incorrect).
*/
@Throws(InvalidKeyException::class, SignatureException::class)
fun verify(content: ByteArray) = by.verify(content, this)
/**
* Utility to simplify the act of verifying a signature.
*
* @throws InvalidKeyException if the key to verify the signature with is not valid (i.e. wrong key type for the
* signature).
* @throws SignatureException if the signature is invalid (i.e. damaged), or does not match the key (incorrect).
*/
@Throws(InvalidKeyException::class, SignatureException::class)
fun verify(content: OpaqueBytes) = by.verify(content.bytes, this)
/**
* Utility to simplify the act of verifying a signature. In comparison to [verify] doesn't throw an
* exception, making it more suitable where a boolean is required, but normally you should use the function
* which throws, as it avoids the risk of failing to test the result.
*
* @throws InvalidKeyException if the key to verify the signature with is not valid (i.e. wrong key type for the
* signature).
* @throws SignatureException if the signature is invalid (i.e. damaged).
* @return whether the signature is correct for this key.
*/
@Throws(InvalidKeyException::class, SignatureException::class)
fun isValid(content: ByteArray) = by.isValid(content, this)
}
// TODO: consider removing this as whoever needs to identify the signer should be able to derive it from the public key
class LegallyIdentifiable(val signer: Party, bits: ByteArray) : WithKey(signer.owningKey, bits)
}

View File

@ -1,12 +0,0 @@
package net.corda.core.crypto
import java.security.KeyFactory
/**
* Custom [KeyFactory] for EdDSA with null security [Provider].
* This is required as a [SignatureScheme] requires a [java.security.KeyFactory] property, but i2p has
* its own KeyFactory for EdDSA, thus this actually a Proxy Pattern over i2p's KeyFactory.
*/
class EdDSAKeyFactory: KeyFactory {
constructor() : super(net.i2p.crypto.eddsa.KeyFactory(), null, "EDDSA_ED25519_SHA512")
}

View File

@ -1,10 +1,14 @@
@file:JvmName("EncodingUtils")
package net.corda.core.crypto
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import java.nio.charset.Charset
import java.security.PublicKey
import java.util.*
import javax.xml.bind.DatatypeConverter
// This file includes useful encoding methods and extension functions for the most common encoding/decoding operations.
// [ByteArray] encoders
@ -33,7 +37,7 @@ fun String.base58ToByteArray(): ByteArray = Base58.decode(this)
fun String.base64ToByteArray(): ByteArray = Base64.getDecoder().decode(this)
/** Hex-String to [ByteArray]. Accept any hex form (capitalized, lowercase, mixed). */
fun String.hexToByteArray(): ByteArray = DatatypeConverter.parseHexBinary(this);
fun String.hexToByteArray(): ByteArray = DatatypeConverter.parseHexBinary(this)
// Encoding changers
@ -56,5 +60,9 @@ fun String.hexToBase58(): String = hexToByteArray().toBase58()
/** Encoding changer. Hex-[String] to Base64-[String], i.e. "48656C6C6F20576F726C64" -> "SGVsbG8gV29ybGQ=" */
fun String.hexToBase64(): String = hexToByteArray().toBase64()
// Helper vars.
private val HEX_ALPHABET = "0123456789ABCDEF".toCharArray()
// TODO We use for both CompositeKeys and EdDSAPublicKey custom Kryo serializers and deserializers. We need to specify encoding.
// TODO: follow the crypto-conditions ASN.1 spec, some changes are needed to be compatible with the condition
// structure, e.g. mapping a PublicKey to a condition with the specific feature (ED25519).
fun parsePublicKeyBase58(base58String: String): PublicKey = base58String.base58ToByteArray().deserialize<PublicKey>()
fun PublicKey.toBase58String(): String = this.serialize().bytes.toBase58()
fun PublicKey.toSHA256Bytes(): ByteArray = this.serialize().bytes.sha256().bytes

View File

@ -0,0 +1,136 @@
package net.corda.core.crypto
import net.corda.core.exists
import net.corda.core.read
import net.corda.core.write
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.nio.file.Path
import java.security.*
import java.security.cert.Certificate
import java.security.cert.X509Certificate
object KeyStoreUtilities {
val KEYSTORE_TYPE = "JKS"
/**
* Helper method to either open an existing keystore for modification, or create a new blank keystore.
* @param keyStoreFilePath location of KeyStore file
* @param storePassword password to open the store. This does not have to be the same password as any keys stored,
* but for SSL purposes this is recommended.
* @return returns the KeyStore opened/created
*/
fun loadOrCreateKeyStore(keyStoreFilePath: Path, storePassword: String): KeyStore {
val pass = storePassword.toCharArray()
val keyStore = KeyStore.getInstance(KEYSTORE_TYPE)
if (keyStoreFilePath.exists()) {
keyStoreFilePath.read { keyStore.load(it, pass) }
} else {
keyStore.load(null, pass)
keyStoreFilePath.write { keyStore.store(it, pass) }
}
return keyStore
}
/**
* Helper method to open an existing keystore for modification/read
* @param keyStoreFilePath location of KeyStore file which must exist, or this will throw FileNotFoundException
* @param storePassword password to open the store. This does not have to be the same password as any keys stored,
* but for SSL purposes this is recommended.
* @return returns the KeyStore opened
* @throws IOException if there was an error reading the key store from the file.
* @throws KeyStoreException if the password is incorrect or the key store is damaged.
*/
@Throws(KeyStoreException::class, IOException::class)
fun loadKeyStore(keyStoreFilePath: Path, storePassword: String): KeyStore {
return keyStoreFilePath.read { loadKeyStore(it, storePassword) }
}
/**
* Helper method to open an existing keystore for modification/read
* @param input stream containing a KeyStore e.g. loaded from a resource file
* @param storePassword password to open the store. This does not have to be the same password as any keys stored,
* but for SSL purposes this is recommended.
* @return returns the KeyStore opened
* @throws IOException if there was an error reading the key store from the stream.
* @throws KeyStoreException if the password is incorrect or the key store is damaged.
*/
@Throws(KeyStoreException::class, IOException::class)
fun loadKeyStore(input: InputStream, storePassword: String): KeyStore {
val pass = storePassword.toCharArray()
val keyStore = KeyStore.getInstance(KEYSTORE_TYPE)
input.use {
keyStore.load(input, pass)
}
return keyStore
}
}
/**
* Helper extension method to add, or overwrite any key data in store
* @param alias name to record the private key and certificate chain under
* @param key cryptographic key to store
* @param password password for unlocking the key entry in the future. This does not have to be the same password as any keys stored,
* but for SSL purposes this is recommended.
* @param chain the sequence of certificates starting with the public key certificate for this key and extending to the root CA cert
*/
fun KeyStore.addOrReplaceKey(alias: String, key: Key, password: CharArray, chain: Array<Certificate>) {
if (containsAlias(alias)) {
this.deleteEntry(alias)
}
this.setKeyEntry(alias, key, password, chain)
}
/**
* Helper extension method to add, or overwrite any public certificate data in store
* @param alias name to record the public certificate under
* @param cert certificate to store
*/
fun KeyStore.addOrReplaceCertificate(alias: String, cert: Certificate) {
if (containsAlias(alias)) {
this.deleteEntry(alias)
}
this.setCertificateEntry(alias, cert)
}
/**
* Helper method save KeyStore to storage
* @param keyStoreFilePath the file location to save to
* @param storePassword password to access the store in future. This does not have to be the same password as any keys stored,
* but for SSL purposes this is recommended.
*/
fun KeyStore.save(keyStoreFilePath: Path, storePassword: String) = keyStoreFilePath.write { store(it, storePassword) }
fun KeyStore.store(out: OutputStream, password: String) = store(out, password.toCharArray())
/**
* Extract public and private keys from a KeyStore file assuming storage alias is known.
* @param keyPassword Password to unlock the private key entries
* @param alias The name to lookup the Key and Certificate chain from
* @return The KeyPair found in the KeyStore under the specified alias
*/
fun KeyStore.getKeyPair(alias: String, keyPassword: String): KeyPair = getCertificateAndKey(alias, keyPassword).keyPair
/**
* Helper method to load a Certificate and KeyPair from their KeyStore.
* The access details should match those of the createCAKeyStoreAndTrustStore call used to manufacture the keys.
* @param keyPassword The password for the PrivateKey (not the store access password)
* @param alias The name to search for the data. Typically if generated with the methods here this will be one of
* CERT_PRIVATE_KEY_ALIAS, ROOT_CA_CERT_PRIVATE_KEY_ALIAS, INTERMEDIATE_CA_PRIVATE_KEY_ALIAS defined above
*/
fun KeyStore.getCertificateAndKey(alias: String, keyPassword: String): CertificateAndKey {
val keyPass = keyPassword.toCharArray()
val key = getKey(alias, keyPass) as PrivateKey
val cert = getCertificate(alias) as X509Certificate
return CertificateAndKey(cert, KeyPair(cert.publicKey, key))
}
/**
* Extract public X509 certificate from a KeyStore file assuming storage alias is know
* @param alias The name to lookup the Key and Certificate chain from
* @return The X509Certificate found in the KeyStore under the specified alias
*/
fun KeyStore.getX509Certificate(alias: String): X509Certificate = getCertificate(alias) as X509Certificate

View File

@ -11,12 +11,14 @@ import java.util.*
* signers, tx type, timestamp. Merkle Tree is kept in a recursive data structure. Building is done bottom up,
* from all leaves' hashes. If number of leaves is not a power of two, the tree is padded with zero hashes.
*/
sealed class MerkleTree(val hash: SecureHash) {
class Leaf(val value: SecureHash) : MerkleTree(value)
class Node(val value: SecureHash, val left: MerkleTree, val right: MerkleTree) : MerkleTree(value)
sealed class MerkleTree {
abstract val hash: SecureHash
data class Leaf(override val hash: SecureHash) : MerkleTree()
data class Node(override val hash: SecureHash, val left: MerkleTree, val right: MerkleTree) : MerkleTree()
companion object {
private fun isPow2(num: Int): Boolean = num and (num-1) == 0
private fun isPow2(num: Int): Boolean = num and (num - 1) == 0
/**
* Merkle tree building using hashes, with zero hash padding to full power of 2.
@ -52,9 +54,9 @@ sealed class MerkleTree(val hash: SecureHash) {
val newLevelHashes: MutableList<MerkleTree> = ArrayList()
val n = lastNodesList.size
require((n and 1) == 0) { "Sanity check: number of nodes should be even." }
for (i in 0..n-2 step 2) {
for (i in 0..n - 2 step 2) {
val left = lastNodesList[i]
val right = lastNodesList[i+1]
val right = lastNodesList[i + 1]
val newHash = left.hash.hashConcat(right.hash)
val combined = Node(newHash, left, right)
newLevelHashes.add(combined)

View File

@ -54,9 +54,9 @@ class PartialMerkleTree(val root: PartialTree) {
*/
@CordaSerializable
sealed class PartialTree {
class IncludedLeaf(val hash: SecureHash) : PartialTree()
class Leaf(val hash: SecureHash) : PartialTree()
class Node(val left: PartialTree, val right: PartialTree) : PartialTree()
data class IncludedLeaf(val hash: SecureHash) : PartialTree()
data class Leaf(val hash: SecureHash) : PartialTree()
data class Node(val left: PartialTree, val right: PartialTree) : PartialTree()
}
companion object {
@ -82,8 +82,8 @@ class PartialMerkleTree(val root: PartialTree) {
return when (tree) {
is MerkleTree.Leaf -> level
is MerkleTree.Node -> {
val l1 = checkFull(tree.left, level+1)
val l2 = checkFull(tree.right, level+1)
val l1 = checkFull(tree.left, level + 1)
val l2 = checkFull(tree.right, level + 1)
if (l1 != l2) throw MerkleTreeException("Got not full binary tree.")
l1
}
@ -104,10 +104,10 @@ class PartialMerkleTree(val root: PartialTree) {
): Pair<Boolean, PartialTree> {
return when (root) {
is MerkleTree.Leaf ->
if (root.value in includeHashes) {
usedHashes.add(root.value)
Pair(true, PartialTree.IncludedLeaf(root.value))
} else Pair(false, PartialTree.Leaf(root.value))
if (root.hash in includeHashes) {
usedHashes.add(root.hash)
Pair(true, PartialTree.IncludedLeaf(root.hash))
} else Pair(false, PartialTree.Leaf(root.hash))
is MerkleTree.Node -> {
val leftNode = buildPartialTree(root.left, includeHashes, usedHashes)
val rightNode = buildPartialTree(root.right, includeHashes, usedHashes)
@ -117,7 +117,7 @@ class PartialMerkleTree(val root: PartialTree) {
Pair(true, newTree)
} else {
// This node has no included leaves below. Cut the tree here and store a hash as a Leaf.
val newTree = PartialTree.Leaf(root.value)
val newTree = PartialTree.Leaf(root.hash)
Pair(false, newTree)
}
}

View File

@ -1,33 +1,7 @@
package net.corda.core.crypto
import net.corda.core.contracts.PartyAndReference
import net.corda.core.serialization.OpaqueBytes
import org.bouncycastle.asn1.x500.X500Name
import java.security.PublicKey
/**
* The [Party] class represents an entity on the network, which is typically identified by a legal [name] and public key
* that it can sign transactions under. As parties may use multiple keys for signing and, for example, have offline backup
* keys, the "public key" of a party is represented by a composite construct a [CompositeKey], which combines multiple
* cryptographic public key primitives into a tree structure.
*
* For example: Alice has two key pairs (pub1/priv1 and pub2/priv2), and wants to be able to sign transactions with either of them.
* Her advertised [Party] then has a legal [name] "Alice" and an [owningKey] "pub1 or pub2".
*
* [Party] is also used for service identities. E.g. Alice may also be running an interest rate oracle on her Corda node,
* which requires a separate signing key (and an identifying name). Services can also be distributed run by a coordinated
* cluster of Corda nodes. A [Party] representing a distributed service will use a composite key containing all
* individual cluster nodes' public keys. Each of the nodes in the cluster will advertise the same group [Party].
*
* Note that equality is based solely on the owning key.
*
* @see CompositeKey
*/
class Party(val name: String, owningKey: CompositeKey) : AbstractParty(owningKey) {
/** A helper constructor that converts the given [PublicKey] in to a [CompositeKey] with a single node */
constructor(name: String, owningKey: PublicKey) : this(name, owningKey.composite)
override fun toAnonymous(): AnonymousParty = AnonymousParty(owningKey)
override fun toString() = "${owningKey.toBase58String()} (${name})"
override fun nameOrNull(): String? = name
override fun ref(bytes: OpaqueBytes): PartyAndReference = PartyAndReference(this.toAnonymous(), bytes)
}
@Deprecated("Party has moved to identity package", ReplaceWith("net.corda.core.identity.Party"))
class Party(name: X500Name, owningKey: PublicKey) : net.corda.core.identity.Party(name, owningKey)

View File

@ -18,7 +18,7 @@ sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) {
}
}
override fun toString() = BaseEncoding.base16().encode(bytes)
override fun toString(): String = BaseEncoding.base16().encode(bytes)
fun prefixChars(prefixLen: Int = 6) = toString().substring(0, prefixLen)
fun hashConcat(other: SecureHash) = (this.bytes + other.bytes).sha256()

View File

@ -1,18 +1,17 @@
package net.corda.core.crypto
import java.security.*
import org.bouncycastle.asn1.ASN1ObjectIdentifier
import java.security.Signature
import java.security.spec.AlgorithmParameterSpec
/**
* This class is used to define a digital signature scheme.
* @param schemeNumberID we assign a number ID for more efficient on-wire serialisation. Please ensure uniqueness between schemes.
* @param schemeCodeName code name for this signature scheme (e.g. RSA_SHA256, ECDSA_SECP256K1_SHA256, ECDSA_SECP256R1_SHA256, EDDSA_ED25519_SHA512, SPHINCS-256_SHA512).
* @param signatureOID object identifier of the signature algorithm (e.g 1.3.101.112 for EdDSA)
* @param providerName the provider's name (e.g. "BC").
* @param algorithmName which signature algorithm is used (e.g. RSA, ECDSA. EdDSA, SPHINCS-256).
* @param sig the [Signature] class that provides the functionality of a digital signature scheme.
* eg. Signature.getInstance("SHA256withECDSA", "BC").
* @param keyFactory the KeyFactory for this scheme (e.g. KeyFactory.getInstance("RSA", "BC")).
* @param keyPairGenerator defines the <i>Service Provider Interface</i> (<b>SPI</b>) for the {@code KeyPairGenerator} class.
* e.g. KeyPairGenerator.getInstance("ECDSA", "BC").
* @param signatureName a signature-scheme name as required to create [Signature] objects (e.g. "SHA256withECDSA")
* @param algSpec parameter specs for the underlying algorithm. Note that RSA is defined by the key size rather than algSpec.
* eg. ECGenParameterSpec("secp256k1").
* @param keySize the private key size (currently used for RSA only).
@ -21,22 +20,10 @@ import java.security.spec.AlgorithmParameterSpec
data class SignatureScheme(
val schemeNumberID: Int,
val schemeCodeName: String,
val signatureOID: ASN1ObjectIdentifier,
val providerName: String,
val algorithmName: String,
val sig: Signature,
val keyFactory: KeyFactory,
val keyPairGenerator: KeyPairGeneratorSpi,
val signatureName: String,
val algSpec: AlgorithmParameterSpec?,
val keySize: Int,
val desc: String) {
/**
* KeyPair generators are always initialized once we create them, as no re-initialization is required.
* Note that RSA is the sole algorithm initialized specifically by its supported keySize.
*/
init {
if (algSpec != null)
keyPairGenerator.initialize(algSpec, newSecureRandom())
else
keyPairGenerator.initialize(keySize, newSecureRandom())
}
}
val desc: String)

Some files were not shown because too many files have changed in this diff Show More