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 # General build files
**/build/* **/build/*
!docs/build/*
lib/dokka.jar lib/dokka.jar
@ -34,6 +33,9 @@ lib/dokka.jar
.idea/shelf .idea/shelf
.idea/dataSources .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: # if you remove the above rule, at least ignore the following:
# User-specific stuff: # 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 ## Development State
Corda is currently in very early development and should not be used in production systems. Breaking Corda is under active development and is maturing rapidly. We are targeting
changes will happen on minor versions until 1.0. Experimentation with Corda is recommended. 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. Pull requests, experiments, and contributions are encouraged and welcomed.

View File

@ -1,16 +1,20 @@
buildscript { buildscript {
// For sharing constants between builds // For sharing constants between builds
Properties props = new Properties() Properties constants = new Properties()
file("publish.properties").withInputStream { props.load(it) } file("$projectDir/constants.properties").withInputStream { constants.load(it) }
// Our version: bump this on release. // Our version: bump this on release.
ext.corda_version = "0.10-SNAPSHOT" ext.corda_release_version = "0.12-SNAPSHOT"
ext.gradle_plugins_version = props.getProperty("gradlePluginsVersion") // 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. // Dependency versions. Can run 'gradle dependencyUpdates' to find new versions of things.
// //
// TODO: Sort this alphabetically. // 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.quasar_version = '0.7.6' // TODO: Upgrade to 0.7.7+ when Quasar bug 238 is resolved.
ext.asm_version = '0.5.3' ext.asm_version = '0.5.3'
ext.artemis_version = '1.5.3' ext.artemis_version = '1.5.3'
@ -19,24 +23,27 @@ buildscript {
ext.jersey_version = '2.25' ext.jersey_version = '2.25'
ext.jolokia_version = '2.0.0-M3' ext.jolokia_version = '2.0.0-M3'
ext.assertj_version = '3.6.1' ext.assertj_version = '3.6.1'
ext.slf4j_version = '1.7.24' ext.slf4j_version = '1.7.25'
ext.log4j_version = '2.7' ext.log4j_version = '2.7'
ext.bouncycastle_version = '1.56' ext.bouncycastle_version = constants.getProperty("bouncycastleVersion")
ext.guava_version = '19.0' ext.guava_version = constants.getProperty("guavaVersion")
ext.quickcheck_version = '0.7' ext.quickcheck_version = '0.7'
ext.okhttp_version = '3.5.0' ext.okhttp_version = '3.5.0'
ext.netty_version = '4.1.5.Final' 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.fileupload_version = '1.3.2'
ext.junit_version = '4.12' ext.junit_version = '4.12'
ext.mockito_version = '1.10.19'
ext.jopt_simple_version = '5.0.2' ext.jopt_simple_version = '5.0.2'
ext.jansi_version = '1.14' ext.jansi_version = '1.14'
ext.hibernate_version = '5.2.6.Final' ext.hibernate_version = '5.2.6.Final'
ext.h2_version = '1.4.194' ext.h2_version = '1.4.194'
ext.rxjava_version = '1.2.4' 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.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 { repositories {
mavenLocal() mavenLocal()
@ -66,13 +73,10 @@ ext {
corda_revision = org.ajoberstar.grgit.Grgit.open(file('.')).head().id corda_revision = org.ajoberstar.grgit.Grgit.open(file('.')).head().id
} }
apply plugin: 'kotlin'
apply plugin: 'project-report' apply plugin: 'project-report'
apply plugin: 'com.github.ben-manes.versions' apply plugin: 'com.github.ben-manes.versions'
apply plugin: 'net.corda.plugins.publish-utils' apply plugin: 'net.corda.plugins.publish-utils'
apply plugin: 'net.corda.plugins.quasar-utils'
apply plugin: 'net.corda.plugins.cordformation' 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 // 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 // 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 { allprojects {
apply plugin: 'kotlin'
apply plugin: 'java' apply plugin: 'java'
apply plugin: 'jacoco' apply plugin: 'jacoco'
sourceCompatibility = 1.8 sourceCompatibility = 1.8
targetCompatibility = 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) { 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' group 'net.corda'
version "$corda_version" version "$corda_release_version"
repositories { repositories {
mavenLocal()
mavenCentral()
jcenter()
// TODO: remove this once we eliminate Exposed
maven {
url 'https://dl.bintray.com/kotlin/exposed'
}
maven { url 'https://jitpack.io' } 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 // 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 // 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. // minor releases, so we can't work with just any Java 8, it has to be a recent one.
if (!JavaVersion.current().java8Compatible) 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 { repositories {
mavenCentral() mavenCentral()
@ -123,8 +170,9 @@ dependencies {
compile project(':node') compile project(':node')
compile "com.google.guava:guava:$guava_version" compile "com.google.guava:guava:$guava_version"
runtime project(path: ":node:capsule", configuration: 'runtimeArtifacts') // Set to compile to ensure it exists now deploy nodes no longer relies on build
runtime project(path: ":node:webserver:webcapsule", configuration: 'runtimeArtifacts') compile project(path: ":node:capsule", configuration: 'runtimeArtifacts')
compile project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts')
// For the buildCordappDependenciesJar task // For the buildCordappDependenciesJar task
runtime project(':client:jfx') runtime project(':client:jfx')
@ -132,7 +180,7 @@ dependencies {
runtime project(':client:rpc') runtime project(':client:rpc')
runtime project(':core') runtime project(':core')
runtime project(':finance') runtime project(':finance')
runtime project(':node:webserver') runtime project(':webserver')
testCompile project(':test-utils') testCompile project(':test-utils')
} }
@ -161,18 +209,18 @@ tasks.withType(Test) {
reports.html.destination = file("${reporting.baseDir}/${name}") 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" directory "./build/nodes"
networkMap "Controller" networkMap "CN=Controller,O=R3,OU=corda,L=London,C=UK"
node { node {
name "Controller" name "CN=Controller,O=R3,OU=corda,L=London,C=UK"
nearestCity "London" nearestCity "London"
advertisedServices = ["corda.notary.validating"] advertisedServices = ["corda.notary.validating"]
p2pPort 10002 p2pPort 10002
cordapps = [] cordapps = []
} }
node { node {
name "Bank A" name "CN=Bank A,O=R3,OU=corda,L=London,C=UK"
nearestCity "London" nearestCity "London"
advertisedServices = [] advertisedServices = []
p2pPort 10012 p2pPort 10012
@ -181,7 +229,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['build']) {
cordapps = [] cordapps = []
} }
node { node {
name "Bank B" name "CN=Bank B,O=R3,OU=corda,L=London,C=UK"
nearestCity "New York" nearestCity "New York"
advertisedServices = [] advertisedServices = []
p2pPort 10007 p2pPort 10007
@ -201,7 +249,7 @@ bintrayConfig {
projectUrl = 'https://github.com/corda/corda' projectUrl = 'https://github.com/corda/corda'
gpgSign = true gpgSign = true
gpgPassphrase = System.getenv('CORDA_BINTRAY_GPG_PASSPHRASE') 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 { license {
name = 'Apache-2.0' name = 'Apache-2.0'
url = 'https://www.apache.org/licenses/LICENSE-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 // 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. // Note: corda.jar is used at runtime so no runtime ZIP is necessary.
// Resulting ZIP can be found in "build/distributions" // 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' apply plugin: 'maven'
repositories { repositories {
@ -5,6 +12,5 @@ repositories {
} }
dependencies { dependencies {
// Cannot use ext.guava_version here :( compile "com.google.guava:guava:$guava_version"
compile "com.google.guava:guava:20.0"
} }

View File

@ -2,18 +2,9 @@ apply plugin: 'java'
apply plugin: 'kotlin' apply plugin: 'kotlin'
apply plugin: 'net.corda.plugins.publish-utils' apply plugin: 'net.corda.plugins.publish-utils'
repositories {
mavenLocal()
mavenCentral()
jcenter()
maven {
url 'https://dl.bintray.com/kotlin/exposed'
}
}
dependencies { dependencies {
compile project(':core') 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" testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
// Jackson and its plugins: parsing to/from JSON and other textual formats. // 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.Amount
import net.corda.core.contracts.BusinessCalendar import net.corda.core.contracts.BusinessCalendar
import net.corda.core.crypto.* 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.messaging.CordaRPCOps
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
import net.corda.core.node.services.IdentityService 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.deserialize
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
import net.i2p.crypto.eddsa.EdDSAPublicKey import net.i2p.crypto.eddsa.EdDSAPublicKey
import org.bouncycastle.asn1.ASN1InputStream
import org.bouncycastle.asn1.x500.X500Name
import java.math.BigDecimal import java.math.BigDecimal
import java.security.PublicKey
import java.util.* import java.util.*
/** /**
@ -31,21 +36,28 @@ object JacksonSupport {
// If you change this API please update the docs in the docsite (json.rst) // If you change this API please update the docs in the docsite (json.rst)
interface PartyObjectMapper { interface PartyObjectMapper {
@Deprecated("Use partyFromX500Name instead")
fun partyFromName(partyName: String): Party? 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) { class RpcObjectMapper(val rpc: CordaRPCOps, factory: JsonFactory) : PartyObjectMapper, ObjectMapper(factory) {
override fun partyFromName(partyName: String): Party? = rpc.partyFromName(partyName) 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) { class IdentityObjectMapper(val identityService: IdentityService, factory: JsonFactory) : PartyObjectMapper, ObjectMapper(factory) {
override fun partyFromName(partyName: String): Party? = identityService.partyFromName(partyName) 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 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 { val cordaModule: Module by lazy {
@ -82,6 +94,10 @@ object JacksonSupport {
// For OpaqueBytes // For OpaqueBytes
addDeserializer(OpaqueBytes::class.java, OpaqueBytesDeserializer) addDeserializer(OpaqueBytes::class.java, OpaqueBytesDeserializer)
addSerializer(OpaqueBytes::class.java, OpaqueBytesSerializer) 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 // 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) return AnonymousParty(key)
} }
} }
object PartySerializer : JsonSerializer<Party>() { object PartySerializer : JsonSerializer<Party>() {
override fun serialize(obj: Party, generator: JsonGenerator, provider: SerializerProvider) { 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 val mapper = parser.codec as PartyObjectMapper
// TODO this needs to use some industry identifier(s) not just these human readable names val principal = X500Name(parser.text)
return mapper.partyFromName(parser.text) ?: throw JsonParseException(parser, "Could not find a Party with name ${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>() { object PublicKeySerializer : JsonSerializer<EdDSAPublicKey>() {
override fun serialize(obj: EdDSAPublicKey, generator: JsonGenerator, provider: SerializerProvider) { 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()) generator.writeString(obj.toBase58String())
} }
} }
@ -212,7 +248,7 @@ object JacksonSupport {
object PublicKeyDeserializer : JsonDeserializer<EdDSAPublicKey>() { object PublicKeyDeserializer : JsonDeserializer<EdDSAPublicKey>() {
override fun deserialize(parser: JsonParser, context: DeserializationContext): EdDSAPublicKey { override fun deserialize(parser: JsonParser, context: DeserializationContext): EdDSAPublicKey {
return try { return try {
parsePublicKeyBase58(parser.text) parsePublicKeyBase58(parser.text) as EdDSAPublicKey
} catch (e: Exception) { } catch (e: Exception) {
throw JsonParseException(parser, "Invalid public key ${parser.text}: ${e.message}") throw JsonParseException(parser, "Invalid public key ${parser.text}: ${e.message}")
} }
@ -228,7 +264,7 @@ object JacksonSupport {
object CompositeKeyDeserializer : JsonDeserializer<CompositeKey>() { object CompositeKeyDeserializer : JsonDeserializer<CompositeKey>() {
override fun deserialize(parser: JsonParser, context: DeserializationContext): CompositeKey { override fun deserialize(parser: JsonParser, context: DeserializationContext): CompositeKey {
return try { return try {
CompositeKey.parseFromBase58(parser.text) parsePublicKeyBase58(parser.text) as CompositeKey
} catch (e: Exception) { } catch (e: Exception) {
throw JsonParseException(parser, "Invalid composite key ${parser.text}: ${e.message}") 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.fasterxml.jackson.dataformat.yaml.YAMLFactory
import com.google.common.collect.HashMultimap import com.google.common.collect.HashMultimap
import com.google.common.collect.Multimap import com.google.common.collect.Multimap
import com.google.common.collect.MultimapBuilder
import net.corda.jackson.StringToMethodCallParser.ParsedMethodCall import net.corda.jackson.StringToMethodCallParser.ParsedMethodCall
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.lang.reflect.Constructor import java.lang.reflect.Constructor
import java.lang.reflect.Method import java.lang.reflect.Method
import java.util.*
import java.util.concurrent.Callable import java.util.concurrent.Callable
import javax.annotation.concurrent.ThreadSafe import javax.annotation.concurrent.ThreadSafe
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.KFunction import kotlin.reflect.KFunction
import kotlin.reflect.KotlinReflectionInternalError import kotlin.reflect.jvm.internal.KotlinReflectionInternalError
import kotlin.reflect.jvm.kotlinFunction import kotlin.reflect.jvm.kotlinFunction
/** /**
@ -75,14 +73,14 @@ import kotlin.reflect.jvm.kotlinFunction
@ThreadSafe @ThreadSafe
open class StringToMethodCallParser<in T : Any> @JvmOverloads constructor( open class StringToMethodCallParser<in T : Any> @JvmOverloads constructor(
targetType: Class<out T>, 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]. */ /** Same as the regular constructor but takes a Kotlin reflection [KClass] instead of a Java [Class]. */
constructor(targetType: KClass<out T>) : this(targetType.java) constructor(targetType: KClass<out T>) : this(targetType.java)
companion object { companion object {
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
private val ignoredNames = Object::class.java.methods.map { it.name } private val ignoredNames = Object::class.java.methods.map { it.name }
private fun methodsFromType(clazz: Class<*>): Multimap<String, Method> { private fun methodsFromType(clazz: Class<*>): Multimap<String, Method> {
val result = HashMultimap.create<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 }) { 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 return result
} }
private val log = LoggerFactory.getLogger(StringToMethodCallParser::class.java)!! 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 -> return method.parameters.mapIndexed { index, param ->
when { when {
param.isNamePresent -> param.name 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) kf != null -> kf.parameters[index + 1].name ?: throw UnparseableCallException.ReflectionDataMissing(method.name, index)
else -> 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 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 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 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 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 parameterString = "{ $args }"
val tree: JsonNode = om.readTree(parameterString) ?: throw UnparseableCallException(args) val tree: JsonNode = om.readTree(parameterString) ?: throw UnparseableCallException(args)
if (tree.size() > parameters.size) throw UnparseableCallException.TooManyParameters(methodNameHint, 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 (argName, argType) = param
val entry = tree[argName] ?: throw UnparseableCallException.MissingParameter(methodNameHint, argName, args) 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) { if (log.isDebugEnabled) {
inOrderParams.forEachIndexed { i, param -> inOrderParams.forEachIndexed { i, param ->

View File

@ -10,7 +10,7 @@ class StringToMethodCallParserTest {
fun simple() = "simple" fun simple() = "simple"
fun string(note: String) = note fun string(note: String) = note
fun twoStrings(a: String, b: String) = a + b 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 complexObject(pair: Pair<Int, String>) = pair
fun overload(a: String) = a fun overload(a: String) = a

View File

@ -4,18 +4,6 @@ apply plugin: 'net.corda.plugins.publish-utils'
description 'Corda client JavaFX modules' 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 //noinspection GroovyAssignabilityCheck
configurations { configurations {
// we don't want isolated.jar in classPath, since we want to test jar being dynamically loaded as an attachment // 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') srcDir file('src/integration-test/kotlin')
} }
} }
test {
resources {
srcDir "../../config/test"
}
}
} }
// To find potential version conflicts, run "gradle htmlDependencyReport" and then look in // To find potential version conflicts, run "gradle htmlDependencyReport" and then look in
@ -48,6 +31,7 @@ dependencies {
compile project(':finance') compile project(':finance')
compile project(':client:rpc') compile project(':client:rpc')
compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
compile "com.google.guava:guava:$guava_version" compile "com.google.guava:guava:$guava_version"
// ReactFX: Functional reactive UI programming. // 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.Amount
import net.corda.core.contracts.DOLLARS import net.corda.core.contracts.DOLLARS
import net.corda.core.contracts.USD 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.flows.StateMachineRunId
import net.corda.core.getOrThrow import net.corda.core.getOrThrow
import net.corda.core.messaging.CordaRPCOps 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.node.services.Vault
import net.corda.core.serialization.OpaqueBytes import net.corda.core.serialization.OpaqueBytes
import net.corda.core.transactions.SignedTransaction 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.CashExitFlow
import net.corda.flows.CashIssueFlow import net.corda.flows.CashIssueFlow
import net.corda.flows.CashPaymentFlow import net.corda.flows.CashPaymentFlow
@ -30,21 +37,25 @@ import net.corda.testing.expect
import net.corda.testing.expectEvents import net.corda.testing.expectEvents
import net.corda.testing.node.DriverBasedTest import net.corda.testing.node.DriverBasedTest
import net.corda.testing.sequence import net.corda.testing.sequence
import org.bouncycastle.asn1.x500.X500Name
import org.junit.Test import org.junit.Test
import rx.Observable import rx.Observable
class NodeMonitorModelTest : DriverBasedTest() { class NodeMonitorModelTest : DriverBasedTest() {
lateinit var aliceNode: NodeInfo lateinit var aliceNode: NodeInfo
lateinit var bobNode: NodeInfo
lateinit var notaryNode: NodeInfo lateinit var notaryNode: NodeInfo
lateinit var rpc: CordaRPCOps lateinit var rpc: CordaRPCOps
lateinit var rpcBob: CordaRPCOps
lateinit var stateMachineTransactionMapping: Observable<StateMachineTransactionMapping> lateinit var stateMachineTransactionMapping: Observable<StateMachineTransactionMapping>
lateinit var stateMachineUpdates: Observable<StateMachineUpdate> lateinit var stateMachineUpdates: Observable<StateMachineUpdate>
lateinit var stateMachineUpdatesBob: Observable<StateMachineUpdate>
lateinit var progressTracking: Observable<ProgressTrackingEvent> lateinit var progressTracking: Observable<ProgressTrackingEvent>
lateinit var transactions: Observable<SignedTransaction> lateinit var transactions: Observable<SignedTransaction>
lateinit var vaultUpdates: Observable<Vault.Update> lateinit var vaultUpdates: Observable<Vault.Update>
lateinit var networkMapUpdates: Observable<NetworkMapCache.MapChange> lateinit var networkMapUpdates: Observable<NetworkMapCache.MapChange>
lateinit var newNode: (String) -> NodeInfo lateinit var newNode: (X500Name) -> NodeInfo
override fun setup() = driver { override fun setup() = driver {
val cashUser = User("user1", "test", permissions = setOf( val cashUser = User("user1", "test", permissions = setOf(
@ -52,15 +63,14 @@ class NodeMonitorModelTest : DriverBasedTest() {
startFlowPermission<CashPaymentFlow>(), startFlowPermission<CashPaymentFlow>(),
startFlowPermission<CashExitFlow>()) startFlowPermission<CashExitFlow>())
) )
val aliceNodeFuture = startNode("Alice", rpcUsers = listOf(cashUser)) val aliceNodeFuture = startNode(ALICE.name, rpcUsers = listOf(cashUser))
val notaryNodeFuture = startNode("Notary", advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))) val notaryNodeFuture = startNode(DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)))
val aliceNodeHandle = aliceNodeFuture.getOrThrow() val aliceNodeHandle = aliceNodeFuture.getOrThrow()
val notaryNodeHandle = notaryNodeFuture.getOrThrow() val notaryNodeHandle = notaryNodeFuture.getOrThrow()
aliceNode = aliceNodeHandle.nodeInfo aliceNode = aliceNodeHandle.nodeInfo
notaryNode = notaryNodeHandle.nodeInfo notaryNode = notaryNodeHandle.nodeInfo
newNode = { nodeName -> startNode(nodeName).getOrThrow().nodeInfo } newNode = { nodeName -> startNode(nodeName).getOrThrow().nodeInfo }
val monitor = NodeMonitorModel() val monitor = NodeMonitorModel()
stateMachineTransactionMapping = monitor.stateMachineTransactionMapping.bufferUntilSubscribed() stateMachineTransactionMapping = monitor.stateMachineTransactionMapping.bufferUntilSubscribed()
stateMachineUpdates = monitor.stateMachineUpdates.bufferUntilSubscribed() stateMachineUpdates = monitor.stateMachineUpdates.bufferUntilSubscribed()
progressTracking = monitor.progressTracking.bufferUntilSubscribed() progressTracking = monitor.progressTracking.bufferUntilSubscribed()
@ -70,26 +80,32 @@ class NodeMonitorModelTest : DriverBasedTest() {
monitor.register(aliceNodeHandle.configuration.rpcAddress!!, cashUser.username, cashUser.password) monitor.register(aliceNodeHandle.configuration.rpcAddress!!, cashUser.username, cashUser.password)
rpc = monitor.proxyObservable.value!! 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() runTest()
} }
@Test @Test
fun `network map update`() { fun `network map update`() {
newNode("Bob") newNode(CHARLIE.name)
newNode("Charlie")
networkMapUpdates.filter { !it.node.advertisedServices.any { it.info.type.isNotary() } } networkMapUpdates.filter { !it.node.advertisedServices.any { it.info.type.isNotary() } }
.filter { !it.node.advertisedServices.any { it.info.type == NetworkMapService.type } } .filter { !it.node.advertisedServices.any { it.info.type == NetworkMapService.type } }
.expectEvents(isStrict = false) { .expectEvents(isStrict = false) {
sequence( sequence(
// TODO : Add test for remove when driver DSL support individual node shutdown. // TODO : Add test for remove when driver DSL support individual node shutdown.
expect { output: NetworkMapCache.MapChange -> 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 -> 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 -> 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( sequence(
// SNAPSHOT // SNAPSHOT
expect { output: Vault.Update -> expect { output: Vault.Update ->
require(output.consumed.size == 0) { output.consumed.size } require(output.consumed.isEmpty()) { output.consumed.size }
require(output.produced.size == 0) { output.produced.size } require(output.produced.isEmpty()) { output.produced.size }
}, },
// ISSUE // ISSUE
expect { output: Vault.Update -> 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 } require(output.produced.size == 1) { output.produced.size }
} }
) )
@ -123,7 +139,7 @@ class NodeMonitorModelTest : DriverBasedTest() {
@Test @Test
fun `cash issue and move`() { fun `cash issue and move`() {
rpc.startFlow(::CashIssueFlow, 100.DOLLARS, OpaqueBytes.of(1), aliceNode.legalIdentity, notaryNode.notaryIdentity).returnValue.getOrThrow() 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 issueSmId: StateMachineRunId? = null
var moveSmId: StateMachineRunId? = null var moveSmId: StateMachineRunId? = null
@ -134,6 +150,8 @@ class NodeMonitorModelTest : DriverBasedTest() {
// ISSUE // ISSUE
expect { add: StateMachineUpdate.Added -> expect { add: StateMachineUpdate.Added ->
issueSmId = add.id issueSmId = add.id
val initiator = add.stateMachineInfo.initiator
require(initiator is FlowInitiator.RPC && initiator.username == "user1")
}, },
expect { remove: StateMachineUpdate.Removed -> expect { remove: StateMachineUpdate.Removed ->
require(remove.id == issueSmId) require(remove.id == issueSmId)
@ -141,6 +159,8 @@ class NodeMonitorModelTest : DriverBasedTest() {
// MOVE // MOVE
expect { add: StateMachineUpdate.Added -> expect { add: StateMachineUpdate.Added ->
moveSmId = add.id moveSmId = add.id
val initiator = add.stateMachineInfo.initiator
require(initiator is FlowInitiator.RPC && initiator.username == "user1")
}, },
expect { remove: StateMachineUpdate.Removed -> expect { remove: StateMachineUpdate.Removed ->
require(remove.id == moveSmId) 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 { transactions.expectEvents {
sequence( sequence(
// ISSUE // ISSUE
expect { tx -> expect { stx ->
require(tx.tx.inputs.isEmpty()) require(stx.tx.inputs.isEmpty())
require(tx.tx.outputs.size == 1) require(stx.tx.outputs.size == 1)
val signaturePubKeys = tx.sigs.map { it.by }.toSet() val signaturePubKeys = stx.sigs.map { it.by }.toSet()
// Only Alice signed // Only Alice signed
val aliceKey = aliceNode.legalIdentity.owningKey val aliceKey = aliceNode.legalIdentity.owningKey
require(signaturePubKeys.size <= aliceKey.keys.size) require(signaturePubKeys.size <= aliceKey.keys.size)
require(aliceKey.isFulfilledBy(signaturePubKeys)) require(aliceKey.isFulfilledBy(signaturePubKeys))
issueTx = tx issueTx = stx
}, },
// MOVE // MOVE
expect { tx -> expect { stx ->
require(tx.tx.inputs.size == 1) require(stx.tx.inputs.size == 1)
require(tx.tx.outputs.size == 1) require(stx.tx.outputs.size == 1)
val signaturePubKeys = tx.sigs.map { it.by }.toSet() val signaturePubKeys = stx.sigs.map { it.by }.toSet()
// Alice and Notary signed // Alice and Notary signed
require(aliceNode.legalIdentity.owningKey.isFulfilledBy(signaturePubKeys)) require(aliceNode.legalIdentity.owningKey.isFulfilledBy(signaturePubKeys))
require(notaryNode.notaryIdentity.owningKey.isFulfilledBy(signaturePubKeys)) require(notaryNode.notaryIdentity.owningKey.isFulfilledBy(signaturePubKeys))
moveTx = tx moveTx = stx
} }
) )
} }
@ -178,18 +208,18 @@ class NodeMonitorModelTest : DriverBasedTest() {
sequence( sequence(
// SNAPSHOT // SNAPSHOT
expect { output: Vault.Update -> expect { output: Vault.Update ->
require(output.consumed.size == 0) { output.consumed.size } require(output.consumed.isEmpty()) { output.consumed.size }
require(output.produced.size == 0) { output.produced.size } require(output.produced.isEmpty()) { output.produced.size }
}, },
// ISSUE // ISSUE
expect { update -> 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 } require(update.produced.size == 1) { update.produced.size }
}, },
// MOVE // MOVE
expect { update -> expect { update ->
require(update.consumed.size == 1) { update.consumed.size } 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.FXCollections
import javafx.collections.ObservableList import javafx.collections.ObservableList
import kotlinx.support.jdk8.collections.removeIf
import net.corda.client.jfx.utils.fold import net.corda.client.jfx.utils.fold
import net.corda.client.jfx.utils.map import net.corda.client.jfx.utils.map
import net.corda.contracts.asset.Cash import net.corda.contracts.asset.Cash
@ -28,7 +27,7 @@ class ContractStateModel {
private val cashStatesDiff: Observable<Diff<Cash.State>> = contractStatesDiff.map { private val cashStatesDiff: Observable<Diff<Cash.State>> = contractStatesDiff.map {
Diff(it.added.filterCashStateAndRefs(), it.removed.filterCashStateAndRefs()) 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.removeIf { it in statesDiff.removed }
list.addAll(statesDiff.added) list.addAll(statesDiff.added)
} }

View File

@ -3,12 +3,11 @@ package net.corda.client.jfx.model
import javafx.beans.value.ObservableValue import javafx.beans.value.ObservableValue
import javafx.collections.FXCollections import javafx.collections.FXCollections
import javafx.collections.ObservableList import javafx.collections.ObservableList
import kotlinx.support.jdk8.collections.removeIf
import net.corda.client.jfx.utils.firstOrDefault import net.corda.client.jfx.utils.firstOrDefault
import net.corda.client.jfx.utils.firstOrNullObservable import net.corda.client.jfx.utils.firstOrNullObservable
import net.corda.client.jfx.utils.fold import net.corda.client.jfx.utils.fold
import net.corda.client.jfx.utils.map 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.NodeInfo
import net.corda.core.node.services.NetworkMapCache.MapChange import net.corda.core.node.services.NetworkMapCache.MapChange
import java.security.PublicKey import java.security.PublicKey
@ -40,10 +39,6 @@ class NetworkIdentityModel {
} }
// TODO: Use Identity Service in service hub instead? // 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 } }) { fun lookup(publicKey: PublicKey): ObservableValue<NodeInfo?> = parties.firstOrDefault(notaries.firstOrNullObservable { it.notaryIdentity.owningKey.keys.any { it == publicKey } }) {
it.legalIdentity.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 com.google.common.net.HostAndPort
import javafx.beans.property.SimpleObjectProperty import javafx.beans.property.SimpleObjectProperty
import net.corda.client.rpc.CordaRPCClient import net.corda.client.rpc.CordaRPCClient
import net.corda.client.rpc.CordaRPCClientConfiguration
import net.corda.core.flows.StateMachineRunId import net.corda.core.flows.StateMachineRunId
import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.StateMachineInfo import net.corda.core.messaging.StateMachineInfo
@ -52,11 +53,14 @@ class NodeMonitorModel {
* TODO provide an unsubscribe mechanism * TODO provide an unsubscribe mechanism
*/ */
fun register(nodeHostAndPort: HostAndPort, username: String, password: String) { fun register(nodeHostAndPort: HostAndPort, username: String, password: String) {
val client = CordaRPCClient(nodeHostAndPort){ val client = CordaRPCClient(
maxRetryInterval = 10.seconds.toMillis() hostAndPort = nodeHostAndPort,
} configuration = CordaRPCClientConfiguration.default.copy(
client.start(username, password) connectionMaxRetryInterval = 10.seconds
val proxy = client.proxy() )
)
val connection = client.start(username, password)
val proxy = connection.proxy
val (stateMachines, stateMachineUpdates) = proxy.stateMachinesAndUpdates() val (stateMachines, stateMachineUpdates) = proxy.stateMachinesAndUpdates()
// Extract the flow tracking stream // Extract the flow tracking stream

View File

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

View File

@ -4,7 +4,6 @@ import javafx.collections.FXCollections
import javafx.collections.ListChangeListener import javafx.collections.ListChangeListener
import javafx.collections.ObservableList import javafx.collections.ObservableList
import javafx.collections.transformation.TransformationList 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 * 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.binding.Bindings
import javafx.beans.value.ObservableValue import javafx.beans.value.ObservableValue
import javafx.collections.ObservableList import javafx.collections.ObservableList
import kotlinx.support.jdk8.collections.stream
import net.corda.client.jfx.model.ExchangeRate import net.corda.client.jfx.model.ExchangeRate
import net.corda.core.contracts.Amount import net.corda.core.contracts.Amount
import org.fxmisc.easybind.EasyBind 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] * Utility bindings for the [Amount] type, similar in spirit to [Bindings]
*/ */
object AmountBindings { 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({ Bindings.createLongBinding({
amounts.stream().collect(Collectors.summingLong { amounts.stream().collect(Collectors.summingLong {
require(it.token == token) require(it.token == token)

View File

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

View File

@ -5,7 +5,6 @@ import javafx.collections.ListChangeListener
import javafx.collections.ObservableList import javafx.collections.ObservableList
import javafx.collections.transformation.TransformationList import javafx.collections.transformation.TransformationList
import java.util.* 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 * [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> { 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 val currentIndex = indexMap[wrapped]!!.first
beginChange() beginChange()
nextReplace(currentIndex, currentIndex + 1, listOf(oldValue)) 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 from = c.from
val to = c.to val to = c.to
val permutation = IntArray(to, { c.getPermutation(it) }) 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) nextPermutation(from, to, permutation)
} else if (c.wasUpdated()) { } else if (c.wasUpdated()) {
throw UnsupportedOperationException("FlattenedList doesn't support Update changes") 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.MapChangeListener
import javafx.collections.ObservableList import javafx.collections.ObservableList
import javafx.collections.ObservableMap import javafx.collections.ObservableMap
import kotlin.comparisons.compareValues
/** /**
* [MapValuesList] takes an [ObservableMap] and returns its values as an [ObservableList]. * [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 toKey Function retrieving the key to associate with.
* @param merge The function to be called if there is an existing element at the key. * @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 -> return fold(FXCollections.observableHashMap<K, A>()) { map, item ->
val key = toKey(item) val key = toKey(item)
map[key] = map[key]?.let { merge(key, it, item) } ?: 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() * val owners: ObservableList<Person> = dogs.map(Dog::owner).filterNotNull()
*/ */
fun <A> ObservableList<out A?>.filterNotNull(): ObservableList<A> { 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") @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) * val nameToPerson: ObservableMap<String, Person> = people.associateBy(Person::name)
*/ */
fun <K, A> ObservableList<out A>.associateBy(toKey: (A) -> K): ObservableMap<K, A> { 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) * 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>> { 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 leftTableMap = associateByAggregation(leftToJoinKey)
val rightTableMap = rightTable.associateByAggregation(rightToJoinKey) val rightTableMap = rightTable.associateByAggregation(rightToJoinKey)
val joinedMap: ObservableMap<K, Pair<ObservableList<A>, ObservableList<B>>> = 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() })) Pair(left, ChosenList(rightValue.map { it ?: FXCollections.emptyObservableList() }))
} }
return joinedMap return joinedMap
@ -285,7 +291,7 @@ fun <A> ObservableList<A>.last(): ObservableValue<A?> {
} }
fun <T : Any> ObservableList<T>.unique(): ObservableList<T> { 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 { fun ObservableValue<*>.isNotNull(): BooleanBinding {

View File

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

View File

@ -4,32 +4,12 @@ apply plugin: 'net.corda.plugins.publish-utils'
description 'Corda client mock modules' 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 //noinspection GroovyAssignabilityCheck
configurations { configurations {
// we don't want isolated.jar in classPath, since we want to test jar being dynamically loaded as an attachment // we don't want isolated.jar in classPath, since we want to test jar being dynamically loaded as an attachment
runtime.exclude module: 'isolated' runtime.exclude module: 'isolated'
} }
sourceSets {
test {
resources {
srcDir "../../config/test"
}
}
}
// To find potential version conflicts, run "gradle htmlDependencyReport" and then look in // 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. // build/reports/project/dependencies/index.html for green highlighted parts of the tree.

View File

@ -1,10 +1,8 @@
package net.corda.client.mock package net.corda.client.mock
import net.corda.contracts.asset.Cash import net.corda.core.contracts.Amount
import net.corda.core.contracts.* import net.corda.core.identity.Party
import net.corda.core.crypto.Party
import net.corda.core.serialization.OpaqueBytes import net.corda.core.serialization.OpaqueBytes
import net.corda.core.transactions.TransactionBuilder
import net.corda.flows.CashFlowCommand import net.corda.flows.CashFlowCommand
import java.util.* 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 * [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! * 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 = class EventGenerator(val parties: List<Party>, val currencies: List<Currency>, val notary: Party) {
Generator.pickOne(issuers).combine(Generator.intRange(0, 1)) { party, ref -> party.ref(ref.toByte()) } 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) private val issueCashGenerator = amountGenerator.combine(partyGenerator, issueRefGenerator, currencyGenerator) { amount, to, issueRef, ccy ->
CashFlowCommand.IssueCash(Amount(amount, ccy), issueRef, to, notary)
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)
} }
val consumedGenerator: Generator<Set<StateRef>> = Generator.frequency( private val exitCashGenerator = amountGenerator.combine(issueRefGenerator, currencyGenerator) { amount, issueRef, ccy ->
0.7 to Generator.pure(setOf()), CashFlowCommand.ExitCash(Amount(amount, ccy), issueRef)
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()
}
}
)
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 issuerGenerator = Generator.frequency(listOf(
0.1 to exitCashGenerator,
val issueCashGenerator = 0.9 to 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
)
} }

View File

@ -144,6 +144,23 @@ fun Generator.Companion.doubleRange(from: Double, to: Double): Generator<Double>
from + it.nextDouble() * (to - from) 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>> { fun <A> Generator.Companion.replicate(number: Int, generator: Generator<A>): Generator<List<A>> {
val generators = mutableListOf<Generator<A>>() val generators = mutableListOf<Generator<A>>()
for (i in 1..number) { for (i in 1..number) {

View File

@ -4,18 +4,6 @@ apply plugin: 'net.corda.plugins.publish-utils'
description 'Corda client RPC modules' 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 //noinspection GroovyAssignabilityCheck
configurations { configurations {
// we don't want isolated.jar in classPath, since we want to test jar being dynamically loaded as an attachment // 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') srcDir file('src/integration-test/kotlin')
} }
} }
test {
resources {
srcDir "../../config/test"
}
}
} }
// To find potential version conflicts, run "gradle htmlDependencyReport" and then look in // 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 "org.assertj:assertj-core:${assertj_version}"
testCompile project(':test-utils') testCompile project(':test-utils')
testCompile project(':client:mock')
// Integration test helpers // Integration test helpers
integrationTestCompile "junit:junit:$junit_version" 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 package net.corda.client.rpc
import com.google.common.net.HostAndPort import com.google.common.net.HostAndPort
import net.corda.nodeapi.config.SSLConfiguration import net.corda.client.rpc.internal.RPCClient
import net.corda.core.ThreadBox import net.corda.client.rpc.internal.RPCClientConfiguration
import net.corda.core.logElapsedTime
import net.corda.core.messaging.CordaRPCOps 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.ArtemisTcpTransport.Companion.tcpTransport
import net.corda.nodeapi.ConnectionDirection import net.corda.nodeapi.ConnectionDirection
import net.corda.nodeapi.RPCException import net.corda.nodeapi.config.SSLConfiguration
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 java.time.Duration import java.time.Duration
import javax.annotation.concurrent.ThreadSafe
/** class CordaRPCConnection internal constructor(
* An RPC client connects to the specified server and allows you to make calls to the server that perform various connection: RPCClient.RPCConnection<CordaRPCOps>
* useful tasks. See the documentation for [proxy] or review the docsite to learn more about how this API works. ) : RPCClient.RPCConnection<CordaRPCOps> by connection
*
* @param host The hostname and messaging port of the node. data class CordaRPCClientConfiguration(
* @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. val connectionMaxRetryInterval: Duration
*/ ) {
@ThreadSafe internal fun toRpcClientConfiguration(): RPCClientConfiguration {
class CordaRPCClient(val host: HostAndPort, override val config: SSLConfiguration? = null, val serviceConfigurationOverride: (ServerLocator.() -> Unit)? = null) : Closeable, ArtemisMessagingComponent() { return RPCClientConfiguration.default.copy(
private companion object { connectionMaxRetryInterval = connectionMaxRetryInterval
val log = loggerFor<CordaRPCClient>() )
}
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. inline fun <A> use(username: String, password: String, block: (CordaRPCConnection) -> A): A {
private inner class State { return start(username, password).use(block)
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()
}
}
} }
} }

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" nearestCity : "London"
keyStorePassword : "cordacadevpass" keyStorePassword : "cordacadevpass"
trustStorePassword : "trustpass" trustStorePassword : "trustpass"
@ -8,6 +8,6 @@ webAddress : "localhost:10004"
extraAdvertisedServiceIds : [ "corda.interest_rates" ] extraAdvertisedServiceIds : [ "corda.interest_rates" ]
networkMapService : { networkMapService : {
address : "localhost:10000" address : "localhost:10000"
legalName : "Network Map Service" legalName : "CN=Network Map Service,O=R3,OU=corda,L=London,C=UK"
} }
useHTTPS : false useHTTPS : false

View File

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

View File

@ -57,5 +57,8 @@
<AppenderRef ref="Console-Appender-Println"/> <AppenderRef ref="Console-Appender-Println"/>
<AppenderRef ref="RollingFile-Appender" /> <AppenderRef ref="RollingFile-Appender" />
</Logger> </Logger>
<Logger name="org.apache.activemq.artemis.core.server" level="error" additivity="false">
<AppenderRef ref="RollingFile-Appender"/>
</Logger>
</Loggers> </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" nearestCity : "London"
keyStorePassword : "cordacadevpass" keyStorePassword : "cordacadevpass"
trustStorePassword : "trustpass" 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 { dependencies {
testCompile "junit:junit:$junit_version" testCompile "junit:junit:$junit_version"
@ -42,9 +22,8 @@ dependencies {
testCompile project(":node") testCompile project(":node")
testCompile project(":test-utils") 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.kotlin:kotlin-reflect:$kotlin_version"
compile "org.jetbrains.kotlinx:kotlinx-support-jdk8:0.3"
testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
// Thread safety annotations // Thread safety annotations
@ -84,7 +63,7 @@ dependencies {
compile "com.fasterxml.jackson.core:jackson-databind:${jackson_version}" compile "com.fasterxml.jackson.core:jackson-databind:${jackson_version}"
// Java ed25519 implementation. See https://github.com/str4d/ed25519-java/ // 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 // Bouncy castle support needed for X509 certificate manipulation
compile "org.bouncycastle:bcprov-jdk15on:${bouncycastle_version}" compile "org.bouncycastle:bcprov-jdk15on:${bouncycastle_version}"
@ -93,20 +72,14 @@ dependencies {
// JPA 2.1 annotations. // JPA 2.1 annotations.
compile "org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final" 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 // Requery: SQL based query & persistence for Kotlin
compile "io.requery:requery-kotlin:$requery_version" compile "io.requery:requery-kotlin:$requery_version"
// For AMQP serialisation.
compile "org.apache.qpid:proton-j:0.18.0"
} }
configurations { 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 testArtifacts.extendsFrom testRuntime
} }

View File

@ -3,21 +3,19 @@
package net.corda.core package net.corda.core
import com.google.common.base.Function
import com.google.common.base.Throwables import com.google.common.base.Throwables
import com.google.common.io.ByteStreams import com.google.common.io.ByteStreams
import com.google.common.util.concurrent.* 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.newSecureRandom
import net.corda.core.crypto.sha256
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import org.slf4j.Logger import org.slf4j.Logger
import rx.Observable import rx.Observable
import rx.Observer import rx.Observer
import rx.subjects.PublishSubject import rx.subjects.PublishSubject
import rx.subjects.UnicastSubject import rx.subjects.UnicastSubject
import java.io.BufferedInputStream import java.io.*
import java.io.InputStream
import java.io.OutputStream
import java.math.BigDecimal import java.math.BigDecimal
import java.nio.charset.Charset import java.nio.charset.Charset
import java.nio.charset.StandardCharsets.UTF_8 import java.nio.charset.StandardCharsets.UTF_8
@ -25,11 +23,22 @@ import java.nio.file.*
import java.nio.file.attribute.FileAttribute import java.nio.file.attribute.FileAttribute
import java.time.Duration import java.time.Duration
import java.time.temporal.Temporal import java.time.temporal.Temporal
import java.util.HashMap
import java.util.concurrent.* import java.util.concurrent.*
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
import java.util.function.BiConsumer import java.util.function.BiConsumer
import java.util.stream.Stream import java.util.stream.Stream
import java.util.zip.Deflater
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream 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.concurrent.withLock
import kotlin.reflect.KProperty 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> { private class CompletableToListenable<T>(private val base: CompletableFuture<T>) : Future<T> by base, ListenableFuture<T> {
override fun addListener(listener: Runnable, executor: Executor) { 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>.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>.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 <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!!) } 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. */ /** 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) { 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) 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 // 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. */ /** Returns the index of the given item or throws [IllegalArgumentException] if not found. */
fun <T> List<T>.indexOfOrThrow(item: T): Int { 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 @Transient private var v: T? = null
@Synchronized @Synchronized
operator fun getValue(thisRef: Any?, property: KProperty<*>): T { operator fun getValue(thisRef: Any?, property: KProperty<*>) = v ?: initializer().also { v = it }
if (v == null)
v = initializer()
return v!!
}
} }
/** /**
@ -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. // TODO: Generic csv printing utility for clases.
val Throwable.rootCause: Throwable get() = Throwables.getRootCause(this) val Throwable.rootCause: Throwable get() = Throwables.getRootCause(this)
/** Representation of an operation that may have thrown an error. */ /** Representation of an operation that may have thrown an error. */
@Suppress("DataClassPrivateConstructor")
@CordaSerializable @CordaSerializable
data class ErrorOr<out A> private constructor(val value: A?, val error: Throwable?) { data class ErrorOr<out A> private constructor(val value: A?, val error: Throwable?) {
// The ErrorOr holds a value iff error == null // 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) 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)) } codePoints.forEach { builder.append(Character.toChars(it)) }
return builder.toString() 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 package net.corda.core.contracts
import net.corda.core.crypto.CompositeKey import net.corda.core.identity.Party
import net.corda.core.crypto.Party import java.security.PublicKey
import java.math.BigDecimal
import java.util.* import java.util.*
/** /**
@ -31,11 +32,13 @@ fun commodity(code: String) = Commodity.getInstance(code)!!
@JvmField val RUB = currency("RUB") @JvmField val RUB = currency("RUB")
@JvmField val FCOJ = commodity("FCOJ") // Frozen concentrated orange juice, yum! @JvmField val FCOJ = commodity("FCOJ") // Frozen concentrated orange juice, yum!
fun DOLLARS(amount: Int): Amount<Currency> = Amount(amount.toLong() * 100, USD) fun <T : Any> AMOUNT(amount: Int, token: T): Amount<T> = Amount.fromDecimal(BigDecimal.valueOf(amount.toLong()), token)
fun DOLLARS(amount: Double): Amount<Currency> = Amount((amount * 100).toLong(), USD) fun <T : Any> AMOUNT(amount: Double, token: T): Amount<T> = Amount.fromDecimal(BigDecimal.valueOf(amount), token)
fun POUNDS(amount: Int): Amount<Currency> = Amount(amount.toLong() * 100, GBP) fun DOLLARS(amount: Int): Amount<Currency> = AMOUNT(amount, USD)
fun SWISS_FRANCS(amount: Int): Amount<Currency> = Amount(amount.toLong() * 100, CHF) fun DOLLARS(amount: Double): Amount<Currency> = AMOUNT(amount, USD)
fun FCOJ(amount: Int): Amount<Commodity> = Amount(amount.toLong() * 100, FCOJ) 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 Int.DOLLARS: Amount<Currency> get() = DOLLARS(this)
val Double.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 Amount<Currency>.`issued by`(deposit: PartyAndReference) = issuedBy(deposit)
infix fun Currency.issuedBy(deposit: PartyAndReference) = Issued(deposit, this) infix fun Currency.issuedBy(deposit: PartyAndReference) = Issued(deposit, this)
infix fun Commodity.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 ///////////////////////////////////////////////////////////////////////////////////////////////////// //// Requirements /////////////////////////////////////////////////////////////////////////////////////////////////////
object Requirements { object Requirements {
@Suppress("NOTHING_TO_INLINE") // Inlining this takes it out of our committed ABI. @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") 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() 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 // TODO: Provide a version of select that interops with Java
/** Filters the command list by type, party and public key all at once. */ /** 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) = party: Party? = null) =
filter { it.value is T }. filter { it.value is T }.
filter { if (signer == null) true else signer in it.signers }. 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 // TODO: Provide a version of select that interops with Java
/** Filters the command list by type, parties and public keys all at once. */ /** 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>?) = parties: Collection<Party>?) =
filter { it.value is T }. filter { it.value is T }.
filter { if (signers == null) true else it.signers.containsAll(signers) }. 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>) = 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() 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. * 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 command = commands.requireSingleCommand<T>()
val keysThatSigned = command.signers.toSet() val keysThatSigned = command.signers.toSet()
requireThat { 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 return command.value
} }

View File

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

View File

@ -1,9 +1,9 @@
package net.corda.core.contracts package net.corda.core.contracts
import net.corda.core.crypto.CompositeKey
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.transactions.WireTransaction import net.corda.core.transactions.WireTransaction
import net.corda.flows.ContractUpgradeFlow import net.corda.flows.ContractUpgradeFlow
import java.security.PublicKey
// The dummy contract doesn't do anything useful. It exists for testing purposes. // The dummy contract doesn't do anything useful. It exists for testing purposes.
val DUMMY_V2_PROGRAM_ID = DummyContractV2() 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. * Dummy contract state for testing of the upgrade process.
*/ */
// DOCSTART 1
class DummyContractV2 : UpgradedContract<DummyContract.State, DummyContractV2.State> { class DummyContractV2 : UpgradedContract<DummyContract.State, DummyContractV2.State> {
override val legacyContract = DummyContract::class.java 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 contract = DUMMY_V2_PROGRAM_ID
override val participants: List<CompositeKey> = owners override val participants: List<PublicKey> = owners
} }
interface Commands : CommandData { interface Commands : CommandData {
@ -35,7 +36,7 @@ class DummyContractV2 : UpgradedContract<DummyContract.State, DummyContractV2.St
// The "empty contract" // The "empty contract"
override val legalContractReference: SecureHash = SecureHash.sha256("") override val legalContractReference: SecureHash = SecureHash.sha256("")
// DOCEND 1
/** /**
* Generate an upgrade transaction from [DummyContract]. * 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. * @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() val notary = states.map { it.state.notary }.single()
require(states.isNotEmpty()) require(states.isNotEmpty())

View File

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

View File

@ -11,16 +11,25 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize
import com.google.common.annotations.VisibleForTesting import com.google.common.annotations.VisibleForTesting
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import java.math.BigDecimal import java.math.BigDecimal
import java.math.BigInteger import java.math.RoundingMode
import java.time.DayOfWeek import java.time.DayOfWeek
import java.time.LocalDate import java.time.LocalDate
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.util.* 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 * 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 * representable units. The nominal quantity represented by each individual token is equal to the [displayTokenSize].
* amount used in whatever underlying thing the amount represents. * 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 * 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 * 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 * multiplication are overflow checked and will throw [ArithmeticException] if the operation would have caused integer
* overflow. * overflow.
* *
* TODO: It may make sense to replace this with convenience extensions over the JSR 354 MonetaryAmount interface, * @param quantity the number of tokens as a Long value.
* in particular for use during calculations. This may also resolve... * @param displayTokenSize the nominal display unit size of a single token,
* TODO: Think about how positive-only vs positive-or-negative amounts can be represented in the type system. * potentially with trailing decimal display places if the scale parameter is non-zero.
* TODO: Add either a scaling factor, or a variant for use in calculations.
*
* @param T the type of the token, for example [Currency]. * @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 @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 { companion object {
/** /**
* Build a currency amount from a decimal representation. For example, with an input of "12.34" GBP, * Build an Amount from a decimal representation. For example, with an input of "12.34 GBP",
* returns an amount with a quantity of "1234". * 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 * @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> { @JvmStatic
val longQuantity = quantity.movePointRight(currency.defaultFractionDigits).toLong() @JvmOverloads
return Amount(longQuantity, currency) 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( 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 { init {
// Negative amounts are of course a vital part of any ledger, but negative values are only valid in certain // Amount represents a static balance of physical assets as managed by the distributed ledger and is not allowed
// contexts: you cannot send a negative amount of cash, but you can (sometimes) have a negative balance. // to become negative a rule further maintained by the Contract verify method.
// If you want to express a negative amount, for now, use a long. // 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" } require(quantity >= 0) { "Negative amounts are not allowed: $quantity" }
} }
/** /**
* Construct the amount using the given decimal value as quantity. Any fractional part * Automatic conversion constructor from number of tokens to an Amount using getDisplayTokenSize to determine
* is discarded. To convert and use the fractional part, see [fromDecimal]. * 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(tokenQuantity: Long, token: T) : this(tokenQuantity, getDisplayTokenSize(token), token)
constructor(quantity: BigInteger, token: T) : this(quantity.toLong(), 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> { operator fun plus(other: Amount<T>): Amount<T> {
checkToken(other) 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> { operator fun minus(other: Amount<T>): Amount<T> {
checkToken(other) 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>) { private fun checkToken(other: Amount<T>) {
require(other.token == token) { "Token mismatch: ${other.token} vs $token" } 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) * The multiplication operator is supported to allow easy calculation for multiples of a primitive Amount.
operator fun div(other: Int): Amount<T> = Amount(quantity / other, token) * Note this is not a conserving operation, so it may not always be correct modelling of proper token behaviour.
operator fun times(other: Int): Amount<T> = Amount(Math.multiplyExact(quantity, other.toLong()), token) * 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 { override fun toString(): String {
val bd = if (token is Currency) return toDecimal().toPlainString() + " " + token
BigDecimal(quantity).movePointLeft(token.defaultFractionDigits)
else
BigDecimal(quantity)
return bd.toPlainString() + " " + token
} }
override fun compareTo(other: Amount<T>): Int { 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 : Any> Iterable<Amount<T>>.sumOrNull() = if (!iterator().hasNext()) null else sumOrThrow()
fun <T> Iterable<Amount<T>>.sumOrThrow() = reduce { left, right -> left + right } fun <T : Any> 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>>.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 adjustedMaturityDate = calendar.applyRollConvention(maturityDate, DateRollConvention.ModifiedFollowing)
val daysToMaturity = calculateDaysBetween(startDate, adjustedMaturityDate, DayCountBasisYear.Y360, DayCountBasisDay.DActual) val daysToMaturity = calculateDaysBetween(startDate, adjustedMaturityDate, DayCountBasisYear.Y360, DayCountBasisDay.DActual)
return daysToMaturity.toInt() return daysToMaturity
} }
override fun toString(): String = name 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. * There are some additional rules which are explained in the individual cases below.
*/ */
@CordaSerializable @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 // direction() cannot be a val due to the throw in the Actual instance
/** Don't roll the date, use the one supplied. */ /** Don't roll the date, use the one supplied. */
Actual { Actual({ throw UnsupportedOperationException("Direction is not relevant for convention Actual") }, false),
override fun direction(): DateRollDirection = throw UnsupportedOperationException("Direction is not relevant for convention Actual")
override val isModified: Boolean = false
},
/** Following is the next business date from this one. */ /** Following is the next business date from this one. */
Following { Following({ DateRollDirection.FORWARD }, false),
override fun direction(): DateRollDirection = DateRollDirection.FORWARD
override val isModified: Boolean = false
},
/** /**
* "Modified following" is the next business date, unless it's in the next month, in which case use the preceeding * "Modified following" is the next business date, unless it's in the next month, in which case use the preceeding
* business date. * business date.
*/ */
ModifiedFollowing { ModifiedFollowing({ DateRollDirection.FORWARD }, true),
override fun direction(): DateRollDirection = DateRollDirection.FORWARD
override val isModified: Boolean = true
},
/** Previous is the previous business date from this one. */ /** Previous is the previous business date from this one. */
Previous { Previous({ DateRollDirection.BACKWARD }, false),
override fun direction(): DateRollDirection = DateRollDirection.BACKWARD
override val isModified: Boolean = false
},
/** /**
* Modified previous is the previous business date, unless it's in the previous month, in which case use the next * Modified previous is the previous business date, unless it's in the previous month, in which case use the next
* business date. * business date.
*/ */
ModifiedPrevious { ModifiedPrevious({ DateRollDirection.BACKWARD }, true);
override fun direction(): DateRollDirection = DateRollDirection.BACKWARD
override val isModified: Boolean = true
};
abstract fun direction(): DateRollDirection
abstract val isModified: Boolean
} }
@ -345,31 +611,14 @@ enum class PaymentRule {
*/ */
@Suppress("unused") // TODO: Revisit post-Vega and see if annualCompoundCount is still needed. @Suppress("unused") // TODO: Revisit post-Vega and see if annualCompoundCount is still needed.
@CordaSerializable @CordaSerializable
enum class Frequency(val annualCompoundCount: Int) { enum class Frequency(val annualCompoundCount: Int, val offset: LocalDate.(Long) -> LocalDate) {
Annual(1) { Annual(1, { plusYears(1 * it) }),
override fun offset(d: LocalDate, n: Long) = d.plusYears(1 * n) SemiAnnual(2, { plusMonths(6 * it) }),
}, Quarterly(4, { plusMonths(3 * it) }),
SemiAnnual(2) { Monthly(12, { plusMonths(1 * it) }),
override fun offset(d: LocalDate, n: Long) = d.plusMonths(6 * n) Weekly(52, { plusWeeks(1 * it) }),
}, BiWeekly(26, { plusWeeks(2 * it) }),
Quarterly(4) { Daily(365, { plusDays(1 * it) });
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.
} }
@ -396,7 +645,7 @@ open class BusinessCalendar private constructor(val holidayDates: List<LocalDate
}.toMap() }.toMap()
/** Parses a date of the form YYYY-MM-DD, like 2016-01-10 for 10th Jan. */ /** 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. */ /** Returns a business calendar that combines all the named holiday calendars into one list of holiday dates. */
fun getInstance(vararg calname: String) = BusinessCalendar( fun getInstance(vararg calname: String) = BusinessCalendar(
@ -546,7 +795,10 @@ enum class NetType {
@CordaSerializable @CordaSerializable
data class Commodity(val commodityCode: String, data class Commodity(val commodityCode: String,
val displayName: String, val displayName: String,
val defaultFractionDigits: Int = 0) { val defaultFractionDigits: Int = 0) : TokenizableAssetInfo {
override val displayTokenSize: BigDecimal
get() = BigDecimal.ONE.scaleByPowerOfTen(-defaultFractionDigits)
companion object { companion object {
private val registry = mapOf( private val registry = mapOf(
// Simple example commodity, as in http://www.investopedia.com/university/commodities/commodities14.asp // Simple example commodity, as in http://www.investopedia.com/university/commodities/commodities14.asp

View File

@ -1,7 +1,12 @@
package net.corda.core.contracts 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.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") 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 * @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.). * (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>> val amount: Amount<Issued<T>>
/** /**
* There must be an ExitCommand signed by these keys to destroy the amount. While all states require their * 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. * 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 */ /** 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 // Just for grouping
interface Commands : CommandData { 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 * A command stating that money has been withdrawn from the shared ledger and is now accounted for
* in some other way. * in some other way.
*/ */
interface Exit<T> : Commands { interface Exit<T : Any> : Commands {
val amount: Amount<Issued<T>> val amount: Amount<Issued<T>>
} }
} }
@ -54,8 +59,8 @@ interface FungibleAsset<T> : OwnableState {
// Small DSL extensions. // Small DSL extensions.
/** Sums the asset states in the list, returning null if there are none. */ /** 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. */ /** 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 package net.corda.core.contracts
import net.corda.core.contracts.clauses.Clause 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.crypto.SecureHash
import net.corda.core.flows.FlowLogicRef import net.corda.core.flows.FlowLogicRef
import net.corda.core.flows.FlowLogicRefFactory 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.node.services.ServiceType
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.*
import net.corda.core.serialization.OpaqueBytes
import net.corda.core.serialization.serialize
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
import java.security.PublicKey import java.security.PublicKey
@ -53,7 +51,7 @@ interface MultilateralNettableState<out T : Any> {
val multilateralNetState: T val multilateralNetState: T
} }
interface NettableState<N : BilateralNettableState<N>, T : Any> : BilateralNettableState<N>, interface NettableState<N : BilateralNettableState<N>, out T : Any> : BilateralNettableState<N>,
MultilateralNettableState<T> 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 * 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. * 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, * Note that an encumbered state that is being consumed must have its encumbrance consumed in the same transaction,
* otherwise the transaction is not valid. * otherwise the transaction is not valid.
*/ */
val encumbrance: Int? = null) { 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)
}
/** Wraps the [ContractState] in a [TransactionState] object */ /** Wraps the [ContractState] in a [TransactionState] object */
infix fun <T : ContractState> T.`with notary`(newNotary: Party) = withNotary(newNotary) infix fun <T : ContractState> T.`with notary`(newNotary: Party) = withNotary(newNotary)
infix fun <T : ContractState> T.withNotary(newNotary: Party) = TransactionState(this, 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 * Definition for an issued product, which can be cash, a cash-like thing, assets, or generally anything else that's
* quantifiable with integer quantities. * quantifiable with integer quantities.
@ -173,7 +158,7 @@ interface IssuanceDefinition
* @param P the type of product underlying the definition, for example [Currency]. * @param P the type of product underlying the definition, for example [Currency].
*/ */
@CordaSerializable @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" 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 * 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. * 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. * A contract state that can have a single owner.
*/ */
interface OwnableState : ContractState { interface OwnableState : ContractState {
/** There must be a MoveCommand signed by this key to claim the amount */ /** 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 */ /** 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 */ /** 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). * True if this should be tracked by our vault(s).
* */ */
fun isRelevant(ourKeys: Set<PublicKey>): Boolean fun isRelevant(ourKeys: Set<PublicKey>): Boolean
/** /**
* Standard clause to verify the LinearState safety properties. * Standard clause to verify the LinearState safety properties.
*/ */
@CordaSerializable @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, override fun verify(tx: TransactionForContract,
inputs: List<S>, inputs: List<S>,
outputs: List<S>, outputs: List<S>,
@ -253,8 +238,8 @@ interface LinearState : ContractState {
val inputIds = inputs.map { it.linearId }.distinct() val inputIds = inputs.map { it.linearId }.distinct()
val outputIds = outputs.map { it.linearId }.distinct() val outputIds = outputs.map { it.linearId }.distinct()
requireThat { requireThat {
"LinearStates are not merged" by (inputIds.count() == inputs.count()) "LinearStates are not merged" using (inputIds.count() == inputs.count())
"LinearStates are not split" by (outputIds.count() == outputs.count()) "LinearStates are not split" using (outputIds.count() == outputs.count())
} }
return emptySet() return emptySet()
} }
@ -360,7 +345,8 @@ inline fun <reified T : ContractState> Iterable<StateAndRef<ContractState>>.filt
@CordaSerializable @CordaSerializable
data class PartyAndReference(val party: AnonymousParty, val reference: OpaqueBytes) { data class PartyAndReference(val party: AnonymousParty, val reference: OpaqueBytes) {
constructor(party: Party, reference: OpaqueBytes) : this(party.toAnonymous(), reference) 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 */ /** 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 */ /** Command data/content plus pubkey pair: the signature is stored at the end of the serialized bytes */
@CordaSerializable @CordaSerializable
data class Command(val value: CommandData, val signers: List<CompositeKey>) { data class Command(val value: CommandData, val signers: List<PublicKey>) {
init { init {
require(signers.isNotEmpty()) 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 } private fun commandDataToString() = value.toString().let { if (it.contains("@")) it.replace('$', '.').split("@")[0] else it }
override fun toString() = "${commandDataToString()} with pubkeys ${signers.joinToString()}" 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. */ /** Wraps an object that was signed by a public key, which may be a well known/recognised institutional key. */
@CordaSerializable @CordaSerializable
data class AuthenticatedObject<out T : Any>( 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 */ /** If any public keys were recognised, the looked up institutions are available here */
val signingParties: List<Party>, val signingParties: List<Party>,
val value: T 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 * - Facts generated by oracles which might be reused a lot
*/ */
interface Attachment : NamedByHash { interface Attachment : NamedByHash {
fun open(): InputStream 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. * 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. * @throws FileNotFoundException if the given path doesn't exist in the attachment.
*/ */
fun extractFile(path: String, outputTo: OutputStream) { fun extractFile(path: String, outputTo: OutputStream) = openAsJAR().use { it.extractFile(path, outputTo) }
val p = path.toLowerCase().split('\\', '/') }
openAsJAR().use { jar ->
while (true) { abstract class AbstractAttachment(dataLoader: () -> ByteArray) : Attachment {
val e = jar.nextJarEntry ?: break companion object {
if (e.name.toLowerCase().split('\\', '/') == p) { fun SerializeAsTokenContext.attachmentDataLoader(id: SecureHash): () -> ByteArray {
jar.copyTo(outputTo) val storage = serviceHub.storageService.attachments
return return {
} val a = storage.openAttachment(id) ?: throw MissingAttachmentsException(listOf(id))
jar.closeEntry() 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 package net.corda.core.contracts
import net.corda.core.crypto.CompositeKey import net.corda.core.identity.Party
import net.corda.core.crypto.Party
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
import java.security.PublicKey
/** Defines transaction build & validation logic for a specific transaction type */ /** Defines transaction build & validation logic for a specific transaction type */
@CordaSerializable @CordaSerializable
sealed class TransactionType { 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: * Check that the transaction is valid based on:
* - General platform rules * - General platform rules
@ -23,16 +20,16 @@ sealed class TransactionType {
fun verify(tx: LedgerTransaction) { fun verify(tx: LedgerTransaction) {
require(tx.notary != null || tx.timestamp == null) { "Transactions with timestamps must be notarised." } require(tx.notary != null || tx.timestamp == null) { "Transactions with timestamps must be notarised." }
val duplicates = detectDuplicateInputs(tx) 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) 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) verifyTransaction(tx)
} }
/** Check that the list of signers includes all the necessary keys */ /** 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() 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 requiredKeys = getRequiredSigners(tx) + notaryKey
val missing = requiredKeys - tx.mustSign 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. * 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. * 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 */ /** Implement type specific transaction validation logic */
abstract fun verifyTransaction(tx: LedgerTransaction) abstract fun verifyTransaction(tx: LedgerTransaction)
/** A general transaction type where transaction validity is determined by custom contract code */ /** 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 */ /** 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) { override fun verifyTransaction(tx: LedgerTransaction) {
verifyNoNotaryChange(tx) verifyNoNotaryChange(tx)
@ -84,7 +81,7 @@ sealed class TransactionType {
if (tx.notary != null && tx.inputs.isNotEmpty()) { if (tx.notary != null && tx.inputs.isNotEmpty()) {
tx.outputs.forEach { tx.outputs.forEach {
if (it.notary != tx.notary) { 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) { private fun verifyEncumbrances(tx: LedgerTransaction) {
// Validate that all encumbrances exist within the set of input states. // Validate that all encumbrances exist within the set of input states.
val encumberedInputs = tx.inputs.filter { it.state.encumbrance != null } val encumberedInputs = tx.inputs.filter { it.state.encumbrance != null }
encumberedInputs.forEach { encumberedInput -> encumberedInputs.forEach { (state, ref) ->
val encumbranceStateExists = tx.inputs.any { 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) { if (!encumbranceStateExists) {
throw TransactionVerificationException.TransactionMissingEncumbranceException( throw TransactionVerificationException.TransactionMissingEncumbranceException(
tx, encumberedInput.state.encumbrance!!, tx.id,
state.encumbrance!!,
TransactionVerificationException.Direction.INPUT TransactionVerificationException.Direction.INPUT
) )
} }
@ -111,7 +109,8 @@ sealed class TransactionType {
val encumbranceIndex = output.encumbrance ?: continue val encumbranceIndex = output.encumbrance ?: continue
if (encumbranceIndex == i || encumbranceIndex >= tx.outputs.size) { if (encumbranceIndex == i || encumbranceIndex >= tx.outputs.size) {
throw TransactionVerificationException.TransactionMissingEncumbranceException( throw TransactionVerificationException.TransactionMissingEncumbranceException(
tx, encumbranceIndex, tx.id,
encumbranceIndex,
TransactionVerificationException.Direction.OUTPUT) TransactionVerificationException.Direction.OUTPUT)
} }
} }
@ -129,7 +128,7 @@ sealed class TransactionType {
try { try {
contract.verify(ctx) contract.verify(ctx)
} catch(e: Throwable) { } 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 * 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. * 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] * 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. * 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<*>) { override fun addInputState(stateAndRef: StateAndRef<*>) {
signers.addAll(stateAndRef.state.data.participants) signers.addAll(stateAndRef.state.data.participants)
super.addInputState(stateAndRef) super.addInputState(stateAndRef)
@ -167,7 +166,7 @@ sealed class TransactionType {
} }
check(tx.commands.isEmpty()) check(tx.commands.isEmpty())
} catch (e: IllegalStateException) { } catch (e: IllegalStateException) {
throw TransactionVerificationException.InvalidNotaryChange(tx) throw TransactionVerificationException.InvalidNotaryChange(tx.id)
} }
} }

View File

@ -1,11 +1,10 @@
package net.corda.core.contracts package net.corda.core.contracts
import net.corda.core.crypto.CompositeKey import net.corda.core.identity.Party
import net.corda.core.crypto.Party
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowException import net.corda.core.flows.FlowException
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.transactions.LedgerTransaction import java.security.PublicKey
import java.util.* import java.util.*
// TODO: Consider moving this out of the core module and providing a different way for unit tests to test contracts. // 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" 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" override fun toString(): String = "Attachment resolution failure for $hash"
} }
@CordaSerializable sealed class TransactionVerificationException(val txId: SecureHash, cause: Throwable?) : FlowException(cause) {
class TransactionConflictException(val conflictRef: StateRef, val tx1: LedgerTransaction, val tx2: LedgerTransaction) : Exception() class ContractRejection(txId: SecureHash, val contract: Contract, cause: Throwable?) : TransactionVerificationException(txId, cause)
class MoreThanOneNotary(txId: SecureHash) : TransactionVerificationException(txId, null)
sealed class TransactionVerificationException(val tx: LedgerTransaction, cause: Throwable?) : FlowException(cause) { class SignersMissing(txId: SecureHash, val missing: List<PublicKey>) : TransactionVerificationException(txId, null) {
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) {
override fun toString(): String = "Signers missing: ${missing.joinToString()}" 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()}" override fun toString(): String = "Duplicate inputs: ${duplicates.joinToString()}"
} }
class InvalidNotaryChange(tx: LedgerTransaction) : TransactionVerificationException(tx, null) class InvalidNotaryChange(txId: SecureHash) : TransactionVerificationException(txId, null)
class NotaryChangeInWrongTransactionType(tx: LedgerTransaction, val outputNotary: Party) : TransactionVerificationException(tx, null) { class NotaryChangeInWrongTransactionType(txId: SecureHash, val txNotary: Party, val outputNotary: Party) : TransactionVerificationException(txId, null) {
override fun toString(): String { 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" 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. * 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>>() override val clauses = ArrayList<Clause<S, C, K>>()
init { 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>> { override fun matchedClauses(commands: List<AuthenticatedObject<C>>): List<Clause<S, C, K>> {
clauses.forEach { clause -> clauses.forEach { clause ->
check(clause.matches(commands)) { "Failed to match clause ${clause}" } check(clause.matches(commands)) { "Failed to match clause $clause" }
} }
return clauses return clauses
} }

View File

@ -20,7 +20,7 @@ fun <C : CommandData> verifyClause(tx: TransactionForContract,
commands: List<AuthenticatedObject<C>>) { commands: List<AuthenticatedObject<C>>) {
if (Clause.log.isTraceEnabled) { if (Clause.log.isTraceEnabled) {
clause.getExecutionPath(commands).forEach { 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) 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. * 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>, 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>() { val filterStates: (List<ContractState>) -> List<S>) : Clause<ContractState, C, K>() {
override val requiredCommands: Set<Class<out CommandData>> override val requiredCommands: Set<Class<out CommandData>>
= clause.requiredCommands = 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.CommandData
import net.corda.core.contracts.ContractState import net.corda.core.contracts.ContractState
import net.corda.core.contracts.TransactionForContract import net.corda.core.contracts.TransactionForContract
import net.corda.core.utilities.loggerFor
import java.util.* import java.util.*
/** /**
* Compose a number of clauses, such that the first match is run, and it errors if none is run. * Compose a number of clauses, such that the first match is run, and it errors if none is run.
*/ */
@Deprecated("Use FirstOf instead") @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>() { class FirstComposition<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<FirstComposition<*, *, *>>()
}
override val clauses = ArrayList<Clause<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) }) 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. * 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 { companion object {
val logger = loggerFor<FirstOf<*, *, *>>() 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 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.CordaSerializable
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
import java.security.PublicKey import java.security.PublicKey
/** /**
* A tree data structure that enables the representation of composite public keys. * 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 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.
* 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.
* The root node would specify *weights* for each of its children and a *threshold* the minimum total weight required * 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. * (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 * 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"*. * 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 @CordaSerializable
sealed class CompositeKey { class CompositeKey private constructor (val threshold: Int,
/** Checks whether [keys] match a sufficient amount of leaf nodes */ children: List<NodeAndWeight>) : PublicKey {
abstract fun isFulfilledBy(keys: Iterable<PublicKey>): Boolean val children = children.sorted()
init {
fun isFulfilledBy(key: PublicKey) = isFulfilledBy(setOf(key)) 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.
/** Returns all [PublicKey]s contained within the tree leaves */ require(children.size > 1) { "Cannot construct CompositeKey with only one child node." }
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()
/** /**
* This is generated by serializing the composite key with Kryo, and encoding the resulting bytes in base58. * Holds node - weight pairs for a CompositeKey. Ordered first by weight, then by node's hashCode.
* 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).
*/ */
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 { companion object {
fun parseFromBase58(encoded: String) = Base58.decode(encoded).deserialize<CompositeKey>() // 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.
/** The leaf node of the tree a wrapper around a [PublicKey] primitive */ val FORMAT = "X-Corda-Kryo"
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()
} }
/** /**
* Represents a node in the key tree. It maintains a list of child nodes sub-trees, and associated * Takes single PublicKey and checks if CompositeKey requirements hold for that key.
* [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.
*/ */
class Node(val threshold: Int, fun isFulfilledBy(key: PublicKey) = isFulfilledBy(setOf(key))
val children: List<CompositeKey>,
val weights: List<Int>) : CompositeKey() {
override fun isFulfilledBy(keys: Iterable<PublicKey>): Boolean { override fun getAlgorithm() = ALGORITHM
val totalWeight = children.mapIndexed { i, childNode -> override fun getEncoded(): ByteArray = this.serialize().bytes
if (childNode.isFulfilledBy(keys)) weights[i] else 0 override fun getFormat() = FORMAT
}.sum()
return totalWeight >= threshold /**
} * 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.
override val keys: Set<PublicKey> * If all thresholds are satisfied, the composite key requirement is considered to be met.
get() = children.flatMap { it.keys }.toSet() */
fun isFulfilledBy(keysToCheck: Iterable<PublicKey>): Boolean {
// Auto-generated. TODO: remove once data class inheritance is enabled if (keysToCheck.any { it is CompositeKey } ) return false
override fun equals(other: Any?): Boolean { val totalWeight = children.map { (node, weight) ->
if (this === other) return true if (node is CompositeKey) {
if (other?.javaClass != javaClass) return false if (node.isFulfilledBy(keysToCheck)) weight else 0
} else {
other as Node if (keysToCheck.contains(node)) weight else 0
}
if (threshold != other.threshold) return false }.sum()
if (weights != other.weights) return false return totalWeight >= threshold
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()})"
} }
/** A helper class for building a [CompositeKey.Node]. */ /**
class Builder() { * Set of all leaf keys of that CompositeKey.
private val children: MutableList<CompositeKey> = mutableListOf() */
private val weights: MutableList<Int> = mutableListOf() 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. */ /** 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 { fun addKey(key: PublicKey, weight: Int = 1): Builder {
children.add(key) children.add(NodeAndWeight(key, weight))
weights.add(weight)
return this return this
} }
fun addKeys(vararg keys: CompositeKey): Builder { fun addKeys(vararg keys: PublicKey): Builder {
keys.forEach { addKey(it) } keys.forEach { addKey(it) }
return this 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. * 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 { @Throws(IllegalArgumentException::class)
return Node(threshold ?: children.size, children.toList(), weights.toList()) 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() 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 package net.corda.core.crypto
import net.i2p.crypto.eddsa.EdDSAEngine import net.corda.core.random63BitValue
import net.i2p.crypto.eddsa.EdDSAKey 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.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.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 org.bouncycastle.pqc.jcajce.spec.SPHINCS256KeyGenParameterSpec
import java.math.BigInteger
import java.security.* 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.InvalidKeySpecException
import java.security.spec.PKCS8EncodedKeySpec import java.security.spec.PKCS8EncodedKeySpec
import java.security.spec.X509EncodedKeySpec import java.security.spec.X509EncodedKeySpec
import java.util.*
/** /**
* This object controls and provides the available and supported signature schemes for Corda. * This object controls and provides the available and supported signature schemes for Corda.
@ -25,58 +52,58 @@ import java.security.spec.X509EncodedKeySpec
* </ul> * </ul>
*/ */
object Crypto { object Crypto {
/** /**
* RSA_SHA256 signature scheme using SHA256 as hash algorithm and MGF1 (with SHA256) as mask generation function. * RSA_SHA256 signature scheme using SHA256 as hash algorithm and MGF1 (with SHA256) as mask generation function.
* Note: Recommended key size >= 3072 bits. * Note: Recommended key size >= 3072 bits.
*/ */
private val RSA_SHA256 = SignatureScheme( val RSA_SHA256 = SignatureScheme(
1, 1,
"RSA_SHA256", "RSA_SHA256",
PKCSObjectIdentifiers.id_RSASSA_PSS,
BouncyCastleProvider.PROVIDER_NAME,
"RSA", "RSA",
Signature.getInstance("SHA256WITHRSAANDMGF1", "BC"), "SHA256WITHRSAANDMGF1",
KeyFactory.getInstance("RSA", "BC"),
KeyPairGenerator.getInstance("RSA", "BC"),
null, null,
3072, 3072,
"RSA_SHA256 signature scheme using SHA256 as hash algorithm and MGF1 (with SHA256) as mask generation function." "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. */ /** ECDSA signature scheme using the secp256k1 Koblitz curve. */
private val ECDSA_SECP256K1_SHA256 = SignatureScheme( val ECDSA_SECP256K1_SHA256 = SignatureScheme(
2, 2,
"ECDSA_SECP256K1_SHA256", "ECDSA_SECP256K1_SHA256",
X9ObjectIdentifiers.ecdsa_with_SHA256,
BouncyCastleProvider.PROVIDER_NAME,
"ECDSA", "ECDSA",
Signature.getInstance("SHA256withECDSA", "BC"), "SHA256withECDSA",
KeyFactory.getInstance("ECDSA", "BC"),
KeyPairGenerator.getInstance("ECDSA", "BC"),
ECNamedCurveTable.getParameterSpec("secp256k1"), ECNamedCurveTable.getParameterSpec("secp256k1"),
256, 256,
"ECDSA signature scheme using the secp256k1 Koblitz curve." "ECDSA signature scheme using the secp256k1 Koblitz curve."
) )
/** ECDSA signature scheme using the secp256r1 (NIST P-256) curve. */ /** ECDSA signature scheme using the secp256r1 (NIST P-256) curve. */
private val ECDSA_SECP256R1_SHA256 = SignatureScheme( val ECDSA_SECP256R1_SHA256 = SignatureScheme(
3, 3,
"ECDSA_SECP256R1_SHA256", "ECDSA_SECP256R1_SHA256",
X9ObjectIdentifiers.ecdsa_with_SHA256,
BouncyCastleProvider.PROVIDER_NAME,
"ECDSA", "ECDSA",
Signature.getInstance("SHA256withECDSA", "BC"), "SHA256withECDSA",
KeyFactory.getInstance("ECDSA", "BC"),
KeyPairGenerator.getInstance("ECDSA", "BC"),
ECNamedCurveTable.getParameterSpec("secp256r1"), ECNamedCurveTable.getParameterSpec("secp256r1"),
256, 256,
"ECDSA signature scheme using the secp256r1 (NIST P-256) curve." "ECDSA signature scheme using the secp256r1 (NIST P-256) curve."
) )
/** EdDSA signature scheme using the ed255519 twisted Edwards curve. */ /** EdDSA signature scheme using the ed255519 twisted Edwards curve. */
private val EDDSA_ED25519_SHA512 = SignatureScheme( val EDDSA_ED25519_SHA512 = SignatureScheme(
4, 4,
"EDDSA_ED25519_SHA512", "EDDSA_ED25519_SHA512",
"EdDSA", ASN1ObjectIdentifier("1.3.101.112"),
EdDSAEngine(), // We added EdDSA to bouncy castle for certificate signing.
EdDSAKeyFactory(), BouncyCastleProvider.PROVIDER_NAME,
net.i2p.crypto.eddsa.KeyPairGenerator(), // EdDSA engine uses a custom KeyPairGenerator Vs BouncyCastle. EdDSAKey.KEY_ALGORITHM,
EdDSANamedCurveTable.getByName("ed25519-sha-512"), EdDSAEngine.SIGNATURE_ALGORITHM,
EdDSANamedCurveTable.getByName("ED25519"),
256, 256,
"EdDSA signature scheme using the ed25519 twisted Edwards curve." "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 * 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. * at the cost of larger key sizes and loss of compatibility.
*/ */
private val SPHINCS256_SHA256 = SignatureScheme( val SPHINCS256_SHA256 = SignatureScheme(
5, 5,
"SPHINCS-256_SHA512", "SPHINCS-256_SHA512",
"SPHINCS-256", BCObjectIdentifiers.sphincs256_with_SHA512,
Signature.getInstance("SHA512WITHSPHINCS256", "BCPQC"), "BCPQC",
KeyFactory.getInstance("SPHINCS256", "BCPQC"), "SPHINCS256",
KeyPairGenerator.getInstance("SPHINCS256", "BCPQC"), "SHA512WITHSPHINCS256",
SPHINCS256KeyGenParameterSpec(SPHINCS256KeyGenParameterSpec.SHA512_256), SPHINCS256KeyGenParameterSpec(SPHINCS256KeyGenParameterSpec.SHA512_256),
256, 256,
"SPHINCS-256 hash-based signature scheme. It provides 128bit security against post-quantum attackers " + "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). */ /** 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. * Supported digital signature schemes.
* Note: Only the schemes added in this map will be supported (see [Crypto]). * 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( val supportedSignatureSchemes = listOf(
RSA_SHA256.schemeCodeName to RSA_SHA256, RSA_SHA256,
ECDSA_SECP256K1_SHA256.schemeCodeName to ECDSA_SECP256K1_SHA256, ECDSA_SECP256K1_SHA256,
ECDSA_SECP256R1_SHA256.schemeCodeName to ECDSA_SECP256R1_SHA256, ECDSA_SECP256R1_SHA256,
EDDSA_ED25519_SHA512.schemeCodeName to EDDSA_ED25519_SHA512, EDDSA_ED25519_SHA512,
SPHINCS256_SHA256.schemeCodeName to SPHINCS256_SHA256 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. * 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. * @return a currently supported SignatureScheme.
* @throws IllegalArgumentException if the requested signature scheme is not supported. * @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}") 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)
/** /**
* Retrieve the corresponding [SignatureScheme] based on the type of the input [Key]. * Retrieve the corresponding [SignatureScheme] based on the type of the input [Key].
@ -144,46 +179,39 @@ object Crypto {
* @return a currently supported SignatureScheme. * @return a currently supported SignatureScheme.
* @throws IllegalArgumentException if the requested key type is not supported. * @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) { 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 (algorithm == sig.algorithmName) {
// If more than one ECDSA schemes are supported, we should distinguish between them by checking their curve parameters. // 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 (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 return sig
} else continue } else break // use continue if in the future we support more than one Edwards curves.
} else if (algorithm == "ECDSA") { } 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 return sig
} else continue } else continue
} else return sig // it's either RSA_SHA256 or SPHINCS-256. } 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. * 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. * @param encodedKey a PKCS8 encoded private key.
* @throws IllegalArgumentException on not supported scheme or if the given key specification * @throws IllegalArgumentException on not supported scheme or if the given key specification
* is inappropriate for this key factory to produce a private key. * is inappropriate for this key factory to produce a private key.
*/ */
@Throws(IllegalArgumentException::class) @Throws(IllegalArgumentException::class)
fun decodePrivateKey(encodedKey: ByteArray): PrivateKey { fun decodePrivateKey(encodedKey: ByteArray): PrivateKey {
for (sig in supportedSignatureSchemes.values) { for ((_, _, _, providerName, algorithmName) in supportedSignatureSchemes.values) {
try { try {
return sig.keyFactory.generatePrivate(PKCS8EncodedKeySpec(encodedKey)) return KeyFactory.getInstance(algorithmName, providerMap[providerName]).generatePrivate(PKCS8EncodedKeySpec(encodedKey))
} catch (ikse: InvalidKeySpecException) { } catch (ikse: InvalidKeySpecException) {
// ignore it - only used to bypass the scheme that causes an exception. // 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. * Decode a PKCS8 encoded key to its [PrivateKey] object based on the input scheme code name.
* This will be used by Kryo deserialisation. * This should be used when the type key is known, e.g. during Kryo deserialisation or with key caches or key managers.
* @param encodedKey a PKCS8 encoded private key.
* @param schemeCodeName a [String] that should match a key in supportedSignatureSchemes map (e.g. ECDSA_SECP256K1_SHA256). * @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 * @throws IllegalArgumentException on not supported scheme or if the given key specification
* is inappropriate for this key factory to produce a private key. * is inappropriate for this key factory to produce a private key.
*/ */
@Throws(IllegalArgumentException::class, InvalidKeySpecException::class) @Throws(IllegalArgumentException::class, InvalidKeySpecException::class)
fun decodePrivateKey(encodedKey: ByteArray, schemeCodeName: String): PrivateKey { fun decodePrivateKey(schemeCodeName: String, encodedKey: ByteArray): PrivateKey = decodePrivateKey(findSignatureScheme(schemeCodeName), encodedKey)
val sig = findSignatureScheme(schemeCodeName)
/**
* 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 { try {
return sig.keyFactory.generatePrivate(PKCS8EncodedKeySpec(encodedKey)) return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePrivate(PKCS8EncodedKeySpec(encodedKey))
} catch (ikse: InvalidKeySpecException) { } 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) 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. * 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. * @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 * @throws IllegalArgumentException on not supported scheme or if the given key specification
* is inappropriate for this key factory to produce a private key. * is inappropriate for this key factory to produce a private key.
*/ */
@Throws(IllegalArgumentException::class) @Throws(IllegalArgumentException::class)
fun decodePublicKey(encodedKey: ByteArray): PublicKey { fun decodePublicKey(encodedKey: ByteArray): PublicKey {
for (sig in supportedSignatureSchemes.values) { for ((_, _, _, providerName, algorithmName) in supportedSignatureSchemes.values) {
try { try {
return sig.keyFactory.generatePublic(X509EncodedKeySpec(encodedKey)) return KeyFactory.getInstance(algorithmName, providerMap[providerName]).generatePublic(X509EncodedKeySpec(encodedKey))
} catch (ikse: InvalidKeySpecException) { } catch (ikse: InvalidKeySpecException) {
// ignore it - only used to bypass the scheme that causes an exception. // 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. * Decode an X509 encoded key to its [PrivateKey] object based on the input scheme code name.
* This will be used by Kryo deserialisation. * This should be used when the type key is known, e.g. during Kryo deserialisation or with key caches or key managers.
* @param encodedKey an X509 encoded public key.
* @param schemeCodeName a [String] that should match a key in supportedSignatureSchemes map (e.g. ECDSA_SECP256K1_SHA256). * @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 IllegalArgumentException if the requested scheme is not supported
* @throws InvalidKeySpecException if the given key specification * @throws InvalidKeySpecException if the given key specification
* is inappropriate for this key factory to produce a public key. * is inappropriate for this key factory to produce a public key.
*/ */
@Throws(IllegalArgumentException::class, InvalidKeySpecException::class) @Throws(IllegalArgumentException::class, InvalidKeySpecException::class)
fun decodePublicKey(encodedKey: ByteArray, schemeCodeName: String): PublicKey { fun decodePublicKey(schemeCodeName: String, encodedKey: ByteArray): PublicKey = decodePublicKey(findSignatureScheme(schemeCodeName), encodedKey)
val sig = findSignatureScheme(schemeCodeName)
/**
* 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 { try {
return sig.keyFactory.generatePublic(X509EncodedKeySpec(encodedKey)) return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePublic(X509EncodedKeySpec(encodedKey))
} catch (ikse: InvalidKeySpecException) { } 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) 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 * 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). * 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 SignatureException if signing is not possible due to malformed data or private key.
*/ */
@Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class) @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]. * 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 SignatureException if signing is not possible due to malformed data or private key.
*/ */
@Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class) @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]. * 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 privateKey the signer's [PrivateKey].
* @param clearData the data/message to be signed in [ByteArray] form (usually the Merkle root). * @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. * @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 SignatureException if signing is not possible due to malformed data or private key.
*/ */
@Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class) @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!") if (clearData.isEmpty()) throw Exception("Signing of an empty array is not permitted!")
signature.initSign(privateKey) signature.initSign(privateKey)
signature.update(clearData) signature.update(clearData)
@ -330,6 +366,7 @@ object Crypto {
/** /**
* Utility to simplify the act of verifying a digital signature. * 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. * 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 publicKey the signer's [PublicKey].
* @param signatureData the signatureData on a message. * @param signatureData the signatureData on a message.
* @param clearData the clear data/message that was signed (usually the Merkle root). * @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 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) @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. * 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 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) @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. * Method to verify a digital signature.
* It returns true if it succeeds, but it always throws an exception if verification fails. * 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 publicKey the signer's [PublicKey].
* @param signatureData the signatureData on a message. * @param signatureData the signatureData on a message.
* @param clearData the clear data/message that was signed (usually the Merkle root). * @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, * @throws SignatureException if this signatureData object is not initialized properly,
* the passed-in signatureData is improperly encoded or of the wrong type, * 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. * 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 (signatureData.isEmpty()) throw IllegalArgumentException("Signature data is empty!")
if (clearData.isEmpty()) throw IllegalArgumentException("Clear data is empty, nothing to verify!") if (clearData.isEmpty()) throw IllegalArgumentException("Clear data is empty, nothing to verify!")
signature.initVerify(publicKey) val verificationResult = isValid(signatureScheme, publicKey, signatureData, clearData)
signature.update(clearData)
val verificationResult = signature.verify(signatureData)
if (verificationResult) { if (verificationResult) {
return true return true
} else { } else {
@ -407,15 +445,181 @@ object Crypto {
} }
/** /**
* Check if the requested signature scheme is supported by the system. * Utility to simplify the act of verifying a digital signature by identifying the signature scheme used from the input public key's type.
* @param schemeCodeName a signature scheme's code name (e.g. ECDSA_SECP256K1_SHA256). * It returns true if it succeeds and false if not. In comparison to [doVerify] if the key and signature
* @return true if the signature scheme is supported. * 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 fun isSupportedSignatureScheme(schemeCodeName: String): Boolean = schemeCodeName in supportedSignatureSchemes
/** @return the default signature scheme's code name. */ fun isSupportedSignatureScheme(signatureScheme: SignatureScheme): Boolean = signatureScheme.schemeCodeName in supportedSignatureSchemes
fun getDefaultSignatureSchemeCodeName(): String = DEFAULT_SIGNATURE_SCHEME.schemeCodeName
/** @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 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 net.corda.core.utilities.SgxSupport
import java.security.* 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. * Utility to simplify the act of signing a byte array.
* @param clearData the data/message to be signed in [ByteArray] form (usually the Merkle root). * @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. * @return the digital signature (in [ByteArray]) on the input message.
* @throws IllegalArgumentException if the signature scheme is not supported for this private key. * @throws IllegalArgumentException if the signature scheme is not supported for this private key.
* @throws InvalidKeyException if the private key is invalid. * @throws InvalidKeyException if the private key is invalid.
* @throws SignatureException if signing is not possible due to malformed data or private key. * @throws SignatureException if signing is not possible due to malformed data or private key.
*/ */
@Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class) @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. * Helper function for signing.
* @param metaDataFull tha attached MetaData object. * @param metaData tha attached MetaData object.
* @return a [DSWithMetaDataFull] object. * @return a [TransactionSignature] object.
* @throws IllegalArgumentException if the signature scheme is not supported for this private key. * @throws IllegalArgumentException if the signature scheme is not supported for this private key.
* @throws InvalidKeyException if the private key is invalid. * @throws InvalidKeyException if the private key is invalid.
* @throws SignatureException if signing is not possible due to malformed data or private key. * @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) @Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class)
fun PrivateKey.sign(metaData: MetaData): TransactionSignature = Crypto.doSign(this, metaData) 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. * Helper function to verify a signature.
* @param signatureData the signature on a message. * @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. * 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). * @param clearData the clear data/message that was signed (usually the Merkle root).
* @throws InvalidKeyException if the key is invalid. * @throws InvalidKeyException if the key is invalid.
* @throws SignatureException if this signatureData object is not initialized properly, * @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. * which should never happen and suggests an unusual JVM or non-standard Java library.
*/ */
@Throws(NoSuchAlgorithmException::class) @Throws(NoSuchAlgorithmException::class)
fun safeRandomBytes(numOfBytes: Int): ByteArray { fun secureRandomBytes(numOfBytes: Int): ByteArray {
return newSecureRandom().generateSeed(numOfBytes) 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 package net.corda.core.crypto
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import java.nio.charset.Charset import java.nio.charset.Charset
import java.security.PublicKey
import java.util.* import java.util.*
import javax.xml.bind.DatatypeConverter import javax.xml.bind.DatatypeConverter
// This file includes useful encoding methods and extension functions for the most common encoding/decoding operations. // This file includes useful encoding methods and extension functions for the most common encoding/decoding operations.
// [ByteArray] encoders // [ByteArray] encoders
@ -33,7 +37,7 @@ fun String.base58ToByteArray(): ByteArray = Base58.decode(this)
fun String.base64ToByteArray(): ByteArray = Base64.getDecoder().decode(this) fun String.base64ToByteArray(): ByteArray = Base64.getDecoder().decode(this)
/** Hex-String to [ByteArray]. Accept any hex form (capitalized, lowercase, mixed). */ /** 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 // Encoding changers
@ -56,5 +60,9 @@ fun String.hexToBase58(): String = hexToByteArray().toBase58()
/** Encoding changer. Hex-[String] to Base64-[String], i.e. "48656C6C6F20576F726C64" -> "SGVsbG8gV29ybGQ=" */ /** Encoding changer. Hex-[String] to Base64-[String], i.e. "48656C6C6F20576F726C64" -> "SGVsbG8gV29ybGQ=" */
fun String.hexToBase64(): String = hexToByteArray().toBase64() fun String.hexToBase64(): String = hexToByteArray().toBase64()
// Helper vars. // TODO We use for both CompositeKeys and EdDSAPublicKey custom Kryo serializers and deserializers. We need to specify encoding.
private val HEX_ALPHABET = "0123456789ABCDEF".toCharArray() // 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, * 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. * 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) { sealed class MerkleTree {
class Leaf(val value: SecureHash) : MerkleTree(value) abstract val hash: SecureHash
class Node(val value: SecureHash, val left: MerkleTree, val right: MerkleTree) : MerkleTree(value)
data class Leaf(override val hash: SecureHash) : MerkleTree()
data class Node(override val hash: SecureHash, val left: MerkleTree, val right: MerkleTree) : MerkleTree()
companion object { 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. * 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 newLevelHashes: MutableList<MerkleTree> = ArrayList()
val n = lastNodesList.size val n = lastNodesList.size
require((n and 1) == 0) { "Sanity check: number of nodes should be even." } 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 left = lastNodesList[i]
val right = lastNodesList[i+1] val right = lastNodesList[i + 1]
val newHash = left.hash.hashConcat(right.hash) val newHash = left.hash.hashConcat(right.hash)
val combined = Node(newHash, left, right) val combined = Node(newHash, left, right)
newLevelHashes.add(combined) newLevelHashes.add(combined)

View File

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

View File

@ -1,33 +1,7 @@
package net.corda.core.crypto package net.corda.core.crypto
import net.corda.core.contracts.PartyAndReference import org.bouncycastle.asn1.x500.X500Name
import net.corda.core.serialization.OpaqueBytes
import java.security.PublicKey import java.security.PublicKey
/** @Deprecated("Party has moved to identity package", ReplaceWith("net.corda.core.identity.Party"))
* The [Party] class represents an entity on the network, which is typically identified by a legal [name] and public key class Party(name: X500Name, owningKey: PublicKey) : net.corda.core.identity.Party(name, owningKey)
* 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)
}

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 prefixChars(prefixLen: Int = 6) = toString().substring(0, prefixLen)
fun hashConcat(other: SecureHash) = (this.bytes + other.bytes).sha256() fun hashConcat(other: SecureHash) = (this.bytes + other.bytes).sha256()

View File

@ -1,18 +1,17 @@
package net.corda.core.crypto package net.corda.core.crypto
import java.security.* import org.bouncycastle.asn1.ASN1ObjectIdentifier
import java.security.Signature
import java.security.spec.AlgorithmParameterSpec import java.security.spec.AlgorithmParameterSpec
/** /**
* This class is used to define a digital signature scheme. * 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 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 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 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. * @param signatureName a signature-scheme name as required to create [Signature] objects (e.g. "SHA256withECDSA")
* 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 algSpec parameter specs for the underlying algorithm. Note that RSA is defined by the key size rather than algSpec. * @param algSpec parameter specs for the underlying algorithm. Note that RSA is defined by the key size rather than algSpec.
* eg. ECGenParameterSpec("secp256k1"). * eg. ECGenParameterSpec("secp256k1").
* @param keySize the private key size (currently used for RSA only). * @param keySize the private key size (currently used for RSA only).
@ -21,22 +20,10 @@ import java.security.spec.AlgorithmParameterSpec
data class SignatureScheme( data class SignatureScheme(
val schemeNumberID: Int, val schemeNumberID: Int,
val schemeCodeName: String, val schemeCodeName: String,
val signatureOID: ASN1ObjectIdentifier,
val providerName: String,
val algorithmName: String, val algorithmName: String,
val sig: Signature, val signatureName: String,
val keyFactory: KeyFactory,
val keyPairGenerator: KeyPairGeneratorSpi,
val algSpec: AlgorithmParameterSpec?, val algSpec: AlgorithmParameterSpec?,
val keySize: Int, val keySize: Int,
val desc: String) { 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())
}
}

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