mirror of
https://github.com/corda/corda.git
synced 2025-01-27 06:39:38 +00:00
Merge remote-tracking branch 'open/master'
This commit is contained in:
commit
dfb63231e3
4
.gitignore
vendored
4
.gitignore
vendored
@ -14,7 +14,6 @@ local.properties
|
||||
|
||||
# General build files
|
||||
**/build/*
|
||||
!docs/build/*
|
||||
|
||||
lib/dokka.jar
|
||||
|
||||
@ -34,6 +33,9 @@ lib/dokka.jar
|
||||
.idea/shelf
|
||||
.idea/dataSources
|
||||
|
||||
# Include the -parameters compiler option by default in IntelliJ required for serialization.
|
||||
!.idea/compiler.xml
|
||||
|
||||
# if you remove the above rule, at least ignore the following:
|
||||
|
||||
# User-specific stuff:
|
||||
|
96
.idea/compiler.xml
generated
Normal file
96
.idea/compiler.xml
generated
Normal 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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
15
.idea/runConfigurations/IRS_Demo__Run_Nodes.xml
generated
15
.idea/runConfigurations/IRS_Demo__Run_Nodes.xml
generated
@ -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>
|
15
.idea/runConfigurations/IRS_Demo__Run_Trade.xml
generated
15
.idea/runConfigurations/IRS_Demo__Run_Trade.xml
generated
@ -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>
|
15
.idea/runConfigurations/IRS_Demo__Upload_Rates.xml
generated
15
.idea/runConfigurations/IRS_Demo__Upload_Rates.xml
generated
@ -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>
|
@ -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>
|
@ -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="build/notary-demo-nodes/Party/certificates"" />
|
||||
<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>
|
15
.idea/runConfigurations/SIMM_Valuation_Demo.xml
generated
15
.idea/runConfigurations/SIMM_Valuation_Demo.xml
generated
@ -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>
|
15
.idea/runConfigurations/Trader_Demo__Run_Buyer.xml
generated
15
.idea/runConfigurations/Trader_Demo__Run_Buyer.xml
generated
@ -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>
|
15
.idea/runConfigurations/Trader_Demo__Run_Nodes.xml
generated
15
.idea/runConfigurations/Trader_Demo__Run_Nodes.xml
generated
@ -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>
|
15
.idea/runConfigurations/Trader_Demo__Run_Seller.xml
generated
15
.idea/runConfigurations/Trader_Demo__Run_Seller.xml
generated
@ -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>
|
@ -61,8 +61,9 @@ To look at the Corda source and run some sample applications:
|
||||
|
||||
## Development State
|
||||
|
||||
Corda is currently in very early development and should not be used in production systems. Breaking
|
||||
changes will happen on minor versions until 1.0. Experimentation with Corda is recommended.
|
||||
Corda is under active development and is maturing rapidly. We are targeting
|
||||
production-readiness in 2017. The API will continue to evolve throughout 2017;
|
||||
backwards compatibility not assured until version 1.0.
|
||||
|
||||
Pull requests, experiments, and contributions are encouraged and welcomed.
|
||||
|
||||
|
119
build.gradle
119
build.gradle
@ -1,16 +1,20 @@
|
||||
|
||||
buildscript {
|
||||
// For sharing constants between builds
|
||||
Properties props = new Properties()
|
||||
file("publish.properties").withInputStream { props.load(it) }
|
||||
Properties constants = new Properties()
|
||||
file("$projectDir/constants.properties").withInputStream { constants.load(it) }
|
||||
|
||||
// Our version: bump this on release.
|
||||
ext.corda_version = "0.10-SNAPSHOT"
|
||||
ext.gradle_plugins_version = props.getProperty("gradlePluginsVersion")
|
||||
ext.corda_release_version = "0.12-SNAPSHOT"
|
||||
// Increment this on any release that changes public APIs anywhere in the Corda platform
|
||||
// TODO This is going to be difficult until we have a clear separation throughout the code of what is public and what is internal
|
||||
ext.corda_platform_version = 1
|
||||
ext.gradle_plugins_version = constants.getProperty("gradlePluginsVersion")
|
||||
|
||||
// Dependency versions. Can run 'gradle dependencyUpdates' to find new versions of things.
|
||||
//
|
||||
// TODO: Sort this alphabetically.
|
||||
ext.kotlin_version = '1.0.7'
|
||||
ext.kotlin_version = constants.getProperty("kotlinVersion")
|
||||
ext.quasar_version = '0.7.6' // TODO: Upgrade to 0.7.7+ when Quasar bug 238 is resolved.
|
||||
ext.asm_version = '0.5.3'
|
||||
ext.artemis_version = '1.5.3'
|
||||
@ -19,24 +23,27 @@ buildscript {
|
||||
ext.jersey_version = '2.25'
|
||||
ext.jolokia_version = '2.0.0-M3'
|
||||
ext.assertj_version = '3.6.1'
|
||||
ext.slf4j_version = '1.7.24'
|
||||
ext.slf4j_version = '1.7.25'
|
||||
ext.log4j_version = '2.7'
|
||||
ext.bouncycastle_version = '1.56'
|
||||
ext.guava_version = '19.0'
|
||||
ext.bouncycastle_version = constants.getProperty("bouncycastleVersion")
|
||||
ext.guava_version = constants.getProperty("guavaVersion")
|
||||
ext.quickcheck_version = '0.7'
|
||||
ext.okhttp_version = '3.5.0'
|
||||
ext.netty_version = '4.1.5.Final'
|
||||
ext.typesafe_config_version = '1.3.1'
|
||||
ext.typesafe_config_version = constants.getProperty("typesafeConfigVersion")
|
||||
ext.fileupload_version = '1.3.2'
|
||||
ext.junit_version = '4.12'
|
||||
ext.mockito_version = '1.10.19'
|
||||
ext.jopt_simple_version = '5.0.2'
|
||||
ext.jansi_version = '1.14'
|
||||
ext.hibernate_version = '5.2.6.Final'
|
||||
ext.h2_version = '1.4.194'
|
||||
ext.rxjava_version = '1.2.4'
|
||||
ext.requery_version = '1.1.1'
|
||||
ext.requery_version = '1.2.1'
|
||||
ext.dokka_version = '0.9.13'
|
||||
ext.crash_version = '1.3.2'
|
||||
|
||||
// Update 121 is required for ObjectInputFilter and at time of writing 131 was latest:
|
||||
ext.java8_minUpdateVersion = '131'
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
@ -66,13 +73,10 @@ ext {
|
||||
corda_revision = org.ajoberstar.grgit.Grgit.open(file('.')).head().id
|
||||
}
|
||||
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'project-report'
|
||||
apply plugin: 'com.github.ben-manes.versions'
|
||||
apply plugin: 'net.corda.plugins.publish-utils'
|
||||
apply plugin: 'net.corda.plugins.quasar-utils'
|
||||
apply plugin: 'net.corda.plugins.cordformation'
|
||||
apply plugin: 'org.jetbrains.dokka'
|
||||
|
||||
// We need the following three lines even though they're inside an allprojects {} block below because otherwise
|
||||
// IntelliJ gets confused when importing the project and ends up erasing and recreating the .idea directory, along
|
||||
@ -84,22 +88,65 @@ targetCompatibility = 1.8
|
||||
|
||||
|
||||
allprojects {
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'java'
|
||||
apply plugin: 'jacoco'
|
||||
|
||||
sourceCompatibility = 1.8
|
||||
targetCompatibility = 1.8
|
||||
|
||||
// Use manual resource copying of log4j2.xml rather than source sets.
|
||||
// This prevents problems in IntelliJ with regard to duplicate source roots.
|
||||
processTestResources {
|
||||
from file("$rootDir/config/test/log4j2.xml")
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" << "-Xlint:-options"
|
||||
options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" << "-Xlint:-options" << "-parameters"
|
||||
}
|
||||
|
||||
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
|
||||
kotlinOptions {
|
||||
languageVersion = "1.1"
|
||||
apiVersion = "1.1"
|
||||
jvmTarget = "1.8"
|
||||
javaParameters = true // Useful for reflection.
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(Jar) { // Includes War and Ear
|
||||
manifest {
|
||||
attributes('Corda-Release-Version': corda_release_version)
|
||||
attributes('Corda-Platform-Version': corda_platform_version)
|
||||
attributes('Corda-Revision': corda_revision)
|
||||
attributes('Corda-Vendor': 'Corda Open Source')
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(Test) {
|
||||
// Prevent the project from creating temporary files outside of the build directory.
|
||||
systemProperties['java.io.tmpdir'] = buildDir
|
||||
}
|
||||
|
||||
group 'net.corda'
|
||||
version "$corda_version"
|
||||
version "$corda_release_version"
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
// TODO: remove this once we eliminate Exposed
|
||||
maven {
|
||||
url 'https://dl.bintray.com/kotlin/exposed'
|
||||
}
|
||||
maven { url 'https://jitpack.io' }
|
||||
}
|
||||
|
||||
configurations.compile {
|
||||
// We want to use SLF4J's version of these bindings: jcl-over-slf4j
|
||||
// Remove any transitive dependency on Apache's version.
|
||||
exclude group: 'commons-logging', module: 'commons-logging'
|
||||
}
|
||||
}
|
||||
|
||||
// Check that we are running on a Java 8 JDK. The source/targetCompatibility values above aren't sufficient to
|
||||
@ -108,7 +155,7 @@ allprojects {
|
||||
// We recommend a specific minor version (unfortunately, not checkable directly) because JavaFX adds APIs in
|
||||
// minor releases, so we can't work with just any Java 8, it has to be a recent one.
|
||||
if (!JavaVersion.current().java8Compatible)
|
||||
throw new GradleException("Corda requires Java 8, please upgrade to at least 1.8.0_112")
|
||||
throw new GradleException("Corda requires Java 8, please upgrade to at least 1.8.0_$java8_minUpdateVersion")
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
@ -123,8 +170,9 @@ dependencies {
|
||||
compile project(':node')
|
||||
compile "com.google.guava:guava:$guava_version"
|
||||
|
||||
runtime project(path: ":node:capsule", configuration: 'runtimeArtifacts')
|
||||
runtime project(path: ":node:webserver:webcapsule", configuration: 'runtimeArtifacts')
|
||||
// Set to compile to ensure it exists now deploy nodes no longer relies on build
|
||||
compile project(path: ":node:capsule", configuration: 'runtimeArtifacts')
|
||||
compile project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts')
|
||||
|
||||
// For the buildCordappDependenciesJar task
|
||||
runtime project(':client:jfx')
|
||||
@ -132,7 +180,7 @@ dependencies {
|
||||
runtime project(':client:rpc')
|
||||
runtime project(':core')
|
||||
runtime project(':finance')
|
||||
runtime project(':node:webserver')
|
||||
runtime project(':webserver')
|
||||
testCompile project(':test-utils')
|
||||
}
|
||||
|
||||
@ -161,18 +209,18 @@ tasks.withType(Test) {
|
||||
reports.html.destination = file("${reporting.baseDir}/${name}")
|
||||
}
|
||||
|
||||
task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['build']) {
|
||||
task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
|
||||
directory "./build/nodes"
|
||||
networkMap "Controller"
|
||||
networkMap "CN=Controller,O=R3,OU=corda,L=London,C=UK"
|
||||
node {
|
||||
name "Controller"
|
||||
name "CN=Controller,O=R3,OU=corda,L=London,C=UK"
|
||||
nearestCity "London"
|
||||
advertisedServices = ["corda.notary.validating"]
|
||||
p2pPort 10002
|
||||
cordapps = []
|
||||
}
|
||||
node {
|
||||
name "Bank A"
|
||||
name "CN=Bank A,O=R3,OU=corda,L=London,C=UK"
|
||||
nearestCity "London"
|
||||
advertisedServices = []
|
||||
p2pPort 10012
|
||||
@ -181,7 +229,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['build']) {
|
||||
cordapps = []
|
||||
}
|
||||
node {
|
||||
name "Bank B"
|
||||
name "CN=Bank B,O=R3,OU=corda,L=London,C=UK"
|
||||
nearestCity "New York"
|
||||
advertisedServices = []
|
||||
p2pPort 10007
|
||||
@ -201,7 +249,7 @@ bintrayConfig {
|
||||
projectUrl = 'https://github.com/corda/corda'
|
||||
gpgSign = true
|
||||
gpgPassphrase = System.getenv('CORDA_BINTRAY_GPG_PASSPHRASE')
|
||||
publications = ['jfx', 'mock', 'rpc', 'core', 'corda', 'corda-webserver', 'finance', 'node', 'node-api', 'node-schemas', 'test-utils', 'jackson', 'webserver']
|
||||
publications = ['jfx', 'mock', 'rpc', 'core', 'corda', 'corda-webserver', 'finance', 'node', 'node-api', 'node-schemas', 'test-utils', 'jackson', 'verifier', 'webserver']
|
||||
license {
|
||||
name = 'Apache-2.0'
|
||||
url = 'https://www.apache.org/licenses/LICENSE-2.0'
|
||||
@ -214,25 +262,6 @@ bintrayConfig {
|
||||
}
|
||||
}
|
||||
|
||||
// API docs
|
||||
|
||||
dokka {
|
||||
moduleName = 'corda'
|
||||
outputDirectory = 'docs/build/html/api/kotlin'
|
||||
processConfigurations = ['compile']
|
||||
sourceDirs = files('core/src/main/kotlin', 'client/jfx/src/main/kotlin', 'client/mock/src/main/kotlin', 'client/rpc/src/main/kotlin', 'node/src/main/kotlin', 'finance/src/main/kotlin', 'client/jackson/src/main/kotlin')
|
||||
}
|
||||
|
||||
task dokkaJavadoc(type: org.jetbrains.dokka.gradle.DokkaTask) {
|
||||
moduleName = 'corda'
|
||||
outputFormat = "javadoc"
|
||||
outputDirectory = 'docs/build/html/api/javadoc'
|
||||
processConfigurations = ['compile']
|
||||
sourceDirs = files('core/src/main/kotlin', 'client/jfx/src/main/kotlin', 'client/mock/src/main/kotlin', 'client/rpc/src/main/kotlin', 'node/src/main/kotlin', 'finance/src/main/kotlin', 'client/jackson/src/main/kotlin')
|
||||
}
|
||||
|
||||
task apidocs(dependsOn: ['dokka', 'dokkaJavadoc'])
|
||||
|
||||
// Build a ZIP of all JARs required to compile the Cordapp template
|
||||
// Note: corda.jar is used at runtime so no runtime ZIP is necessary.
|
||||
// Resulting ZIP can be found in "build/distributions"
|
||||
|
@ -1,3 +1,10 @@
|
||||
buildscript {
|
||||
Properties constants = new Properties()
|
||||
file("../constants.properties").withInputStream { constants.load(it) }
|
||||
|
||||
ext.guava_version = constants.getProperty("guavaVersion")
|
||||
}
|
||||
|
||||
apply plugin: 'maven'
|
||||
|
||||
repositories {
|
||||
@ -5,6 +12,5 @@ repositories {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Cannot use ext.guava_version here :(
|
||||
compile "com.google.guava:guava:20.0"
|
||||
compile "com.google.guava:guava:$guava_version"
|
||||
}
|
||||
|
@ -2,18 +2,9 @@ apply plugin: 'java'
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'net.corda.plugins.publish-utils'
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
maven {
|
||||
url 'https://dl.bintray.com/kotlin/exposed'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile project(':core')
|
||||
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
|
||||
testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
||||
|
||||
// Jackson and its plugins: parsing to/from JSON and other textual formats.
|
||||
|
@ -10,6 +10,8 @@ import com.fasterxml.jackson.module.kotlin.KotlinModule
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.BusinessCalendar
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.services.IdentityService
|
||||
@ -17,7 +19,10 @@ import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.i2p.crypto.eddsa.EdDSAPublicKey
|
||||
import org.bouncycastle.asn1.ASN1InputStream
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import java.math.BigDecimal
|
||||
import java.security.PublicKey
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
@ -31,21 +36,28 @@ object JacksonSupport {
|
||||
// If you change this API please update the docs in the docsite (json.rst)
|
||||
|
||||
interface PartyObjectMapper {
|
||||
@Deprecated("Use partyFromX500Name instead")
|
||||
fun partyFromName(partyName: String): Party?
|
||||
fun partyFromKey(owningKey: CompositeKey): Party?
|
||||
fun partyFromPrincipal(principal: X500Name): Party?
|
||||
fun partyFromKey(owningKey: PublicKey): Party?
|
||||
}
|
||||
|
||||
class RpcObjectMapper(val rpc: CordaRPCOps, factory: JsonFactory) : PartyObjectMapper, ObjectMapper(factory) {
|
||||
override fun partyFromName(partyName: String): Party? = rpc.partyFromName(partyName)
|
||||
override fun partyFromKey(owningKey: CompositeKey): Party? = rpc.partyFromKey(owningKey)
|
||||
override fun partyFromPrincipal(principal: X500Name): Party? = rpc.partyFromX500Name(principal)
|
||||
override fun partyFromKey(owningKey: PublicKey): Party? = rpc.partyFromKey(owningKey)
|
||||
}
|
||||
|
||||
class IdentityObjectMapper(val identityService: IdentityService, factory: JsonFactory) : PartyObjectMapper, ObjectMapper(factory) {
|
||||
override fun partyFromName(partyName: String): Party? = identityService.partyFromName(partyName)
|
||||
override fun partyFromKey(owningKey: CompositeKey): Party? = identityService.partyFromKey(owningKey)
|
||||
override fun partyFromPrincipal(principal: X500Name): Party? = identityService.partyFromX500Name(principal)
|
||||
override fun partyFromKey(owningKey: PublicKey): Party? = identityService.partyFromKey(owningKey)
|
||||
}
|
||||
class NoPartyObjectMapper(factory: JsonFactory): PartyObjectMapper, ObjectMapper(factory) {
|
||||
|
||||
class NoPartyObjectMapper(factory: JsonFactory) : PartyObjectMapper, ObjectMapper(factory) {
|
||||
override fun partyFromName(partyName: String): Party? = throw UnsupportedOperationException()
|
||||
override fun partyFromKey(owningKey: CompositeKey): Party? = throw UnsupportedOperationException()
|
||||
override fun partyFromPrincipal(principal: X500Name): Party? = throw UnsupportedOperationException()
|
||||
override fun partyFromKey(owningKey: PublicKey): Party? = throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
val cordaModule: Module by lazy {
|
||||
@ -82,6 +94,10 @@ object JacksonSupport {
|
||||
// For OpaqueBytes
|
||||
addDeserializer(OpaqueBytes::class.java, OpaqueBytesDeserializer)
|
||||
addSerializer(OpaqueBytes::class.java, OpaqueBytesSerializer)
|
||||
|
||||
// For X.500 distinguished names
|
||||
addDeserializer(X500Name::class.java, X500NameDeserializer)
|
||||
addSerializer(X500Name::class.java, X500NameSerializer)
|
||||
}
|
||||
}
|
||||
|
||||
@ -126,14 +142,14 @@ object JacksonSupport {
|
||||
}
|
||||
|
||||
// TODO this needs to use some industry identifier(s) instead of these keys
|
||||
val key = CompositeKey.parseFromBase58(parser.text)
|
||||
val key = parsePublicKeyBase58(parser.text)
|
||||
return AnonymousParty(key)
|
||||
}
|
||||
}
|
||||
|
||||
object PartySerializer : JsonSerializer<Party>() {
|
||||
override fun serialize(obj: Party, generator: JsonGenerator, provider: SerializerProvider) {
|
||||
generator.writeString(obj.name)
|
||||
generator.writeString(obj.name.toString())
|
||||
}
|
||||
}
|
||||
|
||||
@ -144,8 +160,28 @@ object JacksonSupport {
|
||||
}
|
||||
|
||||
val mapper = parser.codec as PartyObjectMapper
|
||||
// TODO this needs to use some industry identifier(s) not just these human readable names
|
||||
return mapper.partyFromName(parser.text) ?: throw JsonParseException(parser, "Could not find a Party with name ${parser.text}")
|
||||
val principal = X500Name(parser.text)
|
||||
return mapper.partyFromPrincipal(principal) ?: throw JsonParseException(parser, "Could not find a Party with name ${principal}")
|
||||
}
|
||||
}
|
||||
|
||||
object X500NameSerializer : JsonSerializer<X500Name>() {
|
||||
override fun serialize(obj: X500Name, generator: JsonGenerator, provider: SerializerProvider) {
|
||||
generator.writeString(obj.toString())
|
||||
}
|
||||
}
|
||||
|
||||
object X500NameDeserializer : JsonDeserializer<X500Name>() {
|
||||
override fun deserialize(parser: JsonParser, context: DeserializationContext): X500Name {
|
||||
if (parser.currentToken == JsonToken.FIELD_NAME) {
|
||||
parser.nextToken()
|
||||
}
|
||||
|
||||
return try {
|
||||
X500Name(parser.text)
|
||||
} catch(ex: IllegalArgumentException) {
|
||||
throw JsonParseException(parser, "Invalid X.500 name ${parser.text}: ${ex.message}", ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -204,7 +240,7 @@ object JacksonSupport {
|
||||
|
||||
object PublicKeySerializer : JsonSerializer<EdDSAPublicKey>() {
|
||||
override fun serialize(obj: EdDSAPublicKey, generator: JsonGenerator, provider: SerializerProvider) {
|
||||
check(obj.params == ed25519Curve)
|
||||
check(obj.params == Crypto.EDDSA_ED25519_SHA512.algSpec)
|
||||
generator.writeString(obj.toBase58String())
|
||||
}
|
||||
}
|
||||
@ -212,7 +248,7 @@ object JacksonSupport {
|
||||
object PublicKeyDeserializer : JsonDeserializer<EdDSAPublicKey>() {
|
||||
override fun deserialize(parser: JsonParser, context: DeserializationContext): EdDSAPublicKey {
|
||||
return try {
|
||||
parsePublicKeyBase58(parser.text)
|
||||
parsePublicKeyBase58(parser.text) as EdDSAPublicKey
|
||||
} catch (e: Exception) {
|
||||
throw JsonParseException(parser, "Invalid public key ${parser.text}: ${e.message}")
|
||||
}
|
||||
@ -228,7 +264,7 @@ object JacksonSupport {
|
||||
object CompositeKeyDeserializer : JsonDeserializer<CompositeKey>() {
|
||||
override fun deserialize(parser: JsonParser, context: DeserializationContext): CompositeKey {
|
||||
return try {
|
||||
CompositeKey.parseFromBase58(parser.text)
|
||||
parsePublicKeyBase58(parser.text) as CompositeKey
|
||||
} catch (e: Exception) {
|
||||
throw JsonParseException(parser, "Invalid composite key ${parser.text}: ${e.message}")
|
||||
}
|
||||
|
@ -5,17 +5,15 @@ import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
|
||||
import com.google.common.collect.HashMultimap
|
||||
import com.google.common.collect.Multimap
|
||||
import com.google.common.collect.MultimapBuilder
|
||||
import net.corda.jackson.StringToMethodCallParser.ParsedMethodCall
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.lang.reflect.Constructor
|
||||
import java.lang.reflect.Method
|
||||
import java.util.*
|
||||
import java.util.concurrent.Callable
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KFunction
|
||||
import kotlin.reflect.KotlinReflectionInternalError
|
||||
import kotlin.reflect.jvm.internal.KotlinReflectionInternalError
|
||||
import kotlin.reflect.jvm.kotlinFunction
|
||||
|
||||
/**
|
||||
@ -75,14 +73,14 @@ import kotlin.reflect.jvm.kotlinFunction
|
||||
@ThreadSafe
|
||||
open class StringToMethodCallParser<in T : Any> @JvmOverloads constructor(
|
||||
targetType: Class<out T>,
|
||||
private val om: ObjectMapper = JacksonSupport.createNonRpcMapper(YAMLFactory()))
|
||||
{
|
||||
private val om: ObjectMapper = JacksonSupport.createNonRpcMapper(YAMLFactory())) {
|
||||
/** Same as the regular constructor but takes a Kotlin reflection [KClass] instead of a Java [Class]. */
|
||||
constructor(targetType: KClass<out T>) : this(targetType.java)
|
||||
|
||||
companion object {
|
||||
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
|
||||
private val ignoredNames = Object::class.java.methods.map { it.name }
|
||||
|
||||
private fun methodsFromType(clazz: Class<*>): Multimap<String, Method> {
|
||||
val result = HashMultimap.create<String, Method>()
|
||||
for ((key, value) in clazz.methods.filterNot { it.isSynthetic && it.name !in ignoredNames }.map { it.name to it }) {
|
||||
@ -90,6 +88,7 @@ open class StringToMethodCallParser<in T : Any> @JvmOverloads constructor(
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private val log = LoggerFactory.getLogger(StringToMethodCallParser::class.java)!!
|
||||
}
|
||||
|
||||
@ -126,7 +125,7 @@ open class StringToMethodCallParser<in T : Any> @JvmOverloads constructor(
|
||||
return method.parameters.mapIndexed { index, param ->
|
||||
when {
|
||||
param.isNamePresent -> param.name
|
||||
// index + 1 because the first Kotlin reflection param is 'this', but that doesn't match Java reflection.
|
||||
// index + 1 because the first Kotlin reflection param is 'this', but that doesn't match Java reflection.
|
||||
kf != null -> kf.parameters[index + 1].name ?: throw UnparseableCallException.ReflectionDataMissing(method.name, index)
|
||||
else -> throw UnparseableCallException.ReflectionDataMissing(method.name, index)
|
||||
}
|
||||
@ -147,11 +146,12 @@ open class StringToMethodCallParser<in T : Any> @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
|
||||
open class UnparseableCallException(command: String) : Exception("Could not parse as a command: $command") {
|
||||
open class UnparseableCallException(command: String, cause: Throwable? = null) : Exception("Could not parse as a command: $command", cause) {
|
||||
class UnknownMethod(val methodName: String) : UnparseableCallException("Unknown command name: $methodName")
|
||||
class MissingParameter(methodName: String, val paramName: String, command: String) : UnparseableCallException("Parameter $paramName missing from attempt to invoke $methodName in command: $command")
|
||||
class TooManyParameters(methodName: String, command: String) : UnparseableCallException("Too many parameters provided for $methodName: $command")
|
||||
class ReflectionDataMissing(methodName: String, argIndex: Int) : UnparseableCallException("Method $methodName missing parameter name at index $argIndex")
|
||||
class FailedParse(e: Exception) : UnparseableCallException(e.message ?: e.toString(), e)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -193,10 +193,14 @@ open class StringToMethodCallParser<in T : Any> @JvmOverloads constructor(
|
||||
val parameterString = "{ $args }"
|
||||
val tree: JsonNode = om.readTree(parameterString) ?: throw UnparseableCallException(args)
|
||||
if (tree.size() > parameters.size) throw UnparseableCallException.TooManyParameters(methodNameHint, args)
|
||||
val inOrderParams: List<Any?> = parameters.mapIndexed { index, param ->
|
||||
val inOrderParams: List<Any?> = parameters.mapIndexed { _, param ->
|
||||
val (argName, argType) = param
|
||||
val entry = tree[argName] ?: throw UnparseableCallException.MissingParameter(methodNameHint, argName, args)
|
||||
om.readValue(entry.traverse(om), argType)
|
||||
try {
|
||||
om.readValue(entry.traverse(om), argType)
|
||||
} catch(e: Exception) {
|
||||
throw UnparseableCallException.FailedParse(e)
|
||||
}
|
||||
}
|
||||
if (log.isDebugEnabled) {
|
||||
inOrderParams.forEachIndexed { i, param ->
|
||||
|
@ -10,7 +10,7 @@ class StringToMethodCallParserTest {
|
||||
fun simple() = "simple"
|
||||
fun string(note: String) = note
|
||||
fun twoStrings(a: String, b: String) = a + b
|
||||
fun simpleObject(hash: SecureHash.SHA256) = hash.toString()!!
|
||||
fun simpleObject(hash: SecureHash.SHA256) = hash.toString()
|
||||
fun complexObject(pair: Pair<Int, String>) = pair
|
||||
|
||||
fun overload(a: String) = a
|
||||
|
@ -4,18 +4,6 @@ apply plugin: 'net.corda.plugins.publish-utils'
|
||||
|
||||
description 'Corda client JavaFX modules'
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
maven {
|
||||
url 'http://oss.sonatype.org/content/repositories/snapshots'
|
||||
}
|
||||
jcenter()
|
||||
maven {
|
||||
url 'https://dl.bintray.com/kotlin/exposed'
|
||||
}
|
||||
}
|
||||
|
||||
//noinspection GroovyAssignabilityCheck
|
||||
configurations {
|
||||
// we don't want isolated.jar in classPath, since we want to test jar being dynamically loaded as an attachment
|
||||
@ -33,11 +21,6 @@ sourceSets {
|
||||
srcDir file('src/integration-test/kotlin')
|
||||
}
|
||||
}
|
||||
test {
|
||||
resources {
|
||||
srcDir "../../config/test"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// To find potential version conflicts, run "gradle htmlDependencyReport" and then look in
|
||||
@ -48,6 +31,7 @@ dependencies {
|
||||
compile project(':finance')
|
||||
compile project(':client:rpc')
|
||||
|
||||
compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
|
||||
compile "com.google.guava:guava:$guava_version"
|
||||
|
||||
// ReactFX: Functional reactive UI programming.
|
||||
|
@ -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")))
|
||||
}
|
||||
|
||||
}
|
@ -6,6 +6,9 @@ import net.corda.core.bufferUntilSubscribed
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.DOLLARS
|
||||
import net.corda.core.contracts.USD
|
||||
import net.corda.core.crypto.isFulfilledBy
|
||||
import net.corda.core.crypto.keys
|
||||
import net.corda.core.flows.FlowInitiator
|
||||
import net.corda.core.flows.StateMachineRunId
|
||||
import net.corda.core.getOrThrow
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
@ -18,6 +21,10 @@ import net.corda.core.node.services.StateMachineTransactionMapping
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.ALICE
|
||||
import net.corda.core.utilities.BOB
|
||||
import net.corda.core.utilities.CHARLIE
|
||||
import net.corda.core.utilities.DUMMY_NOTARY
|
||||
import net.corda.flows.CashExitFlow
|
||||
import net.corda.flows.CashIssueFlow
|
||||
import net.corda.flows.CashPaymentFlow
|
||||
@ -30,21 +37,25 @@ import net.corda.testing.expect
|
||||
import net.corda.testing.expectEvents
|
||||
import net.corda.testing.node.DriverBasedTest
|
||||
import net.corda.testing.sequence
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.junit.Test
|
||||
import rx.Observable
|
||||
|
||||
class NodeMonitorModelTest : DriverBasedTest() {
|
||||
lateinit var aliceNode: NodeInfo
|
||||
lateinit var bobNode: NodeInfo
|
||||
lateinit var notaryNode: NodeInfo
|
||||
|
||||
lateinit var rpc: CordaRPCOps
|
||||
lateinit var rpcBob: CordaRPCOps
|
||||
lateinit var stateMachineTransactionMapping: Observable<StateMachineTransactionMapping>
|
||||
lateinit var stateMachineUpdates: Observable<StateMachineUpdate>
|
||||
lateinit var stateMachineUpdatesBob: Observable<StateMachineUpdate>
|
||||
lateinit var progressTracking: Observable<ProgressTrackingEvent>
|
||||
lateinit var transactions: Observable<SignedTransaction>
|
||||
lateinit var vaultUpdates: Observable<Vault.Update>
|
||||
lateinit var networkMapUpdates: Observable<NetworkMapCache.MapChange>
|
||||
lateinit var newNode: (String) -> NodeInfo
|
||||
lateinit var newNode: (X500Name) -> NodeInfo
|
||||
|
||||
override fun setup() = driver {
|
||||
val cashUser = User("user1", "test", permissions = setOf(
|
||||
@ -52,15 +63,14 @@ class NodeMonitorModelTest : DriverBasedTest() {
|
||||
startFlowPermission<CashPaymentFlow>(),
|
||||
startFlowPermission<CashExitFlow>())
|
||||
)
|
||||
val aliceNodeFuture = startNode("Alice", rpcUsers = listOf(cashUser))
|
||||
val notaryNodeFuture = startNode("Notary", advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)))
|
||||
val aliceNodeFuture = startNode(ALICE.name, rpcUsers = listOf(cashUser))
|
||||
val notaryNodeFuture = startNode(DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)))
|
||||
val aliceNodeHandle = aliceNodeFuture.getOrThrow()
|
||||
val notaryNodeHandle = notaryNodeFuture.getOrThrow()
|
||||
aliceNode = aliceNodeHandle.nodeInfo
|
||||
notaryNode = notaryNodeHandle.nodeInfo
|
||||
newNode = { nodeName -> startNode(nodeName).getOrThrow().nodeInfo }
|
||||
val monitor = NodeMonitorModel()
|
||||
|
||||
stateMachineTransactionMapping = monitor.stateMachineTransactionMapping.bufferUntilSubscribed()
|
||||
stateMachineUpdates = monitor.stateMachineUpdates.bufferUntilSubscribed()
|
||||
progressTracking = monitor.progressTracking.bufferUntilSubscribed()
|
||||
@ -70,26 +80,32 @@ class NodeMonitorModelTest : DriverBasedTest() {
|
||||
|
||||
monitor.register(aliceNodeHandle.configuration.rpcAddress!!, cashUser.username, cashUser.password)
|
||||
rpc = monitor.proxyObservable.value!!
|
||||
|
||||
val bobNodeHandle = startNode(BOB.name, rpcUsers = listOf(cashUser)).getOrThrow()
|
||||
bobNode = bobNodeHandle.nodeInfo
|
||||
val monitorBob = NodeMonitorModel()
|
||||
stateMachineUpdatesBob = monitorBob.stateMachineUpdates.bufferUntilSubscribed()
|
||||
monitorBob.register(bobNodeHandle.configuration.rpcAddress!!, cashUser.username, cashUser.password)
|
||||
rpcBob = monitorBob.proxyObservable.value!!
|
||||
runTest()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `network map update`() {
|
||||
newNode("Bob")
|
||||
newNode("Charlie")
|
||||
newNode(CHARLIE.name)
|
||||
networkMapUpdates.filter { !it.node.advertisedServices.any { it.info.type.isNotary() } }
|
||||
.filter { !it.node.advertisedServices.any { it.info.type == NetworkMapService.type } }
|
||||
.expectEvents(isStrict = false) {
|
||||
sequence(
|
||||
// TODO : Add test for remove when driver DSL support individual node shutdown.
|
||||
expect { output: NetworkMapCache.MapChange ->
|
||||
require(output.node.legalIdentity.name == "Alice") { "Expecting : Alice, Actual : ${output.node.legalIdentity.name}" }
|
||||
require(output.node.legalIdentity.name == ALICE.name) { "Expecting : ${ALICE.name}, Actual : ${output.node.legalIdentity.name}" }
|
||||
},
|
||||
expect { output: NetworkMapCache.MapChange ->
|
||||
require(output.node.legalIdentity.name == "Bob") { "Expecting : Bob, Actual : ${output.node.legalIdentity.name}" }
|
||||
require(output.node.legalIdentity.name == BOB.name) { "Expecting : ${BOB.name}, Actual : ${output.node.legalIdentity.name}" }
|
||||
},
|
||||
expect { output: NetworkMapCache.MapChange ->
|
||||
require(output.node.legalIdentity.name == "Charlie") { "Expecting : Charlie, Actual : ${output.node.legalIdentity.name}" }
|
||||
require(output.node.legalIdentity.name == CHARLIE.name) { "Expecting : ${CHARLIE.name}, Actual : ${output.node.legalIdentity.name}" }
|
||||
}
|
||||
)
|
||||
}
|
||||
@ -108,12 +124,12 @@ class NodeMonitorModelTest : DriverBasedTest() {
|
||||
sequence(
|
||||
// SNAPSHOT
|
||||
expect { output: Vault.Update ->
|
||||
require(output.consumed.size == 0) { output.consumed.size }
|
||||
require(output.produced.size == 0) { output.produced.size }
|
||||
require(output.consumed.isEmpty()) { output.consumed.size }
|
||||
require(output.produced.isEmpty()) { output.produced.size }
|
||||
},
|
||||
// ISSUE
|
||||
expect { output: Vault.Update ->
|
||||
require(output.consumed.size == 0) { output.consumed.size }
|
||||
require(output.consumed.isEmpty()) { output.consumed.size }
|
||||
require(output.produced.size == 1) { output.produced.size }
|
||||
}
|
||||
)
|
||||
@ -123,7 +139,7 @@ class NodeMonitorModelTest : DriverBasedTest() {
|
||||
@Test
|
||||
fun `cash issue and move`() {
|
||||
rpc.startFlow(::CashIssueFlow, 100.DOLLARS, OpaqueBytes.of(1), aliceNode.legalIdentity, notaryNode.notaryIdentity).returnValue.getOrThrow()
|
||||
rpc.startFlow(::CashPaymentFlow, 100.DOLLARS, aliceNode.legalIdentity).returnValue.getOrThrow()
|
||||
rpc.startFlow(::CashPaymentFlow, 100.DOLLARS, bobNode.legalIdentity).returnValue.getOrThrow()
|
||||
|
||||
var issueSmId: StateMachineRunId? = null
|
||||
var moveSmId: StateMachineRunId? = null
|
||||
@ -134,6 +150,8 @@ class NodeMonitorModelTest : DriverBasedTest() {
|
||||
// ISSUE
|
||||
expect { add: StateMachineUpdate.Added ->
|
||||
issueSmId = add.id
|
||||
val initiator = add.stateMachineInfo.initiator
|
||||
require(initiator is FlowInitiator.RPC && initiator.username == "user1")
|
||||
},
|
||||
expect { remove: StateMachineUpdate.Removed ->
|
||||
require(remove.id == issueSmId)
|
||||
@ -141,6 +159,8 @@ class NodeMonitorModelTest : DriverBasedTest() {
|
||||
// MOVE
|
||||
expect { add: StateMachineUpdate.Added ->
|
||||
moveSmId = add.id
|
||||
val initiator = add.stateMachineInfo.initiator
|
||||
require(initiator is FlowInitiator.RPC && initiator.username == "user1")
|
||||
},
|
||||
expect { remove: StateMachineUpdate.Removed ->
|
||||
require(remove.id == moveSmId)
|
||||
@ -148,28 +168,38 @@ class NodeMonitorModelTest : DriverBasedTest() {
|
||||
)
|
||||
}
|
||||
|
||||
stateMachineUpdatesBob.expectEvents {
|
||||
sequence(
|
||||
// MOVE
|
||||
expect { add: StateMachineUpdate.Added ->
|
||||
val initiator = add.stateMachineInfo.initiator
|
||||
require(initiator is FlowInitiator.Peer && initiator.party.name == aliceNode.legalIdentity.name)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
transactions.expectEvents {
|
||||
sequence(
|
||||
// ISSUE
|
||||
expect { tx ->
|
||||
require(tx.tx.inputs.isEmpty())
|
||||
require(tx.tx.outputs.size == 1)
|
||||
val signaturePubKeys = tx.sigs.map { it.by }.toSet()
|
||||
expect { stx ->
|
||||
require(stx.tx.inputs.isEmpty())
|
||||
require(stx.tx.outputs.size == 1)
|
||||
val signaturePubKeys = stx.sigs.map { it.by }.toSet()
|
||||
// Only Alice signed
|
||||
val aliceKey = aliceNode.legalIdentity.owningKey
|
||||
require(signaturePubKeys.size <= aliceKey.keys.size)
|
||||
require(aliceKey.isFulfilledBy(signaturePubKeys))
|
||||
issueTx = tx
|
||||
issueTx = stx
|
||||
},
|
||||
// MOVE
|
||||
expect { tx ->
|
||||
require(tx.tx.inputs.size == 1)
|
||||
require(tx.tx.outputs.size == 1)
|
||||
val signaturePubKeys = tx.sigs.map { it.by }.toSet()
|
||||
expect { stx ->
|
||||
require(stx.tx.inputs.size == 1)
|
||||
require(stx.tx.outputs.size == 1)
|
||||
val signaturePubKeys = stx.sigs.map { it.by }.toSet()
|
||||
// Alice and Notary signed
|
||||
require(aliceNode.legalIdentity.owningKey.isFulfilledBy(signaturePubKeys))
|
||||
require(notaryNode.notaryIdentity.owningKey.isFulfilledBy(signaturePubKeys))
|
||||
moveTx = tx
|
||||
moveTx = stx
|
||||
}
|
||||
)
|
||||
}
|
||||
@ -178,18 +208,18 @@ class NodeMonitorModelTest : DriverBasedTest() {
|
||||
sequence(
|
||||
// SNAPSHOT
|
||||
expect { output: Vault.Update ->
|
||||
require(output.consumed.size == 0) { output.consumed.size }
|
||||
require(output.produced.size == 0) { output.produced.size }
|
||||
require(output.consumed.isEmpty()) { output.consumed.size }
|
||||
require(output.produced.isEmpty()) { output.produced.size }
|
||||
},
|
||||
// ISSUE
|
||||
expect { update ->
|
||||
require(update.consumed.size == 0) { update.consumed.size }
|
||||
require(update.consumed.isEmpty()) { update.consumed.size }
|
||||
require(update.produced.size == 1) { update.produced.size }
|
||||
},
|
||||
// MOVE
|
||||
expect { update ->
|
||||
require(update.consumed.size == 1) { update.consumed.size }
|
||||
require(update.produced.size == 1) { update.produced.size }
|
||||
require(update.produced.isEmpty()) { update.produced.size }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ package net.corda.client.jfx.model
|
||||
|
||||
import javafx.collections.FXCollections
|
||||
import javafx.collections.ObservableList
|
||||
import kotlinx.support.jdk8.collections.removeIf
|
||||
import net.corda.client.jfx.utils.fold
|
||||
import net.corda.client.jfx.utils.map
|
||||
import net.corda.contracts.asset.Cash
|
||||
@ -28,7 +27,7 @@ class ContractStateModel {
|
||||
private val cashStatesDiff: Observable<Diff<Cash.State>> = contractStatesDiff.map {
|
||||
Diff(it.added.filterCashStateAndRefs(), it.removed.filterCashStateAndRefs())
|
||||
}
|
||||
val cashStates: ObservableList<StateAndRef<Cash.State>> = cashStatesDiff.fold(FXCollections.observableArrayList()) { list, statesDiff ->
|
||||
val cashStates: ObservableList<StateAndRef<Cash.State>> = cashStatesDiff.fold(FXCollections.observableArrayList()) { list: MutableList<StateAndRef<Cash.State>>, statesDiff ->
|
||||
list.removeIf { it in statesDiff.removed }
|
||||
list.addAll(statesDiff.added)
|
||||
}
|
||||
|
@ -3,12 +3,11 @@ package net.corda.client.jfx.model
|
||||
import javafx.beans.value.ObservableValue
|
||||
import javafx.collections.FXCollections
|
||||
import javafx.collections.ObservableList
|
||||
import kotlinx.support.jdk8.collections.removeIf
|
||||
import net.corda.client.jfx.utils.firstOrDefault
|
||||
import net.corda.client.jfx.utils.firstOrNullObservable
|
||||
import net.corda.client.jfx.utils.fold
|
||||
import net.corda.client.jfx.utils.map
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.crypto.keys
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.services.NetworkMapCache.MapChange
|
||||
import java.security.PublicKey
|
||||
@ -40,10 +39,6 @@ class NetworkIdentityModel {
|
||||
}
|
||||
|
||||
// TODO: Use Identity Service in service hub instead?
|
||||
fun lookup(compositeKey: CompositeKey): ObservableValue<NodeInfo?> = parties.firstOrDefault(notaries.firstOrNullObservable { it.notaryIdentity.owningKey == compositeKey }) {
|
||||
it.legalIdentity.owningKey == compositeKey
|
||||
}
|
||||
|
||||
fun lookup(publicKey: PublicKey): ObservableValue<NodeInfo?> = parties.firstOrDefault(notaries.firstOrNullObservable { it.notaryIdentity.owningKey.keys.any { it == publicKey } }) {
|
||||
it.legalIdentity.owningKey.keys.any { it == publicKey }
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package net.corda.client.jfx.model
|
||||
import com.google.common.net.HostAndPort
|
||||
import javafx.beans.property.SimpleObjectProperty
|
||||
import net.corda.client.rpc.CordaRPCClient
|
||||
import net.corda.client.rpc.CordaRPCClientConfiguration
|
||||
import net.corda.core.flows.StateMachineRunId
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.messaging.StateMachineInfo
|
||||
@ -52,11 +53,14 @@ class NodeMonitorModel {
|
||||
* TODO provide an unsubscribe mechanism
|
||||
*/
|
||||
fun register(nodeHostAndPort: HostAndPort, username: String, password: String) {
|
||||
val client = CordaRPCClient(nodeHostAndPort){
|
||||
maxRetryInterval = 10.seconds.toMillis()
|
||||
}
|
||||
client.start(username, password)
|
||||
val proxy = client.proxy()
|
||||
val client = CordaRPCClient(
|
||||
hostAndPort = nodeHostAndPort,
|
||||
configuration = CordaRPCClientConfiguration.default.copy(
|
||||
connectionMaxRetryInterval = 10.seconds
|
||||
)
|
||||
)
|
||||
val connection = client.start(username, password)
|
||||
val proxy = connection.proxy
|
||||
|
||||
val (stateMachines, stateMachineUpdates) = proxy.stateMachinesAndUpdates()
|
||||
// Extract the flow tracking stream
|
||||
|
@ -30,9 +30,13 @@ data class PartiallyResolvedTransaction(
|
||||
val inputs: List<ObservableValue<InputResolution>>) {
|
||||
val id = transaction.id
|
||||
|
||||
sealed class InputResolution(val stateRef: StateRef) {
|
||||
class Unresolved(stateRef: StateRef) : InputResolution(stateRef)
|
||||
class Resolved(val stateAndRef: StateAndRef<ContractState>) : InputResolution(stateAndRef.ref)
|
||||
sealed class InputResolution {
|
||||
abstract val stateRef: StateRef
|
||||
|
||||
data class Unresolved(override val stateRef: StateRef) : InputResolution()
|
||||
data class Resolved(val stateAndRef: StateAndRef<ContractState>) : InputResolution() {
|
||||
override val stateRef: StateRef get() = stateAndRef.ref
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@ -54,22 +58,13 @@ data class PartiallyResolvedTransaction(
|
||||
}
|
||||
}
|
||||
|
||||
sealed class TransactionCreateStatus(val message: String?) {
|
||||
class Started(message: String?) : TransactionCreateStatus(message)
|
||||
class Failed(message: String?) : TransactionCreateStatus(message)
|
||||
data class FlowStatus(val status: String)
|
||||
|
||||
override fun toString(): String = message ?: javaClass.simpleName
|
||||
}
|
||||
sealed class StateMachineStatus {
|
||||
abstract val stateMachineName: String
|
||||
|
||||
data class FlowStatus(
|
||||
val status: String
|
||||
)
|
||||
|
||||
sealed class StateMachineStatus(val stateMachineName: String) {
|
||||
class Added(stateMachineName: String) : StateMachineStatus(stateMachineName)
|
||||
class Removed(stateMachineName: String) : StateMachineStatus(stateMachineName)
|
||||
|
||||
override fun toString(): String = "${javaClass.simpleName}($stateMachineName)"
|
||||
data class Added(override val stateMachineName: String) : StateMachineStatus()
|
||||
data class Removed(override val stateMachineName: String) : StateMachineStatus()
|
||||
}
|
||||
|
||||
data class StateMachineData(
|
||||
|
@ -4,7 +4,6 @@ import javafx.collections.FXCollections
|
||||
import javafx.collections.ListChangeListener
|
||||
import javafx.collections.ObservableList
|
||||
import javafx.collections.transformation.TransformationList
|
||||
import kotlin.comparisons.compareValues
|
||||
|
||||
/**
|
||||
* Given an [ObservableList]<[E]> and a grouping key [K], [AggregatedList] groups the elements by the key into a fresh
|
||||
|
@ -3,7 +3,6 @@ package net.corda.client.jfx.utils
|
||||
import javafx.beans.binding.Bindings
|
||||
import javafx.beans.value.ObservableValue
|
||||
import javafx.collections.ObservableList
|
||||
import kotlinx.support.jdk8.collections.stream
|
||||
import net.corda.client.jfx.model.ExchangeRate
|
||||
import net.corda.core.contracts.Amount
|
||||
import org.fxmisc.easybind.EasyBind
|
||||
@ -14,7 +13,7 @@ import java.util.stream.Collectors
|
||||
* Utility bindings for the [Amount] type, similar in spirit to [Bindings]
|
||||
*/
|
||||
object AmountBindings {
|
||||
fun <T> sum(amounts: ObservableList<Amount<T>>, token: T) = EasyBind.map(
|
||||
fun <T : Any> sum(amounts: ObservableList<Amount<T>>, token: T) = EasyBind.map(
|
||||
Bindings.createLongBinding({
|
||||
amounts.stream().collect(Collectors.summingLong {
|
||||
require(it.token == token)
|
||||
|
@ -31,7 +31,7 @@ class ChosenList<E>(
|
||||
}
|
||||
|
||||
init {
|
||||
chosenListObservable.addListener { observable: Observable -> rechoose() }
|
||||
chosenListObservable.addListener { _: Observable -> rechoose() }
|
||||
currentList.addListener(listener)
|
||||
beginChange()
|
||||
nextAdd(0, currentList.size)
|
||||
|
@ -5,7 +5,6 @@ import javafx.collections.ListChangeListener
|
||||
import javafx.collections.ObservableList
|
||||
import javafx.collections.transformation.TransformationList
|
||||
import java.util.*
|
||||
import kotlin.comparisons.compareValues
|
||||
|
||||
/**
|
||||
* [ConcatenatedList] takes a list of lists and concatenates them. Any change to the underlying lists or the outer list
|
||||
|
@ -38,7 +38,7 @@ class FlattenedList<A>(val sourceList: ObservableList<out ObservableValue<out A>
|
||||
}
|
||||
|
||||
private fun createListener(wrapped: WrappedObservableValue<out A>): ChangeListener<A> {
|
||||
val listener = ChangeListener<A> { _observableValue, oldValue, newValue ->
|
||||
val listener = ChangeListener<A> { _, oldValue, _ ->
|
||||
val currentIndex = indexMap[wrapped]!!.first
|
||||
beginChange()
|
||||
nextReplace(currentIndex, currentIndex + 1, listOf(oldValue))
|
||||
@ -55,7 +55,7 @@ class FlattenedList<A>(val sourceList: ObservableList<out ObservableValue<out A>
|
||||
val from = c.from
|
||||
val to = c.to
|
||||
val permutation = IntArray(to, { c.getPermutation(it) })
|
||||
indexMap.replaceAll { _observableValue, pair -> Pair(permutation[pair.first], pair.second) }
|
||||
indexMap.replaceAll { _, pair -> Pair(permutation[pair.first], pair.second) }
|
||||
nextPermutation(from, to, permutation)
|
||||
} else if (c.wasUpdated()) {
|
||||
throw UnsupportedOperationException("FlattenedList doesn't support Update changes")
|
||||
|
@ -4,7 +4,6 @@ import javafx.collections.FXCollections
|
||||
import javafx.collections.MapChangeListener
|
||||
import javafx.collections.ObservableList
|
||||
import javafx.collections.ObservableMap
|
||||
import kotlin.comparisons.compareValues
|
||||
|
||||
/**
|
||||
* [MapValuesList] takes an [ObservableMap] and returns its values as an [ObservableList].
|
||||
|
@ -67,7 +67,7 @@ fun <A> Observable<A>.recordInSequence(): ObservableList<A> {
|
||||
* @param toKey Function retrieving the key to associate with.
|
||||
* @param merge The function to be called if there is an existing element at the key.
|
||||
*/
|
||||
fun <A, K> Observable<A>.recordAsAssociation(toKey: (A) -> K, merge: (K, oldValue: A, newValue: A) -> A = { _key, _oldValue, newValue -> newValue }): ObservableMap<K, A> {
|
||||
fun <A, K> Observable<A>.recordAsAssociation(toKey: (A) -> K, merge: (K, oldValue: A, newValue: A) -> A = { _, _, newValue -> newValue }): ObservableMap<K, A> {
|
||||
return fold(FXCollections.observableHashMap<K, A>()) { map, item ->
|
||||
val key = toKey(item)
|
||||
map[key] = map[key]?.let { merge(key, it, item) } ?: item
|
||||
|
@ -111,8 +111,14 @@ fun <A> ObservableList<out A>.filter(predicate: ObservableValue<(A) -> Boolean>)
|
||||
* val owners: ObservableList<Person> = dogs.map(Dog::owner).filterNotNull()
|
||||
*/
|
||||
fun <A> ObservableList<out A?>.filterNotNull(): ObservableList<A> {
|
||||
//TODO This is a tactical work round for an issue with SAM conversion (https://youtrack.jetbrains.com/issue/ALL-1552) so that the M10 explorer works.
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return filtered { it != null } as ObservableList<A>
|
||||
return (this as ObservableList<A?>).filtered(object : Predicate<A?> {
|
||||
override fun test(t: A?): Boolean {
|
||||
return t != null
|
||||
|
||||
}
|
||||
}) as ObservableList<A>
|
||||
}
|
||||
|
||||
/**
|
||||
@ -158,7 +164,7 @@ fun <K, A, B> ObservableList<out A>.associateBy(toKey: (A) -> K, assemble: (K, A
|
||||
* val nameToPerson: ObservableMap<String, Person> = people.associateBy(Person::name)
|
||||
*/
|
||||
fun <K, A> ObservableList<out A>.associateBy(toKey: (A) -> K): ObservableMap<K, A> {
|
||||
return associateBy(toKey) { key, value -> value }
|
||||
return associateBy(toKey) { _, value -> value }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -176,7 +182,7 @@ fun <K : Any, A : Any, B> ObservableList<out A>.associateByAggregation(toKey: (A
|
||||
* val heightToPeople: ObservableMap<Long, ObservableList<Person>> = people.associateByAggregation(Person::height)
|
||||
*/
|
||||
fun <K : Any, A : Any> ObservableList<out A>.associateByAggregation(toKey: (A) -> K): ObservableMap<K, ObservableList<A>> {
|
||||
return associateByAggregation(toKey) { key, value -> value }
|
||||
return associateByAggregation(toKey) { _, value -> value }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -260,7 +266,7 @@ fun <A : Any, B : Any, K : Any> ObservableList<A>.leftOuterJoin(
|
||||
val leftTableMap = associateByAggregation(leftToJoinKey)
|
||||
val rightTableMap = rightTable.associateByAggregation(rightToJoinKey)
|
||||
val joinedMap: ObservableMap<K, Pair<ObservableList<A>, ObservableList<B>>> =
|
||||
LeftOuterJoinedMap(leftTableMap, rightTableMap) { _key, left, rightValue ->
|
||||
LeftOuterJoinedMap(leftTableMap, rightTableMap) { _, left, rightValue ->
|
||||
Pair(left, ChosenList(rightValue.map { it ?: FXCollections.emptyObservableList() }))
|
||||
}
|
||||
return joinedMap
|
||||
@ -285,7 +291,7 @@ fun <A> ObservableList<A>.last(): ObservableValue<A?> {
|
||||
}
|
||||
|
||||
fun <T : Any> ObservableList<T>.unique(): ObservableList<T> {
|
||||
return AggregatedList(this, { it }, { key, _list -> key })
|
||||
return AggregatedList(this, { it }, { key, _ -> key })
|
||||
}
|
||||
|
||||
fun ObservableValue<*>.isNotNull(): BooleanBinding {
|
||||
|
@ -16,7 +16,7 @@ class AssociatedListTest {
|
||||
@Before
|
||||
fun setup() {
|
||||
sourceList = FXCollections.observableArrayList(0)
|
||||
associatedList = AssociatedList(sourceList, { it % 3 }) { mod3, number -> number }
|
||||
associatedList = AssociatedList(sourceList, { it % 3 }) { _, number -> number }
|
||||
replayedMap = ReplayedMap(associatedList)
|
||||
}
|
||||
|
||||
|
@ -4,32 +4,12 @@ apply plugin: 'net.corda.plugins.publish-utils'
|
||||
|
||||
description 'Corda client mock modules'
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
maven {
|
||||
url 'http://oss.sonatype.org/content/repositories/snapshots'
|
||||
}
|
||||
jcenter()
|
||||
maven {
|
||||
url 'https://dl.bintray.com/kotlin/exposed'
|
||||
}
|
||||
}
|
||||
|
||||
//noinspection GroovyAssignabilityCheck
|
||||
configurations {
|
||||
// we don't want isolated.jar in classPath, since we want to test jar being dynamically loaded as an attachment
|
||||
runtime.exclude module: 'isolated'
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
test {
|
||||
resources {
|
||||
srcDir "../../config/test"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// To find potential version conflicts, run "gradle htmlDependencyReport" and then look in
|
||||
// build/reports/project/dependencies/index.html for green highlighted parts of the tree.
|
||||
|
||||
|
@ -1,10 +1,8 @@
|
||||
package net.corda.client.mock
|
||||
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.flows.CashFlowCommand
|
||||
import java.util.*
|
||||
|
||||
@ -12,91 +10,27 @@ import java.util.*
|
||||
* [Generator]s for incoming/outgoing events to/from the [WalletMonitorService]. Internally it keeps track of owned
|
||||
* state/ref pairs, but it doesn't necessarily generate "correct" events!
|
||||
*/
|
||||
class EventGenerator(
|
||||
val parties: List<Party>,
|
||||
val notary: Party,
|
||||
val currencies: List<Currency> = listOf(USD, GBP, CHF),
|
||||
val issuers: List<Party> = parties
|
||||
) {
|
||||
private var vault = listOf<StateAndRef<Cash.State>>()
|
||||
|
||||
val issuerGenerator =
|
||||
Generator.pickOne(issuers).combine(Generator.intRange(0, 1)) { party, ref -> party.ref(ref.toByte()) }
|
||||
class EventGenerator(val parties: List<Party>, val currencies: List<Currency>, val notary: Party) {
|
||||
private val partyGenerator = Generator.pickOne(parties)
|
||||
private val issueRefGenerator = Generator.intRange(0, 1).map { number -> OpaqueBytes(ByteArray(1, { number.toByte() })) }
|
||||
private val amountGenerator = Generator.longRange(10000, 1000000)
|
||||
private val currencyGenerator = Generator.pickOne(currencies)
|
||||
|
||||
val currencyGenerator = Generator.pickOne(currencies)
|
||||
|
||||
val issuedGenerator = issuerGenerator.combine(currencyGenerator) { issuer, currency -> Issued(issuer, currency) }
|
||||
val amountIssuedGenerator = generateAmount(1, 10000, issuedGenerator)
|
||||
|
||||
val publicKeyGenerator = Generator.pickOne(parties.map { it.owningKey })
|
||||
val partyGenerator = Generator.pickOne(parties)
|
||||
|
||||
val cashStateGenerator = amountIssuedGenerator.combine(publicKeyGenerator) { amount, from ->
|
||||
val builder = TransactionBuilder(notary = notary)
|
||||
builder.addOutputState(Cash.State(amount, from))
|
||||
builder.addCommand(Command(Cash.Commands.Issue(), amount.token.issuer.party.owningKey))
|
||||
builder.toWireTransaction().outRef<Cash.State>(0)
|
||||
private val issueCashGenerator = amountGenerator.combine(partyGenerator, issueRefGenerator, currencyGenerator) { amount, to, issueRef, ccy ->
|
||||
CashFlowCommand.IssueCash(Amount(amount, ccy), issueRef, to, notary)
|
||||
}
|
||||
|
||||
val consumedGenerator: Generator<Set<StateRef>> = Generator.frequency(
|
||||
0.7 to Generator.pure(setOf()),
|
||||
0.3 to Generator.impure { vault }.bind { states ->
|
||||
Generator.sampleBernoulli(states, 0.2).map { someStates ->
|
||||
val consumedSet = someStates.map { it.ref }.toSet()
|
||||
vault = vault.filter { it.ref !in consumedSet }
|
||||
consumedSet
|
||||
}
|
||||
}
|
||||
)
|
||||
val producedGenerator: Generator<Set<StateAndRef<ContractState>>> = Generator.frequency(
|
||||
// 0.1 to Generator.pure(setOf())
|
||||
0.9 to Generator.impure { vault }.bind { states ->
|
||||
Generator.replicate(2, cashStateGenerator).map {
|
||||
vault = states + it
|
||||
it.toSet()
|
||||
}
|
||||
}
|
||||
)
|
||||
private val exitCashGenerator = amountGenerator.combine(issueRefGenerator, currencyGenerator) { amount, issueRef, ccy ->
|
||||
CashFlowCommand.ExitCash(Amount(amount, ccy), issueRef)
|
||||
}
|
||||
|
||||
val issueRefGenerator = Generator.intRange(0, 1).map { number -> OpaqueBytes(ByteArray(1, { number.toByte() })) }
|
||||
val moveCashGenerator = amountGenerator.combine(partyGenerator, currencyGenerator) { amountIssued, recipient, currency ->
|
||||
CashFlowCommand.PayCash(Amount(amountIssued, currency), recipient)
|
||||
}
|
||||
|
||||
val amountGenerator = Generator.intRange(0, 10000).combine(currencyGenerator) { quantity, currency -> Amount(quantity.toLong(), currency) }
|
||||
|
||||
val issueCashGenerator =
|
||||
amountGenerator.combine(partyGenerator, issueRefGenerator) { amount, to, issueRef ->
|
||||
CashFlowCommand.IssueCash(
|
||||
amount,
|
||||
issueRef,
|
||||
to,
|
||||
notary
|
||||
)
|
||||
}
|
||||
|
||||
val moveCashGenerator =
|
||||
amountIssuedGenerator.combine(partyGenerator) { amountIssued, recipient ->
|
||||
CashFlowCommand.PayCash(
|
||||
amount = amountIssued.withoutIssuer(),
|
||||
recipient = recipient
|
||||
)
|
||||
}
|
||||
|
||||
val exitCashGenerator =
|
||||
amountIssuedGenerator.map {
|
||||
CashFlowCommand.ExitCash(
|
||||
it.withoutIssuer(),
|
||||
it.token.issuer.reference
|
||||
)
|
||||
}
|
||||
|
||||
val clientCommandGenerator = Generator.frequency(
|
||||
1.0 to moveCashGenerator
|
||||
)
|
||||
|
||||
val bankOfCordaExitGenerator = Generator.frequency(
|
||||
0.4 to exitCashGenerator
|
||||
)
|
||||
|
||||
val bankOfCordaIssueGenerator = Generator.frequency(
|
||||
0.6 to issueCashGenerator
|
||||
)
|
||||
val issuerGenerator = Generator.frequency(listOf(
|
||||
0.1 to exitCashGenerator,
|
||||
0.9 to issueCashGenerator
|
||||
))
|
||||
}
|
||||
|
@ -144,6 +144,23 @@ fun Generator.Companion.doubleRange(from: Double, to: Double): Generator<Double>
|
||||
from + it.nextDouble() * (to - from)
|
||||
}
|
||||
|
||||
fun Generator.Companion.char() = Generator {
|
||||
val codePoint = Math.abs(it.nextInt()) % (17 * (1 shl 16))
|
||||
if (Character.isValidCodePoint(codePoint)) {
|
||||
return@Generator ErrorOr(codePoint.toChar())
|
||||
} else {
|
||||
ErrorOr.of(IllegalStateException("Could not generate valid codepoint"))
|
||||
}
|
||||
}
|
||||
|
||||
fun Generator.Companion.string(meanSize: Double = 16.0) = replicatePoisson(meanSize, char()).map {
|
||||
val builder = StringBuilder()
|
||||
it.forEach {
|
||||
builder.append(it)
|
||||
}
|
||||
builder.toString()
|
||||
}
|
||||
|
||||
fun <A> Generator.Companion.replicate(number: Int, generator: Generator<A>): Generator<List<A>> {
|
||||
val generators = mutableListOf<Generator<A>>()
|
||||
for (i in 1..number) {
|
||||
|
@ -4,18 +4,6 @@ apply plugin: 'net.corda.plugins.publish-utils'
|
||||
|
||||
description 'Corda client RPC modules'
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
maven {
|
||||
url 'http://oss.sonatype.org/content/repositories/snapshots'
|
||||
}
|
||||
jcenter()
|
||||
maven {
|
||||
url 'https://dl.bintray.com/kotlin/exposed'
|
||||
}
|
||||
}
|
||||
|
||||
//noinspection GroovyAssignabilityCheck
|
||||
configurations {
|
||||
// we don't want isolated.jar in classPath, since we want to test jar being dynamically loaded as an attachment
|
||||
@ -33,11 +21,6 @@ sourceSets {
|
||||
srcDir file('src/integration-test/kotlin')
|
||||
}
|
||||
}
|
||||
test {
|
||||
resources {
|
||||
srcDir "../../config/test"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// To find potential version conflicts, run "gradle htmlDependencyReport" and then look in
|
||||
@ -53,6 +36,7 @@ dependencies {
|
||||
testCompile "org.assertj:assertj-core:${assertj_version}"
|
||||
|
||||
testCompile project(':test-utils')
|
||||
testCompile project(':client:mock')
|
||||
|
||||
// Integration test helpers
|
||||
integrationTestCompile "junit:junit:$junit_version"
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
@ -1,164 +1,49 @@
|
||||
package net.corda.client.rpc
|
||||
|
||||
import com.google.common.net.HostAndPort
|
||||
import net.corda.nodeapi.config.SSLConfiguration
|
||||
import net.corda.core.ThreadBox
|
||||
import net.corda.core.logElapsedTime
|
||||
import net.corda.client.rpc.internal.RPCClient
|
||||
import net.corda.client.rpc.internal.RPCClientConfiguration
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.minutes
|
||||
import net.corda.core.seconds
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.nodeapi.ArtemisMessagingComponent
|
||||
import net.corda.nodeapi.ArtemisTcpTransport.Companion.tcpTransport
|
||||
import net.corda.nodeapi.ConnectionDirection
|
||||
import net.corda.nodeapi.RPCException
|
||||
import net.corda.nodeapi.rpcLog
|
||||
import org.apache.activemq.artemis.api.core.ActiveMQException
|
||||
import org.apache.activemq.artemis.api.core.client.ActiveMQClient
|
||||
import org.apache.activemq.artemis.api.core.client.ClientSession
|
||||
import org.apache.activemq.artemis.api.core.client.ClientSessionFactory
|
||||
import org.apache.activemq.artemis.api.core.client.ServerLocator
|
||||
import rx.Observable
|
||||
import java.io.Closeable
|
||||
import net.corda.nodeapi.config.SSLConfiguration
|
||||
import java.time.Duration
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
|
||||
/**
|
||||
* An RPC client connects to the specified server and allows you to make calls to the server that perform various
|
||||
* useful tasks. See the documentation for [proxy] or review the docsite to learn more about how this API works.
|
||||
*
|
||||
* @param host The hostname and messaging port of the node.
|
||||
* @param config If specified, the SSL configuration to use. If not specified, SSL will be disabled and the node will only be authenticated on non-SSL RPC port, the RPC traffic with not be encrypted when SSL is disabled.
|
||||
*/
|
||||
@ThreadSafe
|
||||
class CordaRPCClient(val host: HostAndPort, override val config: SSLConfiguration? = null, val serviceConfigurationOverride: (ServerLocator.() -> Unit)? = null) : Closeable, ArtemisMessagingComponent() {
|
||||
private companion object {
|
||||
val log = loggerFor<CordaRPCClient>()
|
||||
class CordaRPCConnection internal constructor(
|
||||
connection: RPCClient.RPCConnection<CordaRPCOps>
|
||||
) : RPCClient.RPCConnection<CordaRPCOps> by connection
|
||||
|
||||
data class CordaRPCClientConfiguration(
|
||||
val connectionMaxRetryInterval: Duration
|
||||
) {
|
||||
internal fun toRpcClientConfiguration(): RPCClientConfiguration {
|
||||
return RPCClientConfiguration.default.copy(
|
||||
connectionMaxRetryInterval = connectionMaxRetryInterval
|
||||
)
|
||||
}
|
||||
companion object {
|
||||
@JvmStatic
|
||||
val default = CordaRPCClientConfiguration(
|
||||
connectionMaxRetryInterval = RPCClientConfiguration.default.connectionMaxRetryInterval
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class CordaRPCClient(
|
||||
hostAndPort: HostAndPort,
|
||||
sslConfiguration: SSLConfiguration? = null,
|
||||
configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.default
|
||||
) {
|
||||
private val rpcClient = RPCClient<CordaRPCOps>(
|
||||
tcpTransport(ConnectionDirection.Outbound(), hostAndPort, sslConfiguration),
|
||||
configuration.toRpcClientConfiguration()
|
||||
)
|
||||
|
||||
fun start(username: String, password: String): CordaRPCConnection {
|
||||
return CordaRPCConnection(rpcClient.start(CordaRPCOps::class.java, username, password))
|
||||
}
|
||||
|
||||
// TODO: Certificate handling for clients needs more work.
|
||||
private inner class State {
|
||||
var running = false
|
||||
lateinit var sessionFactory: ClientSessionFactory
|
||||
lateinit var session: ClientSession
|
||||
lateinit var clientImpl: CordaRPCClientImpl
|
||||
}
|
||||
|
||||
private val state = ThreadBox(State())
|
||||
|
||||
/**
|
||||
* Opens the connection to the server with the given username and password, then returns itself.
|
||||
* Registers a JVM shutdown hook to cleanly disconnect.
|
||||
*/
|
||||
@Throws(ActiveMQException::class)
|
||||
fun start(username: String, password: String): CordaRPCClient {
|
||||
state.locked {
|
||||
check(!running)
|
||||
log.logElapsedTime("Startup") {
|
||||
checkStorePasswords()
|
||||
val serverLocator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport(ConnectionDirection.Outbound(), host, config, enableSSL = config != null)).apply {
|
||||
// TODO: Put these in config file or make it user configurable?
|
||||
threadPoolMaxSize = 1
|
||||
confirmationWindowSize = 100000 // a guess
|
||||
retryInterval = 5.seconds.toMillis()
|
||||
retryIntervalMultiplier = 1.5 // Exponential backoff
|
||||
maxRetryInterval = 3.minutes.toMillis()
|
||||
serviceConfigurationOverride?.invoke(this)
|
||||
}
|
||||
sessionFactory = serverLocator.createSessionFactory()
|
||||
session = sessionFactory.createSession(username, password, false, true, true, serverLocator.isPreAcknowledge, serverLocator.ackBatchSize)
|
||||
session.start()
|
||||
clientImpl = CordaRPCClientImpl(session, state.lock, username)
|
||||
running = true
|
||||
}
|
||||
}
|
||||
|
||||
Runtime.getRuntime().addShutdownHook(Thread {
|
||||
close()
|
||||
})
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* A convenience function that opens a connection with the given credentials, executes the given code block with all
|
||||
* available RPCs in scope and shuts down the RPC connection again. It's meant for quick prototyping and demos. For
|
||||
* more control you probably want to control the lifecycle of the client and proxies independently, as well as
|
||||
* configuring a timeout and other such features via the [proxy] method.
|
||||
*
|
||||
* After this method returns the client is closed and can't be restarted.
|
||||
*/
|
||||
@Throws(ActiveMQException::class)
|
||||
fun <T> use(username: String, password: String, block: CordaRPCOps.() -> T): T {
|
||||
require(!state.locked { running })
|
||||
start(username, password)
|
||||
(this as Closeable).use {
|
||||
return proxy().block()
|
||||
}
|
||||
}
|
||||
|
||||
/** Shuts down the client and lets the server know it can free the used resources (in a nice way). */
|
||||
override fun close() {
|
||||
state.locked {
|
||||
if (!running) return
|
||||
session.close()
|
||||
sessionFactory.close()
|
||||
running = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a fresh proxy that lets you invoke RPCs on the server. Calls on it block, and if the server throws an
|
||||
* exception then it will be rethrown on the client. Proxies are thread safe but only one RPC can be in flight at
|
||||
* once. If you'd like to perform multiple RPCs in parallel, use this function multiple times to get multiple
|
||||
* proxies.
|
||||
*
|
||||
* Creation of a proxy is a somewhat expensive operation that involves calls to the server, so if you want to do
|
||||
* calls from many threads at once you should cache one proxy per thread and reuse them. This function itself is
|
||||
* thread safe though so requires no extra synchronisation.
|
||||
*
|
||||
* RPC sends and receives are logged on the net.corda.rpc logger.
|
||||
*
|
||||
* By default there are no timeouts on calls. This is deliberate, RPCs without timeouts can survive restarts,
|
||||
* maintenance downtime and moves of the server. RPCs can survive temporary losses or changes in client connectivity,
|
||||
* like switching between wifi networks. You can specify a timeout on the level of a proxy. If a call times
|
||||
* out it will throw [RPCException.Deadline].
|
||||
*
|
||||
* The [CordaRPCOps] defines what client RPCs are available. If an RPC returns an [Observable] anywhere in the
|
||||
* object graph returned then the server-side observable is transparently linked to a messaging queue, and that
|
||||
* queue linked to another observable on the client side here. *You are expected to use it*. The server will begin
|
||||
* buffering messages immediately that it will expect you to drain by subscribing to the returned observer. You can
|
||||
* opt-out of this by simply casting the [Observable] to [Closeable] or [AutoCloseable] and then calling the close
|
||||
* method on it. You don't have to explicitly close the observable if you actually subscribe to it: it will close
|
||||
* itself and free up the server-side resources either when the client or JVM itself is shutdown, or when there are
|
||||
* no more subscribers to it. Once all the subscribers to a returned observable are unsubscribed, the observable is
|
||||
* closed and you can't then re-subscribe again: you'll have to re-request a fresh observable with another RPC.
|
||||
*
|
||||
* The proxy and linked observables consume some small amount of resources on the server. It's OK to just exit your
|
||||
* process and let the server clean up, but in a long running process where you only need something for a short
|
||||
* amount of time it is polite to cast the objects to [Closeable] or [AutoCloseable] and close it when you are done.
|
||||
* Finalizers are in place to warn you if you lose a reference to an unclosed proxy or observable.
|
||||
*
|
||||
* @throws RPCException if the server version is too low or if the server isn't reachable within the given time.
|
||||
*/
|
||||
@JvmOverloads
|
||||
@Throws(RPCException::class)
|
||||
fun proxy(timeout: Duration? = null, minVersion: Int = 0): CordaRPCOps {
|
||||
return state.locked {
|
||||
check(running) { "Client must have been started first" }
|
||||
log.logElapsedTime("Proxy build") {
|
||||
clientImpl.proxyFor(CordaRPCOps::class.java, timeout, minVersion)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNUSED")
|
||||
private fun finalize() {
|
||||
state.locked {
|
||||
if (running) {
|
||||
rpcLog.warn("A CordaMQClient is being finalised whilst still running, did you forget to call close?")
|
||||
close()
|
||||
}
|
||||
}
|
||||
inline fun <A> use(username: String, password: String, block: (CordaRPCConnection) -> A): A {
|
||||
return start(username, password).use(block)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
19
client/rpc/src/main/kotlin/net/corda/client/rpc/Utils.kt
Normal file
19
client/rpc/src/main/kotlin/net/corda/client/rpc/Utils.kt
Normal 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.
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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" }
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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) })
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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) {}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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) })
|
||||
}
|
||||
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
myLegalName : "Bank A"
|
||||
myLegalName : "CN=Bank A,O=Bank A,L=London,C=UK"
|
||||
nearestCity : "London"
|
||||
keyStorePassword : "cordacadevpass"
|
||||
trustStorePassword : "trustpass"
|
||||
@ -8,6 +8,6 @@ webAddress : "localhost:10004"
|
||||
extraAdvertisedServiceIds : [ "corda.interest_rates" ]
|
||||
networkMapService : {
|
||||
address : "localhost:10000"
|
||||
legalName : "Network Map Service"
|
||||
legalName : "CN=Network Map Service,O=R3,OU=corda,L=London,C=UK"
|
||||
}
|
||||
useHTTPS : false
|
||||
|
@ -1,4 +1,4 @@
|
||||
myLegalName : "Bank B"
|
||||
myLegalName : "CN=Bank B,O=Bank A,L=London,C=UK"
|
||||
nearestCity : "London"
|
||||
keyStorePassword : "cordacadevpass"
|
||||
trustStorePassword : "trustpass"
|
||||
@ -8,6 +8,6 @@ webAddress : "localhost:10007"
|
||||
extraAdvertisedServiceIds : [ "corda.interest_rates" ]
|
||||
networkMapService : {
|
||||
address : "localhost:10000"
|
||||
legalName : "Network Map Service"
|
||||
legalName : "CN=Network Map Service,O=R3,OU=corda,L=London,C=UK"
|
||||
}
|
||||
useHTTPS : false
|
||||
|
@ -57,5 +57,8 @@
|
||||
<AppenderRef ref="Console-Appender-Println"/>
|
||||
<AppenderRef ref="RollingFile-Appender" />
|
||||
</Logger>
|
||||
<Logger name="org.apache.activemq.artemis.core.server" level="error" additivity="false">
|
||||
<AppenderRef ref="RollingFile-Appender"/>
|
||||
</Logger>
|
||||
</Loggers>
|
||||
</Configuration>
|
||||
</Configuration>
|
||||
|
@ -1,4 +1,4 @@
|
||||
myLegalName : "Notary Service"
|
||||
myLegalName : "CN=Notary Service,O=R3,OU=corda,L=London,C=UK"
|
||||
nearestCity : "London"
|
||||
keyStorePassword : "cordacadevpass"
|
||||
trustStorePassword : "trustpass"
|
||||
|
5
constants.properties
Normal file
5
constants.properties
Normal file
@ -0,0 +1,5 @@
|
||||
gradlePluginsVersion=0.12.0
|
||||
kotlinVersion=1.1.2
|
||||
guavaVersion=21.0
|
||||
bouncycastleVersion=1.56
|
||||
typesafeConfigVersion=1.3.1
|
@ -10,26 +10,6 @@ buildscript {
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
maven {
|
||||
url 'http://oss.sonatype.org/content/repositories/snapshots'
|
||||
}
|
||||
maven {
|
||||
url 'https://dl.bintray.com/kotlin/exposed'
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
test {
|
||||
resources {
|
||||
srcDir "../config/test"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
testCompile "junit:junit:$junit_version"
|
||||
@ -42,9 +22,8 @@ dependencies {
|
||||
testCompile project(":node")
|
||||
testCompile project(":test-utils")
|
||||
|
||||
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
|
||||
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
compile "org.jetbrains.kotlinx:kotlinx-support-jdk8:0.3"
|
||||
testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
||||
|
||||
// Thread safety annotations
|
||||
@ -84,7 +63,7 @@ dependencies {
|
||||
compile "com.fasterxml.jackson.core:jackson-databind:${jackson_version}"
|
||||
|
||||
// Java ed25519 implementation. See https://github.com/str4d/ed25519-java/
|
||||
compile 'net.i2p.crypto:eddsa:0.1.0'
|
||||
compile 'net.i2p.crypto:eddsa:0.2.0'
|
||||
|
||||
// Bouncy castle support needed for X509 certificate manipulation
|
||||
compile "org.bouncycastle:bcprov-jdk15on:${bouncycastle_version}"
|
||||
@ -93,20 +72,14 @@ dependencies {
|
||||
// JPA 2.1 annotations.
|
||||
compile "org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final"
|
||||
|
||||
// RS API: Response type and codes for ApiUtils.
|
||||
compile "javax.ws.rs:javax.ws.rs-api:2.0.1"
|
||||
|
||||
// Requery: SQL based query & persistence for Kotlin
|
||||
compile "io.requery:requery-kotlin:$requery_version"
|
||||
|
||||
// For AMQP serialisation.
|
||||
compile "org.apache.qpid:proton-j:0.18.0"
|
||||
}
|
||||
|
||||
configurations {
|
||||
compile {
|
||||
// We want to use SLF4J's version of these binding: jcl-over-slf4j
|
||||
// Remove any transitive dependency on Apache's version.
|
||||
exclude group: 'commons-logging', module: 'commons-logging'
|
||||
}
|
||||
|
||||
testArtifacts.extendsFrom testRuntime
|
||||
}
|
||||
|
||||
|
@ -3,21 +3,19 @@
|
||||
|
||||
package net.corda.core
|
||||
|
||||
import com.google.common.base.Function
|
||||
import com.google.common.base.Throwables
|
||||
import com.google.common.io.ByteStreams
|
||||
import com.google.common.util.concurrent.*
|
||||
import kotlinx.support.jdk7.use
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.newSecureRandom
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import org.slf4j.Logger
|
||||
import rx.Observable
|
||||
import rx.Observer
|
||||
import rx.subjects.PublishSubject
|
||||
import rx.subjects.UnicastSubject
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.io.*
|
||||
import java.math.BigDecimal
|
||||
import java.nio.charset.Charset
|
||||
import java.nio.charset.StandardCharsets.UTF_8
|
||||
@ -25,11 +23,22 @@ import java.nio.file.*
|
||||
import java.nio.file.attribute.FileAttribute
|
||||
import java.time.Duration
|
||||
import java.time.temporal.Temporal
|
||||
import java.util.HashMap
|
||||
import java.util.concurrent.*
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import java.util.function.BiConsumer
|
||||
import java.util.stream.Stream
|
||||
import java.util.zip.Deflater
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
import java.util.zip.ZipOutputStream
|
||||
import kotlin.collections.Iterable
|
||||
import kotlin.collections.LinkedHashMap
|
||||
import kotlin.collections.List
|
||||
import kotlin.collections.filter
|
||||
import kotlin.collections.firstOrNull
|
||||
import kotlin.collections.fold
|
||||
import kotlin.collections.forEach
|
||||
import kotlin.concurrent.withLock
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
@ -75,7 +84,7 @@ fun <T> future(block: () -> T): ListenableFuture<T> = CompletableToListenable(Co
|
||||
|
||||
private class CompletableToListenable<T>(private val base: CompletableFuture<T>) : Future<T> by base, ListenableFuture<T> {
|
||||
override fun addListener(listener: Runnable, executor: Executor) {
|
||||
base.whenCompleteAsync(BiConsumer { result, exception -> listener.run() }, executor)
|
||||
base.whenCompleteAsync(BiConsumer { _, _ -> listener.run() }, executor)
|
||||
}
|
||||
}
|
||||
|
||||
@ -102,7 +111,9 @@ fun <T> ListenableFuture<T>.failure(executor: Executor, body: (Throwable) -> Uni
|
||||
infix fun <T> ListenableFuture<T>.then(body: () -> Unit): ListenableFuture<T> = apply { then(RunOnCallerThread, body) }
|
||||
infix fun <T> ListenableFuture<T>.success(body: (T) -> Unit): ListenableFuture<T> = apply { success(RunOnCallerThread, body) }
|
||||
infix fun <T> ListenableFuture<T>.failure(body: (Throwable) -> Unit): ListenableFuture<T> = apply { failure(RunOnCallerThread, body) }
|
||||
infix fun <F, T> ListenableFuture<F>.map(mapper: (F) -> T): ListenableFuture<T> = Futures.transform(this, Function { mapper(it!!) })
|
||||
@Suppress("UNCHECKED_CAST") // We need the awkward cast because otherwise F cannot be nullable, even though it's safe.
|
||||
infix fun <F, T> ListenableFuture<F>.map(mapper: (F) -> T): ListenableFuture<T> = Futures.transform(this, { (mapper as (F?) -> T)(it) })
|
||||
|
||||
infix fun <F, T> ListenableFuture<F>.flatMap(mapper: (F) -> ListenableFuture<T>): ListenableFuture<T> = Futures.transformAsync(this) { mapper(it!!) }
|
||||
/** Executes the given block and sets the future to either the result, or any exception that was thrown. */
|
||||
inline fun <T> SettableFuture<T>.catch(block: () -> T) {
|
||||
@ -152,7 +163,7 @@ fun Path.writeLines(lines: Iterable<CharSequence>, charset: Charset = UTF_8, var
|
||||
fun InputStream.copyTo(target: Path, vararg options: CopyOption): Long = Files.copy(this, target, *options)
|
||||
|
||||
// Simple infix function to add back null safety that the JDK lacks: timeA until timeB
|
||||
infix fun Temporal.until(endExclusive: Temporal) = Duration.between(this, endExclusive)
|
||||
infix fun Temporal.until(endExclusive: Temporal): Duration = Duration.between(this, endExclusive)
|
||||
|
||||
/** Returns the index of the given item or throws [IllegalArgumentException] if not found. */
|
||||
fun <T> List<T>.indexOfOrThrow(item: T): Int {
|
||||
@ -271,11 +282,7 @@ class TransientProperty<out T>(private val initializer: () -> T) {
|
||||
@Transient private var v: T? = null
|
||||
|
||||
@Synchronized
|
||||
operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
|
||||
if (v == null)
|
||||
v = initializer()
|
||||
return v!!
|
||||
}
|
||||
operator fun getValue(thisRef: Any?, property: KProperty<*>) = v ?: initializer().also { v = it }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -308,11 +315,44 @@ fun extractZipFile(inputStream: InputStream, toDirectory: Path) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a valid InputStream from an in-memory zip as required for tests.
|
||||
* Note that a slightly bigger than numOfExpectedBytes size is expected.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class)
|
||||
fun sizedInputStreamAndHash(numOfExpectedBytes: Int): InputStreamAndHash {
|
||||
if (numOfExpectedBytes <= 0) throw IllegalArgumentException("A positive number of numOfExpectedBytes is required.")
|
||||
val baos = ByteArrayOutputStream()
|
||||
ZipOutputStream(baos).use({ zos ->
|
||||
val arraySize = 1024
|
||||
val bytes = ByteArray(arraySize)
|
||||
val n = (numOfExpectedBytes - 1) / arraySize + 1 // same as Math.ceil(numOfExpectedBytes/arraySize).
|
||||
zos.setLevel(Deflater.NO_COMPRESSION)
|
||||
zos.putNextEntry(ZipEntry("z"))
|
||||
for (i in 0 until n) {
|
||||
zos.write(bytes, 0, arraySize)
|
||||
}
|
||||
zos.closeEntry()
|
||||
})
|
||||
return getInputStreamAndHashFromOutputStream(baos)
|
||||
}
|
||||
|
||||
/** Convert a [ByteArrayOutputStream] to [InputStreamAndHash]. */
|
||||
fun getInputStreamAndHashFromOutputStream(baos: ByteArrayOutputStream): InputStreamAndHash {
|
||||
// TODO: Consider converting OutputStream to InputStream without creating a ByteArray, probably using piped streams.
|
||||
val bytes = baos.toByteArray()
|
||||
// TODO: Consider calculating sha256 on the fly using a DigestInputStream.
|
||||
return InputStreamAndHash(ByteArrayInputStream(bytes), bytes.sha256())
|
||||
}
|
||||
|
||||
data class InputStreamAndHash(val inputStream: InputStream, val sha256: SecureHash.SHA256)
|
||||
|
||||
// TODO: Generic csv printing utility for clases.
|
||||
|
||||
val Throwable.rootCause: Throwable get() = Throwables.getRootCause(this)
|
||||
|
||||
/** Representation of an operation that may have thrown an error. */
|
||||
@Suppress("DataClassPrivateConstructor")
|
||||
@CordaSerializable
|
||||
data class ErrorOr<out A> private constructor(val value: A?, val error: Throwable?) {
|
||||
// The ErrorOr holds a value iff error == null
|
||||
@ -364,6 +404,8 @@ data class ErrorOr<out A> private constructor(val value: A?, val error: Throwabl
|
||||
ErrorOr.of(error)
|
||||
}
|
||||
}
|
||||
|
||||
fun mapError(function: (Throwable) -> Throwable) = ErrorOr(value, error?.let(function))
|
||||
}
|
||||
|
||||
/**
|
||||
@ -419,3 +461,9 @@ fun codePointsString(vararg codePoints: Int): String {
|
||||
codePoints.forEach { builder.append(Character.toChars(it)) }
|
||||
return builder.toString()
|
||||
}
|
||||
|
||||
fun <T> Class<T>.checkNotUnorderedHashMap() {
|
||||
if (HashMap::class.java.isAssignableFrom(this) && !LinkedHashMap::class.java.isAssignableFrom(this)) {
|
||||
throw NotSerializableException("Map type $this is unstable under iteration. Suggested fix: use LinkedHashMap instead.")
|
||||
}
|
||||
}
|
@ -2,8 +2,9 @@
|
||||
|
||||
package net.corda.core.contracts
|
||||
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.identity.Party
|
||||
import java.security.PublicKey
|
||||
import java.math.BigDecimal
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
@ -31,11 +32,13 @@ fun commodity(code: String) = Commodity.getInstance(code)!!
|
||||
@JvmField val RUB = currency("RUB")
|
||||
@JvmField val FCOJ = commodity("FCOJ") // Frozen concentrated orange juice, yum!
|
||||
|
||||
fun DOLLARS(amount: Int): Amount<Currency> = Amount(amount.toLong() * 100, USD)
|
||||
fun DOLLARS(amount: Double): Amount<Currency> = Amount((amount * 100).toLong(), USD)
|
||||
fun POUNDS(amount: Int): Amount<Currency> = Amount(amount.toLong() * 100, GBP)
|
||||
fun SWISS_FRANCS(amount: Int): Amount<Currency> = Amount(amount.toLong() * 100, CHF)
|
||||
fun FCOJ(amount: Int): Amount<Commodity> = Amount(amount.toLong() * 100, FCOJ)
|
||||
fun <T : Any> AMOUNT(amount: Int, token: T): Amount<T> = Amount.fromDecimal(BigDecimal.valueOf(amount.toLong()), token)
|
||||
fun <T : Any> AMOUNT(amount: Double, token: T): Amount<T> = Amount.fromDecimal(BigDecimal.valueOf(amount), token)
|
||||
fun DOLLARS(amount: Int): Amount<Currency> = AMOUNT(amount, USD)
|
||||
fun DOLLARS(amount: Double): Amount<Currency> = AMOUNT(amount, USD)
|
||||
fun POUNDS(amount: Int): Amount<Currency> = AMOUNT(amount, GBP)
|
||||
fun SWISS_FRANCS(amount: Int): Amount<Currency> = AMOUNT(amount, CHF)
|
||||
fun FCOJ(amount: Int): Amount<Commodity> = AMOUNT(amount, FCOJ)
|
||||
|
||||
val Int.DOLLARS: Amount<Currency> get() = DOLLARS(this)
|
||||
val Double.DOLLARS: Amount<Currency> get() = DOLLARS(this)
|
||||
@ -48,15 +51,22 @@ infix fun Commodity.`issued by`(deposit: PartyAndReference) = issuedBy(deposit)
|
||||
infix fun Amount<Currency>.`issued by`(deposit: PartyAndReference) = issuedBy(deposit)
|
||||
infix fun Currency.issuedBy(deposit: PartyAndReference) = Issued(deposit, this)
|
||||
infix fun Commodity.issuedBy(deposit: PartyAndReference) = Issued(deposit, this)
|
||||
infix fun Amount<Currency>.issuedBy(deposit: PartyAndReference) = Amount(quantity, token.issuedBy(deposit))
|
||||
infix fun Amount<Currency>.issuedBy(deposit: PartyAndReference) = Amount(quantity, displayTokenSize, token.issuedBy(deposit))
|
||||
|
||||
//// Requirements /////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
object Requirements {
|
||||
@Suppress("NOTHING_TO_INLINE") // Inlining this takes it out of our committed ABI.
|
||||
infix inline fun String.by(expr: Boolean) {
|
||||
infix inline fun String.using(expr: Boolean) {
|
||||
if (!expr) throw IllegalArgumentException("Failed requirement: $this")
|
||||
}
|
||||
// Avoid overloading Kotlin keywords
|
||||
@Deprecated("This function is deprecated, use 'using' instead",
|
||||
ReplaceWith("using (expr)", "net.corda.core.contracts.Requirements.using"))
|
||||
@Suppress("NOTHING_TO_INLINE") // Inlining this takes it out of our committed ABI.
|
||||
infix inline fun String.by(expr: Boolean) {
|
||||
using(expr)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <R> requireThat(body: Requirements.() -> R) = Requirements.body()
|
||||
@ -66,7 +76,7 @@ inline fun <R> requireThat(body: Requirements.() -> R) = Requirements.body()
|
||||
// TODO: Provide a version of select that interops with Java
|
||||
|
||||
/** Filters the command list by type, party and public key all at once. */
|
||||
inline fun <reified T : CommandData> Collection<AuthenticatedObject<CommandData>>.select(signer: CompositeKey? = null,
|
||||
inline fun <reified T : CommandData> Collection<AuthenticatedObject<CommandData>>.select(signer: PublicKey? = null,
|
||||
party: Party? = null) =
|
||||
filter { it.value is T }.
|
||||
filter { if (signer == null) true else signer in it.signers }.
|
||||
@ -76,7 +86,7 @@ inline fun <reified T : CommandData> Collection<AuthenticatedObject<CommandData>
|
||||
// TODO: Provide a version of select that interops with Java
|
||||
|
||||
/** Filters the command list by type, parties and public keys all at once. */
|
||||
inline fun <reified T : CommandData> Collection<AuthenticatedObject<CommandData>>.select(signers: Collection<CompositeKey>?,
|
||||
inline fun <reified T : CommandData> Collection<AuthenticatedObject<CommandData>>.select(signers: Collection<PublicKey>?,
|
||||
parties: Collection<Party>?) =
|
||||
filter { it.value is T }.
|
||||
filter { if (signers == null) true else it.signers.containsAll(signers) }.
|
||||
@ -93,18 +103,6 @@ inline fun <reified T : CommandData> Collection<AuthenticatedObject<CommandData>
|
||||
fun <C : CommandData> Collection<AuthenticatedObject<CommandData>>.requireSingleCommand(klass: Class<C>) =
|
||||
mapNotNull { @Suppress("UNCHECKED_CAST") if (klass.isInstance(it.value)) it as AuthenticatedObject<C> else null }.single()
|
||||
|
||||
/**
|
||||
* Simple functionality for verifying a move command. Verifies that each input has a signature from its owning key.
|
||||
*
|
||||
* @param T the type of the move command.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class)
|
||||
// TODO: Can we have a common Move command for all contracts and avoid the reified type parameter here?
|
||||
inline fun <reified T : MoveCommand> verifyMoveCommand(inputs: List<OwnableState>,
|
||||
tx: TransactionForContract)
|
||||
: MoveCommand
|
||||
= verifyMoveCommand<T>(inputs, tx.commands)
|
||||
|
||||
/**
|
||||
* Simple functionality for verifying a move command. Verifies that each input has a signature from its owning key.
|
||||
*
|
||||
@ -121,7 +119,7 @@ inline fun <reified T : MoveCommand> verifyMoveCommand(inputs: List<OwnableState
|
||||
val command = commands.requireSingleCommand<T>()
|
||||
val keysThatSigned = command.signers.toSet()
|
||||
requireThat {
|
||||
"the owning keys are a subset of the signing keys" by keysThatSigned.containsAll(owningPubKeys)
|
||||
"the owning keys are a subset of the signing keys" using keysThatSigned.containsAll(owningPubKeys)
|
||||
}
|
||||
return command.value
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
package net.corda.core.contracts
|
||||
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import java.security.PublicKey
|
||||
|
||||
// The dummy contract doesn't do anything useful. It exists for testing purposes.
|
||||
|
||||
@ -14,12 +14,12 @@ data class DummyContract(override val legalContractReference: SecureHash = Secur
|
||||
val magicNumber: Int
|
||||
}
|
||||
|
||||
data class SingleOwnerState(override val magicNumber: Int = 0, override val owner: CompositeKey) : OwnableState, State {
|
||||
data class SingleOwnerState(override val magicNumber: Int = 0, override val owner: PublicKey) : OwnableState, State {
|
||||
override val contract = DUMMY_PROGRAM_ID
|
||||
override val participants: List<CompositeKey>
|
||||
override val participants: List<PublicKey>
|
||||
get() = listOf(owner)
|
||||
|
||||
override fun withNewOwner(newOwner: CompositeKey) = Pair(Commands.Move(), copy(owner = newOwner))
|
||||
override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner))
|
||||
}
|
||||
|
||||
/**
|
||||
@ -28,9 +28,9 @@ data class DummyContract(override val legalContractReference: SecureHash = Secur
|
||||
* in a different field, however this is a good example of a contract with multiple states.
|
||||
*/
|
||||
data class MultiOwnerState(override val magicNumber: Int = 0,
|
||||
val owners: List<CompositeKey>) : ContractState, State {
|
||||
val owners: List<PublicKey>) : ContractState, State {
|
||||
override val contract = DUMMY_PROGRAM_ID
|
||||
override val participants: List<CompositeKey> get() = owners
|
||||
override val participants: List<PublicKey> get() = owners
|
||||
}
|
||||
|
||||
interface Commands : CommandData {
|
||||
@ -55,8 +55,8 @@ data class DummyContract(override val legalContractReference: SecureHash = Secur
|
||||
}
|
||||
}
|
||||
|
||||
fun move(prior: StateAndRef<DummyContract.SingleOwnerState>, newOwner: CompositeKey) = move(listOf(prior), newOwner)
|
||||
fun move(priors: List<StateAndRef<DummyContract.SingleOwnerState>>, newOwner: CompositeKey): TransactionBuilder {
|
||||
fun move(prior: StateAndRef<DummyContract.SingleOwnerState>, newOwner: PublicKey) = move(listOf(prior), newOwner)
|
||||
fun move(priors: List<StateAndRef<DummyContract.SingleOwnerState>>, newOwner: PublicKey): TransactionBuilder {
|
||||
require(priors.isNotEmpty())
|
||||
val priorState = priors[0].state.data
|
||||
val (cmd, state) = priorState.withNewOwner(newOwner)
|
||||
|
@ -1,9 +1,9 @@
|
||||
package net.corda.core.contracts
|
||||
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.flows.ContractUpgradeFlow
|
||||
import java.security.PublicKey
|
||||
|
||||
// The dummy contract doesn't do anything useful. It exists for testing purposes.
|
||||
val DUMMY_V2_PROGRAM_ID = DummyContractV2()
|
||||
@ -11,12 +11,13 @@ val DUMMY_V2_PROGRAM_ID = DummyContractV2()
|
||||
/**
|
||||
* Dummy contract state for testing of the upgrade process.
|
||||
*/
|
||||
// DOCSTART 1
|
||||
class DummyContractV2 : UpgradedContract<DummyContract.State, DummyContractV2.State> {
|
||||
override val legacyContract = DummyContract::class.java
|
||||
|
||||
data class State(val magicNumber: Int = 0, val owners: List<CompositeKey>) : ContractState {
|
||||
data class State(val magicNumber: Int = 0, val owners: List<PublicKey>) : ContractState {
|
||||
override val contract = DUMMY_V2_PROGRAM_ID
|
||||
override val participants: List<CompositeKey> = owners
|
||||
override val participants: List<PublicKey> = owners
|
||||
}
|
||||
|
||||
interface Commands : CommandData {
|
||||
@ -35,7 +36,7 @@ class DummyContractV2 : UpgradedContract<DummyContract.State, DummyContractV2.St
|
||||
|
||||
// The "empty contract"
|
||||
override val legalContractReference: SecureHash = SecureHash.sha256("")
|
||||
|
||||
// DOCEND 1
|
||||
/**
|
||||
* Generate an upgrade transaction from [DummyContract].
|
||||
*
|
||||
@ -43,7 +44,7 @@ class DummyContractV2 : UpgradedContract<DummyContract.State, DummyContractV2.St
|
||||
*
|
||||
* @return a pair of wire transaction, and a set of those who should sign the transaction for it to be valid.
|
||||
*/
|
||||
fun generateUpgradeFromV1(vararg states: StateAndRef<DummyContract.State>): Pair<WireTransaction, Set<CompositeKey>> {
|
||||
fun generateUpgradeFromV1(vararg states: StateAndRef<DummyContract.State>): Pair<WireTransaction, Set<PublicKey>> {
|
||||
val notary = states.map { it.state.notary }.single()
|
||||
require(states.isNotEmpty())
|
||||
|
||||
|
@ -1,12 +1,12 @@
|
||||
package net.corda.core.contracts
|
||||
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import java.security.PublicKey
|
||||
|
||||
/**
|
||||
* Dummy state for use in testing. Not part of any contract, not even the [DummyContract].
|
||||
*/
|
||||
data class DummyState(val magicNumber: Int = 0) : ContractState {
|
||||
override val contract = DUMMY_PROGRAM_ID
|
||||
override val participants: List<CompositeKey>
|
||||
override val participants: List<PublicKey>
|
||||
get() = emptyList()
|
||||
}
|
||||
|
@ -11,16 +11,25 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize
|
||||
import com.google.common.annotations.VisibleForTesting
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import java.math.BigDecimal
|
||||
import java.math.BigInteger
|
||||
import java.math.RoundingMode
|
||||
import java.time.DayOfWeek
|
||||
import java.time.LocalDate
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* This interface is used by [Amount] to determine the conversion ratio from
|
||||
* indicative/displayed asset amounts in [BigDecimal] to fungible tokens represented by Amount objects.
|
||||
*/
|
||||
interface TokenizableAssetInfo {
|
||||
val displayTokenSize: BigDecimal
|
||||
}
|
||||
|
||||
/**
|
||||
* Amount represents a positive quantity of some token (currency, asset, etc.), measured in quantity of the smallest
|
||||
* representable units. Note that quantity is not necessarily 1/100ths of a currency unit, but are the actual smallest
|
||||
* amount used in whatever underlying thing the amount represents.
|
||||
* representable units. The nominal quantity represented by each individual token is equal to the [displayTokenSize].
|
||||
* The scale property of the [displayTokenSize] should correctly reflect the displayed decimal places and is used
|
||||
* when rounding conversions from indicative/displayed amounts in [BigDecimal] to Amount occur via the Amount.fromDecimal method.
|
||||
*
|
||||
* Amounts of different tokens *do not mix* and attempting to add or subtract two amounts of different currencies
|
||||
* will throw [IllegalArgumentException]. Amounts may not be negative. Amounts are represented internally using a signed
|
||||
@ -28,25 +37,62 @@ import java.util.*
|
||||
* multiplication are overflow checked and will throw [ArithmeticException] if the operation would have caused integer
|
||||
* overflow.
|
||||
*
|
||||
* TODO: It may make sense to replace this with convenience extensions over the JSR 354 MonetaryAmount interface,
|
||||
* in particular for use during calculations. This may also resolve...
|
||||
* TODO: Think about how positive-only vs positive-or-negative amounts can be represented in the type system.
|
||||
* TODO: Add either a scaling factor, or a variant for use in calculations.
|
||||
*
|
||||
* @param quantity the number of tokens as a Long value.
|
||||
* @param displayTokenSize the nominal display unit size of a single token,
|
||||
* potentially with trailing decimal display places if the scale parameter is non-zero.
|
||||
* @param T the type of the token, for example [Currency].
|
||||
* T should implement TokenizableAssetInfo if automatic conversion to/from a display format is required.
|
||||
*
|
||||
* TODO Proper lookup of currencies in a locale and context sensitive fashion is not supported and is left to the application.
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class Amount<T>(val quantity: Long, val token: T) : Comparable<Amount<T>> {
|
||||
data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal, val token: T) : Comparable<Amount<T>> {
|
||||
companion object {
|
||||
/**
|
||||
* Build a currency amount from a decimal representation. For example, with an input of "12.34" GBP,
|
||||
* returns an amount with a quantity of "1234".
|
||||
* Build an Amount from a decimal representation. For example, with an input of "12.34 GBP",
|
||||
* returns an amount with a quantity of "1234" tokens. The displayTokenSize as determined via
|
||||
* getDisplayTokenSize is used to determine the conversion scaling.
|
||||
* e.g. Bonds might be in nominal amounts of 100, currencies in 0.01 penny units.
|
||||
*
|
||||
* @see Amount<Currency>.toDecimal
|
||||
* @throws ArithmeticException if the intermediate calculations cannot be converted to an unsigned 63-bit token amount.
|
||||
*/
|
||||
fun fromDecimal(quantity: BigDecimal, currency: Currency) : Amount<Currency> {
|
||||
val longQuantity = quantity.movePointRight(currency.defaultFractionDigits).toLong()
|
||||
return Amount(longQuantity, currency)
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
fun <T : Any> fromDecimal(displayQuantity: BigDecimal, token: T, rounding: RoundingMode = RoundingMode.FLOOR): Amount<T> {
|
||||
val tokenSize = getDisplayTokenSize(token)
|
||||
val tokenCount = displayQuantity.divide(tokenSize).setScale(0, rounding).longValueExact()
|
||||
return Amount(tokenCount, tokenSize, token)
|
||||
}
|
||||
|
||||
/**
|
||||
* For a particular token returns a zero sized Amount<T>
|
||||
*/
|
||||
@JvmStatic
|
||||
fun <T : Any> zero(token: T): Amount<T> {
|
||||
val tokenSize = getDisplayTokenSize(token)
|
||||
return Amount(0L, tokenSize, token)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Determines the representation of one Token quantity in BigDecimal. For Currency and Issued<Currency>
|
||||
* the definitions is taken from Currency defaultFractionDigits property e.g. 2 for USD, or 0 for JPY
|
||||
* so that the automatic token size is the conventional minimum penny amount.
|
||||
* For other possible token types the asset token should implement TokenizableAssetInfo to
|
||||
* correctly report the designed nominal amount.
|
||||
*/
|
||||
fun getDisplayTokenSize(token: Any): BigDecimal {
|
||||
if (token is TokenizableAssetInfo) {
|
||||
return token.displayTokenSize
|
||||
}
|
||||
if (token is Currency) {
|
||||
return BigDecimal.ONE.scaleByPowerOfTen(-token.defaultFractionDigits)
|
||||
}
|
||||
if (token is Issued<*>) {
|
||||
return getDisplayTokenSize(token.product)
|
||||
}
|
||||
return BigDecimal.ONE
|
||||
}
|
||||
|
||||
private val currencySymbols: Map<String, Currency> = mapOf(
|
||||
@ -111,44 +157,93 @@ data class Amount<T>(val quantity: Long, val token: T) : Comparable<Amount<T>> {
|
||||
}
|
||||
|
||||
init {
|
||||
// Negative amounts are of course a vital part of any ledger, but negative values are only valid in certain
|
||||
// contexts: you cannot send a negative amount of cash, but you can (sometimes) have a negative balance.
|
||||
// If you want to express a negative amount, for now, use a long.
|
||||
// Amount represents a static balance of physical assets as managed by the distributed ledger and is not allowed
|
||||
// to become negative a rule further maintained by the Contract verify method.
|
||||
// N.B. If concepts such as an account overdraft are required this should be modelled separately via Obligations,
|
||||
// or similar second order smart contract concepts.
|
||||
require(quantity >= 0) { "Negative amounts are not allowed: $quantity" }
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct the amount using the given decimal value as quantity. Any fractional part
|
||||
* is discarded. To convert and use the fractional part, see [fromDecimal].
|
||||
* Automatic conversion constructor from number of tokens to an Amount using getDisplayTokenSize to determine
|
||||
* the displayTokenSize.
|
||||
*
|
||||
* @param tokenQuantity the number of tokens represented.
|
||||
* @param token the type of the token, for example a [Currency] object.
|
||||
*/
|
||||
constructor(quantity: BigDecimal, token: T) : this(quantity.toLong(), token)
|
||||
constructor(quantity: BigInteger, token: T) : this(quantity.toLong(), token)
|
||||
constructor(tokenQuantity: Long, token: T) : this(tokenQuantity, getDisplayTokenSize(token), token)
|
||||
|
||||
/**
|
||||
* A checked addition operator is supported to simplify aggregation of Amounts.
|
||||
* @throws ArithmeticException if there is overflow of Amount tokens during the summation
|
||||
* Mixing non-identical token types will throw [IllegalArgumentException]
|
||||
*/
|
||||
operator fun plus(other: Amount<T>): Amount<T> {
|
||||
checkToken(other)
|
||||
return Amount(Math.addExact(quantity, other.quantity), token)
|
||||
return Amount(Math.addExact(quantity, other.quantity), displayTokenSize, token)
|
||||
}
|
||||
|
||||
/**
|
||||
* A checked addition operator is supported to simplify netting of Amounts.
|
||||
* If this leads to the Amount going negative this will throw [IllegalArgumentException].
|
||||
* @throws ArithmeticException if there is Numeric underflow
|
||||
* Mixing non-identical token types will throw [IllegalArgumentException]
|
||||
*/
|
||||
operator fun minus(other: Amount<T>): Amount<T> {
|
||||
checkToken(other)
|
||||
return Amount(Math.subtractExact(quantity, other.quantity), token)
|
||||
return Amount(Math.subtractExact(quantity, other.quantity), displayTokenSize, token)
|
||||
}
|
||||
|
||||
private fun checkToken(other: Amount<T>) {
|
||||
require(other.token == token) { "Token mismatch: ${other.token} vs $token" }
|
||||
require(other.displayTokenSize == displayTokenSize) { "Token size mismatch: ${other.displayTokenSize} vs $displayTokenSize" }
|
||||
}
|
||||
|
||||
operator fun div(other: Long): Amount<T> = Amount(quantity / other, token)
|
||||
operator fun times(other: Long): Amount<T> = Amount(Math.multiplyExact(quantity, other), token)
|
||||
operator fun div(other: Int): Amount<T> = Amount(quantity / other, token)
|
||||
operator fun times(other: Int): Amount<T> = Amount(Math.multiplyExact(quantity, other.toLong()), token)
|
||||
/**
|
||||
* The multiplication operator is supported to allow easy calculation for multiples of a primitive Amount.
|
||||
* Note this is not a conserving operation, so it may not always be correct modelling of proper token behaviour.
|
||||
* N.B. Division is not supported as fractional tokens are not representable by an Amount.
|
||||
*/
|
||||
operator fun times(other: Long): Amount<T> = Amount(Math.multiplyExact(quantity, other), displayTokenSize, token)
|
||||
|
||||
operator fun times(other: Int): Amount<T> = Amount(Math.multiplyExact(quantity, other.toLong()), displayTokenSize, token)
|
||||
|
||||
/**
|
||||
* This method provides a token conserving divide mechanism.
|
||||
* @param partitions the number of amounts to divide the current quantity into.
|
||||
* @result Returns [partitions] separate Amount objects which sum to the same quantity as this Amount
|
||||
* and differ by no more than a single token in size.
|
||||
*/
|
||||
fun splitEvenly(partitions: Int): List<Amount<T>> {
|
||||
require(partitions >= 1) { "Must split amount into one, or more pieces" }
|
||||
val commonTokensPerPartition = quantity.div(partitions)
|
||||
val residualTokens = quantity - (commonTokensPerPartition * partitions)
|
||||
val splitAmount = Amount(commonTokensPerPartition, displayTokenSize, token)
|
||||
val splitAmountPlusOne = Amount(commonTokensPerPartition + 1L, displayTokenSize, token)
|
||||
return (0..partitions - 1).map { if (it < residualTokens) splitAmountPlusOne else splitAmount }.toList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a currency [Amount] to a decimal representation. For example, with an amount with a quantity
|
||||
* of "1234" GBP, returns "12.34". The precise representation is controlled by the displayTokenSize,
|
||||
* which determines the size of a single token and controls the trailing decimal places via it's scale property.
|
||||
*
|
||||
* @see Amount.Companion.fromDecimal
|
||||
*/
|
||||
fun toDecimal(): BigDecimal = BigDecimal.valueOf(quantity, 0) * displayTokenSize
|
||||
|
||||
|
||||
/**
|
||||
* Convert a currency [Amount] to a display string representation.
|
||||
*
|
||||
* For example, with an amount with a quantity of "1234" GBP, returns "12.34 GBP".
|
||||
* The result of fromDecimal is used to control the numerical formatting and
|
||||
* the token specifier appended is taken from token.toString.
|
||||
*
|
||||
* @see Amount.Companion.fromDecimal
|
||||
*/
|
||||
override fun toString(): String {
|
||||
val bd = if (token is Currency)
|
||||
BigDecimal(quantity).movePointLeft(token.defaultFractionDigits)
|
||||
else
|
||||
BigDecimal(quantity)
|
||||
return bd.toPlainString() + " " + token
|
||||
return toDecimal().toPlainString() + " " + token
|
||||
}
|
||||
|
||||
override fun compareTo(other: Amount<T>): Int {
|
||||
@ -157,17 +252,206 @@ data class Amount<T>(val quantity: Long, val token: T) : Comparable<Amount<T>> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a currency [Amount] to a decimal representation. For example, with an amount with a quantity
|
||||
* of "1234" GBP, returns "12.34".
|
||||
*
|
||||
* @see Amount.Companion.fromDecimal
|
||||
*/
|
||||
fun Amount<Currency>.toDecimal() : BigDecimal = BigDecimal(quantity).movePointLeft(token.defaultFractionDigits)
|
||||
|
||||
fun <T> Iterable<Amount<T>>.sumOrNull() = if (!iterator().hasNext()) null else sumOrThrow()
|
||||
fun <T> Iterable<Amount<T>>.sumOrThrow() = reduce { left, right -> left + right }
|
||||
fun <T> Iterable<Amount<T>>.sumOrZero(currency: T) = if (iterator().hasNext()) sumOrThrow() else Amount(0, currency)
|
||||
fun <T : Any> Iterable<Amount<T>>.sumOrNull() = if (!iterator().hasNext()) null else sumOrThrow()
|
||||
fun <T : Any> Iterable<Amount<T>>.sumOrThrow() = reduce { left, right -> left + right }
|
||||
fun <T : Any> Iterable<Amount<T>>.sumOrZero(token: T) = if (iterator().hasNext()) sumOrThrow() else Amount.zero(token)
|
||||
|
||||
|
||||
/**
|
||||
* Simple data class to associate the origin, owner, or holder of a particular Amount object.
|
||||
* @param source the holder of the Amount.
|
||||
* @param amount the Amount of asset available.
|
||||
* @param ref is an optional field used for housekeeping in the caller.
|
||||
* e.g. to point back at the original Vault state objects.
|
||||
* @see SourceAndAmount.apply which processes a list of SourceAndAmount objects
|
||||
* and calculates the resulting Amount distribution as a new list of SourceAndAmount objects.
|
||||
*/
|
||||
data class SourceAndAmount<T : Any, out P : Any>(val source: P, val amount: Amount<T>, val ref: Any? = null)
|
||||
|
||||
/**
|
||||
* This class represents a possibly negative transfer of tokens from one vault state to another, possibly at a future date.
|
||||
*
|
||||
* @param quantityDelta is a signed Long value representing the exchanged number of tokens. If positive then
|
||||
* it represents the movement of Math.abs(quantityDelta) tokens away from source and receipt of Math.abs(quantityDelta)
|
||||
* at the destination. If the quantityDelta is negative then the source will receive Math.abs(quantityDelta) tokens
|
||||
* and the destination will lose Math.abs(quantityDelta) tokens.
|
||||
* Where possible the source and destination should be coded to ensure a positive quantityDelta,
|
||||
* but in various scenarios it may be more consistent to allow positive and negative values.
|
||||
* For example it is common for a bank to code asset flows as gains and losses from its perspective i.e. always the destination.
|
||||
* @param token represents the type of asset token as would be used to construct Amount<T> objects.
|
||||
* @param source is the [Party], [Account], [CompositeKey], or other identifier of the token source if quantityDelta is positive,
|
||||
* or the token sink if quantityDelta is negative. The type P should support value equality.
|
||||
* @param destination is the [Party], [Account], [CompositeKey], or other identifier of the token sink if quantityDelta is positive,
|
||||
* or the token source if quantityDelta is negative. The type P should support value equality.
|
||||
*/
|
||||
@CordaSerializable
|
||||
class AmountTransfer<T : Any, P : Any>(val quantityDelta: Long,
|
||||
val token: T,
|
||||
val source: P,
|
||||
val destination: P) {
|
||||
companion object {
|
||||
/**
|
||||
* Construct an AmountTransfer object from an indicative/displayable BigDecimal source, applying rounding as specified.
|
||||
* The token size is determined from the token type and is the same as for [Amount] of the same token.
|
||||
* @param displayQuantityDelta is the signed amount to transfer between source and destination in displayable units.
|
||||
* Positive values mean transfers from source to destination. Negative values mean transfers from destination to source.
|
||||
* @param token defines the asset being represented in the transfer. The token should implement [TokenizableAssetInfo] if custom
|
||||
* conversion logic is required.
|
||||
* @param source The payer of the transfer if displayQuantityDelta is positive, the payee if displayQuantityDelta is negative
|
||||
* @param destination The payee of the transfer if displayQuantityDelta is positive, the payer if displayQuantityDelta is negative
|
||||
* @param rounding The mode of rounding to apply after scaling to integer token units.
|
||||
*/
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
fun <T : Any, P : Any> fromDecimal(displayQuantityDelta: BigDecimal,
|
||||
token: T,
|
||||
source: P,
|
||||
destination: P,
|
||||
rounding: RoundingMode = RoundingMode.DOWN): AmountTransfer<T, P> {
|
||||
val tokenSize = Amount.getDisplayTokenSize(token)
|
||||
val deltaTokenCount = displayQuantityDelta.divide(tokenSize).setScale(0, rounding).longValueExact()
|
||||
return AmountTransfer(deltaTokenCount, token, source, destination)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to make a zero size AmountTransfer
|
||||
*/
|
||||
@JvmStatic
|
||||
fun <T : Any, P : Any> zero(token: T,
|
||||
source: P,
|
||||
destination: P): AmountTransfer<T, P> = AmountTransfer(0L, token, source, destination)
|
||||
}
|
||||
|
||||
init {
|
||||
require(source != destination) { "The source and destination cannot be the same ($source)" }
|
||||
}
|
||||
|
||||
/**
|
||||
* Add together two [AmountTransfer] objects to produce the single equivalent net flow.
|
||||
* The addition only applies to AmountTransfer objects with the same token type.
|
||||
* Also the pair of parties must be aligned, although source destination may be
|
||||
* swapped in the second item.
|
||||
* @throws ArithmeticException if there is underflow, or overflow in the summations.
|
||||
*/
|
||||
operator fun plus(other: AmountTransfer<T, P>): AmountTransfer<T, P> {
|
||||
require(other.token == token) { "Token mismatch: ${other.token} vs $token" }
|
||||
require((other.source == source && other.destination == destination)
|
||||
|| (other.source == destination && other.destination == source)) {
|
||||
"Only AmountTransfer between the same two parties can be aggregated/netted"
|
||||
}
|
||||
return if (other.source == source) {
|
||||
AmountTransfer(Math.addExact(quantityDelta, other.quantityDelta), token, source, destination)
|
||||
} else {
|
||||
AmountTransfer(Math.subtractExact(quantityDelta, other.quantityDelta), token, source, destination)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the quantityDelta to a displayable format BigDecimal value. The conversion ratio is the same as for
|
||||
* [Amount] of the same token type.
|
||||
*/
|
||||
fun toDecimal(): BigDecimal = BigDecimal.valueOf(quantityDelta, 0) * Amount.getDisplayTokenSize(token)
|
||||
|
||||
fun copy(quantityDelta: Long = this.quantityDelta,
|
||||
token: T = this.token,
|
||||
source: P = this.source,
|
||||
destination: P = this.destination): AmountTransfer<T, P> = AmountTransfer(quantityDelta, token, source, destination)
|
||||
|
||||
/**
|
||||
* Checks value equality of AmountTransfer objects, but also matches the reversed source and destination equivalent.
|
||||
*/
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other?.javaClass != javaClass) return false
|
||||
|
||||
other as AmountTransfer<*, *>
|
||||
|
||||
if (token != other.token) return false
|
||||
if (source == other.source) {
|
||||
if (destination != other.destination) return false
|
||||
if (quantityDelta != other.quantityDelta) return false
|
||||
return true
|
||||
} else if (source == other.destination) {
|
||||
if (destination != other.source) return false
|
||||
if (quantityDelta != -other.quantityDelta) return false
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* HashCode ensures that reversed source and destination equivalents will hash to the same value.
|
||||
*/
|
||||
override fun hashCode(): Int {
|
||||
var result = Math.abs(quantityDelta).hashCode() // ignore polarity reversed values
|
||||
result = 31 * result + token.hashCode()
|
||||
result = 31 * result + (source.hashCode() xor destination.hashCode()) // XOR to ensure the same hash for swapped source and destination
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "Transfer from $source to $destination of ${this.toDecimal().toPlainString()} $token"
|
||||
}
|
||||
|
||||
/**
|
||||
* Novation is a common financial operation in which a bilateral exchange is modified so that the same
|
||||
* relative asset exchange happens, but with each party exchanging versus a central counterparty, or clearing house.
|
||||
*
|
||||
* @param centralParty The central party to face the exchange against.
|
||||
* @return Returns two new AmountTransfers each between one of the original parties and the centralParty.
|
||||
* The net total exchange is the same as in the original input.
|
||||
*/
|
||||
fun novate(centralParty: P): Pair<AmountTransfer<T, P>, AmountTransfer<T, P>> = Pair(copy(destination = centralParty), copy(source = centralParty))
|
||||
|
||||
/**
|
||||
* Applies this AmountTransfer to a list of [SourceAndAmount] objects representing balances.
|
||||
* The list can be heterogeneous in terms of token types and parties, so long as there is sufficient balance
|
||||
* of the correct token type held with the party paying for the transfer.
|
||||
* @param balances The source list of [SourceAndAmount] objects containing the funds to satisfy the exchange.
|
||||
* @param newRef An optional marker object which is attached to any new [SourceAndAmount] objects created in the output.
|
||||
* i.e. To the new payment destination entry and to any residual change output.
|
||||
* @return The returned list is a copy of the original list, except that funds needed to cover the exchange
|
||||
* will have been removed and a new output and possibly residual amount entry will be added at the end of the list.
|
||||
* @throws ArithmeticException if there is underflow in the summations.
|
||||
*/
|
||||
fun apply(balances: List<SourceAndAmount<T, P>>, newRef: Any? = null): List<SourceAndAmount<T, P>> {
|
||||
val (payer, payee) = if (quantityDelta >= 0L) Pair(source, destination) else Pair(destination, source)
|
||||
val transfer = Math.abs(quantityDelta)
|
||||
var residual = transfer
|
||||
val outputs = mutableListOf<SourceAndAmount<T, P>>()
|
||||
var remaining: SourceAndAmount<T, P>? = null
|
||||
var newAmount: SourceAndAmount<T, P>? = null
|
||||
for (balance in balances) {
|
||||
if (balance.source != payer
|
||||
|| balance.amount.token != token
|
||||
|| residual == 0L) {
|
||||
// Just copy across unmodified.
|
||||
outputs += balance
|
||||
} else if (balance.amount.quantity < residual) {
|
||||
// Consume the payers amount and do not copy across.
|
||||
residual -= balance.amount.quantity
|
||||
} else {
|
||||
// Calculate any residual spend left on the payers balance.
|
||||
if (balance.amount.quantity > residual) {
|
||||
remaining = SourceAndAmount(payer, balance.amount.copy(quantity = Math.subtractExact(balance.amount.quantity, residual)), newRef)
|
||||
}
|
||||
// Build the new output payment to the payee.
|
||||
newAmount = SourceAndAmount(payee, balance.amount.copy(quantity = transfer), newRef)
|
||||
// Clear the residual.
|
||||
residual = 0L
|
||||
}
|
||||
}
|
||||
require(residual == 0L) { "Insufficient funds. Unable to process $this" }
|
||||
if (remaining != null) {
|
||||
outputs += remaining
|
||||
}
|
||||
outputs += newAmount!!
|
||||
return outputs
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
@ -232,7 +516,7 @@ data class Tenor(val name: String) {
|
||||
val adjustedMaturityDate = calendar.applyRollConvention(maturityDate, DateRollConvention.ModifiedFollowing)
|
||||
val daysToMaturity = calculateDaysBetween(startDate, adjustedMaturityDate, DayCountBasisYear.Y360, DayCountBasisDay.DActual)
|
||||
|
||||
return daysToMaturity.toInt()
|
||||
return daysToMaturity
|
||||
}
|
||||
|
||||
override fun toString(): String = name
|
||||
@ -265,43 +549,25 @@ enum class DateRollDirection(val value: Long) { FORWARD(1), BACKWARD(-1) }
|
||||
* There are some additional rules which are explained in the individual cases below.
|
||||
*/
|
||||
@CordaSerializable
|
||||
enum class DateRollConvention {
|
||||
enum class DateRollConvention(val direction: () -> DateRollDirection, val isModified: Boolean) {
|
||||
// direction() cannot be a val due to the throw in the Actual instance
|
||||
|
||||
/** Don't roll the date, use the one supplied. */
|
||||
Actual {
|
||||
override fun direction(): DateRollDirection = throw UnsupportedOperationException("Direction is not relevant for convention Actual")
|
||||
override val isModified: Boolean = false
|
||||
},
|
||||
Actual({ throw UnsupportedOperationException("Direction is not relevant for convention Actual") }, false),
|
||||
/** Following is the next business date from this one. */
|
||||
Following {
|
||||
override fun direction(): DateRollDirection = DateRollDirection.FORWARD
|
||||
override val isModified: Boolean = false
|
||||
},
|
||||
Following({ DateRollDirection.FORWARD }, false),
|
||||
/**
|
||||
* "Modified following" is the next business date, unless it's in the next month, in which case use the preceeding
|
||||
* business date.
|
||||
*/
|
||||
ModifiedFollowing {
|
||||
override fun direction(): DateRollDirection = DateRollDirection.FORWARD
|
||||
override val isModified: Boolean = true
|
||||
},
|
||||
ModifiedFollowing({ DateRollDirection.FORWARD }, true),
|
||||
/** Previous is the previous business date from this one. */
|
||||
Previous {
|
||||
override fun direction(): DateRollDirection = DateRollDirection.BACKWARD
|
||||
override val isModified: Boolean = false
|
||||
},
|
||||
Previous({ DateRollDirection.BACKWARD }, false),
|
||||
/**
|
||||
* Modified previous is the previous business date, unless it's in the previous month, in which case use the next
|
||||
* business date.
|
||||
*/
|
||||
ModifiedPrevious {
|
||||
override fun direction(): DateRollDirection = DateRollDirection.BACKWARD
|
||||
override val isModified: Boolean = true
|
||||
};
|
||||
|
||||
abstract fun direction(): DateRollDirection
|
||||
abstract val isModified: Boolean
|
||||
ModifiedPrevious({ DateRollDirection.BACKWARD }, true);
|
||||
}
|
||||
|
||||
|
||||
@ -345,31 +611,14 @@ enum class PaymentRule {
|
||||
*/
|
||||
@Suppress("unused") // TODO: Revisit post-Vega and see if annualCompoundCount is still needed.
|
||||
@CordaSerializable
|
||||
enum class Frequency(val annualCompoundCount: Int) {
|
||||
Annual(1) {
|
||||
override fun offset(d: LocalDate, n: Long) = d.plusYears(1 * n)
|
||||
},
|
||||
SemiAnnual(2) {
|
||||
override fun offset(d: LocalDate, n: Long) = d.plusMonths(6 * n)
|
||||
},
|
||||
Quarterly(4) {
|
||||
override fun offset(d: LocalDate, n: Long) = d.plusMonths(3 * n)
|
||||
},
|
||||
Monthly(12) {
|
||||
override fun offset(d: LocalDate, n: Long) = d.plusMonths(1 * n)
|
||||
},
|
||||
Weekly(52) {
|
||||
override fun offset(d: LocalDate, n: Long) = d.plusWeeks(1 * n)
|
||||
},
|
||||
BiWeekly(26) {
|
||||
override fun offset(d: LocalDate, n: Long) = d.plusWeeks(2 * n)
|
||||
},
|
||||
Daily(365) {
|
||||
override fun offset(d: LocalDate, n: Long) = d.plusDays(1 * n)
|
||||
};
|
||||
|
||||
abstract fun offset(d: LocalDate, n: Long = 1): LocalDate
|
||||
// Daily() // Let's not worry about this for now.
|
||||
enum class Frequency(val annualCompoundCount: Int, val offset: LocalDate.(Long) -> LocalDate) {
|
||||
Annual(1, { plusYears(1 * it) }),
|
||||
SemiAnnual(2, { plusMonths(6 * it) }),
|
||||
Quarterly(4, { plusMonths(3 * it) }),
|
||||
Monthly(12, { plusMonths(1 * it) }),
|
||||
Weekly(52, { plusWeeks(1 * it) }),
|
||||
BiWeekly(26, { plusWeeks(2 * it) }),
|
||||
Daily(365, { plusDays(1 * it) });
|
||||
}
|
||||
|
||||
|
||||
@ -396,7 +645,7 @@ open class BusinessCalendar private constructor(val holidayDates: List<LocalDate
|
||||
}.toMap()
|
||||
|
||||
/** Parses a date of the form YYYY-MM-DD, like 2016-01-10 for 10th Jan. */
|
||||
fun parseDateFromString(it: String) = LocalDate.parse(it, DateTimeFormatter.ISO_LOCAL_DATE)
|
||||
fun parseDateFromString(it: String): LocalDate = LocalDate.parse(it, DateTimeFormatter.ISO_LOCAL_DATE)
|
||||
|
||||
/** Returns a business calendar that combines all the named holiday calendars into one list of holiday dates. */
|
||||
fun getInstance(vararg calname: String) = BusinessCalendar(
|
||||
@ -546,7 +795,10 @@ enum class NetType {
|
||||
@CordaSerializable
|
||||
data class Commodity(val commodityCode: String,
|
||||
val displayName: String,
|
||||
val defaultFractionDigits: Int = 0) {
|
||||
val defaultFractionDigits: Int = 0) : TokenizableAssetInfo {
|
||||
override val displayTokenSize: BigDecimal
|
||||
get() = BigDecimal.ONE.scaleByPowerOfTen(-defaultFractionDigits)
|
||||
|
||||
companion object {
|
||||
private val registry = mapOf(
|
||||
// Simple example commodity, as in http://www.investopedia.com/university/commodities/commodities14.asp
|
||||
|
@ -1,7 +1,12 @@
|
||||
package net.corda.core.contracts
|
||||
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.flows.FlowException
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.core.utilities.trace
|
||||
import java.security.PublicKey
|
||||
import java.util.*
|
||||
|
||||
class InsufficientBalanceException(val amountMissing: Amount<*>) : FlowException("Insufficient balance, missing $amountMissing")
|
||||
|
||||
@ -19,17 +24,17 @@ class InsufficientBalanceException(val amountMissing: Amount<*>) : FlowException
|
||||
* @param T a type that represents the asset in question. This should describe the basic type of the asset
|
||||
* (GBP, USD, oil, shares in company <X>, etc.) and any additional metadata (issuer, grade, class, etc.).
|
||||
*/
|
||||
interface FungibleAsset<T> : OwnableState {
|
||||
interface FungibleAsset<T : Any> : OwnableState {
|
||||
val amount: Amount<Issued<T>>
|
||||
/**
|
||||
* There must be an ExitCommand signed by these keys to destroy the amount. While all states require their
|
||||
* owner to sign, some (i.e. cash) also require the issuer.
|
||||
*/
|
||||
val exitKeys: Collection<CompositeKey>
|
||||
val exitKeys: Collection<PublicKey>
|
||||
/** There must be a MoveCommand signed by this key to claim the amount */
|
||||
override val owner: CompositeKey
|
||||
override val owner: PublicKey
|
||||
|
||||
fun move(newAmount: Amount<Issued<T>>, newOwner: CompositeKey): FungibleAsset<T>
|
||||
fun move(newAmount: Amount<Issued<T>>, newOwner: PublicKey): FungibleAsset<T>
|
||||
|
||||
// Just for grouping
|
||||
interface Commands : CommandData {
|
||||
@ -45,7 +50,7 @@ interface FungibleAsset<T> : OwnableState {
|
||||
* A command stating that money has been withdrawn from the shared ledger and is now accounted for
|
||||
* in some other way.
|
||||
*/
|
||||
interface Exit<T> : Commands {
|
||||
interface Exit<T : Any> : Commands {
|
||||
val amount: Amount<Issued<T>>
|
||||
}
|
||||
}
|
||||
@ -54,8 +59,8 @@ interface FungibleAsset<T> : OwnableState {
|
||||
// Small DSL extensions.
|
||||
|
||||
/** Sums the asset states in the list, returning null if there are none. */
|
||||
fun <T> Iterable<ContractState>.sumFungibleOrNull() = filterIsInstance<FungibleAsset<T>>().map { it.amount }.sumOrNull()
|
||||
fun <T : Any> Iterable<ContractState>.sumFungibleOrNull() = filterIsInstance<FungibleAsset<T>>().map { it.amount }.sumOrNull()
|
||||
|
||||
/** Sums the asset states in the list, returning zero of the given token if there are none. */
|
||||
fun <T> Iterable<ContractState>.sumFungibleOrZero(token: Issued<T>) = filterIsInstance<FungibleAsset<T>>().map { it.amount }.sumOrZero(token)
|
||||
fun <T : Any> Iterable<ContractState>.sumFungibleOrZero(token: Issued<T>) = filterIsInstance<FungibleAsset<T>>().map { it.amount }.sumOrZero(token)
|
||||
|
||||
|
@ -1,18 +1,16 @@
|
||||
package net.corda.core.contracts
|
||||
|
||||
import net.corda.core.contracts.clauses.Clause
|
||||
import net.corda.core.crypto.AnonymousParty
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowLogicRef
|
||||
import net.corda.core.flows.FlowLogicRefFactory
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.services.ServiceType
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.serialization.*
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.security.PublicKey
|
||||
@ -53,7 +51,7 @@ interface MultilateralNettableState<out T : Any> {
|
||||
val multilateralNetState: T
|
||||
}
|
||||
|
||||
interface NettableState<N : BilateralNettableState<N>, T : Any> : BilateralNettableState<N>,
|
||||
interface NettableState<N : BilateralNettableState<N>, out T : Any> : BilateralNettableState<N>,
|
||||
MultilateralNettableState<T>
|
||||
|
||||
/**
|
||||
@ -116,7 +114,7 @@ interface ContractState {
|
||||
* The participants list should normally be derived from the contents of the state. E.g. for [Cash] the participants
|
||||
* list should just contain the owner.
|
||||
*/
|
||||
val participants: List<CompositeKey>
|
||||
val participants: List<PublicKey>
|
||||
}
|
||||
|
||||
/**
|
||||
@ -146,26 +144,13 @@ data class TransactionState<out T : ContractState> @JvmOverloads constructor(
|
||||
* Note that an encumbered state that is being consumed must have its encumbrance consumed in the same transaction,
|
||||
* otherwise the transaction is not valid.
|
||||
*/
|
||||
val encumbrance: Int? = null) {
|
||||
|
||||
/**
|
||||
* Copies the underlying state, replacing the notary field with the new value.
|
||||
* To replace the notary, we need an approval (signature) from _all_ participants of the [ContractState].
|
||||
*/
|
||||
fun withNotary(newNotary: Party) = TransactionState(this.data, newNotary, encumbrance)
|
||||
}
|
||||
val encumbrance: Int? = null)
|
||||
|
||||
/** Wraps the [ContractState] in a [TransactionState] object */
|
||||
infix fun <T : ContractState> T.`with notary`(newNotary: Party) = withNotary(newNotary)
|
||||
|
||||
infix fun <T : ContractState> T.withNotary(newNotary: Party) = TransactionState(this, newNotary)
|
||||
|
||||
/**
|
||||
* Marker interface for data classes that represent the issuance state for a contract. These are intended as templates
|
||||
* from which the state object is initialised.
|
||||
*/
|
||||
interface IssuanceDefinition
|
||||
|
||||
/**
|
||||
* Definition for an issued product, which can be cash, a cash-like thing, assets, or generally anything else that's
|
||||
* quantifiable with integer quantities.
|
||||
@ -173,7 +158,7 @@ interface IssuanceDefinition
|
||||
* @param P the type of product underlying the definition, for example [Currency].
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class Issued<out P>(val issuer: PartyAndReference, val product: P) {
|
||||
data class Issued<out P : Any>(val issuer: PartyAndReference, val product: P) {
|
||||
override fun toString() = "$product issued by $issuer"
|
||||
}
|
||||
|
||||
@ -182,17 +167,17 @@ data class Issued<out P>(val issuer: PartyAndReference, val product: P) {
|
||||
* cares about specific issuers with code that will accept any, or which is imposing issuer constraints via some
|
||||
* other mechanism and the additional type safety is not wanted.
|
||||
*/
|
||||
fun <T> Amount<Issued<T>>.withoutIssuer(): Amount<T> = Amount(quantity, token.product)
|
||||
fun <T : Any> Amount<Issued<T>>.withoutIssuer(): Amount<T> = Amount(quantity, token.product)
|
||||
|
||||
/**
|
||||
* A contract state that can have a single owner.
|
||||
*/
|
||||
interface OwnableState : ContractState {
|
||||
/** There must be a MoveCommand signed by this key to claim the amount */
|
||||
val owner: CompositeKey
|
||||
val owner: PublicKey
|
||||
|
||||
/** Copies the underlying data structure, replacing the owner field with this new value and leaving the rest alone */
|
||||
fun withNewOwner(newOwner: CompositeKey): Pair<CommandData, OwnableState>
|
||||
fun withNewOwner(newOwner: PublicKey): Pair<CommandData, OwnableState>
|
||||
}
|
||||
|
||||
/** Something which is scheduled to happen at a point in time */
|
||||
@ -237,14 +222,14 @@ interface LinearState : ContractState {
|
||||
|
||||
/**
|
||||
* True if this should be tracked by our vault(s).
|
||||
* */
|
||||
*/
|
||||
fun isRelevant(ourKeys: Set<PublicKey>): Boolean
|
||||
|
||||
/**
|
||||
* Standard clause to verify the LinearState safety properties.
|
||||
*/
|
||||
@CordaSerializable
|
||||
class ClauseVerifier<S : LinearState, C : CommandData>() : Clause<S, C, Unit>() {
|
||||
class ClauseVerifier<in S : LinearState, C : CommandData> : Clause<S, C, Unit>() {
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<S>,
|
||||
outputs: List<S>,
|
||||
@ -253,8 +238,8 @@ interface LinearState : ContractState {
|
||||
val inputIds = inputs.map { it.linearId }.distinct()
|
||||
val outputIds = outputs.map { it.linearId }.distinct()
|
||||
requireThat {
|
||||
"LinearStates are not merged" by (inputIds.count() == inputs.count())
|
||||
"LinearStates are not split" by (outputIds.count() == outputs.count())
|
||||
"LinearStates are not merged" using (inputIds.count() == inputs.count())
|
||||
"LinearStates are not split" using (outputIds.count() == outputs.count())
|
||||
}
|
||||
return emptySet()
|
||||
}
|
||||
@ -360,7 +345,8 @@ inline fun <reified T : ContractState> Iterable<StateAndRef<ContractState>>.filt
|
||||
@CordaSerializable
|
||||
data class PartyAndReference(val party: AnonymousParty, val reference: OpaqueBytes) {
|
||||
constructor(party: Party, reference: OpaqueBytes) : this(party.toAnonymous(), reference)
|
||||
override fun toString() = "${party}$reference"
|
||||
|
||||
override fun toString() = "$party$reference"
|
||||
}
|
||||
|
||||
/** Marker interface for classes that represent commands */
|
||||
@ -375,12 +361,12 @@ abstract class TypeOnlyCommandData : CommandData {
|
||||
|
||||
/** Command data/content plus pubkey pair: the signature is stored at the end of the serialized bytes */
|
||||
@CordaSerializable
|
||||
data class Command(val value: CommandData, val signers: List<CompositeKey>) {
|
||||
data class Command(val value: CommandData, val signers: List<PublicKey>) {
|
||||
init {
|
||||
require(signers.isNotEmpty())
|
||||
}
|
||||
|
||||
constructor(data: CommandData, key: CompositeKey) : this(data, listOf(key))
|
||||
constructor(data: CommandData, key: PublicKey) : this(data, listOf(key))
|
||||
|
||||
private fun commandDataToString() = value.toString().let { if (it.contains("@")) it.replace('$', '.').split("@")[0] else it }
|
||||
override fun toString() = "${commandDataToString()} with pubkeys ${signers.joinToString()}"
|
||||
@ -414,7 +400,7 @@ data class UpgradeCommand(val upgradedContractClass: Class<out UpgradedContract<
|
||||
/** Wraps an object that was signed by a public key, which may be a well known/recognised institutional key. */
|
||||
@CordaSerializable
|
||||
data class AuthenticatedObject<out T : Any>(
|
||||
val signers: List<CompositeKey>,
|
||||
val signers: List<PublicKey>,
|
||||
/** If any public keys were recognised, the looked up institutions are available here */
|
||||
val signingParties: List<Party>,
|
||||
val value: T
|
||||
@ -494,26 +480,54 @@ interface UpgradedContract<in OldState : ContractState, out NewState : ContractS
|
||||
* - Facts generated by oracles which might be reused a lot
|
||||
*/
|
||||
interface Attachment : NamedByHash {
|
||||
|
||||
fun open(): InputStream
|
||||
fun openAsJAR() = JarInputStream(open())
|
||||
|
||||
fun openAsJAR(): JarInputStream {
|
||||
val stream = open()
|
||||
try {
|
||||
return JarInputStream(stream)
|
||||
} catch (t: Throwable) {
|
||||
stream.use { throw t }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the named file case insensitively and copies it to the output stream.
|
||||
*
|
||||
* @throws FileNotFoundException if the given path doesn't exist in the attachment.
|
||||
*/
|
||||
fun extractFile(path: String, outputTo: OutputStream) {
|
||||
val p = path.toLowerCase().split('\\', '/')
|
||||
openAsJAR().use { jar ->
|
||||
while (true) {
|
||||
val e = jar.nextJarEntry ?: break
|
||||
if (e.name.toLowerCase().split('\\', '/') == p) {
|
||||
jar.copyTo(outputTo)
|
||||
return
|
||||
}
|
||||
jar.closeEntry()
|
||||
fun extractFile(path: String, outputTo: OutputStream) = openAsJAR().use { it.extractFile(path, outputTo) }
|
||||
}
|
||||
|
||||
abstract class AbstractAttachment(dataLoader: () -> ByteArray) : Attachment {
|
||||
companion object {
|
||||
fun SerializeAsTokenContext.attachmentDataLoader(id: SecureHash): () -> ByteArray {
|
||||
val storage = serviceHub.storageService.attachments
|
||||
return {
|
||||
val a = storage.openAttachment(id) ?: throw MissingAttachmentsException(listOf(id))
|
||||
if (a is AbstractAttachment) a.attachmentData else a.open().use { it.readBytes() }
|
||||
}
|
||||
}
|
||||
throw FileNotFoundException()
|
||||
}
|
||||
|
||||
protected val attachmentData: ByteArray by lazy(dataLoader)
|
||||
override fun open(): InputStream = attachmentData.inputStream()
|
||||
override fun equals(other: Any?) = other === this || other is Attachment && other.id == this.id
|
||||
override fun hashCode() = id.hashCode()
|
||||
override fun toString() = "${javaClass.simpleName}(id=$id)"
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun JarInputStream.extractFile(path: String, outputTo: OutputStream) {
|
||||
val p = path.toLowerCase().split('\\', '/')
|
||||
while (true) {
|
||||
val e = nextJarEntry ?: break
|
||||
if (!e.isDirectory && e.name.toLowerCase().split('\\', '/') == p) {
|
||||
copyTo(outputTo)
|
||||
return
|
||||
}
|
||||
closeEntry()
|
||||
}
|
||||
throw FileNotFoundException(path)
|
||||
}
|
||||
|
@ -1,17 +1,14 @@
|
||||
package net.corda.core.contracts
|
||||
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import java.security.PublicKey
|
||||
|
||||
/** Defines transaction build & validation logic for a specific transaction type */
|
||||
@CordaSerializable
|
||||
sealed class TransactionType {
|
||||
override fun equals(other: Any?) = other?.javaClass == javaClass
|
||||
override fun hashCode() = javaClass.name.hashCode()
|
||||
|
||||
/**
|
||||
* Check that the transaction is valid based on:
|
||||
* - General platform rules
|
||||
@ -23,16 +20,16 @@ sealed class TransactionType {
|
||||
fun verify(tx: LedgerTransaction) {
|
||||
require(tx.notary != null || tx.timestamp == null) { "Transactions with timestamps must be notarised." }
|
||||
val duplicates = detectDuplicateInputs(tx)
|
||||
if (duplicates.isNotEmpty()) throw TransactionVerificationException.DuplicateInputStates(tx, duplicates)
|
||||
if (duplicates.isNotEmpty()) throw TransactionVerificationException.DuplicateInputStates(tx.id, duplicates)
|
||||
val missing = verifySigners(tx)
|
||||
if (missing.isNotEmpty()) throw TransactionVerificationException.SignersMissing(tx, missing.toList())
|
||||
if (missing.isNotEmpty()) throw TransactionVerificationException.SignersMissing(tx.id, missing.toList())
|
||||
verifyTransaction(tx)
|
||||
}
|
||||
|
||||
/** Check that the list of signers includes all the necessary keys */
|
||||
fun verifySigners(tx: LedgerTransaction): Set<CompositeKey> {
|
||||
fun verifySigners(tx: LedgerTransaction): Set<PublicKey> {
|
||||
val notaryKey = tx.inputs.map { it.state.notary.owningKey }.toSet()
|
||||
if (notaryKey.size > 1) throw TransactionVerificationException.MoreThanOneNotary(tx)
|
||||
if (notaryKey.size > 1) throw TransactionVerificationException.MoreThanOneNotary(tx.id)
|
||||
|
||||
val requiredKeys = getRequiredSigners(tx) + notaryKey
|
||||
val missing = requiredKeys - tx.mustSign
|
||||
@ -57,15 +54,15 @@ sealed class TransactionType {
|
||||
* Return the list of public keys that that require signatures for the transaction type.
|
||||
* Note: the notary key is checked separately for all transactions and need not be included.
|
||||
*/
|
||||
abstract fun getRequiredSigners(tx: LedgerTransaction): Set<CompositeKey>
|
||||
abstract fun getRequiredSigners(tx: LedgerTransaction): Set<PublicKey>
|
||||
|
||||
/** Implement type specific transaction validation logic */
|
||||
abstract fun verifyTransaction(tx: LedgerTransaction)
|
||||
|
||||
/** A general transaction type where transaction validity is determined by custom contract code */
|
||||
class General : TransactionType() {
|
||||
object General : TransactionType() {
|
||||
/** Just uses the default [TransactionBuilder] with no special logic */
|
||||
class Builder(notary: Party?) : TransactionBuilder(General(), notary) {}
|
||||
class Builder(notary: Party?) : TransactionBuilder(General, notary)
|
||||
|
||||
override fun verifyTransaction(tx: LedgerTransaction) {
|
||||
verifyNoNotaryChange(tx)
|
||||
@ -84,7 +81,7 @@ sealed class TransactionType {
|
||||
if (tx.notary != null && tx.inputs.isNotEmpty()) {
|
||||
tx.outputs.forEach {
|
||||
if (it.notary != tx.notary) {
|
||||
throw TransactionVerificationException.NotaryChangeInWrongTransactionType(tx, it.notary)
|
||||
throw TransactionVerificationException.NotaryChangeInWrongTransactionType(tx.id, tx.notary, it.notary)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -93,13 +90,14 @@ sealed class TransactionType {
|
||||
private fun verifyEncumbrances(tx: LedgerTransaction) {
|
||||
// Validate that all encumbrances exist within the set of input states.
|
||||
val encumberedInputs = tx.inputs.filter { it.state.encumbrance != null }
|
||||
encumberedInputs.forEach { encumberedInput ->
|
||||
encumberedInputs.forEach { (state, ref) ->
|
||||
val encumbranceStateExists = tx.inputs.any {
|
||||
it.ref.txhash == encumberedInput.ref.txhash && it.ref.index == encumberedInput.state.encumbrance
|
||||
it.ref.txhash == ref.txhash && it.ref.index == state.encumbrance
|
||||
}
|
||||
if (!encumbranceStateExists) {
|
||||
throw TransactionVerificationException.TransactionMissingEncumbranceException(
|
||||
tx, encumberedInput.state.encumbrance!!,
|
||||
tx.id,
|
||||
state.encumbrance!!,
|
||||
TransactionVerificationException.Direction.INPUT
|
||||
)
|
||||
}
|
||||
@ -111,7 +109,8 @@ sealed class TransactionType {
|
||||
val encumbranceIndex = output.encumbrance ?: continue
|
||||
if (encumbranceIndex == i || encumbranceIndex >= tx.outputs.size) {
|
||||
throw TransactionVerificationException.TransactionMissingEncumbranceException(
|
||||
tx, encumbranceIndex,
|
||||
tx.id,
|
||||
encumbranceIndex,
|
||||
TransactionVerificationException.Direction.OUTPUT)
|
||||
}
|
||||
}
|
||||
@ -129,7 +128,7 @@ sealed class TransactionType {
|
||||
try {
|
||||
contract.verify(ctx)
|
||||
} catch(e: Throwable) {
|
||||
throw TransactionVerificationException.ContractRejection(tx, contract, e)
|
||||
throw TransactionVerificationException.ContractRejection(tx.id, contract, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -141,12 +140,12 @@ sealed class TransactionType {
|
||||
* A special transaction type for reassigning a notary for a state. Validation does not involve running
|
||||
* any contract code, it just checks that the states are unmodified apart from the notary field.
|
||||
*/
|
||||
class NotaryChange : TransactionType() {
|
||||
object NotaryChange : TransactionType() {
|
||||
/**
|
||||
* A transaction builder that automatically sets the transaction type to [NotaryChange]
|
||||
* and adds the list of participants to the signers set for every input state.
|
||||
*/
|
||||
class Builder(notary: Party) : TransactionBuilder(NotaryChange(), notary) {
|
||||
class Builder(notary: Party) : TransactionBuilder(NotaryChange, notary) {
|
||||
override fun addInputState(stateAndRef: StateAndRef<*>) {
|
||||
signers.addAll(stateAndRef.state.data.participants)
|
||||
super.addInputState(stateAndRef)
|
||||
@ -167,7 +166,7 @@ sealed class TransactionType {
|
||||
}
|
||||
check(tx.commands.isEmpty())
|
||||
} catch (e: IllegalStateException) {
|
||||
throw TransactionVerificationException.InvalidNotaryChange(tx)
|
||||
throw TransactionVerificationException.InvalidNotaryChange(tx.id)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,10 @@
|
||||
package net.corda.core.contracts
|
||||
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowException
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import java.security.PublicKey
|
||||
import java.util.*
|
||||
|
||||
// TODO: Consider moving this out of the core module and providing a different way for unit tests to test contracts.
|
||||
@ -91,31 +90,29 @@ class TransactionResolutionException(val hash: SecureHash) : FlowException() {
|
||||
override fun toString(): String = "Transaction resolution failure for $hash"
|
||||
}
|
||||
|
||||
class AttachmentResolutionException(val hash : SecureHash) : FlowException() {
|
||||
class AttachmentResolutionException(val hash: SecureHash) : FlowException() {
|
||||
override fun toString(): String = "Attachment resolution failure for $hash"
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
class TransactionConflictException(val conflictRef: StateRef, val tx1: LedgerTransaction, val tx2: LedgerTransaction) : Exception()
|
||||
|
||||
sealed class TransactionVerificationException(val tx: LedgerTransaction, cause: Throwable?) : FlowException(cause) {
|
||||
class ContractRejection(tx: LedgerTransaction, val contract: Contract, cause: Throwable?) : TransactionVerificationException(tx, cause)
|
||||
class MoreThanOneNotary(tx: LedgerTransaction) : TransactionVerificationException(tx, null)
|
||||
class SignersMissing(tx: LedgerTransaction, val missing: List<CompositeKey>) : TransactionVerificationException(tx, null) {
|
||||
sealed class TransactionVerificationException(val txId: SecureHash, cause: Throwable?) : FlowException(cause) {
|
||||
class ContractRejection(txId: SecureHash, val contract: Contract, cause: Throwable?) : TransactionVerificationException(txId, cause)
|
||||
class MoreThanOneNotary(txId: SecureHash) : TransactionVerificationException(txId, null)
|
||||
class SignersMissing(txId: SecureHash, val missing: List<PublicKey>) : TransactionVerificationException(txId, null) {
|
||||
override fun toString(): String = "Signers missing: ${missing.joinToString()}"
|
||||
}
|
||||
class DuplicateInputStates(tx: LedgerTransaction, val duplicates: Set<StateRef>) : TransactionVerificationException(tx, null) {
|
||||
|
||||
class DuplicateInputStates(txId: SecureHash, val duplicates: Set<StateRef>) : TransactionVerificationException(txId, null) {
|
||||
override fun toString(): String = "Duplicate inputs: ${duplicates.joinToString()}"
|
||||
}
|
||||
|
||||
class InvalidNotaryChange(tx: LedgerTransaction) : TransactionVerificationException(tx, null)
|
||||
class NotaryChangeInWrongTransactionType(tx: LedgerTransaction, val outputNotary: Party) : TransactionVerificationException(tx, null) {
|
||||
class InvalidNotaryChange(txId: SecureHash) : TransactionVerificationException(txId, null)
|
||||
class NotaryChangeInWrongTransactionType(txId: SecureHash, val txNotary: Party, val outputNotary: Party) : TransactionVerificationException(txId, null) {
|
||||
override fun toString(): String {
|
||||
return "Found unexpected notary change in transaction. Tx notary: ${tx.notary}, found: $outputNotary"
|
||||
return "Found unexpected notary change in transaction. Tx notary: $txNotary, found: $outputNotary"
|
||||
}
|
||||
}
|
||||
|
||||
class TransactionMissingEncumbranceException(tx: LedgerTransaction, val missing: Int, val inOut: Direction) : TransactionVerificationException(tx, null) {
|
||||
class TransactionMissingEncumbranceException(txId: SecureHash, val missing: Int, val inOut: Direction) : TransactionVerificationException(txId, null) {
|
||||
override val message: String get() = "Missing required encumbrance $missing in $inOut"
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,7 @@ import java.util.*
|
||||
/**
|
||||
* Compose a number of clauses, such that all of the clauses must run for verification to pass.
|
||||
*/
|
||||
open class AllOf<S : ContractState, C : CommandData, K : Any>(firstClause: Clause<S, C, K>, vararg remainingClauses: Clause<S, C, K>) : CompositeClause<S, C, K>() {
|
||||
open class AllOf<S : ContractState, C : CommandData, K : Any>(firstClause: Clause<S, C, K>, vararg remainingClauses: Clause<S, C, K>) : CompositeClause<S, C, K>() {
|
||||
override val clauses = ArrayList<Clause<S, C, K>>()
|
||||
|
||||
init {
|
||||
@ -19,7 +19,7 @@ open class AllOf<S : ContractState, C : CommandData, K : Any>(firstClause: Claus
|
||||
|
||||
override fun matchedClauses(commands: List<AuthenticatedObject<C>>): List<Clause<S, C, K>> {
|
||||
clauses.forEach { clause ->
|
||||
check(clause.matches(commands)) { "Failed to match clause ${clause}" }
|
||||
check(clause.matches(commands)) { "Failed to match clause $clause" }
|
||||
}
|
||||
return clauses
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ fun <C : CommandData> verifyClause(tx: TransactionForContract,
|
||||
commands: List<AuthenticatedObject<C>>) {
|
||||
if (Clause.log.isTraceEnabled) {
|
||||
clause.getExecutionPath(commands).forEach {
|
||||
Clause.log.trace("Tx ${tx.origHash} clause: ${clause}")
|
||||
Clause.log.trace("Tx ${tx.origHash} clause: $clause")
|
||||
}
|
||||
}
|
||||
val matchedCommands = clause.verify(tx, tx.inputs, tx.outputs, commands, null)
|
||||
|
@ -8,8 +8,8 @@ import net.corda.core.contracts.TransactionForContract
|
||||
/**
|
||||
* Filter the states that are passed through to the wrapped clause, to restrict them to a specific type.
|
||||
*/
|
||||
class FilterOn<S : ContractState, C : CommandData, K : Any>(val clause: Clause<S, C, K>,
|
||||
val filterStates: (List<ContractState>) -> List<S>) : Clause<ContractState, C, K>() {
|
||||
class FilterOn<S : ContractState, C : CommandData, in K : Any>(val clause: Clause<S, C, K>,
|
||||
val filterStates: (List<ContractState>) -> List<S>) : Clause<ContractState, C, K>() {
|
||||
override val requiredCommands: Set<Class<out CommandData>>
|
||||
= clause.requiredCommands
|
||||
|
||||
|
@ -4,18 +4,13 @@ import net.corda.core.contracts.AuthenticatedObject
|
||||
import net.corda.core.contracts.CommandData
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.TransactionForContract
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Compose a number of clauses, such that the first match is run, and it errors if none is run.
|
||||
*/
|
||||
@Deprecated("Use FirstOf instead")
|
||||
class FirstComposition<S : ContractState, C : CommandData, K : Any>(val firstClause: Clause<S, C, K>, vararg remainingClauses: Clause<S, C, K>) : CompositeClause<S, C, K>() {
|
||||
companion object {
|
||||
val logger = loggerFor<FirstComposition<*, *, *>>()
|
||||
}
|
||||
|
||||
class FirstComposition<S : ContractState, C : CommandData, K : Any>(firstClause: Clause<S, C, K>, vararg remainingClauses: Clause<S, C, K>) : CompositeClause<S, C, K>() {
|
||||
override val clauses = ArrayList<Clause<S, C, K>>()
|
||||
override fun matchedClauses(commands: List<AuthenticatedObject<C>>): List<Clause<S, C, K>> = listOf(clauses.first { it.matches(commands) })
|
||||
|
||||
|
@ -10,7 +10,7 @@ import java.util.*
|
||||
/**
|
||||
* Compose a number of clauses, such that the first match is run, and it errors if none is run.
|
||||
*/
|
||||
class FirstOf<S : ContractState, C : CommandData, K : Any>(val firstClause: Clause<S, C, K>, vararg remainingClauses: Clause<S, C, K>) : CompositeClause<S, C, K>() {
|
||||
class FirstOf<S : ContractState, C : CommandData, K : Any>(firstClause: Clause<S, C, K>, vararg remainingClauses: Clause<S, C, K>) : CompositeClause<S, C, K>() {
|
||||
companion object {
|
||||
val logger = loggerFor<FirstOf<*, *, *>>()
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
@ -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
|
||||
}
|
@ -1,151 +1,146 @@
|
||||
package net.corda.core.crypto
|
||||
|
||||
import net.corda.core.crypto.CompositeKey.Leaf
|
||||
import net.corda.core.crypto.CompositeKey.Node
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import java.security.PublicKey
|
||||
|
||||
/**
|
||||
* A tree data structure that enables the representation of composite public keys.
|
||||
* Notice that with that implementation CompositeKey extends PublicKey. Leaves are represented by single public keys.
|
||||
*
|
||||
* In the simplest case it may just contain a single node encapsulating a [PublicKey] – a [Leaf].
|
||||
*
|
||||
* For more complex scenarios, such as *"Both Alice and Bob need to sign to consume a state S"*, we can represent
|
||||
* the requirement by creating a tree with a root [Node], and Alice and Bob as children – [Leaf]s.
|
||||
* For complex scenarios, such as *"Both Alice and Bob need to sign to consume a state S"*, we can represent
|
||||
* the requirement by creating a tree with a root [CompositeKey], and Alice and Bob as children.
|
||||
* The root node would specify *weights* for each of its children and a *threshold* – the minimum total weight required
|
||||
* (e.g. the minimum number of child signatures required) to satisfy the tree signature requirement.
|
||||
*
|
||||
* Using these constructs we can express e.g. 1 of N (OR) or N of N (AND) signature requirements. By nesting we can
|
||||
* create multi-level requirements such as *"either the CEO or 3 of 5 of his assistants need to sign"*.
|
||||
*
|
||||
* [CompositeKey] maintains a list of [NodeAndWeight]s which holds child subtree with associated weight carried by child node signatures.
|
||||
*
|
||||
* The [threshold] specifies the minimum total weight required (in the simple case – the minimum number of child
|
||||
* signatures required) to satisfy the sub-tree rooted at this node.
|
||||
*/
|
||||
@CordaSerializable
|
||||
sealed class CompositeKey {
|
||||
/** Checks whether [keys] match a sufficient amount of leaf nodes */
|
||||
abstract fun isFulfilledBy(keys: Iterable<PublicKey>): Boolean
|
||||
|
||||
fun isFulfilledBy(key: PublicKey) = isFulfilledBy(setOf(key))
|
||||
|
||||
/** Returns all [PublicKey]s contained within the tree leaves */
|
||||
abstract val keys: Set<PublicKey>
|
||||
|
||||
/** Checks whether any of the given [keys] matches a leaf on the tree */
|
||||
fun containsAny(otherKeys: Iterable<PublicKey>) = keys.intersect(otherKeys).isNotEmpty()
|
||||
class CompositeKey private constructor (val threshold: Int,
|
||||
children: List<NodeAndWeight>) : PublicKey {
|
||||
val children = children.sorted()
|
||||
init {
|
||||
require (children.size == children.toSet().size) { "Trying to construct CompositeKey with duplicated child nodes." }
|
||||
// If we want PublicKey we only keep one key, otherwise it will lead to semantically equivalent trees but having different structures.
|
||||
require(children.size > 1) { "Cannot construct CompositeKey with only one child node." }
|
||||
}
|
||||
|
||||
/**
|
||||
* This is generated by serializing the composite key with Kryo, and encoding the resulting bytes in base58.
|
||||
* A custom serialization format is being used.
|
||||
*
|
||||
* TODO: follow the crypto-conditions ASN.1 spec, some changes are needed to be compatible with the condition
|
||||
* structure, e.g. mapping a PublicKey to a condition with the specific feature (ED25519).
|
||||
* Holds node - weight pairs for a CompositeKey. Ordered first by weight, then by node's hashCode.
|
||||
*/
|
||||
fun toBase58String(): String = Base58.encode(this.serialize().bytes)
|
||||
@CordaSerializable
|
||||
data class NodeAndWeight(val node: PublicKey, val weight: Int): Comparable<NodeAndWeight> {
|
||||
override fun compareTo(other: NodeAndWeight): Int {
|
||||
if (weight == other.weight) {
|
||||
return node.hashCode().compareTo(other.node.hashCode())
|
||||
}
|
||||
else return weight.compareTo(other.weight)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun parseFromBase58(encoded: String) = Base58.decode(encoded).deserialize<CompositeKey>()
|
||||
}
|
||||
|
||||
/** The leaf node of the tree – a wrapper around a [PublicKey] primitive */
|
||||
class Leaf(val publicKey: PublicKey) : CompositeKey() {
|
||||
override fun isFulfilledBy(keys: Iterable<PublicKey>) = publicKey in keys
|
||||
|
||||
override val keys: Set<PublicKey>
|
||||
get() = setOf(publicKey)
|
||||
|
||||
// TODO: remove once data class inheritance is enabled
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return this === other || other is Leaf && other.publicKey == this.publicKey
|
||||
}
|
||||
|
||||
override fun hashCode() = publicKey.hashCode()
|
||||
|
||||
override fun toString() = publicKey.toStringShort()
|
||||
// TODO: Get the design standardised and from there define a recognised name
|
||||
val ALGORITHM = "X-Corda-CompositeKey"
|
||||
// TODO: We should be using a well defined format.
|
||||
val FORMAT = "X-Corda-Kryo"
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a node in the key tree. It maintains a list of child nodes – sub-trees, and associated
|
||||
* [weights] carried by child node signatures.
|
||||
*
|
||||
* The [threshold] specifies the minimum total weight required (in the simple case – the minimum number of child
|
||||
* signatures required) to satisfy the sub-tree rooted at this node.
|
||||
* Takes single PublicKey and checks if CompositeKey requirements hold for that key.
|
||||
*/
|
||||
class Node(val threshold: Int,
|
||||
val children: List<CompositeKey>,
|
||||
val weights: List<Int>) : CompositeKey() {
|
||||
fun isFulfilledBy(key: PublicKey) = isFulfilledBy(setOf(key))
|
||||
|
||||
override fun isFulfilledBy(keys: Iterable<PublicKey>): Boolean {
|
||||
val totalWeight = children.mapIndexed { i, childNode ->
|
||||
if (childNode.isFulfilledBy(keys)) weights[i] else 0
|
||||
}.sum()
|
||||
override fun getAlgorithm() = ALGORITHM
|
||||
override fun getEncoded(): ByteArray = this.serialize().bytes
|
||||
override fun getFormat() = FORMAT
|
||||
|
||||
return totalWeight >= threshold
|
||||
}
|
||||
|
||||
override val keys: Set<PublicKey>
|
||||
get() = children.flatMap { it.keys }.toSet()
|
||||
|
||||
// Auto-generated. TODO: remove once data class inheritance is enabled
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other?.javaClass != javaClass) return false
|
||||
|
||||
other as Node
|
||||
|
||||
if (threshold != other.threshold) return false
|
||||
if (weights != other.weights) return false
|
||||
if (children != other.children) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = threshold
|
||||
result = 31 * result + weights.hashCode()
|
||||
result = 31 * result + children.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString() = "(${children.joinToString()})"
|
||||
/**
|
||||
* Function checks if the public keys corresponding to the signatures are matched against the leaves of the composite
|
||||
* key tree in question, and the total combined weight of all children is calculated for every intermediary node.
|
||||
* If all thresholds are satisfied, the composite key requirement is considered to be met.
|
||||
*/
|
||||
fun isFulfilledBy(keysToCheck: Iterable<PublicKey>): Boolean {
|
||||
if (keysToCheck.any { it is CompositeKey } ) return false
|
||||
val totalWeight = children.map { (node, weight) ->
|
||||
if (node is CompositeKey) {
|
||||
if (node.isFulfilledBy(keysToCheck)) weight else 0
|
||||
} else {
|
||||
if (keysToCheck.contains(node)) weight else 0
|
||||
}
|
||||
}.sum()
|
||||
return totalWeight >= threshold
|
||||
}
|
||||
|
||||
/** A helper class for building a [CompositeKey.Node]. */
|
||||
class Builder() {
|
||||
private val children: MutableList<CompositeKey> = mutableListOf()
|
||||
private val weights: MutableList<Int> = mutableListOf()
|
||||
/**
|
||||
* Set of all leaf keys of that CompositeKey.
|
||||
*/
|
||||
val leafKeys: Set<PublicKey>
|
||||
get() = children.flatMap { it.node.keys }.toSet() // Uses PublicKey.keys extension.
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is CompositeKey) return false
|
||||
if (threshold != other.threshold) return false
|
||||
if (children != other.children) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = threshold
|
||||
result = 31 * result + children.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString() = "(${children.joinToString()})"
|
||||
|
||||
/** A helper class for building a [CompositeKey]. */
|
||||
class Builder {
|
||||
private val children: MutableList<NodeAndWeight> = mutableListOf()
|
||||
|
||||
/** Adds a child [CompositeKey] node. Specifying a [weight] for the child is optional and will default to 1. */
|
||||
fun addKey(key: CompositeKey, weight: Int = 1): Builder {
|
||||
children.add(key)
|
||||
weights.add(weight)
|
||||
fun addKey(key: PublicKey, weight: Int = 1): Builder {
|
||||
children.add(NodeAndWeight(key, weight))
|
||||
return this
|
||||
}
|
||||
|
||||
fun addKeys(vararg keys: CompositeKey): Builder {
|
||||
fun addKeys(vararg keys: PublicKey): Builder {
|
||||
keys.forEach { addKey(it) }
|
||||
return this
|
||||
}
|
||||
|
||||
fun addKeys(keys: List<CompositeKey>): Builder = addKeys(*keys.toTypedArray())
|
||||
fun addKeys(keys: List<PublicKey>): Builder = addKeys(*keys.toTypedArray())
|
||||
|
||||
/**
|
||||
* Builds the [CompositeKey.Node]. If [threshold] is not specified, it will default to
|
||||
* Builds the [CompositeKey]. If [threshold] is not specified, it will default to
|
||||
* the size of the children, effectively generating an "N of N" requirement.
|
||||
* During process removes single keys wrapped in [CompositeKey] and enforces ordering on child nodes.
|
||||
*/
|
||||
fun build(threshold: Int? = null): CompositeKey.Node {
|
||||
return Node(threshold ?: children.size, children.toList(), weights.toList())
|
||||
@Throws(IllegalArgumentException::class)
|
||||
fun build(threshold: Int? = null): PublicKey {
|
||||
val n = children.size
|
||||
if (n > 1)
|
||||
return CompositeKey(threshold ?: n, children)
|
||||
else if (n == 1) {
|
||||
require(threshold == null || threshold == children.first().weight)
|
||||
{ "Trying to build invalid CompositeKey, threshold value different than weight of single child node." }
|
||||
return children.first().node // We can assume that this node is a correct CompositeKey.
|
||||
}
|
||||
else throw IllegalArgumentException("Trying to build CompositeKey without child nodes.")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the enclosed [PublicKey] for a [CompositeKey] with a single leaf node
|
||||
*
|
||||
* @throws IllegalArgumentException if the [CompositeKey] contains more than one node
|
||||
*/
|
||||
val singleKey: PublicKey
|
||||
get() = keys.singleOrNull() ?: throw IllegalStateException("The key is composed of more than one PublicKey primitive")
|
||||
}
|
||||
|
||||
/** Returns the set of all [PublicKey]s contained in the leaves of the [CompositeKey]s */
|
||||
val Iterable<CompositeKey>.keys: Set<PublicKey>
|
||||
/**
|
||||
* Expands all [CompositeKey]s present in PublicKey iterable to set of single [PublicKey]s.
|
||||
* If an element of the set is a single PublicKey it gives just that key, if it is a [CompositeKey] it returns all leaf
|
||||
* keys for that composite element.
|
||||
*/
|
||||
val Iterable<PublicKey>.expandedCompositeKeys: Set<PublicKey>
|
||||
get() = flatMap { it.keys }.toSet()
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
@ -1,15 +1,42 @@
|
||||
package net.corda.core.crypto
|
||||
|
||||
import net.i2p.crypto.eddsa.EdDSAEngine
|
||||
import net.i2p.crypto.eddsa.EdDSAKey
|
||||
import net.corda.core.random63BitValue
|
||||
import net.i2p.crypto.eddsa.*
|
||||
import net.i2p.crypto.eddsa.math.GroupElement
|
||||
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec
|
||||
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable
|
||||
import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec
|
||||
import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec
|
||||
import org.bouncycastle.asn1.ASN1EncodableVector
|
||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier
|
||||
import org.bouncycastle.asn1.DERSequence
|
||||
import org.bouncycastle.asn1.bc.BCObjectIdentifiers
|
||||
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers
|
||||
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.bouncycastle.asn1.x509.*
|
||||
import org.bouncycastle.asn1.x9.X9ObjectIdentifiers
|
||||
import org.bouncycastle.cert.bc.BcX509ExtensionUtils
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
|
||||
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder
|
||||
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey
|
||||
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey
|
||||
import org.bouncycastle.jcajce.provider.util.AsymmetricKeyInfoConverter
|
||||
import org.bouncycastle.jce.ECNamedCurveTable
|
||||
import org.bouncycastle.jce.interfaces.ECKey
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||
import org.bouncycastle.pkcs.PKCS10CertificationRequest
|
||||
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder
|
||||
import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider
|
||||
import org.bouncycastle.pqc.jcajce.spec.SPHINCS256KeyGenParameterSpec
|
||||
import java.math.BigInteger
|
||||
import java.security.*
|
||||
import java.security.KeyFactory
|
||||
import java.security.KeyPairGenerator
|
||||
import java.security.cert.X509Certificate
|
||||
import java.security.spec.InvalidKeySpecException
|
||||
import java.security.spec.PKCS8EncodedKeySpec
|
||||
import java.security.spec.X509EncodedKeySpec
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* This object controls and provides the available and supported signature schemes for Corda.
|
||||
@ -25,58 +52,58 @@ import java.security.spec.X509EncodedKeySpec
|
||||
* </ul>
|
||||
*/
|
||||
object Crypto {
|
||||
|
||||
/**
|
||||
* RSA_SHA256 signature scheme using SHA256 as hash algorithm and MGF1 (with SHA256) as mask generation function.
|
||||
* Note: Recommended key size >= 3072 bits.
|
||||
*/
|
||||
private val RSA_SHA256 = SignatureScheme(
|
||||
val RSA_SHA256 = SignatureScheme(
|
||||
1,
|
||||
"RSA_SHA256",
|
||||
PKCSObjectIdentifiers.id_RSASSA_PSS,
|
||||
BouncyCastleProvider.PROVIDER_NAME,
|
||||
"RSA",
|
||||
Signature.getInstance("SHA256WITHRSAANDMGF1", "BC"),
|
||||
KeyFactory.getInstance("RSA", "BC"),
|
||||
KeyPairGenerator.getInstance("RSA", "BC"),
|
||||
"SHA256WITHRSAANDMGF1",
|
||||
null,
|
||||
3072,
|
||||
"RSA_SHA256 signature scheme using SHA256 as hash algorithm and MGF1 (with SHA256) as mask generation function."
|
||||
)
|
||||
|
||||
/** ECDSA signature scheme using the secp256k1 Koblitz curve. */
|
||||
private val ECDSA_SECP256K1_SHA256 = SignatureScheme(
|
||||
val ECDSA_SECP256K1_SHA256 = SignatureScheme(
|
||||
2,
|
||||
"ECDSA_SECP256K1_SHA256",
|
||||
X9ObjectIdentifiers.ecdsa_with_SHA256,
|
||||
BouncyCastleProvider.PROVIDER_NAME,
|
||||
"ECDSA",
|
||||
Signature.getInstance("SHA256withECDSA", "BC"),
|
||||
KeyFactory.getInstance("ECDSA", "BC"),
|
||||
KeyPairGenerator.getInstance("ECDSA", "BC"),
|
||||
"SHA256withECDSA",
|
||||
ECNamedCurveTable.getParameterSpec("secp256k1"),
|
||||
256,
|
||||
"ECDSA signature scheme using the secp256k1 Koblitz curve."
|
||||
)
|
||||
|
||||
/** ECDSA signature scheme using the secp256r1 (NIST P-256) curve. */
|
||||
private val ECDSA_SECP256R1_SHA256 = SignatureScheme(
|
||||
val ECDSA_SECP256R1_SHA256 = SignatureScheme(
|
||||
3,
|
||||
"ECDSA_SECP256R1_SHA256",
|
||||
X9ObjectIdentifiers.ecdsa_with_SHA256,
|
||||
BouncyCastleProvider.PROVIDER_NAME,
|
||||
"ECDSA",
|
||||
Signature.getInstance("SHA256withECDSA", "BC"),
|
||||
KeyFactory.getInstance("ECDSA", "BC"),
|
||||
KeyPairGenerator.getInstance("ECDSA", "BC"),
|
||||
"SHA256withECDSA",
|
||||
ECNamedCurveTable.getParameterSpec("secp256r1"),
|
||||
256,
|
||||
"ECDSA signature scheme using the secp256r1 (NIST P-256) curve."
|
||||
)
|
||||
|
||||
/** EdDSA signature scheme using the ed255519 twisted Edwards curve. */
|
||||
private val EDDSA_ED25519_SHA512 = SignatureScheme(
|
||||
val EDDSA_ED25519_SHA512 = SignatureScheme(
|
||||
4,
|
||||
"EDDSA_ED25519_SHA512",
|
||||
"EdDSA",
|
||||
EdDSAEngine(),
|
||||
EdDSAKeyFactory(),
|
||||
net.i2p.crypto.eddsa.KeyPairGenerator(), // EdDSA engine uses a custom KeyPairGenerator Vs BouncyCastle.
|
||||
EdDSANamedCurveTable.getByName("ed25519-sha-512"),
|
||||
ASN1ObjectIdentifier("1.3.101.112"),
|
||||
// We added EdDSA to bouncy castle for certificate signing.
|
||||
BouncyCastleProvider.PROVIDER_NAME,
|
||||
EdDSAKey.KEY_ALGORITHM,
|
||||
EdDSAEngine.SIGNATURE_ALGORITHM,
|
||||
EdDSANamedCurveTable.getByName("ED25519"),
|
||||
256,
|
||||
"EdDSA signature scheme using the ed25519 twisted Edwards curve."
|
||||
)
|
||||
@ -85,13 +112,13 @@ object Crypto {
|
||||
* SPHINCS-256 hash-based signature scheme. It provides 128bit security against post-quantum attackers
|
||||
* at the cost of larger key sizes and loss of compatibility.
|
||||
*/
|
||||
private val SPHINCS256_SHA256 = SignatureScheme(
|
||||
val SPHINCS256_SHA256 = SignatureScheme(
|
||||
5,
|
||||
"SPHINCS-256_SHA512",
|
||||
"SPHINCS-256",
|
||||
Signature.getInstance("SHA512WITHSPHINCS256", "BCPQC"),
|
||||
KeyFactory.getInstance("SPHINCS256", "BCPQC"),
|
||||
KeyPairGenerator.getInstance("SPHINCS256", "BCPQC"),
|
||||
BCObjectIdentifiers.sphincs256_with_SHA512,
|
||||
"BCPQC",
|
||||
"SPHINCS256",
|
||||
"SHA512WITHSPHINCS256",
|
||||
SPHINCS256KeyGenParameterSpec(SPHINCS256KeyGenParameterSpec.SHA512_256),
|
||||
256,
|
||||
"SPHINCS-256 hash-based signature scheme. It provides 128bit security against post-quantum attackers " +
|
||||
@ -99,20 +126,38 @@ object Crypto {
|
||||
)
|
||||
|
||||
/** Our default signature scheme if no algorithm is specified (e.g. for key generation). */
|
||||
private val DEFAULT_SIGNATURE_SCHEME = EDDSA_ED25519_SHA512
|
||||
val DEFAULT_SIGNATURE_SCHEME = EDDSA_ED25519_SHA512
|
||||
|
||||
/**
|
||||
* Supported digital signature schemes.
|
||||
* Note: Only the schemes added in this map will be supported (see [Crypto]).
|
||||
* Do not forget to add the DEFAULT_SIGNATURE_SCHEME as well.
|
||||
*/
|
||||
private val supportedSignatureSchemes = mapOf(
|
||||
RSA_SHA256.schemeCodeName to RSA_SHA256,
|
||||
ECDSA_SECP256K1_SHA256.schemeCodeName to ECDSA_SECP256K1_SHA256,
|
||||
ECDSA_SECP256R1_SHA256.schemeCodeName to ECDSA_SECP256R1_SHA256,
|
||||
EDDSA_ED25519_SHA512.schemeCodeName to EDDSA_ED25519_SHA512,
|
||||
SPHINCS256_SHA256.schemeCodeName to SPHINCS256_SHA256
|
||||
)
|
||||
val supportedSignatureSchemes = listOf(
|
||||
RSA_SHA256,
|
||||
ECDSA_SECP256K1_SHA256,
|
||||
ECDSA_SECP256R1_SHA256,
|
||||
EDDSA_ED25519_SHA512,
|
||||
SPHINCS256_SHA256
|
||||
).associateBy { it.schemeCodeName }
|
||||
|
||||
// This map is required to defend against users that forcibly call Security.addProvider / Security.removeProvider
|
||||
// that could cause unexpected and suspicious behaviour.
|
||||
// i.e. if someone removes a Provider and then he/she adds a new one with the same name.
|
||||
// The val is private to avoid any harmful state changes.
|
||||
private val providerMap: Map<String, Provider> = mapOf(
|
||||
BouncyCastleProvider.PROVIDER_NAME to getBouncyCastleProvider(),
|
||||
"BCPQC" to BouncyCastlePQCProvider()) // unfortunately, provider's name is not final in BouncyCastlePQCProvider, so we explicitly set it.
|
||||
|
||||
private fun getBouncyCastleProvider() = BouncyCastleProvider().apply {
|
||||
putAll(EdDSASecurityProvider())
|
||||
addKeyInfoConverter(EDDSA_ED25519_SHA512.signatureOID, KeyInfoConverter(EDDSA_ED25519_SHA512))
|
||||
}
|
||||
|
||||
init {
|
||||
// This registration is needed for reading back EdDSA key from java keystore.
|
||||
// TODO: Find a way to make JKS work with bouncy castle provider or implement our own provide so we don't have to register bouncy castle provider.
|
||||
Security.addProvider(getBouncyCastleProvider())
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory pattern to retrieve the corresponding [SignatureScheme] based on the type of the [String] input.
|
||||
@ -122,17 +167,7 @@ object Crypto {
|
||||
* @return a currently supported SignatureScheme.
|
||||
* @throws IllegalArgumentException if the requested signature scheme is not supported.
|
||||
*/
|
||||
private fun findSignatureScheme(schemeCodeName: String): SignatureScheme = supportedSignatureSchemes[schemeCodeName] ?: throw IllegalArgumentException("Unsupported key/algorithm for metadata schemeCodeName: ${schemeCodeName}")
|
||||
|
||||
/**
|
||||
* Retrieve the corresponding [SignatureScheme] based on the type of the input [KeyPair].
|
||||
* Note that only the Corda platform standard schemes are supported (see [Crypto]).
|
||||
* This function is usually called when requiring to sign signatures.
|
||||
* @param keyPair a cryptographic [KeyPair].
|
||||
* @return a currently supported SignatureScheme or null.
|
||||
* @throws IllegalArgumentException if the requested signature scheme is not supported.
|
||||
*/
|
||||
private fun findSignatureScheme(keyPair: KeyPair): SignatureScheme = findSignatureScheme(keyPair.private)
|
||||
fun findSignatureScheme(schemeCodeName: String): SignatureScheme = supportedSignatureSchemes[schemeCodeName] ?: throw IllegalArgumentException("Unsupported key/algorithm for metadata schemeCodeName: $schemeCodeName")
|
||||
|
||||
/**
|
||||
* Retrieve the corresponding [SignatureScheme] based on the type of the input [Key].
|
||||
@ -144,46 +179,39 @@ object Crypto {
|
||||
* @return a currently supported SignatureScheme.
|
||||
* @throws IllegalArgumentException if the requested key type is not supported.
|
||||
*/
|
||||
private fun findSignatureScheme(key: Key): SignatureScheme {
|
||||
fun findSignatureScheme(key: Key): SignatureScheme {
|
||||
for (sig in supportedSignatureSchemes.values) {
|
||||
val algorithm = key.algorithm
|
||||
var algorithm = key.algorithm
|
||||
if (algorithm == "EC") algorithm = "ECDSA" // required to read ECC keys from Keystore, because encoding may change algorithm name from ECDSA to EC.
|
||||
if (algorithm == "SPHINCS-256") algorithm = "SPHINCS256" // because encoding may change algorithm name from SPHINCS256 to SPHINCS-256.
|
||||
if (algorithm == sig.algorithmName) {
|
||||
// If more than one ECDSA schemes are supported, we should distinguish between them by checking their curve parameters.
|
||||
// TODO: change 'continue' to 'break' if only one EdDSA curve will be used.
|
||||
if (algorithm == "EdDSA") {
|
||||
if ((key as EdDSAKey).params == sig.algSpec) {
|
||||
if ((key is EdDSAPublicKey && publicKeyOnCurve(sig, key)) || (key is EdDSAPrivateKey && key.params == sig.algSpec)) {
|
||||
return sig
|
||||
} else continue
|
||||
} else break // use continue if in the future we support more than one Edwards curves.
|
||||
} else if (algorithm == "ECDSA") {
|
||||
if ((key as ECKey).parameters == sig.algSpec) {
|
||||
if ((key is BCECPublicKey && publicKeyOnCurve(sig, key)) || (key is BCECPrivateKey && key.parameters == sig.algSpec)) {
|
||||
return sig
|
||||
} else continue
|
||||
} else return sig // it's either RSA_SHA256 or SPHINCS-256.
|
||||
}
|
||||
}
|
||||
throw IllegalArgumentException("Unsupported key/algorithm for the private key: ${key.encoded.toBase58()}")
|
||||
throw IllegalArgumentException("Unsupported key/algorithm for the key: ${key.encoded.toBase58()}")
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the corresponding signature scheme code name based on the type of the input [Key].
|
||||
* See [Crypto] for the supported scheme code names.
|
||||
* @param key either private or public.
|
||||
* @return signatureSchemeCodeName for a [Key].
|
||||
* @throws IllegalArgumentException if the requested key type is not supported.
|
||||
*/
|
||||
fun findSignatureSchemeCodeName(key: Key): String = findSignatureScheme(key).schemeCodeName
|
||||
|
||||
/**
|
||||
* Decode a PKCS8 encoded key to its [PrivateKey] object.
|
||||
* Use this method if the key type is a-priori unknown.
|
||||
* @param encodedKey a PKCS8 encoded private key.
|
||||
* @throws IllegalArgumentException on not supported scheme or if the given key specification
|
||||
* is inappropriate for this key factory to produce a private key.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class)
|
||||
fun decodePrivateKey(encodedKey: ByteArray): PrivateKey {
|
||||
for (sig in supportedSignatureSchemes.values) {
|
||||
for ((_, _, _, providerName, algorithmName) in supportedSignatureSchemes.values) {
|
||||
try {
|
||||
return sig.keyFactory.generatePrivate(PKCS8EncodedKeySpec(encodedKey))
|
||||
return KeyFactory.getInstance(algorithmName, providerMap[providerName]).generatePrivate(PKCS8EncodedKeySpec(encodedKey))
|
||||
} catch (ikse: InvalidKeySpecException) {
|
||||
// ignore it - only used to bypass the scheme that causes an exception.
|
||||
}
|
||||
@ -193,17 +221,27 @@ object Crypto {
|
||||
|
||||
/**
|
||||
* Decode a PKCS8 encoded key to its [PrivateKey] object based on the input scheme code name.
|
||||
* This will be used by Kryo deserialisation.
|
||||
* @param encodedKey a PKCS8 encoded private key.
|
||||
* This should be used when the type key is known, e.g. during Kryo deserialisation or with key caches or key managers.
|
||||
* @param schemeCodeName a [String] that should match a key in supportedSignatureSchemes map (e.g. ECDSA_SECP256K1_SHA256).
|
||||
* @param encodedKey a PKCS8 encoded private key.
|
||||
* @throws IllegalArgumentException on not supported scheme or if the given key specification
|
||||
* is inappropriate for this key factory to produce a private key.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class, InvalidKeySpecException::class)
|
||||
fun decodePrivateKey(encodedKey: ByteArray, schemeCodeName: String): PrivateKey {
|
||||
val sig = findSignatureScheme(schemeCodeName)
|
||||
fun decodePrivateKey(schemeCodeName: String, encodedKey: ByteArray): PrivateKey = decodePrivateKey(findSignatureScheme(schemeCodeName), encodedKey)
|
||||
|
||||
/**
|
||||
* Decode a PKCS8 encoded key to its [PrivateKey] object based on the input scheme code name.
|
||||
* This should be used when the type key is known, e.g. during Kryo deserialisation or with key caches or key managers.
|
||||
* @param signatureScheme a signature scheme (e.g. ECDSA_SECP256K1_SHA256).
|
||||
* @param encodedKey a PKCS8 encoded private key.
|
||||
* @throws IllegalArgumentException on not supported scheme or if the given key specification
|
||||
* is inappropriate for this key factory to produce a private key.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class, InvalidKeySpecException::class)
|
||||
fun decodePrivateKey(signatureScheme: SignatureScheme, encodedKey: ByteArray): PrivateKey {
|
||||
try {
|
||||
return sig.keyFactory.generatePrivate(PKCS8EncodedKeySpec(encodedKey))
|
||||
return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePrivate(PKCS8EncodedKeySpec(encodedKey))
|
||||
} catch (ikse: InvalidKeySpecException) {
|
||||
throw InvalidKeySpecException("This private key cannot be decoded, please ensure it is PKCS8 encoded and that it corresponds to the input scheme's code name.", ikse)
|
||||
}
|
||||
@ -211,16 +249,16 @@ object Crypto {
|
||||
|
||||
/**
|
||||
* Decode an X509 encoded key to its [PublicKey] object.
|
||||
* Use this method if the key type is a-priori unknown.
|
||||
* @param encodedKey an X509 encoded public key.
|
||||
* @throws UnsupportedSchemeException on not supported scheme.
|
||||
* @throws IllegalArgumentException on not supported scheme or if the given key specification
|
||||
* is inappropriate for this key factory to produce a private key.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class)
|
||||
fun decodePublicKey(encodedKey: ByteArray): PublicKey {
|
||||
for (sig in supportedSignatureSchemes.values) {
|
||||
for ((_, _, _, providerName, algorithmName) in supportedSignatureSchemes.values) {
|
||||
try {
|
||||
return sig.keyFactory.generatePublic(X509EncodedKeySpec(encodedKey))
|
||||
return KeyFactory.getInstance(algorithmName, providerMap[providerName]).generatePublic(X509EncodedKeySpec(encodedKey))
|
||||
} catch (ikse: InvalidKeySpecException) {
|
||||
// ignore it - only used to bypass the scheme that causes an exception.
|
||||
}
|
||||
@ -230,39 +268,34 @@ object Crypto {
|
||||
|
||||
/**
|
||||
* Decode an X509 encoded key to its [PrivateKey] object based on the input scheme code name.
|
||||
* This will be used by Kryo deserialisation.
|
||||
* @param encodedKey an X509 encoded public key.
|
||||
* This should be used when the type key is known, e.g. during Kryo deserialisation or with key caches or key managers.
|
||||
* @param schemeCodeName a [String] that should match a key in supportedSignatureSchemes map (e.g. ECDSA_SECP256K1_SHA256).
|
||||
* @param encodedKey an X509 encoded public key.
|
||||
* @throws IllegalArgumentException if the requested scheme is not supported
|
||||
* @throws InvalidKeySpecException if the given key specification
|
||||
* is inappropriate for this key factory to produce a public key.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class, InvalidKeySpecException::class)
|
||||
fun decodePublicKey(encodedKey: ByteArray, schemeCodeName: String): PublicKey {
|
||||
val sig = findSignatureScheme(schemeCodeName)
|
||||
fun decodePublicKey(schemeCodeName: String, encodedKey: ByteArray): PublicKey = decodePublicKey(findSignatureScheme(schemeCodeName), encodedKey)
|
||||
|
||||
/**
|
||||
* Decode an X509 encoded key to its [PrivateKey] object based on the input scheme code name.
|
||||
* This should be used when the type key is known, e.g. during Kryo deserialisation or with key caches or key managers.
|
||||
* @param signatureScheme a signature scheme (e.g. ECDSA_SECP256K1_SHA256).
|
||||
* @param encodedKey an X509 encoded public key.
|
||||
* @throws IllegalArgumentException if the requested scheme is not supported
|
||||
* @throws InvalidKeySpecException if the given key specification
|
||||
* is inappropriate for this key factory to produce a public key.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class, InvalidKeySpecException::class)
|
||||
fun decodePublicKey(signatureScheme: SignatureScheme, encodedKey: ByteArray): PublicKey {
|
||||
try {
|
||||
return sig.keyFactory.generatePublic(X509EncodedKeySpec(encodedKey))
|
||||
return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePublic(X509EncodedKeySpec(encodedKey))
|
||||
} catch (ikse: InvalidKeySpecException) {
|
||||
throw throw InvalidKeySpecException("This public key cannot be decoded, please ensure it is X509 encoded and that it corresponds to the input scheme's code name.", ikse)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility to simplify the act of generating keys.
|
||||
* Normally, we don't expect other errors here, assuming that key generation parameters for every supported signature scheme have been unit-tested.
|
||||
* @param schemeCodeName a signature scheme's code name (e.g. ECDSA_SECP256K1_SHA256).
|
||||
* @return a KeyPair for the requested scheme.
|
||||
* @throws IllegalArgumentException if the requested signature scheme is not supported.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class)
|
||||
fun generateKeyPair(schemeCodeName: String): KeyPair = findSignatureScheme(schemeCodeName).keyPairGenerator.generateKeyPair()
|
||||
|
||||
/**
|
||||
* Generate a KeyPair using the default signature scheme.
|
||||
* @return a new KeyPair.
|
||||
*/
|
||||
fun generateKeyPair(): KeyPair = DEFAULT_SIGNATURE_SCHEME.keyPairGenerator.generateKeyPair()
|
||||
|
||||
/**
|
||||
* Generic way to sign [ByteArray] data with a [PrivateKey]. Strategy on on identifying the actual signing scheme is based
|
||||
* on the [PrivateKey] type, but if the schemeCodeName is known, then better use doSign(signatureScheme: String, privateKey: PrivateKey, clearData: ByteArray).
|
||||
@ -274,7 +307,7 @@ object Crypto {
|
||||
* @throws SignatureException if signing is not possible due to malformed data or private key.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class)
|
||||
fun doSign(privateKey: PrivateKey, clearData: ByteArray) = doSign(findSignatureScheme(privateKey).sig, privateKey, clearData)
|
||||
fun doSign(privateKey: PrivateKey, clearData: ByteArray) = doSign(findSignatureScheme(privateKey), privateKey, clearData)
|
||||
|
||||
/**
|
||||
* Generic way to sign [ByteArray] data with a [PrivateKey] and a known schemeCodeName [String].
|
||||
@ -287,11 +320,11 @@ object Crypto {
|
||||
* @throws SignatureException if signing is not possible due to malformed data or private key.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class)
|
||||
fun doSign(schemeCodeName: String, privateKey: PrivateKey, clearData: ByteArray) = doSign(findSignatureScheme(schemeCodeName).sig, privateKey, clearData)
|
||||
fun doSign(schemeCodeName: String, privateKey: PrivateKey, clearData: ByteArray) = doSign(findSignatureScheme(schemeCodeName), privateKey, clearData)
|
||||
|
||||
/**
|
||||
* Generic way to sign [ByteArray] data with a [PrivateKey] and a known [Signature].
|
||||
* @param signature a [Signature] object, retrieved from supported signature schemes, see [Crypto].
|
||||
* @param signatureScheme a [SignatureScheme] object, retrieved from supported signature schemes, see [Crypto].
|
||||
* @param privateKey the signer's [PrivateKey].
|
||||
* @param clearData the data/message to be signed in [ByteArray] form (usually the Merkle root).
|
||||
* @return the digital signature (in [ByteArray]) on the input message.
|
||||
@ -300,7 +333,10 @@ object Crypto {
|
||||
* @throws SignatureException if signing is not possible due to malformed data or private key.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class)
|
||||
private fun doSign(signature: Signature, privateKey: PrivateKey, clearData: ByteArray): ByteArray {
|
||||
fun doSign(signatureScheme: SignatureScheme, privateKey: PrivateKey, clearData: ByteArray): ByteArray {
|
||||
if (!supportedSignatureSchemes.containsKey(signatureScheme.schemeCodeName))
|
||||
throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $signatureScheme.schemeCodeName")
|
||||
val signature = Signature.getInstance(signatureScheme.signatureName, providerMap[signatureScheme.providerName])
|
||||
if (clearData.isEmpty()) throw Exception("Signing of an empty array is not permitted!")
|
||||
signature.initSign(privateKey)
|
||||
signature.update(clearData)
|
||||
@ -330,6 +366,7 @@ object Crypto {
|
||||
/**
|
||||
* Utility to simplify the act of verifying a digital signature.
|
||||
* It returns true if it succeeds, but it always throws an exception if verification fails.
|
||||
* @param schemeCodeName a signature scheme's code name (e.g. ECDSA_SECP256K1_SHA256).
|
||||
* @param publicKey the signer's [PublicKey].
|
||||
* @param signatureData the signatureData on a message.
|
||||
* @param clearData the clear data/message that was signed (usually the Merkle root).
|
||||
@ -341,7 +378,7 @@ object Crypto {
|
||||
* @throws IllegalArgumentException if the signature scheme is not supported or if any of the clear or signature data is empty.
|
||||
*/
|
||||
@Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class)
|
||||
fun doVerify(schemeCodeName: String, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray) = doVerify(findSignatureScheme(schemeCodeName).sig, publicKey, signatureData, clearData)
|
||||
fun doVerify(schemeCodeName: String, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray) = doVerify(findSignatureScheme(schemeCodeName), publicKey, signatureData, clearData)
|
||||
|
||||
/**
|
||||
* Utility to simplify the act of verifying a digital signature by identifying the signature scheme used from the input public key's type.
|
||||
@ -359,12 +396,12 @@ object Crypto {
|
||||
* @throws IllegalArgumentException if the signature scheme is not supported or if any of the clear or signature data is empty.
|
||||
*/
|
||||
@Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class)
|
||||
fun doVerify(publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray) = doVerify(findSignatureScheme(publicKey).sig, publicKey, signatureData, clearData)
|
||||
fun doVerify(publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray) = doVerify(findSignatureScheme(publicKey), publicKey, signatureData, clearData)
|
||||
|
||||
/**
|
||||
* Method to verify a digital signature.
|
||||
* It returns true if it succeeds, but it always throws an exception if verification fails.
|
||||
* @param signature a [Signature] object, retrieved from supported signature schemes, see [Crypto].
|
||||
* @param signatureScheme a [SignatureScheme] object, retrieved from supported signature schemes, see [Crypto].
|
||||
* @param publicKey the signer's [PublicKey].
|
||||
* @param signatureData the signatureData on a message.
|
||||
* @param clearData the clear data/message that was signed (usually the Merkle root).
|
||||
@ -373,14 +410,15 @@ object Crypto {
|
||||
* @throws SignatureException if this signatureData object is not initialized properly,
|
||||
* the passed-in signatureData is improperly encoded or of the wrong type,
|
||||
* if this signatureData scheme is unable to process the input data provided, if the verification is not possible.
|
||||
* @throws IllegalArgumentException if any of the clear or signature data is empty.
|
||||
* @throws IllegalArgumentException if the signature scheme is not supported or if any of the clear or signature data is empty.
|
||||
*/
|
||||
private fun doVerify(signature: Signature, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray): Boolean {
|
||||
@Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class)
|
||||
fun doVerify(signatureScheme: SignatureScheme, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray): Boolean {
|
||||
if (!supportedSignatureSchemes.containsKey(signatureScheme.schemeCodeName))
|
||||
throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $signatureScheme.schemeCodeName")
|
||||
if (signatureData.isEmpty()) throw IllegalArgumentException("Signature data is empty!")
|
||||
if (clearData.isEmpty()) throw IllegalArgumentException("Clear data is empty, nothing to verify!")
|
||||
signature.initVerify(publicKey)
|
||||
signature.update(clearData)
|
||||
val verificationResult = signature.verify(signatureData)
|
||||
val verificationResult = isValid(signatureScheme, publicKey, signatureData, clearData)
|
||||
if (verificationResult) {
|
||||
return true
|
||||
} else {
|
||||
@ -407,15 +445,181 @@ object Crypto {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the requested signature scheme is supported by the system.
|
||||
* @param schemeCodeName a signature scheme's code name (e.g. ECDSA_SECP256K1_SHA256).
|
||||
* @return true if the signature scheme is supported.
|
||||
* Utility to simplify the act of verifying a digital signature by identifying the signature scheme used from the input public key's type.
|
||||
* It returns true if it succeeds and false if not. In comparison to [doVerify] if the key and signature
|
||||
* do not match it returns false rather than throwing an exception. Normally you should use the function which throws,
|
||||
* as it avoids the risk of failing to test the result.
|
||||
* Use this method if the signature scheme is not a-priori known.
|
||||
* @param publicKey the signer's [PublicKey].
|
||||
* @param signatureData the signatureData on a message.
|
||||
* @param clearData the clear data/message that was signed (usually the Merkle root).
|
||||
* @return true if verification passes or false if verification fails.
|
||||
* @throws SignatureException if this signatureData object is not initialized properly,
|
||||
* the passed-in signatureData is improperly encoded or of the wrong type,
|
||||
* if this signatureData scheme is unable to process the input data provided, if the verification is not possible.
|
||||
*/
|
||||
@Throws(SignatureException::class)
|
||||
fun isValid(publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray) = isValid(findSignatureScheme(publicKey), publicKey, signatureData, clearData)
|
||||
|
||||
/**
|
||||
* Method to verify a digital signature. In comparison to [doVerify] if the key and signature
|
||||
* do not match it returns false rather than throwing an exception.
|
||||
* Use this method if the signature scheme type is a-priori unknown.
|
||||
* @param signatureScheme a [SignatureScheme] object, retrieved from supported signature schemes, see [Crypto].
|
||||
* @param publicKey the signer's [PublicKey].
|
||||
* @param signatureData the signatureData on a message.
|
||||
* @param clearData the clear data/message that was signed (usually the Merkle root).
|
||||
* @return true if verification passes or false if verification fails.
|
||||
* @throws SignatureException if this signatureData object is not initialized properly,
|
||||
* the passed-in signatureData is improperly encoded or of the wrong type,
|
||||
* if this signatureData scheme is unable to process the input data provided, if the verification is not possible.
|
||||
* @throws IllegalArgumentException if the requested signature scheme is not supported.
|
||||
*/
|
||||
@Throws(SignatureException::class, IllegalArgumentException::class)
|
||||
fun isValid(signatureScheme: SignatureScheme, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray): Boolean {
|
||||
if (!supportedSignatureSchemes.containsKey(signatureScheme.schemeCodeName))
|
||||
throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $signatureScheme.schemeCodeName")
|
||||
val signature = Signature.getInstance(signatureScheme.signatureName, providerMap[signatureScheme.providerName])
|
||||
signature.initVerify(publicKey)
|
||||
signature.update(clearData)
|
||||
return signature.verify(signatureData)
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility to simplify the act of generating keys.
|
||||
* Normally, we don't expect other errors here, assuming that key generation parameters for every supported signature scheme have been unit-tested.
|
||||
* @param schemeCodeName a signature scheme's code name (e.g. ECDSA_SECP256K1_SHA256).
|
||||
* @return a KeyPair for the requested signature scheme code name.
|
||||
* @throws IllegalArgumentException if the requested signature scheme is not supported.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class)
|
||||
fun generateKeyPair(schemeCodeName: String): KeyPair = generateKeyPair(findSignatureScheme(schemeCodeName))
|
||||
|
||||
/**
|
||||
* Generate a [KeyPair] for the selected [SignatureScheme].
|
||||
* Note that RSA is the sole algorithm initialized specifically by its supported keySize.
|
||||
* @param signatureScheme a supported [SignatureScheme], see [Crypto], default to [DEFAULT_SIGNATURE_SCHEME] if not provided.
|
||||
* @return a new [KeyPair] for the requested [SignatureScheme].
|
||||
* @throws IllegalArgumentException if the requested signature scheme is not supported.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class)
|
||||
@JvmOverloads
|
||||
fun generateKeyPair(signatureScheme: SignatureScheme = DEFAULT_SIGNATURE_SCHEME): KeyPair {
|
||||
if (!supportedSignatureSchemes.containsKey(signatureScheme.schemeCodeName))
|
||||
throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $signatureScheme.schemeCodeName")
|
||||
val keyPairGenerator = KeyPairGenerator.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName])
|
||||
if (signatureScheme.algSpec != null)
|
||||
keyPairGenerator.initialize(signatureScheme.algSpec, newSecureRandom())
|
||||
else
|
||||
keyPairGenerator.initialize(signatureScheme.keySize, newSecureRandom())
|
||||
return keyPairGenerator.generateKeyPair()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a key pair derived from the given [BigInteger] entropy. This is useful for unit tests
|
||||
* and other cases where you want hard-coded private keys.
|
||||
* Currently, [EDDSA_ED25519_SHA512] is the sole scheme supported for this operation.
|
||||
* @param signatureScheme a supported [SignatureScheme], see [Crypto].
|
||||
* @param entropy a [BigInteger] value.
|
||||
* @return a new [KeyPair] from an entropy input.
|
||||
* @throws IllegalArgumentException if the requested signature scheme is not supported for KeyPair generation using an entropy input.
|
||||
*/
|
||||
fun generateKeyPairFromEntropy(signatureScheme: SignatureScheme, entropy: BigInteger): KeyPair {
|
||||
when (signatureScheme) {
|
||||
EDDSA_ED25519_SHA512 -> return generateEdDSAKeyPairFromEntropy(entropy)
|
||||
}
|
||||
throw IllegalArgumentException("Unsupported signature scheme for fixed entropy-based key pair generation: $signatureScheme.schemeCodeName")
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a [DEFAULT_SIGNATURE_SCHEME] key pair derived from the given [BigInteger] entropy.
|
||||
* @param entropy a [BigInteger] value.
|
||||
* @return a new [KeyPair] from an entropy input.
|
||||
*/
|
||||
fun generateKeyPairFromEntropy(entropy: BigInteger): KeyPair = generateKeyPairFromEntropy(DEFAULT_SIGNATURE_SCHEME, entropy)
|
||||
|
||||
// custom key pair generator from entropy.
|
||||
private fun generateEdDSAKeyPairFromEntropy(entropy: BigInteger): KeyPair {
|
||||
val params = EDDSA_ED25519_SHA512.algSpec as EdDSANamedCurveSpec
|
||||
val bytes = entropy.toByteArray().copyOf(params.curve.field.getb() / 8) // need to pad the entropy to the valid seed length.
|
||||
val priv = EdDSAPrivateKeySpec(bytes, params)
|
||||
val pub = EdDSAPublicKeySpec(priv.a, params)
|
||||
return KeyPair(EdDSAPublicKey(pub), EdDSAPrivateKey(priv))
|
||||
}
|
||||
|
||||
/** Check if the requested signature scheme is supported by the system. */
|
||||
fun isSupportedSignatureScheme(schemeCodeName: String): Boolean = schemeCodeName in supportedSignatureSchemes
|
||||
|
||||
/** @return the default signature scheme's code name. */
|
||||
fun getDefaultSignatureSchemeCodeName(): String = DEFAULT_SIGNATURE_SCHEME.schemeCodeName
|
||||
fun isSupportedSignatureScheme(signatureScheme: SignatureScheme): Boolean = signatureScheme.schemeCodeName in supportedSignatureSchemes
|
||||
|
||||
/** @return a [List] of Strings with the scheme code names defined in [SignatureScheme] for all of our supported signature schemes, see [Crypto]. */
|
||||
fun listSupportedSignatureSchemes(): List<String> = supportedSignatureSchemes.keys.toList()
|
||||
/**
|
||||
* Use bouncy castle utilities to sign completed X509 certificate with CA cert private key
|
||||
*/
|
||||
fun createCertificate(issuer: X500Name, issuerKeyPair: KeyPair,
|
||||
subject: X500Name, subjectPublicKey: PublicKey,
|
||||
keyUsage: KeyUsage, purposes: List<KeyPurposeId>,
|
||||
signatureScheme: SignatureScheme, validityWindow: Pair<Date, Date>,
|
||||
pathLength: Int? = null, subjectAlternativeName: List<GeneralName>? = null): X509Certificate {
|
||||
|
||||
val provider = providerMap[signatureScheme.providerName]
|
||||
val serial = BigInteger.valueOf(random63BitValue())
|
||||
val keyPurposes = DERSequence(ASN1EncodableVector().apply { purposes.forEach { add(it) } })
|
||||
|
||||
val builder = JcaX509v3CertificateBuilder(issuer, serial, validityWindow.first, validityWindow.second, subject, subjectPublicKey)
|
||||
.addExtension(Extension.subjectKeyIdentifier, false, BcX509ExtensionUtils().createSubjectKeyIdentifier(SubjectPublicKeyInfo.getInstance(subjectPublicKey.encoded)))
|
||||
.addExtension(Extension.basicConstraints, pathLength != null, if (pathLength == null) BasicConstraints(false) else BasicConstraints(pathLength))
|
||||
.addExtension(Extension.keyUsage, false, keyUsage)
|
||||
.addExtension(Extension.extendedKeyUsage, false, keyPurposes)
|
||||
|
||||
if (subjectAlternativeName != null && subjectAlternativeName.isNotEmpty()) {
|
||||
builder.addExtension(Extension.subjectAlternativeName, false, DERSequence(subjectAlternativeName.toTypedArray()))
|
||||
}
|
||||
val signer = ContentSignerBuilder.build(signatureScheme, issuerKeyPair.private, provider)
|
||||
return JcaX509CertificateConverter().setProvider(provider).getCertificate(builder.build(signer)).apply {
|
||||
checkValidity(Date())
|
||||
verify(issuerKeyPair.public, provider)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create certificate signing request using provided information.
|
||||
*/
|
||||
fun createCertificateSigningRequest(subject: X500Name, keyPair: KeyPair, signatureScheme: SignatureScheme): PKCS10CertificationRequest {
|
||||
val signer = ContentSignerBuilder.build(signatureScheme, keyPair.private, providerMap[signatureScheme.providerName])
|
||||
return JcaPKCS10CertificationRequestBuilder(subject, keyPair.public).build(signer)
|
||||
}
|
||||
|
||||
private class KeyInfoConverter(val signatureScheme: SignatureScheme) : AsymmetricKeyInfoConverter {
|
||||
override fun generatePublic(keyInfo: SubjectPublicKeyInfo?): PublicKey? = keyInfo?.let { decodePublicKey(signatureScheme, it.encoded) }
|
||||
override fun generatePrivate(keyInfo: PrivateKeyInfo?): PrivateKey? = keyInfo?.let { decodePrivateKey(signatureScheme, it.encoded) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a point's coordinates are on the expected curve to avoid certain types of ECC attacks.
|
||||
* Point-at-infinity is not permitted as well.
|
||||
* @see <a href="https://safecurves.cr.yp.to/twist.html">Small subgroup and invalid-curve attacks</a> for a more descriptive explanation on such attacks.
|
||||
* We use this function on [findSignatureScheme] for a [PublicKey]; currently used for signature verification only.
|
||||
* Thus, as these attacks are mostly not relevant to signature verification, we should note that
|
||||
* we're doing it out of an abundance of caution and specifically to proactively protect developers
|
||||
* against using these points as part of a DH key agreement or for use cases as yet unimagined.
|
||||
* This method currently applies to BouncyCastle's ECDSA (both R1 and K1 curves) and I2P's EdDSA (ed25519 curve).
|
||||
* @param publicKey a [PublicKey], usually used to validate a signer's public key in on the Curve.
|
||||
* @param signatureScheme a [SignatureScheme] object, retrieved from supported signature schemes, see [Crypto].
|
||||
* @return true if the point lies on the curve or false if it doesn't.
|
||||
* @throws IllegalArgumentException if the requested signature scheme or the key type is not supported.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class)
|
||||
fun publicKeyOnCurve(signatureScheme: SignatureScheme, publicKey: PublicKey): Boolean {
|
||||
if (!isSupportedSignatureScheme(signatureScheme))
|
||||
throw IllegalArgumentException("Unsupported signature scheme: $signatureScheme.schemeCodeName")
|
||||
when (publicKey) {
|
||||
is BCECPublicKey -> return (publicKey.parameters == signatureScheme.algSpec && !publicKey.q.isInfinity && publicKey.q.isValid)
|
||||
is EdDSAPublicKey -> return (publicKey.params == signatureScheme.algSpec && !isEdDSAPointAtInfinity(publicKey) && publicKey.a.isOnCurve)
|
||||
else -> throw IllegalArgumentException("Unsupported key type: ${publicKey::class}")
|
||||
}
|
||||
}
|
||||
|
||||
// return true if EdDSA publicKey is point at infinity.
|
||||
// For EdDSA a custom function is required as it is not supported by the I2P implementation.
|
||||
private fun isEdDSAPointAtInfinity(publicKey: EdDSAPublicKey) = publicKey.a.toP3() == (EDDSA_ED25519_SHA512.algSpec as EdDSANamedCurveSpec).curve.getZero(GroupElement.Representation.P3)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -1,23 +1,155 @@
|
||||
@file:JvmName("CryptoUtils")
|
||||
|
||||
package net.corda.core.crypto
|
||||
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.i2p.crypto.eddsa.EdDSAPublicKey
|
||||
import java.math.BigInteger
|
||||
import net.corda.core.utilities.SgxSupport
|
||||
import java.security.*
|
||||
|
||||
@CordaSerializable
|
||||
object NullPublicKey : PublicKey, Comparable<PublicKey> {
|
||||
override fun getAlgorithm() = "NULL"
|
||||
override fun getEncoded() = byteArrayOf(0)
|
||||
override fun getFormat() = "NULL"
|
||||
override fun compareTo(other: PublicKey): Int = if (other == NullPublicKey) 0 else -1
|
||||
override fun toString() = "NULL_KEY"
|
||||
}
|
||||
|
||||
// TODO: Clean up this duplication between Null and Dummy public key
|
||||
@CordaSerializable
|
||||
class DummyPublicKey(val s: String) : PublicKey, Comparable<PublicKey> {
|
||||
override fun getAlgorithm() = "DUMMY"
|
||||
override fun getEncoded() = s.toByteArray()
|
||||
override fun getFormat() = "ASN.1"
|
||||
override fun compareTo(other: PublicKey): Int = BigInteger(encoded).compareTo(BigInteger(other.encoded))
|
||||
override fun equals(other: Any?) = other is DummyPublicKey && other.s == s
|
||||
override fun hashCode(): Int = s.hashCode()
|
||||
override fun toString() = "PUBKEY[$s]"
|
||||
}
|
||||
|
||||
/** A signature with a key and value of zero. Useful when you want a signature object that you know won't ever be used. */
|
||||
@CordaSerializable
|
||||
object NullSignature : DigitalSignature.WithKey(NullPublicKey, ByteArray(32))
|
||||
|
||||
/**
|
||||
* Helper function for signing.
|
||||
* @param clearData the data/message to be signed in [ByteArray] form (usually the Merkle root).
|
||||
* Utility to simplify the act of signing a byte array.
|
||||
* @param bytesToSign the data/message to be signed in [ByteArray] form (usually the Merkle root).
|
||||
* @return the [DigitalSignature] object on the input message.
|
||||
* @throws IllegalArgumentException if the signature scheme is not supported for this private key.
|
||||
* @throws InvalidKeyException if the private key is invalid.
|
||||
* @throws SignatureException if signing is not possible due to malformed data or private key.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class)
|
||||
fun PrivateKey.sign(bytesToSign: ByteArray): DigitalSignature {
|
||||
return DigitalSignature(Crypto.doSign(this, bytesToSign))
|
||||
}
|
||||
|
||||
fun PrivateKey.sign(bytesToSign: ByteArray, publicKey: PublicKey): DigitalSignature.WithKey {
|
||||
return DigitalSignature.WithKey(publicKey, this.sign(bytesToSign).bytes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to sign with a key pair.
|
||||
* @param bytesToSign the data/message to be signed in [ByteArray] form (usually the Merkle root).
|
||||
* @return the digital signature (in [ByteArray]) on the input message.
|
||||
* @throws IllegalArgumentException if the signature scheme is not supported for this private key.
|
||||
* @throws InvalidKeyException if the private key is invalid.
|
||||
* @throws SignatureException if signing is not possible due to malformed data or private key.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class)
|
||||
fun PrivateKey.sign(clearData: ByteArray): ByteArray = Crypto.doSign(this, clearData)
|
||||
fun KeyPair.sign(bytesToSign: ByteArray) = private.sign(bytesToSign, public)
|
||||
fun KeyPair.sign(bytesToSign: OpaqueBytes) = private.sign(bytesToSign.bytes, public)
|
||||
fun KeyPair.sign(bytesToSign: OpaqueBytes, party: Party) = sign(bytesToSign.bytes, party)
|
||||
|
||||
// TODO This case will need more careful thinking, as party owningKey can be a CompositeKey. One way of doing that is
|
||||
// implementation of CompositeSignature.
|
||||
@Throws(InvalidKeyException::class)
|
||||
fun KeyPair.sign(bytesToSign: ByteArray, party: Party): DigitalSignature.LegallyIdentifiable {
|
||||
val sig = sign(bytesToSign)
|
||||
val sigKey = when (party.owningKey) { // Quick workaround when we have CompositeKey as Party owningKey.
|
||||
is CompositeKey -> throw InvalidKeyException("Signing for parties with CompositeKey not supported.")
|
||||
else -> party.owningKey
|
||||
}
|
||||
return DigitalSignature.LegallyIdentifiable(party, sig.bytes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility to simplify the act of verifying a signature.
|
||||
*
|
||||
* @throws InvalidKeyException if the key to verify the signature with is not valid (i.e. wrong key type for the
|
||||
* signature).
|
||||
* @throws SignatureException if the signature is invalid (i.e. damaged), or does not match the key (incorrect).
|
||||
* @throws IllegalArgumentException if the signature scheme is not supported or if any of the clear or signature data is empty.
|
||||
*/
|
||||
// TODO: SignatureException should be used only for a damaged signature, as per `java.security.Signature.verify()`,
|
||||
@Throws(SignatureException::class, IllegalArgumentException::class, InvalidKeyException::class)
|
||||
fun PublicKey.verify(content: ByteArray, signature: DigitalSignature) = Crypto.doVerify(this, signature.bytes, content)
|
||||
|
||||
/**
|
||||
* Utility to simplify the act of verifying a signature. In comparison to [verify] if the key and signature
|
||||
* do not match it returns false rather than throwing an exception. Normally you should use the function which throws,
|
||||
* as it avoids the risk of failing to test the result, but this is for uses such as [java.security.Signature.verify]
|
||||
* implementations.
|
||||
*
|
||||
* @throws InvalidKeyException if the key to verify the signature with is not valid (i.e. wrong key type for the
|
||||
* signature).
|
||||
* @throws SignatureException if the signature is invalid (i.e. damaged).
|
||||
* @throws IllegalArgumentException if the signature scheme is not supported or if any of the clear or signature data is empty.
|
||||
* @return whether the signature is correct for this key.
|
||||
*/
|
||||
@Throws(IllegalStateException::class, SignatureException::class, IllegalArgumentException::class)
|
||||
fun PublicKey.isValid(content: ByteArray, signature: DigitalSignature) : Boolean {
|
||||
if (this is CompositeKey)
|
||||
throw IllegalStateException("Verification of CompositeKey signatures currently not supported.") // TODO CompositeSignature verification.
|
||||
return Crypto.isValid(this, signature.bytes, content)
|
||||
}
|
||||
|
||||
/** Render a public key to its hash (in Base58) of its serialised form using the DL prefix. */
|
||||
fun PublicKey.toStringShort(): String = "DL" + this.toSHA256Bytes().toBase58()
|
||||
|
||||
val PublicKey.keys: Set<PublicKey> get() {
|
||||
return if (this is CompositeKey) this.leafKeys
|
||||
else setOf(this)
|
||||
}
|
||||
|
||||
fun PublicKey.isFulfilledBy(otherKey: PublicKey): Boolean = isFulfilledBy(setOf(otherKey))
|
||||
fun PublicKey.isFulfilledBy(otherKeys: Iterable<PublicKey>): Boolean {
|
||||
return if (this is CompositeKey) this.isFulfilledBy(otherKeys)
|
||||
else this in otherKeys
|
||||
}
|
||||
|
||||
/** Checks whether any of the given [keys] matches a leaf on the CompositeKey tree or a single PublicKey */
|
||||
fun PublicKey.containsAny(otherKeys: Iterable<PublicKey>): Boolean {
|
||||
return if (this is CompositeKey) keys.intersect(otherKeys).isNotEmpty()
|
||||
else this in otherKeys
|
||||
}
|
||||
|
||||
/** Returns the set of all [PublicKey]s of the signatures */
|
||||
fun Iterable<DigitalSignature.WithKey>.byKeys() = map { it.by }.toSet()
|
||||
|
||||
// Allow Kotlin destructuring: val (private, public) = keyPair
|
||||
operator fun KeyPair.component1(): PrivateKey = this.private
|
||||
|
||||
operator fun KeyPair.component2(): PublicKey = this.public
|
||||
|
||||
/** A simple wrapper that will make it easier to swap out the EC algorithm we use in future */
|
||||
fun generateKeyPair(): KeyPair = Crypto.generateKeyPair()
|
||||
|
||||
/**
|
||||
* Returns a key pair derived from the given private key entropy. This is useful for unit tests and other cases where
|
||||
* you want hard-coded private keys.
|
||||
* This currently works for the default signature scheme EdDSA ed25519 only.
|
||||
*/
|
||||
fun entropyToKeyPair(entropy: BigInteger): KeyPair = Crypto.generateKeyPairFromEntropy(entropy)
|
||||
|
||||
/**
|
||||
* Helper function for signing.
|
||||
* @param metaDataFull tha attached MetaData object.
|
||||
* @return a [DSWithMetaDataFull] object.
|
||||
* @param metaData tha attached MetaData object.
|
||||
* @return a [TransactionSignature] object.
|
||||
* @throws IllegalArgumentException if the signature scheme is not supported for this private key.
|
||||
* @throws InvalidKeyException if the private key is invalid.
|
||||
* @throws SignatureException if signing is not possible due to malformed data or private key.
|
||||
@ -25,17 +157,6 @@ fun PrivateKey.sign(clearData: ByteArray): ByteArray = Crypto.doSign(this, clear
|
||||
@Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class)
|
||||
fun PrivateKey.sign(metaData: MetaData): TransactionSignature = Crypto.doSign(this, metaData)
|
||||
|
||||
/**
|
||||
* Helper function to sign with a key pair.
|
||||
* @param clearData the data/message to be signed in [ByteArray] form (usually the Merkle root).
|
||||
* @return the digital signature (in [ByteArray]) on the input message.
|
||||
* @throws IllegalArgumentException if the signature scheme is not supported for this private key.
|
||||
* @throws InvalidKeyException if the private key is invalid.
|
||||
* @throws SignatureException if signing is not possible due to malformed data or private key.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class)
|
||||
fun KeyPair.sign(clearData: ByteArray): ByteArray = Crypto.doSign(this.private, clearData)
|
||||
|
||||
/**
|
||||
* Helper function to verify a signature.
|
||||
* @param signatureData the signature on a message.
|
||||
@ -66,7 +187,7 @@ fun PublicKey.verify(transactionSignature: TransactionSignature): Boolean {
|
||||
|
||||
/**
|
||||
* Helper function for the signers to verify their own signature.
|
||||
* @param signature the signature on a message.
|
||||
* @param signatureData the signature on a message.
|
||||
* @param clearData the clear data/message that was signed (usually the Merkle root).
|
||||
* @throws InvalidKeyException if the key is invalid.
|
||||
* @throws SignatureException if this signatureData object is not initialized properly,
|
||||
@ -86,7 +207,7 @@ fun KeyPair.verify(signatureData: ByteArray, clearData: ByteArray): Boolean = Cr
|
||||
* which should never happen and suggests an unusual JVM or non-standard Java library.
|
||||
*/
|
||||
@Throws(NoSuchAlgorithmException::class)
|
||||
fun safeRandomBytes(numOfBytes: Int): ByteArray {
|
||||
fun secureRandomBytes(numOfBytes: Int): ByteArray {
|
||||
return newSecureRandom().generateSeed(numOfBytes)
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
@ -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")
|
||||
}
|
@ -1,10 +1,14 @@
|
||||
@file:JvmName("EncodingUtils")
|
||||
|
||||
package net.corda.core.crypto
|
||||
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import java.nio.charset.Charset
|
||||
import java.security.PublicKey
|
||||
import java.util.*
|
||||
import javax.xml.bind.DatatypeConverter
|
||||
|
||||
|
||||
// This file includes useful encoding methods and extension functions for the most common encoding/decoding operations.
|
||||
|
||||
// [ByteArray] encoders
|
||||
@ -33,7 +37,7 @@ fun String.base58ToByteArray(): ByteArray = Base58.decode(this)
|
||||
fun String.base64ToByteArray(): ByteArray = Base64.getDecoder().decode(this)
|
||||
|
||||
/** Hex-String to [ByteArray]. Accept any hex form (capitalized, lowercase, mixed). */
|
||||
fun String.hexToByteArray(): ByteArray = DatatypeConverter.parseHexBinary(this);
|
||||
fun String.hexToByteArray(): ByteArray = DatatypeConverter.parseHexBinary(this)
|
||||
|
||||
|
||||
// Encoding changers
|
||||
@ -56,5 +60,9 @@ fun String.hexToBase58(): String = hexToByteArray().toBase58()
|
||||
/** Encoding changer. Hex-[String] to Base64-[String], i.e. "48656C6C6F20576F726C64" -> "SGVsbG8gV29ybGQ=" */
|
||||
fun String.hexToBase64(): String = hexToByteArray().toBase64()
|
||||
|
||||
// Helper vars.
|
||||
private val HEX_ALPHABET = "0123456789ABCDEF".toCharArray()
|
||||
// TODO We use for both CompositeKeys and EdDSAPublicKey custom Kryo serializers and deserializers. We need to specify encoding.
|
||||
// TODO: follow the crypto-conditions ASN.1 spec, some changes are needed to be compatible with the condition
|
||||
// structure, e.g. mapping a PublicKey to a condition with the specific feature (ED25519).
|
||||
fun parsePublicKeyBase58(base58String: String): PublicKey = base58String.base58ToByteArray().deserialize<PublicKey>()
|
||||
fun PublicKey.toBase58String(): String = this.serialize().bytes.toBase58()
|
||||
fun PublicKey.toSHA256Bytes(): ByteArray = this.serialize().bytes.sha256().bytes
|
||||
|
136
core/src/main/kotlin/net/corda/core/crypto/KeyStoreUtilities.kt
Normal file
136
core/src/main/kotlin/net/corda/core/crypto/KeyStoreUtilities.kt
Normal 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
|
@ -11,12 +11,14 @@ import java.util.*
|
||||
* signers, tx type, timestamp. Merkle Tree is kept in a recursive data structure. Building is done bottom up,
|
||||
* from all leaves' hashes. If number of leaves is not a power of two, the tree is padded with zero hashes.
|
||||
*/
|
||||
sealed class MerkleTree(val hash: SecureHash) {
|
||||
class Leaf(val value: SecureHash) : MerkleTree(value)
|
||||
class Node(val value: SecureHash, val left: MerkleTree, val right: MerkleTree) : MerkleTree(value)
|
||||
sealed class MerkleTree {
|
||||
abstract val hash: SecureHash
|
||||
|
||||
data class Leaf(override val hash: SecureHash) : MerkleTree()
|
||||
data class Node(override val hash: SecureHash, val left: MerkleTree, val right: MerkleTree) : MerkleTree()
|
||||
|
||||
companion object {
|
||||
private fun isPow2(num: Int): Boolean = num and (num-1) == 0
|
||||
private fun isPow2(num: Int): Boolean = num and (num - 1) == 0
|
||||
|
||||
/**
|
||||
* Merkle tree building using hashes, with zero hash padding to full power of 2.
|
||||
@ -52,9 +54,9 @@ sealed class MerkleTree(val hash: SecureHash) {
|
||||
val newLevelHashes: MutableList<MerkleTree> = ArrayList()
|
||||
val n = lastNodesList.size
|
||||
require((n and 1) == 0) { "Sanity check: number of nodes should be even." }
|
||||
for (i in 0..n-2 step 2) {
|
||||
for (i in 0..n - 2 step 2) {
|
||||
val left = lastNodesList[i]
|
||||
val right = lastNodesList[i+1]
|
||||
val right = lastNodesList[i + 1]
|
||||
val newHash = left.hash.hashConcat(right.hash)
|
||||
val combined = Node(newHash, left, right)
|
||||
newLevelHashes.add(combined)
|
||||
|
@ -54,9 +54,9 @@ class PartialMerkleTree(val root: PartialTree) {
|
||||
*/
|
||||
@CordaSerializable
|
||||
sealed class PartialTree {
|
||||
class IncludedLeaf(val hash: SecureHash) : PartialTree()
|
||||
class Leaf(val hash: SecureHash) : PartialTree()
|
||||
class Node(val left: PartialTree, val right: PartialTree) : PartialTree()
|
||||
data class IncludedLeaf(val hash: SecureHash) : PartialTree()
|
||||
data class Leaf(val hash: SecureHash) : PartialTree()
|
||||
data class Node(val left: PartialTree, val right: PartialTree) : PartialTree()
|
||||
}
|
||||
|
||||
companion object {
|
||||
@ -82,8 +82,8 @@ class PartialMerkleTree(val root: PartialTree) {
|
||||
return when (tree) {
|
||||
is MerkleTree.Leaf -> level
|
||||
is MerkleTree.Node -> {
|
||||
val l1 = checkFull(tree.left, level+1)
|
||||
val l2 = checkFull(tree.right, level+1)
|
||||
val l1 = checkFull(tree.left, level + 1)
|
||||
val l2 = checkFull(tree.right, level + 1)
|
||||
if (l1 != l2) throw MerkleTreeException("Got not full binary tree.")
|
||||
l1
|
||||
}
|
||||
@ -104,10 +104,10 @@ class PartialMerkleTree(val root: PartialTree) {
|
||||
): Pair<Boolean, PartialTree> {
|
||||
return when (root) {
|
||||
is MerkleTree.Leaf ->
|
||||
if (root.value in includeHashes) {
|
||||
usedHashes.add(root.value)
|
||||
Pair(true, PartialTree.IncludedLeaf(root.value))
|
||||
} else Pair(false, PartialTree.Leaf(root.value))
|
||||
if (root.hash in includeHashes) {
|
||||
usedHashes.add(root.hash)
|
||||
Pair(true, PartialTree.IncludedLeaf(root.hash))
|
||||
} else Pair(false, PartialTree.Leaf(root.hash))
|
||||
is MerkleTree.Node -> {
|
||||
val leftNode = buildPartialTree(root.left, includeHashes, usedHashes)
|
||||
val rightNode = buildPartialTree(root.right, includeHashes, usedHashes)
|
||||
@ -117,7 +117,7 @@ class PartialMerkleTree(val root: PartialTree) {
|
||||
Pair(true, newTree)
|
||||
} else {
|
||||
// This node has no included leaves below. Cut the tree here and store a hash as a Leaf.
|
||||
val newTree = PartialTree.Leaf(root.value)
|
||||
val newTree = PartialTree.Leaf(root.hash)
|
||||
Pair(false, newTree)
|
||||
}
|
||||
}
|
||||
|
@ -1,33 +1,7 @@
|
||||
package net.corda.core.crypto
|
||||
|
||||
import net.corda.core.contracts.PartyAndReference
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import java.security.PublicKey
|
||||
|
||||
/**
|
||||
* The [Party] class represents an entity on the network, which is typically identified by a legal [name] and public key
|
||||
* that it can sign transactions under. As parties may use multiple keys for signing and, for example, have offline backup
|
||||
* keys, the "public key" of a party is represented by a composite construct – a [CompositeKey], which combines multiple
|
||||
* cryptographic public key primitives into a tree structure.
|
||||
*
|
||||
* For example: Alice has two key pairs (pub1/priv1 and pub2/priv2), and wants to be able to sign transactions with either of them.
|
||||
* Her advertised [Party] then has a legal [name] "Alice" and an [owningKey] "pub1 or pub2".
|
||||
*
|
||||
* [Party] is also used for service identities. E.g. Alice may also be running an interest rate oracle on her Corda node,
|
||||
* which requires a separate signing key (and an identifying name). Services can also be distributed – run by a coordinated
|
||||
* cluster of Corda nodes. A [Party] representing a distributed service will use a composite key containing all
|
||||
* individual cluster nodes' public keys. Each of the nodes in the cluster will advertise the same group [Party].
|
||||
*
|
||||
* Note that equality is based solely on the owning key.
|
||||
*
|
||||
* @see CompositeKey
|
||||
*/
|
||||
class Party(val name: String, owningKey: CompositeKey) : AbstractParty(owningKey) {
|
||||
/** A helper constructor that converts the given [PublicKey] in to a [CompositeKey] with a single node */
|
||||
constructor(name: String, owningKey: PublicKey) : this(name, owningKey.composite)
|
||||
override fun toAnonymous(): AnonymousParty = AnonymousParty(owningKey)
|
||||
override fun toString() = "${owningKey.toBase58String()} (${name})"
|
||||
override fun nameOrNull(): String? = name
|
||||
|
||||
override fun ref(bytes: OpaqueBytes): PartyAndReference = PartyAndReference(this.toAnonymous(), bytes)
|
||||
}
|
||||
@Deprecated("Party has moved to identity package", ReplaceWith("net.corda.core.identity.Party"))
|
||||
class Party(name: X500Name, owningKey: PublicKey) : net.corda.core.identity.Party(name, owningKey)
|
@ -18,7 +18,7 @@ sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) {
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString() = BaseEncoding.base16().encode(bytes)
|
||||
override fun toString(): String = BaseEncoding.base16().encode(bytes)
|
||||
|
||||
fun prefixChars(prefixLen: Int = 6) = toString().substring(0, prefixLen)
|
||||
fun hashConcat(other: SecureHash) = (this.bytes + other.bytes).sha256()
|
||||
|
@ -1,18 +1,17 @@
|
||||
package net.corda.core.crypto
|
||||
|
||||
import java.security.*
|
||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier
|
||||
import java.security.Signature
|
||||
import java.security.spec.AlgorithmParameterSpec
|
||||
|
||||
/**
|
||||
* This class is used to define a digital signature scheme.
|
||||
* @param schemeNumberID we assign a number ID for more efficient on-wire serialisation. Please ensure uniqueness between schemes.
|
||||
* @param schemeCodeName code name for this signature scheme (e.g. RSA_SHA256, ECDSA_SECP256K1_SHA256, ECDSA_SECP256R1_SHA256, EDDSA_ED25519_SHA512, SPHINCS-256_SHA512).
|
||||
* @param signatureOID object identifier of the signature algorithm (e.g 1.3.101.112 for EdDSA)
|
||||
* @param providerName the provider's name (e.g. "BC").
|
||||
* @param algorithmName which signature algorithm is used (e.g. RSA, ECDSA. EdDSA, SPHINCS-256).
|
||||
* @param sig the [Signature] class that provides the functionality of a digital signature scheme.
|
||||
* eg. Signature.getInstance("SHA256withECDSA", "BC").
|
||||
* @param keyFactory the KeyFactory for this scheme (e.g. KeyFactory.getInstance("RSA", "BC")).
|
||||
* @param keyPairGenerator defines the <i>Service Provider Interface</i> (<b>SPI</b>) for the {@code KeyPairGenerator} class.
|
||||
* e.g. KeyPairGenerator.getInstance("ECDSA", "BC").
|
||||
* @param signatureName a signature-scheme name as required to create [Signature] objects (e.g. "SHA256withECDSA")
|
||||
* @param algSpec parameter specs for the underlying algorithm. Note that RSA is defined by the key size rather than algSpec.
|
||||
* eg. ECGenParameterSpec("secp256k1").
|
||||
* @param keySize the private key size (currently used for RSA only).
|
||||
@ -21,22 +20,10 @@ import java.security.spec.AlgorithmParameterSpec
|
||||
data class SignatureScheme(
|
||||
val schemeNumberID: Int,
|
||||
val schemeCodeName: String,
|
||||
val signatureOID: ASN1ObjectIdentifier,
|
||||
val providerName: String,
|
||||
val algorithmName: String,
|
||||
val sig: Signature,
|
||||
val keyFactory: KeyFactory,
|
||||
val keyPairGenerator: KeyPairGeneratorSpi,
|
||||
val signatureName: String,
|
||||
val algSpec: AlgorithmParameterSpec?,
|
||||
val keySize: Int,
|
||||
val desc: String) {
|
||||
|
||||
/**
|
||||
* KeyPair generators are always initialized once we create them, as no re-initialization is required.
|
||||
* Note that RSA is the sole algorithm initialized specifically by its supported keySize.
|
||||
*/
|
||||
init {
|
||||
if (algSpec != null)
|
||||
keyPairGenerator.initialize(algSpec, newSecureRandom())
|
||||
else
|
||||
keyPairGenerator.initialize(keySize, newSecureRandom())
|
||||
}
|
||||
}
|
||||
val desc: String)
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user