mirror of
https://github.com/corda/corda.git
synced 2025-03-23 12:35:23 +00:00
Merge branch 'master' into m4ksio_gradle_no_o_fix
# Conflicts: # constants.properties
This commit is contained in:
commit
e7e8bff3dd
11
.ci/README.md
Normal file
11
.ci/README.md
Normal file
@ -0,0 +1,11 @@
|
||||
# !! DO NOT MODIFY THE API FILE IN THIS DIRECTORY !!
|
||||
|
||||
The `api-current.txt` file contains a summary of Corda's current public APIs,
|
||||
as generated by the `api-scanner` Gradle plugin. (See [here](../gradle-plugins/api-scanner/README.md) for a detailed description of this plugin.) It will be regenerated and the copy in this repository updated by the Release Manager with
|
||||
each new Corda release. It will not be modified otherwise except under special circumstances that will require extra approval.
|
||||
|
||||
Deleting or changing the existing Corda APIs listed in `api-current.txt` may
|
||||
break developers' CorDapps in the next Corda release! Please remember that we
|
||||
have committed to API Stability for CorDapps.
|
||||
|
||||
# !! DO NOT MODIFY THE API FILE IN THIS DIRECTORY !!
|
3272
.ci/api-current.txt
Normal file
3272
.ci/api-current.txt
Normal file
File diff suppressed because it is too large
Load Diff
47
.ci/check-api-changes.sh
Executable file
47
.ci/check-api-changes.sh
Executable file
@ -0,0 +1,47 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "Starting API Diff"
|
||||
|
||||
APIHOME=$(dirname $0)
|
||||
|
||||
apiCurrent=$APIHOME/api-current.txt
|
||||
if [ ! -f $apiCurrent ]; then
|
||||
echo "Missing $apiCurrent file - cannot check API diff. Please rebase or add it to this release"
|
||||
exit -1
|
||||
fi
|
||||
|
||||
diffContents=`diff -u $apiCurrent $APIHOME/../build/api/api-corda-*.txt`
|
||||
echo "Diff contents:"
|
||||
echo "$diffContents"
|
||||
echo
|
||||
|
||||
# A removed line means that an API was either deleted or modified.
|
||||
removals=$(echo "$diffContents" | grep "^-\s")
|
||||
removalCount=`grep -v "^$" <<EOF | wc -l
|
||||
$removals
|
||||
EOF
|
||||
`
|
||||
|
||||
echo "Number of API removals/changes: "$removalCount
|
||||
if [ $removalCount -gt 0 ]; then
|
||||
echo "$removals"
|
||||
echo
|
||||
fi
|
||||
|
||||
# Adding new abstract methods could also break the API.
|
||||
newAbstracts=$(echo "$diffContents" | grep "^+\s" | grep "\(public\|protected\) abstract")
|
||||
abstractCount=`grep -v "^$" <<EOF | wc -l
|
||||
$newAbstracts
|
||||
EOF
|
||||
`
|
||||
|
||||
echo "Number of new abstract APIs: "$abstractCount
|
||||
if [ $abstractCount -gt 0 ]; then
|
||||
echo "$newAbstracts"
|
||||
echo
|
||||
fi
|
||||
|
||||
badChanges=$(($removalCount + $abstractCount))
|
||||
|
||||
echo "Exiting with exit code" $badChanges
|
||||
exit $badChanges
|
18
.github/PULL_REQUEST_TEMPLATE.md
vendored
18
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -1,15 +1,11 @@
|
||||
Thank you for choosing to contribute to Corda.
|
||||
👮🏻👮🏻👮🏻 !!!! DESCRIBE YOUR CHANGES HERE !!!! DO NOT FORGET !!!! 👮🏻👮🏻👮🏻
|
||||
|
||||
Your PR must be approved by one or more reviewers and all tests must be passed on TeamCity (https://ci.corda.r3cev.com) in order to be merged.
|
||||
|
||||
Once you have submitted a PR you are responsible for keeping it up to date until the time it is merged.
|
||||
# PR Checklist:
|
||||
|
||||
PR Checklist:
|
||||
- [ ] Have you run the unit, integration and smoke tests as described here? https://docs.corda.net/head/testing.html
|
||||
- [ ] If you added/changed public APIs, did you write/update the JavaDocs?
|
||||
- [ ] If the changes are of interest to application developers, have you added them to the changelog, and potentially release notes?
|
||||
- [ ] If you are contributing for the first time, please read the agreement in CONTRIBUTING.md now and add to this Pull Request that you agree to it.
|
||||
|
||||
1. Ensure any new code is tested as described in https://docs.corda.net/testing.html
|
||||
2. Ensure you have done any relevant automated testing and manual testing
|
||||
3. Add your changes to docs/source/changelog.rst
|
||||
4. Update any documentation in docs/source relating to your changes and learn how to build them in https://docs.corda.net/building-the-docs.html
|
||||
5. If you are contributing for the first time please read the agreement in CONTRIBUTING.md now and add to this Pull Request that you have read, and agreed to, the agreement.
|
||||
|
||||
Please remove this message when you have read it.
|
||||
Thanks for your code, it's appreciated! :)
|
||||
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -34,10 +34,12 @@ lib/quasar.jar
|
||||
.idea/shelf
|
||||
.idea/dataSources
|
||||
.idea/markdown-navigator
|
||||
.idea/runConfigurations
|
||||
/gradle-plugins/.idea/
|
||||
|
||||
# Include the -parameters compiler option by default in IntelliJ required for serialization.
|
||||
!.idea/compiler.xml
|
||||
!.idea/codeStyleSettings.xml
|
||||
|
||||
# if you remove the above rule, at least ignore the following:
|
||||
|
||||
@ -88,6 +90,9 @@ docs/virtualenv/
|
||||
# bft-smart
|
||||
**/config/currentView
|
||||
|
||||
# Node Explorer
|
||||
/tools/explorer/conf/CordaExplorer.properties
|
||||
|
||||
# vim
|
||||
*.swp
|
||||
*.swn
|
||||
|
19
.idea/codeStyleSettings.xml
generated
Normal file
19
.idea/codeStyleSettings.xml
generated
Normal file
@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectCodeStyleSettingsManager">
|
||||
<option name="PER_PROJECT_SETTINGS">
|
||||
<value>
|
||||
<JetCodeStyleSettings>
|
||||
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
|
||||
<value>
|
||||
<package name="java.util" withSubpackages="false" static="false" />
|
||||
<package name="kotlinx.android.synthetic" withSubpackages="true" static="false" />
|
||||
<package name="tornadofx" withSubpackages="false" static="false" />
|
||||
</value>
|
||||
</option>
|
||||
</JetCodeStyleSettings>
|
||||
</value>
|
||||
</option>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</component>
|
||||
</project>
|
10
.idea/compiler.xml
generated
10
.idea/compiler.xml
generated
@ -2,6 +2,8 @@
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="1.8">
|
||||
<module name="api-scanner_main" target="1.8" />
|
||||
<module name="api-scanner_test" 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" />
|
||||
@ -12,11 +14,15 @@
|
||||
<module name="buildSrc_test" target="1.8" />
|
||||
<module name="client_main" target="1.8" />
|
||||
<module name="client_test" target="1.8" />
|
||||
<module name="confidential-identities_main" target="1.8" />
|
||||
<module name="confidential-identities_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="cordapp_main" target="1.8" />
|
||||
<module name="cordapp_test" target="1.8" />
|
||||
<module name="cordform-common_main" target="1.8" />
|
||||
<module name="cordform-common_test" target="1.8" />
|
||||
<module name="cordformation_main" target="1.8" />
|
||||
@ -37,8 +43,11 @@
|
||||
<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_integrationTest" target="1.8" />
|
||||
<module name="finance_main" target="1.8" />
|
||||
<module name="finance_test" target="1.8" />
|
||||
<module name="gradle-plugins-cordform-common_main" target="1.8" />
|
||||
<module name="gradle-plugins-cordform-common_test" target="1.8" />
|
||||
<module name="graphs_main" target="1.8" />
|
||||
<module name="graphs_test" target="1.8" />
|
||||
<module name="irs-demo_integrationTest" target="1.8" />
|
||||
@ -91,6 +100,7 @@
|
||||
<module name="smoke-test-utils_test" target="1.8" />
|
||||
<module name="test-common_main" target="1.8" />
|
||||
<module name="test-common_test" target="1.8" />
|
||||
<module name="test-utils_integrationTest" target="1.8" />
|
||||
<module name="test-utils_main" target="1.8" />
|
||||
<module name="test-utils_test" target="1.8" />
|
||||
<module name="testing-node-driver_integrationTest" target="1.8" />
|
||||
|
@ -1,5 +1,5 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="explorer" type="TORNADOFX_RUNCONFIGURATION" factoryName="TornadoFX Configuration Factory" run-type="App" live-views="false" live-stylesheets="false" dump-stylesheets="false">
|
||||
<configuration default="false" name="Explorer - GUI" type="TORNADOFX_RUNCONFIGURATION" factoryName="TornadoFX Configuration Factory" run-type="App" live-views="false" live-stylesheets="false" dump-stylesheets="false">
|
||||
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
|
||||
<option name="RUN_TYPE" value="App" />
|
||||
<option name="VIEW_CLASS_NAME" />
|
3
.idea/scopes/Corda_API.xml
generated
Normal file
3
.idea/scopes/Corda_API.xml
generated
Normal file
@ -0,0 +1,3 @@
|
||||
<component name="DependencyValidationManager">
|
||||
<scope name="Corda API" pattern="src[core_main]:*..*&&!src[core_main]:net.corda.core.internal..*||src[rpc_main]:net.corda.client.rpc.*||src[jackson_main]:net.corda.client.jackson.*" />
|
||||
</component>
|
69
README.md
69
README.md
@ -8,72 +8,29 @@ Corda is a decentralised database system in which nodes trust each other as litt
|
||||
|
||||
## Features
|
||||
|
||||
* A P2P network of nodes
|
||||
* Smart contracts
|
||||
* Flow framework
|
||||
* "Notary" infrastructure to validate uniqueness of transactions
|
||||
* Written as a platform for distributed apps called CorDapps
|
||||
* Smart contracts that can be written in Java and other JVM languages
|
||||
* Flow framework to manage communication and negotiation between participants
|
||||
* Peer-to-peer network of nodes
|
||||
* "Notary" infrastructure to validate uniqueness and sequencing of transactions without global broadcast
|
||||
* Enables the development and deployment of distributed apps called CorDapps
|
||||
* Written in [Kotlin](https://kotlinlang.org), targeting the JVM
|
||||
|
||||
## Getting started
|
||||
|
||||
Firstly, read the [Getting started](https://docs.corda.net/getting-set-up.html) documentation.
|
||||
|
||||
Next, use the following guides to set up your dev environment:
|
||||
|
||||
* If you are on **Windows** [use this getting started guide](https://www.corda.net/wp-content/uploads/2017/01/Corda-Windows-Quick-start-guide-1.pdf) which also explains through how to run the sample apps.
|
||||
|
||||
* Alternatively if you are on **Mac/Linux**, [watch this brief Webinar](https://vimeo.com/200167665) which walks through getting Corda, installing it, building it, running nodes and opening projects in IntelliJ.
|
||||
|
||||
After the above, watching the following webinars will give you a great introduction to Corda:
|
||||
|
||||
### Webinar 1 – [Introduction to Corda](https://vimeo.com/192757743/c2ec39c1e1)
|
||||
|
||||
Richard Brown, R3 Chief Technology Officer, explains Corda's unique architecture, the only distributed ledger platform designed by and for the financial industry's unique requirements. You may want to read the [Corda non-technical whitepaper](https://www.r3cev.com/s/corda-introductory-whitepaper-final.pdf) as pre-reading for this session.
|
||||
|
||||
### Webinar 2 – [Corda Developers’ Tutorial](https://vimeo.com/192797322/aab499b152)
|
||||
|
||||
Roger Willis, R3 Developer Relations Lead, provides an overview of Corda from a developer’s perspective and guidance on how to start building CorDapps. You may want to view [Webinar 1 - Introduction to Corda](https://vimeo.com/192757743/c2ec39c1e1) as preparation for this session. **NB. This was recorded for the M5 release.**
|
||||
|
||||
## Building on Corda
|
||||
|
||||
To build your own CorDapps:
|
||||
|
||||
1. Clone the [CorDapp Template repository](https://github.com/corda/cordapp-template)
|
||||
2. Read the [README](https://github.com/corda/cordapp-template/blob/master/README.md) (**IMPORTANT!**)
|
||||
3. Read the [Writing a CorDapp](https://docs.corda.net/tutorial-cordapp.html) documentation
|
||||
|
||||
To look at the Corda source and run some sample applications:
|
||||
|
||||
1. Clone this repository
|
||||
2. To run some sample CorDapps, read the [running the demos documentation](https://docs.corda.r3cev.com/running-the-demos.html)
|
||||
3. Start hacking and [contribute](./CONTRIBUTING.md)!
|
||||
1. Read the [Getting Started](https://docs.corda.net/getting-set-up.html) documentation
|
||||
2. Run the [Example CorDapp](https://docs.corda.net/tutorial-cordapp.html)
|
||||
3. Read about Corda's [Key Concepts](https://docs.corda.net/key-concepts.html)
|
||||
3. Follow the [Hello, World! tutorial](https://docs.corda.net/hello-world-index.html)
|
||||
|
||||
## Useful links
|
||||
|
||||
* [Project website](https://corda.net)
|
||||
* [Project Website](https://corda.net)
|
||||
* [Documentation](https://docs.corda.net)
|
||||
* [Slack channel](https://slack.corda.net/)
|
||||
* [Slack Channel](https://slack.corda.net/)
|
||||
* [Stack Overflow tag](https://stackoverflow.com/questions/tagged/corda)
|
||||
* [Forum](https://discourse.corda.net)
|
||||
* [Meetups](https://www.meetup.com/pro/corda/)
|
||||
* [Training Course](https://www.corda.net/corda-training/)
|
||||
|
||||
|
||||
## Development State
|
||||
|
||||
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.
|
||||
|
||||
## Background
|
||||
|
||||
The project is supported by R3, a financial industry consortium, which is why it
|
||||
contains some code for financial use cases and why the documentation focuses on finance. The goal is to use it
|
||||
to construct a global ledger, simplifying finance and reducing the overheads of banking. But it is run as
|
||||
an open source project and the basic technology of a peer-to-peer decentralised database may be useful
|
||||
for many different projects.
|
||||
* [Training Courses](https://www.corda.net/corda-training/)
|
||||
|
||||
## Contributing
|
||||
|
||||
|
65
build.gradle
65
build.gradle
@ -4,7 +4,7 @@ buildscript {
|
||||
file("$projectDir/constants.properties").withInputStream { constants.load(it) }
|
||||
|
||||
// Our version: bump this on release.
|
||||
ext.corda_release_version = "0.16-SNAPSHOT"
|
||||
ext.corda_release_version = "1.1-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
|
||||
@ -36,11 +36,11 @@ buildscript {
|
||||
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.mockito_version = '2.10.0'
|
||||
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.h2_version = '1.4.194' // Update docs if renamed or removed.
|
||||
ext.rxjava_version = '1.2.4'
|
||||
ext.dokka_version = '0.9.14'
|
||||
ext.eddsa_version = '0.2.0'
|
||||
@ -59,7 +59,9 @@ buildscript {
|
||||
classpath "net.corda.plugins:publish-utils:$gradle_plugins_version"
|
||||
classpath "net.corda.plugins:quasar-utils:$gradle_plugins_version"
|
||||
classpath "net.corda.plugins:cordformation:$gradle_plugins_version"
|
||||
classpath 'com.github.ben-manes:gradle-versions-plugin:0.13.0'
|
||||
classpath "net.corda.plugins:cordapp:$gradle_plugins_version"
|
||||
classpath "net.corda.plugins:api-scanner:$gradle_plugins_version"
|
||||
classpath 'com.github.ben-manes:gradle-versions-plugin:0.15.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-noarg:$kotlin_version"
|
||||
classpath "org.jetbrains.dokka:dokka-gradle-plugin:${dokka_version}"
|
||||
classpath "org.ajoberstar:grgit:1.1.0"
|
||||
@ -115,12 +117,14 @@ allprojects {
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(Jar) { // Includes War and Ear
|
||||
tasks.withType(Jar) { task ->
|
||||
// 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')
|
||||
attributes('Automatic-Module-Name': "net.corda.${task.project.name.replaceAll('-', '.')}")
|
||||
}
|
||||
}
|
||||
|
||||
@ -146,10 +150,16 @@ allprojects {
|
||||
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'
|
||||
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'
|
||||
}
|
||||
runtime {
|
||||
// We never want isolated.jar on classPath, since we want to test jar being dynamically loaded as an attachment
|
||||
exclude module: 'isolated'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -169,26 +179,31 @@ repositories {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Corda root project currently produces a dummy cordapp when it shouldn't.
|
||||
// Required for building out the fat JAR.
|
||||
dependencies {
|
||||
cordaCompile project(':node')
|
||||
compile project(':node')
|
||||
compile "com.google.guava:guava:$guava_version"
|
||||
|
||||
// Set to corda compile to ensure it exists now deploy nodes no longer relies on build
|
||||
cordaCompile project(path: ":node:capsule", configuration: 'runtimeArtifacts')
|
||||
cordaCompile project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts')
|
||||
compile project(path: ":node:capsule", configuration: 'runtimeArtifacts')
|
||||
compile project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts')
|
||||
|
||||
// For the buildCordappDependenciesJar task
|
||||
cordaRuntime project(':client:jfx')
|
||||
cordaRuntime project(':client:mock')
|
||||
cordaRuntime project(':client:rpc')
|
||||
cordaRuntime project(':core')
|
||||
cordaRuntime project(':finance')
|
||||
cordaRuntime project(':webserver')
|
||||
runtime project(':client:jfx')
|
||||
runtime project(':client:mock')
|
||||
runtime project(':client:rpc')
|
||||
runtime project(':core')
|
||||
runtime project(':confidential-identities')
|
||||
runtime project(':finance')
|
||||
runtime project(':webserver')
|
||||
testCompile project(':test-utils')
|
||||
}
|
||||
|
||||
jar {
|
||||
// Prevent the root project from building an unwanted dummy CorDapp.
|
||||
enabled = false
|
||||
}
|
||||
|
||||
task jacocoRootReport(type: org.gradle.testing.jacoco.tasks.JacocoReport) {
|
||||
dependsOn = subprojects.test
|
||||
additionalSourceDirs = files(subprojects.sourceSets.main.allSource.srcDirs)
|
||||
@ -219,13 +234,12 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
|
||||
networkMap "O=Controller,OU=corda,L=London,C=GB"
|
||||
node {
|
||||
name "O=Controller,OU=corda,L=London,C=GB"
|
||||
advertisedServices = ["corda.notary.validating"]
|
||||
notary = [validating : true]
|
||||
p2pPort 10002
|
||||
cordapps = []
|
||||
}
|
||||
node {
|
||||
name "O=Bank A,OU=corda,L=London,C=GB"
|
||||
advertisedServices = []
|
||||
p2pPort 10012
|
||||
rpcPort 10013
|
||||
webPort 10014
|
||||
@ -233,7 +247,6 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
|
||||
}
|
||||
node {
|
||||
name "O=Bank B,OU=corda,L=London,C=GB"
|
||||
advertisedServices = []
|
||||
p2pPort 10007
|
||||
rpcPort 10008
|
||||
webPort 10009
|
||||
@ -251,7 +264,7 @@ bintrayConfig {
|
||||
projectUrl = 'https://github.com/corda/corda'
|
||||
gpgSign = true
|
||||
gpgPassphrase = System.getenv('CORDA_BINTRAY_GPG_PASSPHRASE')
|
||||
publications = ['corda-jfx', 'corda-mock', 'corda-rpc', 'corda-core', 'corda', 'corda-finance', 'corda-node', 'corda-node-api', 'corda-test-common', 'corda-test-utils', 'corda-jackson', 'corda-verifier', 'corda-webserver-impl', 'corda-webserver', 'corda-node-driver']
|
||||
publications = ['corda-jfx', 'corda-mock', 'corda-rpc', 'corda-core', 'corda', 'corda-finance', 'corda-node', 'corda-node-api', 'corda-test-common', 'corda-test-utils', 'corda-jackson', 'corda-verifier', 'corda-webserver-impl', 'corda-webserver', 'corda-node-driver', 'corda-confidential-identities']
|
||||
license {
|
||||
name = 'Apache-2.0'
|
||||
url = 'https://www.apache.org/licenses/LICENSE-2.0'
|
||||
@ -286,7 +299,11 @@ artifactory {
|
||||
password = System.getenv('CORDA_ARTIFACTORY_PASSWORD')
|
||||
}
|
||||
defaults {
|
||||
publications('corda-jfx', 'corda-mock', 'corda-rpc', 'corda-core', 'corda', 'cordform-common', 'corda-finance', 'corda-node', 'corda-node-api', 'corda-test-common', 'corda-test-utils', 'corda-jackson', 'corda-verifier', 'corda-webserver-impl', 'corda-webserver', 'corda-node-driver')
|
||||
publications('corda-jfx', 'corda-mock', 'corda-rpc', 'corda-core', 'corda', 'cordform-common', 'corda-finance', 'corda-node', 'corda-node-api', 'corda-test-common', 'corda-test-utils', 'corda-jackson', 'corda-verifier', 'corda-webserver-impl', 'corda-webserver', 'corda-node-driver', 'corda-confidential-identities')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task generateApi(type: net.corda.plugins.GenerateApi){
|
||||
baseName = "api-corda"
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ class CanonicalizerPlugin implements Plugin<Project> {
|
||||
output.setMethod(ZipOutputStream.DEFLATED)
|
||||
|
||||
entries.each {
|
||||
def newEntry = new ZipEntry( it.name )
|
||||
def newEntry = new ZipEntry(it.name)
|
||||
|
||||
newEntry.setLastModifiedTime(zeroTime)
|
||||
newEntry.setCreationTime(zeroTime)
|
||||
|
@ -1,6 +1,7 @@
|
||||
apply plugin: 'java'
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'net.corda.plugins.publish-utils'
|
||||
apply plugin: 'net.corda.plugins.api-scanner'
|
||||
apply plugin: 'com.jfrog.artifactory'
|
||||
|
||||
dependencies {
|
||||
@ -24,6 +25,9 @@ dependencies {
|
||||
|
||||
jar {
|
||||
baseName 'corda-jackson'
|
||||
manifest {
|
||||
attributes 'Automatic-Module-Name': 'net.corda.client.jackson'
|
||||
}
|
||||
}
|
||||
|
||||
publish {
|
||||
|
@ -17,6 +17,7 @@ import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.services.IdentityService
|
||||
@ -28,8 +29,9 @@ import net.corda.core.transactions.NotaryChangeWireTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.parsePublicKeyBase58
|
||||
import net.corda.core.utilities.toBase58String
|
||||
import net.corda.core.utilities.base58ToByteArray
|
||||
import net.corda.core.utilities.base64ToByteArray
|
||||
import net.corda.core.utilities.toBase64
|
||||
import net.i2p.crypto.eddsa.EdDSAPublicKey
|
||||
import java.math.BigDecimal
|
||||
import java.security.PublicKey
|
||||
@ -46,25 +48,25 @@ object JacksonSupport {
|
||||
// If you change this API please update the docs in the docsite (json.rst)
|
||||
|
||||
interface PartyObjectMapper {
|
||||
fun partyFromX500Name(name: CordaX500Name): Party?
|
||||
fun wellKnownPartyFromX500Name(name: CordaX500Name): Party?
|
||||
fun partyFromKey(owningKey: PublicKey): Party?
|
||||
fun partiesFromName(query: String): Set<Party>
|
||||
}
|
||||
|
||||
class RpcObjectMapper(val rpc: CordaRPCOps, factory: JsonFactory, val fuzzyIdentityMatch: Boolean) : PartyObjectMapper, ObjectMapper(factory) {
|
||||
override fun partyFromX500Name(name: CordaX500Name): Party? = rpc.partyFromX500Name(name)
|
||||
override fun wellKnownPartyFromX500Name(name: CordaX500Name): Party? = rpc.wellKnownPartyFromX500Name(name)
|
||||
override fun partyFromKey(owningKey: PublicKey): Party? = rpc.partyFromKey(owningKey)
|
||||
override fun partiesFromName(query: String) = rpc.partiesFromName(query, fuzzyIdentityMatch)
|
||||
}
|
||||
|
||||
class IdentityObjectMapper(val identityService: IdentityService, factory: JsonFactory, val fuzzyIdentityMatch: Boolean) : PartyObjectMapper, ObjectMapper(factory) {
|
||||
override fun partyFromX500Name(name: CordaX500Name): Party? = identityService.partyFromX500Name(name)
|
||||
override fun wellKnownPartyFromX500Name(name: CordaX500Name): Party? = identityService.wellKnownPartyFromX500Name(name)
|
||||
override fun partyFromKey(owningKey: PublicKey): Party? = identityService.partyFromKey(owningKey)
|
||||
override fun partiesFromName(query: String) = identityService.partiesFromName(query, fuzzyIdentityMatch)
|
||||
}
|
||||
|
||||
class NoPartyObjectMapper(factory: JsonFactory) : PartyObjectMapper, ObjectMapper(factory) {
|
||||
override fun partyFromX500Name(name: CordaX500Name): Party? = throw UnsupportedOperationException()
|
||||
override fun wellKnownPartyFromX500Name(name: CordaX500Name): Party? = throw UnsupportedOperationException()
|
||||
override fun partyFromKey(owningKey: PublicKey): Party? = throw UnsupportedOperationException()
|
||||
override fun partiesFromName(query: String) = throw UnsupportedOperationException()
|
||||
}
|
||||
@ -83,13 +85,9 @@ object JacksonSupport {
|
||||
addDeserializer(SecureHash::class.java, SecureHashDeserializer())
|
||||
addDeserializer(SecureHash.SHA256::class.java, SecureHashDeserializer())
|
||||
|
||||
// For ed25519 pubkeys
|
||||
addSerializer(EdDSAPublicKey::class.java, PublicKeySerializer)
|
||||
addDeserializer(EdDSAPublicKey::class.java, PublicKeyDeserializer)
|
||||
|
||||
// For composite keys
|
||||
addSerializer(CompositeKey::class.java, CompositeKeySerializer)
|
||||
addDeserializer(CompositeKey::class.java, CompositeKeyDeserializer)
|
||||
// Public key types
|
||||
addSerializer(PublicKey::class.java, PublicKeySerializer)
|
||||
addDeserializer(PublicKey::class.java, PublicKeyDeserializer)
|
||||
|
||||
// For NodeInfo
|
||||
// TODO this tunnels the Kryo representation as a Base58 encoded string. Replace when RPC supports this.
|
||||
@ -121,12 +119,14 @@ object JacksonSupport {
|
||||
* match an identity known from the network map. If true, the name is matched more leniently but if the match
|
||||
* is ambiguous a [JsonParseException] is thrown.
|
||||
*/
|
||||
@JvmStatic @JvmOverloads
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
fun createDefaultMapper(rpc: CordaRPCOps, factory: JsonFactory = JsonFactory(),
|
||||
fuzzyIdentityMatch: Boolean = false): ObjectMapper = configureMapper(RpcObjectMapper(rpc, factory, fuzzyIdentityMatch))
|
||||
|
||||
/** For testing or situations where deserialising parties is not required */
|
||||
@JvmStatic @JvmOverloads
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
fun createNonRpcMapper(factory: JsonFactory = JsonFactory()): ObjectMapper = configureMapper(NoPartyObjectMapper(factory))
|
||||
|
||||
/**
|
||||
@ -136,7 +136,8 @@ object JacksonSupport {
|
||||
* match an identity known from the network map. If true, the name is matched more leniently but if the match
|
||||
* is ambiguous a [JsonParseException] is thrown.
|
||||
*/
|
||||
@JvmStatic @JvmOverloads
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
fun createInMemoryMapper(identityService: IdentityService, factory: JsonFactory = JsonFactory(),
|
||||
fuzzyIdentityMatch: Boolean = false) = configureMapper(IdentityObjectMapper(identityService, factory, fuzzyIdentityMatch))
|
||||
|
||||
@ -158,7 +159,7 @@ object JacksonSupport {
|
||||
|
||||
object AnonymousPartySerializer : JsonSerializer<AnonymousParty>() {
|
||||
override fun serialize(obj: AnonymousParty, generator: JsonGenerator, provider: SerializerProvider) {
|
||||
generator.writeString(obj.owningKey.toBase58String())
|
||||
PublicKeySerializer.serialize(obj.owningKey, generator, provider)
|
||||
}
|
||||
}
|
||||
|
||||
@ -168,8 +169,7 @@ object JacksonSupport {
|
||||
parser.nextToken()
|
||||
}
|
||||
|
||||
// TODO this needs to use some industry identifier(s) instead of these keys
|
||||
val key = parsePublicKeyBase58(parser.text)
|
||||
val key = PublicKeyDeserializer.deserialize(parser, context)
|
||||
return AnonymousParty(key)
|
||||
}
|
||||
}
|
||||
@ -187,19 +187,24 @@ object JacksonSupport {
|
||||
}
|
||||
|
||||
val mapper = parser.codec as PartyObjectMapper
|
||||
// TODO: We should probably have a better specified way of identifying X.500 names vs keys
|
||||
// Base58 keys never include an equals character, while X.500 names always will, so we use that to determine
|
||||
// how to parse the content
|
||||
return if (parser.text.contains("=")) {
|
||||
// The comma character is invalid in base64, and required as a separator for X.500 names. As Corda
|
||||
// X.500 names all involve at least three attributes (organisation, locality, country), they must
|
||||
// include a comma. As such we can use it as a distinguisher between the two types.
|
||||
return if (parser.text.contains(",")) {
|
||||
val principal = CordaX500Name.parse(parser.text)
|
||||
mapper.partyFromX500Name(principal) ?: throw JsonParseException(parser, "Could not find a Party with name $principal")
|
||||
mapper.wellKnownPartyFromX500Name(principal) ?: throw JsonParseException(parser, "Could not find a Party with name $principal")
|
||||
} else {
|
||||
val nameMatches = mapper.partiesFromName(parser.text)
|
||||
if (nameMatches.isEmpty()) {
|
||||
val derBytes = try {
|
||||
parser.text.base64ToByteArray()
|
||||
} catch (e: AddressFormatException) {
|
||||
throw JsonParseException(parser, "Could not find a matching party for '${parser.text}' and is not a base64 encoded public key: " + e.message)
|
||||
}
|
||||
val key = try {
|
||||
parsePublicKeyBase58(parser.text)
|
||||
Crypto.decodePublicKey(derBytes)
|
||||
} catch (e: Exception) {
|
||||
throw JsonParseException(parser, "Could not find a matching party for '${parser.text}' and is not a base58 encoded public key")
|
||||
throw JsonParseException(parser, "Could not find a matching party for '${parser.text}' and is not a valid public key: " + e.message)
|
||||
}
|
||||
mapper.partyFromKey(key) ?: throw JsonParseException(parser, "Could not find a Party with key ${key.toStringShort()}")
|
||||
} else if (nameMatches.size == 1) {
|
||||
@ -225,7 +230,7 @@ object JacksonSupport {
|
||||
|
||||
return try {
|
||||
CordaX500Name.parse(parser.text)
|
||||
} catch(ex: IllegalArgumentException) {
|
||||
} catch (ex: IllegalArgumentException) {
|
||||
throw JsonParseException(parser, "Invalid Corda X.500 name ${parser.text}: ${ex.message}", ex)
|
||||
}
|
||||
}
|
||||
@ -265,47 +270,30 @@ object JacksonSupport {
|
||||
parser.nextToken()
|
||||
}
|
||||
try {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return SecureHash.parse(parser.text) as T
|
||||
return uncheckedCast(SecureHash.parse(parser.text))
|
||||
} catch (e: Exception) {
|
||||
throw JsonParseException(parser, "Invalid hash ${parser.text}: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object PublicKeySerializer : JsonSerializer<EdDSAPublicKey>() {
|
||||
override fun serialize(obj: EdDSAPublicKey, generator: JsonGenerator, provider: SerializerProvider) {
|
||||
check(obj.params == Crypto.EDDSA_ED25519_SHA512.algSpec)
|
||||
generator.writeString(obj.toBase58String())
|
||||
object PublicKeySerializer : JsonSerializer<PublicKey>() {
|
||||
override fun serialize(obj: PublicKey, generator: JsonGenerator, provider: SerializerProvider) {
|
||||
generator.writeString(obj.encoded.toBase64())
|
||||
}
|
||||
}
|
||||
|
||||
object PublicKeyDeserializer : JsonDeserializer<EdDSAPublicKey>() {
|
||||
override fun deserialize(parser: JsonParser, context: DeserializationContext): EdDSAPublicKey {
|
||||
object PublicKeyDeserializer : JsonDeserializer<PublicKey>() {
|
||||
override fun deserialize(parser: JsonParser, context: DeserializationContext): PublicKey {
|
||||
return try {
|
||||
parsePublicKeyBase58(parser.text) as EdDSAPublicKey
|
||||
val derBytes = parser.text.base64ToByteArray()
|
||||
Crypto.decodePublicKey(derBytes)
|
||||
} catch (e: Exception) {
|
||||
throw JsonParseException(parser, "Invalid public key ${parser.text}: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object CompositeKeySerializer : JsonSerializer<CompositeKey>() {
|
||||
override fun serialize(obj: CompositeKey, generator: JsonGenerator, provider: SerializerProvider) {
|
||||
generator.writeString(obj.toBase58String())
|
||||
}
|
||||
}
|
||||
|
||||
object CompositeKeyDeserializer : JsonDeserializer<CompositeKey>() {
|
||||
override fun deserialize(parser: JsonParser, context: DeserializationContext): CompositeKey {
|
||||
return try {
|
||||
parsePublicKeyBase58(parser.text) as CompositeKey
|
||||
} catch (e: Exception) {
|
||||
throw JsonParseException(parser, "Invalid composite key ${parser.text}: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object AmountSerializer : JsonSerializer<Amount<*>>() {
|
||||
override fun serialize(value: Amount<*>, gen: JsonGenerator, serializers: SerializerProvider) {
|
||||
gen.writeString(value.toString())
|
||||
@ -325,7 +313,7 @@ object JacksonSupport {
|
||||
// Attempt parsing as a currency token. TODO: This needs thought about how to extend to other token types.
|
||||
val currency = Currency.getInstance(token)
|
||||
return Amount(quantity, currency)
|
||||
} catch(e2: Exception) {
|
||||
} catch (e2: Exception) {
|
||||
throw JsonParseException(parser, "Invalid amount ${parser.text}", e2)
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
|
||||
import com.google.common.collect.HashMultimap
|
||||
import com.google.common.collect.Multimap
|
||||
import net.corda.client.jackson.StringToMethodCallParser.ParsedMethodCall
|
||||
import net.corda.core.CordaException
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.lang.reflect.Constructor
|
||||
import java.lang.reflect.Method
|
||||
@ -99,7 +100,7 @@ open class StringToMethodCallParser<in T : Any> @JvmOverloads constructor(
|
||||
val methodParamNames: Map<String, List<String>> = targetType.declaredMethods.mapNotNull {
|
||||
try {
|
||||
it.name to paramNamesFromMethod(it)
|
||||
} catch(e: KotlinReflectionInternalError) {
|
||||
} catch (e: KotlinReflectionInternalError) {
|
||||
// Kotlin reflection doesn't support every method that can exist on an object (in particular, reified
|
||||
// inline methods) so we just ignore those here.
|
||||
null
|
||||
@ -146,7 +147,7 @@ open class StringToMethodCallParser<in T : Any> @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
|
||||
open class UnparseableCallException(command: String, cause: Throwable? = null) : Exception("Could not parse as a command: $command", cause) {
|
||||
open class UnparseableCallException(command: String, cause: Throwable? = null) : CordaException("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")
|
||||
@ -174,7 +175,7 @@ open class StringToMethodCallParser<in T : Any> @JvmOverloads constructor(
|
||||
try {
|
||||
val args = parseArguments(name, paramNamesFromMethod(method).zip(method.parameterTypes), argStr)
|
||||
return ParsedMethodCall(target, method, args)
|
||||
} catch(e: UnparseableCallException) {
|
||||
} catch (e: UnparseableCallException) {
|
||||
if (index == methods.size - 1)
|
||||
throw e
|
||||
}
|
||||
@ -197,7 +198,7 @@ open class StringToMethodCallParser<in T : Any> @JvmOverloads constructor(
|
||||
val entry = tree[argName] ?: throw UnparseableCallException.MissingParameter(methodNameHint, argName, args)
|
||||
try {
|
||||
om.readValue(entry.traverse(om), argType)
|
||||
} catch(e: Exception) {
|
||||
} catch (e: Exception) {
|
||||
throw UnparseableCallException.FailedParse(e)
|
||||
}
|
||||
}
|
||||
@ -211,16 +212,17 @@ open class StringToMethodCallParser<in T : Any> @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
/** Returns a string-to-string map of commands to a string describing available parameter types. */
|
||||
val availableCommands: Map<String, String> get() {
|
||||
return methodMap.entries().map { entry ->
|
||||
val (name, args) = entry // TODO: Kotlin 1.1
|
||||
val argStr = if (args.parameterCount == 0) "" else {
|
||||
val paramNames = methodParamNames[name]!!
|
||||
val typeNames = args.parameters.map { it.type.simpleName }
|
||||
val paramTypes = paramNames.zip(typeNames)
|
||||
paramTypes.map { "${it.first}: ${it.second}" }.joinToString(", ")
|
||||
}
|
||||
Pair(name, argStr)
|
||||
}.toMap()
|
||||
}
|
||||
val availableCommands: Map<String, String>
|
||||
get() {
|
||||
return methodMap.entries().map { entry ->
|
||||
val (name, args) = entry // TODO: Kotlin 1.1
|
||||
val argStr = if (args.parameterCount == 0) "" else {
|
||||
val paramNames = methodParamNames[name]!!
|
||||
val typeNames = args.parameters.map { it.type.simpleName }
|
||||
val paramTypes = paramNames.zip(typeNames)
|
||||
paramTypes.map { "${it.first}: ${it.second}" }.joinToString(", ")
|
||||
}
|
||||
Pair(name, argStr)
|
||||
}.toMap()
|
||||
}
|
||||
}
|
||||
|
@ -1,38 +1,71 @@
|
||||
package net.corda.client.jackson
|
||||
|
||||
import com.fasterxml.jackson.databind.SerializationFeature
|
||||
import com.nhaarman.mockito_kotlin.mock
|
||||
import com.nhaarman.mockito_kotlin.whenever
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.finance.USD
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.SignatureMetadata
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.crypto.generateKeyPair
|
||||
import net.corda.core.cordapp.CordappProvider
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.finance.USD
|
||||
import net.corda.testing.ALICE_PUBKEY
|
||||
import net.corda.testing.DUMMY_NOTARY
|
||||
import net.corda.testing.MINI_CORP
|
||||
import net.corda.testing.TestDependencyInjectionBase
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.i2p.crypto.eddsa.EdDSAPublicKey
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.math.BigInteger
|
||||
import java.security.PublicKey
|
||||
import java.util.*
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class JacksonSupportTest : TestDependencyInjectionBase() {
|
||||
companion object {
|
||||
private val SEED = BigInteger.valueOf(20170922L)
|
||||
val mapper = JacksonSupport.createNonRpcMapper()
|
||||
}
|
||||
|
||||
private lateinit var services: ServiceHub
|
||||
private lateinit var cordappProvider: CordappProvider
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
services = mock()
|
||||
cordappProvider = mock()
|
||||
whenever(services.cordappProvider).thenReturn(cordappProvider)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun publicKeySerializingWorks() {
|
||||
val publicKey = generateKeyPair().public
|
||||
fun `should serialize Composite keys`() {
|
||||
val expected = "\"MIHAMBUGE2mtoq+J1bjir/ONk6yd5pab0FoDgaYAMIGiAgECMIGcMDIDLQAwKjAFBgMrZXADIQAgIX1QlJRgaLlD0ttLlJF5kNqT/7P7QwCvrWc9+/248gIBATAyAy0AMCowBQYDK2VwAyEAqS0JPGlzdviBZjB9FaNY+w6cVs3/CQ2A5EimE9Lyng4CAQEwMgMtADAqMAUGAytlcAMhALq4GG0gBQZIlaKE6ucooZsuoKUbH4MtGSmA6cwj136+AgEB\""
|
||||
val innerKeys = (1..3).map { i ->
|
||||
Crypto.deriveKeyPairFromEntropy(Crypto.EDDSA_ED25519_SHA512, SEED.plus(BigInteger.valueOf(i.toLong()))).public
|
||||
}
|
||||
// Build a 2 of 3 composite key
|
||||
val publicKey = CompositeKey.Builder().let {
|
||||
innerKeys.forEach { key -> it.addKey(key, 1) }
|
||||
it.build(2)
|
||||
}
|
||||
val serialized = mapper.writeValueAsString(publicKey)
|
||||
val parsedKey = mapper.readValue(serialized, EdDSAPublicKey::class.java)
|
||||
assertEquals(expected, serialized)
|
||||
val parsedKey = mapper.readValue(serialized, PublicKey::class.java)
|
||||
assertEquals(publicKey, parsedKey)
|
||||
}
|
||||
|
||||
private class Dummy(val notional: Amount<Currency>)
|
||||
|
||||
@Test
|
||||
fun `should serialize EdDSA keys`() {
|
||||
val expected = "\"MCowBQYDK2VwAyEACFTgLk1NOqYXAfxLoR7ctSbZcl9KMXu58Mq31Kv1Dwk=\""
|
||||
val publicKey = Crypto.deriveKeyPairFromEntropy(Crypto.EDDSA_ED25519_SHA512, SEED).public
|
||||
val serialized = mapper.writeValueAsString(publicKey)
|
||||
assertEquals(expected, serialized)
|
||||
val parsedKey = mapper.readValue(serialized, PublicKey::class.java)
|
||||
assertEquals(publicKey, parsedKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun readAmount() {
|
||||
val oldJson = """
|
||||
@ -57,8 +90,12 @@ class JacksonSupportTest : TestDependencyInjectionBase() {
|
||||
|
||||
@Test
|
||||
fun writeTransaction() {
|
||||
val attachmentRef = SecureHash.randomSHA256()
|
||||
whenever(cordappProvider.getContractAttachmentID(DummyContract.PROGRAM_ID))
|
||||
.thenReturn(attachmentRef)
|
||||
fun makeDummyTx(): SignedTransaction {
|
||||
val wtx = DummyContract.generateInitial(1, DUMMY_NOTARY, MINI_CORP.ref(1)).toWireTransaction()
|
||||
val wtx = DummyContract.generateInitial(1, DUMMY_NOTARY, MINI_CORP.ref(1))
|
||||
.toWireTransaction(services)
|
||||
val signatures = TransactionSignature(
|
||||
ByteArray(1),
|
||||
ALICE_PUBKEY,
|
||||
|
@ -7,9 +7,6 @@ description 'Corda client JavaFX modules'
|
||||
|
||||
//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'
|
||||
|
||||
integrationTestCompile.extendsFrom testCompile
|
||||
integrationTestRuntime.extendsFrom testRuntime
|
||||
}
|
||||
@ -60,8 +57,11 @@ task integrationTest(type: Test) {
|
||||
|
||||
jar {
|
||||
baseName 'corda-jfx'
|
||||
manifest {
|
||||
attributes 'Automatic-Module-Name': 'net.corda.client.jfx'
|
||||
}
|
||||
}
|
||||
|
||||
publish {
|
||||
name jar.baseName
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import net.corda.core.crypto.keys
|
||||
import net.corda.core.flows.FlowInitiator
|
||||
import net.corda.core.flows.StateMachineRunId
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.bufferUntilSubscribed
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.messaging.StateMachineTransactionMapping
|
||||
@ -16,7 +17,6 @@ import net.corda.core.messaging.StateMachineUpdate
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.services.NetworkMapCache
|
||||
import net.corda.core.node.services.ServiceInfo
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
@ -27,8 +27,6 @@ import net.corda.finance.flows.CashExitFlow
|
||||
import net.corda.finance.flows.CashIssueFlow
|
||||
import net.corda.finance.flows.CashPaymentFlow
|
||||
import net.corda.node.services.FlowPermissions.Companion.startFlowPermission
|
||||
import net.corda.node.services.network.NetworkMapService
|
||||
import net.corda.node.services.transactions.SimpleNotaryService
|
||||
import net.corda.nodeapi.User
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.driver.driver
|
||||
@ -39,7 +37,7 @@ import rx.Observable
|
||||
class NodeMonitorModelTest : DriverBasedTest() {
|
||||
lateinit var aliceNode: NodeInfo
|
||||
lateinit var bobNode: NodeInfo
|
||||
lateinit var notaryNode: NodeInfo
|
||||
lateinit var notaryParty: Party
|
||||
|
||||
lateinit var rpc: CordaRPCOps
|
||||
lateinit var rpcBob: CordaRPCOps
|
||||
@ -52,18 +50,16 @@ class NodeMonitorModelTest : DriverBasedTest() {
|
||||
lateinit var networkMapUpdates: Observable<NetworkMapCache.MapChange>
|
||||
lateinit var newNode: (CordaX500Name) -> NodeInfo
|
||||
|
||||
override fun setup() = driver {
|
||||
override fun setup() = driver(extraCordappPackagesToScan = listOf("net.corda.finance")) {
|
||||
val cashUser = User("user1", "test", permissions = setOf(
|
||||
startFlowPermission<CashIssueFlow>(),
|
||||
startFlowPermission<CashPaymentFlow>(),
|
||||
startFlowPermission<CashExitFlow>())
|
||||
)
|
||||
val aliceNodeFuture = startNode(providedName = ALICE.name, rpcUsers = listOf(cashUser))
|
||||
val notaryNodeFuture = startNode(providedName = DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)))
|
||||
val notaryHandle = startNotaryNode(DUMMY_NOTARY.name, validating = false).getOrThrow()
|
||||
val aliceNodeHandle = aliceNodeFuture.getOrThrow()
|
||||
val notaryNodeHandle = notaryNodeFuture.getOrThrow()
|
||||
aliceNode = aliceNodeHandle.nodeInfo
|
||||
notaryNode = notaryNodeHandle.nodeInfo
|
||||
newNode = { nodeName -> startNode(providedName = nodeName).getOrThrow().nodeInfo }
|
||||
val monitor = NodeMonitorModel()
|
||||
stateMachineTransactionMapping = monitor.stateMachineTransactionMapping.bufferUntilSubscribed()
|
||||
@ -73,23 +69,24 @@ class NodeMonitorModelTest : DriverBasedTest() {
|
||||
vaultUpdates = monitor.vaultUpdates.bufferUntilSubscribed()
|
||||
networkMapUpdates = monitor.networkMap.bufferUntilSubscribed()
|
||||
|
||||
monitor.register(aliceNodeHandle.configuration.rpcAddress!!, cashUser.username, cashUser.password, initialiseSerialization = false)
|
||||
monitor.register(aliceNodeHandle.configuration.rpcAddress!!, cashUser.username, cashUser.password)
|
||||
rpc = monitor.proxyObservable.value!!
|
||||
notaryParty = notaryHandle.nodeInfo.legalIdentities[1]
|
||||
|
||||
val bobNodeHandle = startNode(providedName = 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, initialiseSerialization = false)
|
||||
monitorBob.register(bobNodeHandle.configuration.rpcAddress!!, cashUser.username, cashUser.password)
|
||||
rpcBob = monitorBob.proxyObservable.value!!
|
||||
runTest()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `network map update`() {
|
||||
newNode(CHARLIE.name)
|
||||
networkMapUpdates.filter { !it.node.advertisedServices.any { it.info.type.isNotary() } }
|
||||
.filter { !it.node.advertisedServices.any { it.info.type == NetworkMapService.type } }
|
||||
val charlieNode = newNode(CHARLIE.name)
|
||||
val nonServiceIdentities = aliceNode.legalIdentitiesAndCerts + bobNode.legalIdentitiesAndCerts + charlieNode.legalIdentitiesAndCerts
|
||||
networkMapUpdates.filter { it.node.legalIdentitiesAndCerts.any { it in nonServiceIdentities } }
|
||||
.expectEvents(isStrict = false) {
|
||||
sequence(
|
||||
// TODO : Add test for remove when driver DSL support individual node shutdown.
|
||||
@ -111,7 +108,7 @@ class NodeMonitorModelTest : DriverBasedTest() {
|
||||
rpc.startFlow(::CashIssueFlow,
|
||||
Amount(100, USD),
|
||||
OpaqueBytes(ByteArray(1, { 1 })),
|
||||
notaryNode.notaryIdentity
|
||||
notaryParty
|
||||
)
|
||||
|
||||
vaultUpdates.expectEvents(isStrict = false) {
|
||||
@ -132,9 +129,8 @@ class NodeMonitorModelTest : DriverBasedTest() {
|
||||
|
||||
@Test
|
||||
fun `cash issue and move`() {
|
||||
val anonymous = false
|
||||
val (_, issueIdentity) = rpc.startFlow(::CashIssueFlow, 100.DOLLARS, OpaqueBytes.of(1), notaryNode.notaryIdentity).returnValue.getOrThrow()
|
||||
val (_, paymentIdentity) = rpc.startFlow(::CashPaymentFlow, 100.DOLLARS, bobNode.chooseIdentity()).returnValue.getOrThrow()
|
||||
val (_, issueIdentity) = rpc.startFlow(::CashIssueFlow, 100.DOLLARS, OpaqueBytes.of(1), notaryParty).returnValue.getOrThrow()
|
||||
rpc.startFlow(::CashPaymentFlow, 100.DOLLARS, bobNode.chooseIdentity()).returnValue.getOrThrow()
|
||||
|
||||
var issueSmId: StateMachineRunId? = null
|
||||
var moveSmId: StateMachineRunId? = null
|
||||
@ -152,7 +148,7 @@ class NodeMonitorModelTest : DriverBasedTest() {
|
||||
require(remove.id == issueSmId)
|
||||
},
|
||||
// MOVE - N.B. There are other framework flows that happen in parallel for the remote resolve transactions flow
|
||||
expect(match = { it is StateMachineUpdate.Added && it.stateMachineInfo.flowLogicClassName == CashPaymentFlow::class.java.name }) { add: StateMachineUpdate.Added ->
|
||||
expect(match = { it.stateMachineInfo.flowLogicClassName == CashPaymentFlow::class.java.name }) { add: StateMachineUpdate.Added ->
|
||||
moveSmId = add.id
|
||||
val initiator = add.stateMachineInfo.initiator
|
||||
require(initiator is FlowInitiator.RPC && initiator.username == "user1")
|
||||
@ -167,7 +163,7 @@ class NodeMonitorModelTest : DriverBasedTest() {
|
||||
// MOVE
|
||||
expect { add: StateMachineUpdate.Added ->
|
||||
val initiator = add.stateMachineInfo.initiator
|
||||
require(initiator is FlowInitiator.Peer && initiator.party.name == aliceNode.chooseIdentity().name)
|
||||
require(initiator is FlowInitiator.Peer && aliceNode.isLegalIdentity(initiator.party))
|
||||
}
|
||||
)
|
||||
}
|
||||
@ -192,7 +188,7 @@ class NodeMonitorModelTest : DriverBasedTest() {
|
||||
val signaturePubKeys = stx.sigs.map { it.by }.toSet()
|
||||
// Alice and Notary signed
|
||||
require(issueIdentity!!.owningKey.isFulfilledBy(signaturePubKeys))
|
||||
require(notaryNode.notaryIdentity.owningKey.isFulfilledBy(signaturePubKeys))
|
||||
require(notaryParty.owningKey.isFulfilledBy(signaturePubKeys))
|
||||
moveTx = stx
|
||||
}
|
||||
)
|
||||
|
@ -6,6 +6,7 @@ import net.corda.client.jfx.utils.fold
|
||||
import net.corda.client.jfx.utils.map
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import rx.Observable
|
||||
@ -37,10 +38,9 @@ class ContractStateModel {
|
||||
companion object {
|
||||
private fun Collection<StateAndRef<ContractState>>.filterCashStateAndRefs(): List<StateAndRef<Cash.State>> {
|
||||
return this.map { stateAndRef ->
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
if (stateAndRef.state.data is Cash.State) {
|
||||
// Kotlin doesn't unify here for some reason
|
||||
stateAndRef as StateAndRef<Cash.State>
|
||||
uncheckedCast(stateAndRef)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import javafx.beans.property.ObjectProperty
|
||||
import javafx.beans.value.ObservableValue
|
||||
import javafx.beans.value.WritableValue
|
||||
import javafx.collections.ObservableList
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import org.reactfx.EventSink
|
||||
import org.reactfx.EventStream
|
||||
import rx.Observable
|
||||
@ -78,9 +79,7 @@ object Models {
|
||||
if (model.javaClass != klass.java) {
|
||||
throw IllegalStateException("Model stored as ${klass.qualifiedName} has type ${model.javaClass}")
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return model as M
|
||||
return uncheckedCast(model)
|
||||
}
|
||||
|
||||
inline fun <reified M : Any> get(origin: KClass<*>): M = get(M::class, origin)
|
||||
|
@ -1,4 +1,5 @@
|
||||
@file:JvmName("ModelsUtils")
|
||||
|
||||
package net.corda.client.jfx.model
|
||||
|
||||
import javafx.beans.property.ObjectProperty
|
||||
|
@ -5,18 +5,20 @@ import com.google.common.cache.CacheLoader
|
||||
import javafx.beans.value.ObservableValue
|
||||
import javafx.collections.FXCollections
|
||||
import javafx.collections.ObservableList
|
||||
import net.corda.client.jfx.utils.filterNotNull
|
||||
import net.corda.client.jfx.utils.fold
|
||||
import net.corda.client.jfx.utils.map
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.services.NetworkMapCache.MapChange
|
||||
import net.corda.nodeapi.internal.ServiceType
|
||||
import java.security.PublicKey
|
||||
|
||||
class NetworkIdentityModel {
|
||||
private val networkIdentityObservable by observable(NodeMonitorModel::networkMap)
|
||||
|
||||
val networkIdentities: ObservableList<NodeInfo> =
|
||||
private val networkIdentities: ObservableList<NodeInfo> =
|
||||
networkIdentityObservable.fold(FXCollections.observableArrayList()) { list, update ->
|
||||
list.removeIf {
|
||||
when (update) {
|
||||
@ -31,27 +33,20 @@ class NetworkIdentityModel {
|
||||
private val rpcProxy by observableValue(NodeMonitorModel::proxyObservable)
|
||||
|
||||
private val identityCache = CacheBuilder.newBuilder()
|
||||
.build<PublicKey, ObservableValue<NodeInfo?>>(CacheLoader.from {
|
||||
publicKey ->
|
||||
publicKey?.let { rpcProxy.map { it?.nodeIdentityFromParty(AnonymousParty(publicKey)) } }
|
||||
.build<PublicKey, ObservableValue<NodeInfo?>>(CacheLoader.from { publicKey ->
|
||||
publicKey?.let { rpcProxy.map { it?.nodeInfoFromParty(AnonymousParty(publicKey)) } }
|
||||
})
|
||||
|
||||
val parties: ObservableList<NodeInfo> = networkIdentities.filtered { !it.isCordaService() }
|
||||
val notaries: ObservableList<NodeInfo> = networkIdentities.filtered { it.advertisedServices.any { it.info.type.isNotary() } }
|
||||
val myNodeInfo = rpcProxy.map { it?.nodeInfo() } // TODO Used only for querying for advertised services, remove with services.
|
||||
val myIdentity = myNodeInfo.map { it?.legalIdentitiesAndCerts?.first()?.party }
|
||||
val notaries: ObservableList<Party> = networkIdentities.map {
|
||||
it.legalIdentitiesAndCerts.find { it.name.commonName?.let { ServiceType.parse(it).isNotary() } == true }
|
||||
}.map { it?.party }.filterNotNull()
|
||||
|
||||
private fun NodeInfo.isCordaService(): Boolean {
|
||||
// TODO: better way to identify Corda service?
|
||||
return advertisedServices.any { it.info.type.isNetworkMap() || it.info.type.isNotary() }
|
||||
}
|
||||
val notaryNodes: ObservableList<NodeInfo> = notaries.map { rpcProxy.value?.nodeInfoFromParty(it) }.filterNotNull()
|
||||
val parties: ObservableList<NodeInfo> = networkIdentities
|
||||
.filtered { it.legalIdentities.all { it !in notaries } }
|
||||
// TODO: REMOVE THIS HACK WHEN NETWORK MAP REDESIGN WORK IS COMPLETED.
|
||||
.filtered { it.legalIdentities.all { it.name.organisation != "Network Map Service" } }
|
||||
val myIdentity = rpcProxy.map { it?.nodeInfo()?.legalIdentitiesAndCerts?.first()?.party }
|
||||
|
||||
fun partyFromPublicKey(publicKey: PublicKey): ObservableValue<NodeInfo?> = identityCache[publicKey]
|
||||
//TODO rebase fix
|
||||
// // TODO: Use Identity Service in service hub instead?
|
||||
// fun lookup(publicKey: PublicKey): ObservableValue<PartyAndCertificate?> {
|
||||
// val party = parties.flatMap { it.legalIdentitiesAndCerts }.firstOrNull { publicKey in it.owningKey.keys } ?:
|
||||
// notaries.flatMap { it.legalIdentitiesAndCerts }.firstOrNull { it.owningKey.keys.any { it == publicKey }}
|
||||
// return ReadOnlyObjectWrapper(party)
|
||||
// }
|
||||
}
|
||||
|
@ -5,13 +5,17 @@ import net.corda.client.rpc.CordaRPCClient
|
||||
import net.corda.client.rpc.CordaRPCClientConfiguration
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.flows.StateMachineRunId
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.messaging.*
|
||||
import net.corda.core.node.services.NetworkMapCache.MapChange
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.node.services.vault.*
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.core.node.services.vault.DEFAULT_PAGE_NUM
|
||||
import net.corda.core.node.services.vault.MAX_PAGE_SIZE
|
||||
import net.corda.core.node.services.vault.PageSpecification
|
||||
import net.corda.core.node.services.vault.QueryCriteria
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.seconds
|
||||
import rx.Observable
|
||||
import rx.subjects.PublishSubject
|
||||
|
||||
@ -45,21 +49,22 @@ class NodeMonitorModel {
|
||||
val networkMap: Observable<MapChange> = networkMapSubject
|
||||
|
||||
val proxyObservable = SimpleObjectProperty<CordaRPCOps?>()
|
||||
lateinit var notaryIdentities: List<Party>
|
||||
|
||||
/**
|
||||
* Register for updates to/from a given vault.
|
||||
* TODO provide an unsubscribe mechanism
|
||||
*/
|
||||
fun register(nodeHostAndPort: NetworkHostAndPort, username: String, password: String, initialiseSerialization: Boolean = true) {
|
||||
fun register(nodeHostAndPort: NetworkHostAndPort, username: String, password: String) {
|
||||
val client = CordaRPCClient(
|
||||
hostAndPort = nodeHostAndPort,
|
||||
configuration = CordaRPCClientConfiguration.default.copy(
|
||||
nodeHostAndPort,
|
||||
CordaRPCClientConfiguration.DEFAULT.copy(
|
||||
connectionMaxRetryInterval = 10.seconds
|
||||
),
|
||||
initialiseSerialization = initialiseSerialization
|
||||
)
|
||||
)
|
||||
val connection = client.start(username, password)
|
||||
val proxy = connection.proxy
|
||||
notaryIdentities = proxy.notaryIdentities()
|
||||
|
||||
val (stateMachines, stateMachineUpdates) = proxy.stateMachinesFeed()
|
||||
// Extract the flow tracking stream
|
||||
@ -83,8 +88,13 @@ class NodeMonitorModel {
|
||||
stateMachineUpdates.startWith(currentStateMachines).subscribe(stateMachineUpdatesSubject)
|
||||
|
||||
// Vault snapshot (force single page load with MAX_PAGE_SIZE) + updates
|
||||
val (vaultSnapshot, vaultUpdates) = proxy.vaultTrackBy<ContractState>(QueryCriteria.VaultQueryCriteria(Vault.StateStatus.ALL),
|
||||
PageSpecification(DEFAULT_PAGE_NUM, MAX_PAGE_SIZE))
|
||||
val (_, vaultUpdates) = proxy.vaultTrackBy<ContractState>(QueryCriteria.VaultQueryCriteria(Vault.StateStatus.ALL),
|
||||
PageSpecification(DEFAULT_PAGE_NUM, MAX_PAGE_SIZE))
|
||||
|
||||
val vaultSnapshot = proxy.vaultQueryBy<ContractState>(QueryCriteria.VaultQueryCriteria(Vault.StateStatus.UNCONSUMED),
|
||||
PageSpecification(DEFAULT_PAGE_NUM, MAX_PAGE_SIZE))
|
||||
// We have to fetch the snapshot separately since vault query API doesn't allow different criteria for snapshot and updates.
|
||||
// TODO : This will create a small window of opportunity for inconsistent updates, might need to change the vault API to handle this case.
|
||||
val initialVaultUpdate = Vault.Update(setOf(), vaultSnapshot.states.toSet())
|
||||
vaultUpdates.startWith(initialVaultUpdate).subscribe(vaultUpdatesSubject)
|
||||
|
||||
|
@ -54,7 +54,7 @@ data class PartiallyResolvedTransaction(
|
||||
class TransactionDataModel {
|
||||
private val transactions by observable(NodeMonitorModel::transactions)
|
||||
private val collectedTransactions = transactions.recordInSequence()
|
||||
private val transactionMap = collectedTransactions.associateBy(SignedTransaction::id)
|
||||
private val transactionMap = transactions.recordAsAssociation(SignedTransaction::id)
|
||||
|
||||
val partiallyResolvedTransactions = collectedTransactions.map {
|
||||
PartiallyResolvedTransaction.fromSignedTransaction(it, transactionMap)
|
||||
|
@ -253,14 +253,15 @@ class ConcatenatedList<A>(sourceList: ObservableList<ObservableList<A>>) : Trans
|
||||
}
|
||||
}
|
||||
|
||||
override val size: Int get() {
|
||||
recalculateOffsets()
|
||||
if (nestedIndexOffsets.size > 0) {
|
||||
return nestedIndexOffsets.last()
|
||||
} else {
|
||||
return 0
|
||||
override val size: Int
|
||||
get() {
|
||||
recalculateOffsets()
|
||||
if (nestedIndexOffsets.size > 0) {
|
||||
return nestedIndexOffsets.last()
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getSourceIndex(index: Int): Int {
|
||||
throw UnsupportedOperationException("Source index not supported in concatenation")
|
||||
|
@ -1,4 +1,5 @@
|
||||
@file:JvmName("ObservableFold")
|
||||
|
||||
package net.corda.client.jfx.utils
|
||||
|
||||
import javafx.application.Platform
|
||||
|
@ -1,4 +1,5 @@
|
||||
@file:JvmName("ObservableUtilities")
|
||||
|
||||
package net.corda.client.jfx.utils
|
||||
|
||||
import javafx.application.Platform
|
||||
@ -14,6 +15,7 @@ import javafx.collections.ObservableMap
|
||||
import javafx.collections.transformation.FilteredList
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.messaging.DataFeed
|
||||
import net.corda.core.node.services.Vault
|
||||
import org.fxmisc.easybind.EasyBind
|
||||
@ -92,8 +94,7 @@ fun <A, B> ObservableValue<out A>.bind(function: (A) -> ObservableValue<B>): Obs
|
||||
* propagate variance constraints and type inference fails.
|
||||
*/
|
||||
fun <A, B> ObservableValue<out A>.bindOut(function: (A) -> ObservableValue<out B>): ObservableValue<out B> =
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
EasyBind.monadic(this).flatMap(function as (A) -> ObservableValue<B>)
|
||||
EasyBind.monadic(this).flatMap(uncheckedCast(function))
|
||||
|
||||
/**
|
||||
* enum class FilterCriterion { HEIGHT, NAME }
|
||||
@ -105,8 +106,7 @@ fun <A, B> ObservableValue<out A>.bindOut(function: (A) -> ObservableValue<out B
|
||||
*/
|
||||
fun <A> ObservableList<out A>.filter(predicate: ObservableValue<(A) -> Boolean>): ObservableList<A> {
|
||||
// We cast here to enforce variance, FilteredList should be covariant
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return FilteredList<A>(this as ObservableList<A>).apply {
|
||||
return FilteredList<A>(uncheckedCast(this)).apply {
|
||||
predicateProperty().bind(predicate.map { predicateFunction ->
|
||||
Predicate<A> { predicateFunction(it) }
|
||||
})
|
||||
@ -120,13 +120,11 @@ fun <A> ObservableList<out A>.filter(predicate: ObservableValue<(A) -> Boolean>)
|
||||
*/
|
||||
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 (this as ObservableList<A?>).filtered(object : Predicate<A?> {
|
||||
return uncheckedCast(uncheckedCast<Any, ObservableList<A?>>(this).filtered(object : Predicate<A?> {
|
||||
override fun test(t: A?): Boolean {
|
||||
return t != null
|
||||
|
||||
}
|
||||
}) as ObservableList<A>
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
@ -353,4 +351,4 @@ fun <T : ContractState> DataFeed<Vault.Page<T>, Vault.Update<T>>.toFXListOfState
|
||||
*/
|
||||
fun <T : ContractState> DataFeed<Vault.Page<T>, Vault.Update<T>>.toFXListOfStates(): ObservableList<T> {
|
||||
return toFXListOfStateRefs().map { it.state.data }
|
||||
}
|
||||
}
|
||||
|
@ -50,18 +50,19 @@ open class ReadOnlyBackedObservableMapBase<K, A, B> : ObservableMap<K, A> {
|
||||
|
||||
override fun isEmpty() = backingMap.isEmpty()
|
||||
|
||||
override val entries: MutableSet<MutableMap.MutableEntry<K, A>> get() = backingMap.entries.fold(mutableSetOf()) { set, entry ->
|
||||
set.add(object : MutableMap.MutableEntry<K, A> {
|
||||
override var value: A = entry.value.first
|
||||
override val key = entry.key
|
||||
override fun setValue(newValue: A): A {
|
||||
val old = value
|
||||
value = newValue
|
||||
return old
|
||||
}
|
||||
})
|
||||
set
|
||||
}
|
||||
override val entries: MutableSet<MutableMap.MutableEntry<K, A>>
|
||||
get() = backingMap.entries.fold(mutableSetOf()) { set, entry ->
|
||||
set.add(object : MutableMap.MutableEntry<K, A> {
|
||||
override var value: A = entry.value.first
|
||||
override val key = entry.key
|
||||
override fun setValue(newValue: A): A {
|
||||
val old = value
|
||||
value = newValue
|
||||
return old
|
||||
}
|
||||
})
|
||||
set
|
||||
}
|
||||
override val keys: MutableSet<K> get() = backingMap.keys
|
||||
override val values: MutableCollection<A> get() = ArrayList(backingMap.values.map { it.first })
|
||||
|
||||
|
@ -5,12 +5,6 @@ apply plugin: 'com.jfrog.artifactory'
|
||||
|
||||
description 'Corda client mock modules'
|
||||
|
||||
//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'
|
||||
}
|
||||
|
||||
// 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.
|
||||
|
||||
@ -27,8 +21,11 @@ dependencies {
|
||||
|
||||
jar {
|
||||
baseName 'corda-mock'
|
||||
manifest {
|
||||
attributes 'Automatic-Module-Name': 'net.corda.client.mock'
|
||||
}
|
||||
}
|
||||
|
||||
publish {
|
||||
name jar.baseName
|
||||
}
|
||||
}
|
||||
|
@ -50,7 +50,7 @@ open class EventGenerator(val parties: List<Party>, val currencies: List<Currenc
|
||||
* [Generator]s for incoming/outgoing events of starting different cash flows. It invokes flows that throw exceptions
|
||||
* for use in explorer flow triage. Exceptions are of kind spending/exiting too much cash.
|
||||
*/
|
||||
class ErrorFlowsEventGenerator(parties: List<Party>, currencies: List<Currency>, notary: Party): EventGenerator(parties, currencies, notary) {
|
||||
class ErrorFlowsEventGenerator(parties: List<Party>, currencies: List<Currency>, notary: Party) : EventGenerator(parties, currencies, notary) {
|
||||
enum class IssuerEvents {
|
||||
NORMAL_EXIT,
|
||||
EXIT_ERROR
|
||||
@ -62,7 +62,7 @@ class ErrorFlowsEventGenerator(parties: List<Party>, currencies: List<Currency>,
|
||||
when (errorType) {
|
||||
IssuerEvents.NORMAL_EXIT -> {
|
||||
println("Normal exit")
|
||||
if (currencyMap[ccy]!! <= amount) addToMap(ccy, -amount)
|
||||
if (currencyMap[ccy]!! <= amount) addToMap(ccy, -amount)
|
||||
ExitRequest(Amount(amount, ccy), issueRef) // It may fail at the beginning, but we don't care.
|
||||
}
|
||||
IssuerEvents.EXIT_ERROR -> {
|
||||
|
@ -1,6 +1,7 @@
|
||||
package net.corda.client.mock
|
||||
|
||||
import net.corda.client.mock.Generator.Companion.choice
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.utilities.Try
|
||||
import java.util.*
|
||||
|
||||
@ -115,14 +116,13 @@ class Generator<out A>(val generate: (SplittableRandom) -> Try<A>) {
|
||||
|
||||
fun <A> frequency(vararg generators: Pair<Double, Generator<A>>) = frequency(generators.toList())
|
||||
|
||||
fun <A> sequence(generators: List<Generator<A>>) = Generator {
|
||||
fun <A> sequence(generators: List<Generator<A>>) = Generator<List<A>> {
|
||||
val result = mutableListOf<A>()
|
||||
for (generator in generators) {
|
||||
val element = generator.generate(it)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
when (element) {
|
||||
is Try.Success -> result.add(element.value)
|
||||
is Try.Failure -> return@Generator element as Try<List<A>>
|
||||
is Try.Failure -> return@Generator uncheckedCast(element)
|
||||
}
|
||||
}
|
||||
Try.Success(result)
|
||||
@ -175,7 +175,7 @@ class Generator<out A>(val generate: (SplittableRandom) -> Try<A>) {
|
||||
}
|
||||
|
||||
|
||||
fun <A> replicatePoisson(meanSize: Double, generator: Generator<A>, atLeastOne: Boolean = false) = Generator {
|
||||
fun <A> replicatePoisson(meanSize: Double, generator: Generator<A>, atLeastOne: Boolean = false) = Generator<List<A>> {
|
||||
val chance = (meanSize - 1) / meanSize
|
||||
val result = mutableListOf<A>()
|
||||
var finish = false
|
||||
@ -191,8 +191,7 @@ class Generator<out A>(val generate: (SplittableRandom) -> Try<A>) {
|
||||
}
|
||||
}
|
||||
if (res is Try.Failure) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return@Generator res as Try<List<A>>
|
||||
return@Generator uncheckedCast(res)
|
||||
}
|
||||
}
|
||||
Try.Success(result)
|
||||
|
@ -1,4 +1,5 @@
|
||||
@file:JvmName("Generators")
|
||||
|
||||
package net.corda.client.mock
|
||||
|
||||
import net.corda.core.contracts.Amount
|
||||
|
@ -1,15 +1,13 @@
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'net.corda.plugins.quasar-utils'
|
||||
apply plugin: 'net.corda.plugins.publish-utils'
|
||||
apply plugin: 'net.corda.plugins.api-scanner'
|
||||
apply plugin: 'com.jfrog.artifactory'
|
||||
|
||||
description 'Corda client RPC modules'
|
||||
|
||||
//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'
|
||||
|
||||
integrationTestCompile.extendsFrom testCompile
|
||||
integrationTestRuntime.extendsFrom testRuntime
|
||||
|
||||
@ -92,6 +90,9 @@ task smokeTest(type: Test) {
|
||||
|
||||
jar {
|
||||
baseName 'corda-rpc'
|
||||
manifest {
|
||||
attributes 'Automatic-Module-Name': 'net.corda.client.rpc'
|
||||
}
|
||||
}
|
||||
|
||||
publish {
|
||||
|
@ -1,19 +1,16 @@
|
||||
package net.corda.client.rpc;
|
||||
|
||||
import net.corda.client.rpc.internal.RPCClient;
|
||||
import net.corda.core.concurrent.CordaFuture;
|
||||
import net.corda.core.contracts.Amount;
|
||||
import net.corda.core.messaging.CordaRPCOps;
|
||||
import net.corda.core.messaging.FlowHandle;
|
||||
import net.corda.core.node.services.ServiceInfo;
|
||||
import net.corda.core.utilities.OpaqueBytes;
|
||||
import net.corda.finance.flows.AbstractCashFlow;
|
||||
import net.corda.finance.flows.CashIssueFlow;
|
||||
import net.corda.finance.flows.CashPaymentFlow;
|
||||
import net.corda.finance.schemas.*;
|
||||
import net.corda.finance.schemas.CashSchemaV1;
|
||||
import net.corda.node.internal.Node;
|
||||
import net.corda.node.internal.StartedNode;
|
||||
import net.corda.node.services.transactions.ValidatingNotaryService;
|
||||
import net.corda.nodeapi.User;
|
||||
import net.corda.testing.CoreTestUtils;
|
||||
import net.corda.testing.node.NodeBasedTest;
|
||||
@ -25,14 +22,14 @@ import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import static java.util.Collections.emptyMap;
|
||||
import static java.util.Collections.singletonList;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static kotlin.test.AssertionsKt.assertEquals;
|
||||
import static net.corda.client.rpc.CordaRPCClientConfiguration.getDefault;
|
||||
import static net.corda.finance.Currencies.DOLLARS;
|
||||
import static net.corda.finance.contracts.GetBalances.getCashBalance;
|
||||
import static net.corda.node.services.FlowPermissions.startFlowPermission;
|
||||
import static net.corda.testing.CoreTestUtils.setCordappPackages;
|
||||
import static net.corda.testing.CoreTestUtils.unsetCordappPackages;
|
||||
import static net.corda.testing.TestConstants.getALICE;
|
||||
|
||||
public class CordaRPCJavaClientTest extends NodeBasedTest {
|
||||
@ -42,7 +39,7 @@ public class CordaRPCJavaClientTest extends NodeBasedTest {
|
||||
|
||||
private StartedNode<Node> node;
|
||||
private CordaRPCClient client;
|
||||
private RPCClient.RPCConnection<CordaRPCOps> connection = null;
|
||||
private RPCConnection<CordaRPCOps> connection = null;
|
||||
private CordaRPCOps rpcProxy;
|
||||
|
||||
private void login(String username, String password) {
|
||||
@ -52,16 +49,17 @@ public class CordaRPCJavaClientTest extends NodeBasedTest {
|
||||
|
||||
@Before
|
||||
public void setUp() throws ExecutionException, InterruptedException {
|
||||
Set<ServiceInfo> services = new HashSet<>(singletonList(new ServiceInfo(ValidatingNotaryService.Companion.getType(), null)));
|
||||
CordaFuture<StartedNode<Node>> nodeFuture = startNode(getALICE().getName(), 1, services, singletonList(rpcUser), emptyMap());
|
||||
setCordappPackages("net.corda.finance.contracts");
|
||||
CordaFuture<StartedNode<Node>> nodeFuture = startNotaryNode(getALICE().getName(), singletonList(rpcUser), true);
|
||||
node = nodeFuture.get();
|
||||
node.getInternals().registerCustomSchemas(Collections.singleton(CashSchemaV1.INSTANCE));
|
||||
client = new CordaRPCClient(requireNonNull(node.getInternals().getConfiguration().getRpcAddress()), null, getDefault(), false);
|
||||
client = new CordaRPCClient(requireNonNull(node.getInternals().getConfiguration().getRpcAddress()));
|
||||
}
|
||||
|
||||
@After
|
||||
public void done() throws IOException {
|
||||
connection.close();
|
||||
unsetCordappPackages();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -0,0 +1,92 @@
|
||||
package net.corda.client.rpc
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import com.esotericsoftware.kryo.KryoException
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.node.internal.Node
|
||||
import net.corda.node.internal.StartedNode
|
||||
import net.corda.nodeapi.User
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.node.NodeBasedTest
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.ExpectedException
|
||||
|
||||
@CordaSerializable
|
||||
data class Packet(val x: () -> Long)
|
||||
|
||||
class BlacklistKotlinClosureTest : NodeBasedTest() {
|
||||
companion object {
|
||||
@Suppress("UNUSED") val logger = loggerFor<BlacklistKotlinClosureTest>()
|
||||
const val EVIL: Long = 666
|
||||
}
|
||||
|
||||
@StartableByRPC
|
||||
@InitiatingFlow
|
||||
class FlowC(private val remoteParty: Party, private val data: Packet) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val session = initiateFlow(remoteParty)
|
||||
val x = session.sendAndReceive<Packet>(data).unwrap { x -> x }
|
||||
logger.info("FlowC: ${x.x()}")
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(FlowC::class)
|
||||
class RemoteFlowC(private val session: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val packet = session.receive<Packet>().unwrap { x -> x }
|
||||
logger.info("RemoteFlowC: ${packet.x() + 1}")
|
||||
session.send(Packet({ packet.x() + 1 }))
|
||||
}
|
||||
}
|
||||
|
||||
@JvmField
|
||||
@Rule
|
||||
val expectedEx: ExpectedException = ExpectedException.none()
|
||||
|
||||
private val rpcUser = User("user1", "test", permissions = setOf("ALL"))
|
||||
private lateinit var aliceNode: StartedNode<Node>
|
||||
private lateinit var bobNode: StartedNode<Node>
|
||||
private lateinit var aliceClient: CordaRPCClient
|
||||
private var connection: CordaRPCConnection? = null
|
||||
|
||||
private fun login(username: String, password: String) {
|
||||
connection = aliceClient.start(username, password)
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
setCordappPackages("net.corda.client.rpc")
|
||||
aliceNode = startNode(ALICE.name, rpcUsers = listOf(rpcUser)).getOrThrow()
|
||||
bobNode = startNode(BOB.name, rpcUsers = listOf(rpcUser)).getOrThrow()
|
||||
bobNode.registerInitiatedFlow(RemoteFlowC::class.java)
|
||||
aliceClient = CordaRPCClient(aliceNode.internals.configuration.rpcAddress!!)
|
||||
}
|
||||
|
||||
@After
|
||||
fun done() {
|
||||
connection?.close()
|
||||
bobNode.internals.stop()
|
||||
aliceNode.internals.stop()
|
||||
unsetCordappPackages()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `closure sent via RPC`() {
|
||||
login(rpcUser.username, rpcUser.password)
|
||||
val proxy = connection!!.proxy
|
||||
expectedEx.expect(KryoException::class.java)
|
||||
expectedEx.expectMessage("is not annotated or on the whitelist, so cannot be used in serialization")
|
||||
proxy.startFlow(::FlowC, bobNode.info.chooseIdentity(), Packet{ EVIL }).returnValue.getOrThrow()
|
||||
}
|
||||
}
|
@ -2,8 +2,10 @@ package net.corda.client.rpc
|
||||
|
||||
import net.corda.core.crypto.random63BitValue
|
||||
import net.corda.core.flows.FlowInitiator
|
||||
import net.corda.core.messaging.*
|
||||
import net.corda.core.node.services.ServiceInfo
|
||||
import net.corda.core.messaging.FlowProgressHandle
|
||||
import net.corda.core.messaging.StateMachineUpdate
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.messaging.startTrackedFlow
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.finance.DOLLARS
|
||||
@ -17,11 +19,12 @@ import net.corda.finance.schemas.CashSchemaV1
|
||||
import net.corda.node.internal.Node
|
||||
import net.corda.node.internal.StartedNode
|
||||
import net.corda.node.services.FlowPermissions.Companion.startFlowPermission
|
||||
import net.corda.node.services.transactions.ValidatingNotaryService
|
||||
import net.corda.nodeapi.User
|
||||
import net.corda.testing.ALICE
|
||||
import net.corda.testing.chooseIdentity
|
||||
import net.corda.testing.node.NodeBasedTest
|
||||
import net.corda.testing.setCordappPackages
|
||||
import net.corda.testing.unsetCordappPackages
|
||||
import org.apache.activemq.artemis.api.core.ActiveMQSecurityException
|
||||
import org.assertj.core.api.Assertions.assertThatExceptionOfType
|
||||
import org.junit.After
|
||||
@ -46,14 +49,16 @@ class CordaRPCClientTest : NodeBasedTest() {
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
node = startNode(ALICE.name, rpcUsers = listOf(rpcUser), advertisedServices = setOf(ServiceInfo(ValidatingNotaryService.type))).getOrThrow()
|
||||
setCordappPackages("net.corda.finance.contracts")
|
||||
node = startNotaryNode(ALICE.name, rpcUsers = listOf(rpcUser)).getOrThrow()
|
||||
node.internals.registerCustomSchemas(setOf(CashSchemaV1))
|
||||
client = CordaRPCClient(node.internals.configuration.rpcAddress!!, initialiseSerialization = false)
|
||||
client = CordaRPCClient(node.internals.configuration.rpcAddress!!)
|
||||
}
|
||||
|
||||
@After
|
||||
fun done() {
|
||||
connection?.close()
|
||||
unsetCordappPackages()
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -105,7 +110,6 @@ class CordaRPCClientTest : NodeBasedTest() {
|
||||
login(rpcUser.username, rpcUser.password)
|
||||
connection!!.proxy.startFlow(::CashPaymentFlow, 100.DOLLARS, node.info.chooseIdentity()).use {
|
||||
assertFalse(it is FlowProgressHandle<*>)
|
||||
assertTrue(it is FlowHandle<*>)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ import net.corda.core.internal.concurrent.fork
|
||||
import net.corda.core.internal.concurrent.transpose
|
||||
import net.corda.core.messaging.RPCOps
|
||||
import net.corda.core.serialization.SerializationDefaults
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.*
|
||||
import net.corda.node.services.messaging.RPCServerConfiguration
|
||||
import net.corda.nodeapi.RPCApi
|
||||
@ -75,10 +76,12 @@ class RPCStabilityTests {
|
||||
rpcDriver {
|
||||
Try.on { startRpcClient<RPCOps>(NetworkHostAndPort("localhost", 9999)).get() }
|
||||
val server = startRpcServer<RPCOps>(ops = DummyOps)
|
||||
Try.on { startRpcClient<RPCOps>(
|
||||
server.get().broker.hostAndPort!!,
|
||||
configuration = RPCClientConfiguration.default.copy(minimumServerProtocolVersion = 1)
|
||||
).get() }
|
||||
Try.on {
|
||||
startRpcClient<RPCOps>(
|
||||
server.get().broker.hostAndPort!!,
|
||||
configuration = RPCClientConfiguration.default.copy(minimumServerProtocolVersion = 1)
|
||||
).get()
|
||||
}
|
||||
}
|
||||
}
|
||||
repeat(5) {
|
||||
@ -172,7 +175,7 @@ class RPCStabilityTests {
|
||||
}
|
||||
}
|
||||
|
||||
interface LeakObservableOps: RPCOps {
|
||||
interface LeakObservableOps : RPCOps {
|
||||
fun leakObservable(): Observable<Nothing>
|
||||
}
|
||||
|
||||
@ -248,6 +251,7 @@ class RPCStabilityTests {
|
||||
val trackSubscriberCountObservable = UnicastSubject.create<Unit>().share().
|
||||
doOnSubscribe { subscriberCount.incrementAndGet() }.
|
||||
doOnUnsubscribe { subscriberCount.decrementAndGet() }
|
||||
|
||||
override fun subscribe(): Observable<Unit> {
|
||||
return trackSubscriberCountObservable
|
||||
}
|
||||
@ -260,7 +264,7 @@ class RPCStabilityTests {
|
||||
).get()
|
||||
|
||||
val numberOfClients = 4
|
||||
val clients = (1 .. numberOfClients).map {
|
||||
val clients = (1..numberOfClients).map {
|
||||
startRandomRpcClient<TrackSubscriberOps>(server.broker.hostAndPort!!)
|
||||
}.transpose().get()
|
||||
|
||||
@ -271,7 +275,7 @@ class RPCStabilityTests {
|
||||
clients[0].destroyForcibly()
|
||||
pollUntilClientNumber(server, numberOfClients - 1)
|
||||
// Kill the rest
|
||||
(1 .. numberOfClients - 1).forEach {
|
||||
(1..numberOfClients - 1).forEach {
|
||||
clients[it].destroyForcibly()
|
||||
}
|
||||
pollUntilClientNumber(server, 0)
|
||||
@ -283,6 +287,7 @@ class RPCStabilityTests {
|
||||
interface SlowConsumerRPCOps : RPCOps {
|
||||
fun streamAtInterval(interval: Duration, size: Int): Observable<ByteArray>
|
||||
}
|
||||
|
||||
class SlowConsumerRPCOpsImpl : SlowConsumerRPCOps {
|
||||
override val protocolVersion = 0
|
||||
|
||||
@ -291,6 +296,7 @@ class RPCStabilityTests {
|
||||
return Observable.interval(interval.toMillis(), TimeUnit.MILLISECONDS).map { chunk }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `slow consumers are kicked`() {
|
||||
rpcDriver {
|
||||
@ -315,9 +321,9 @@ class RPCStabilityTests {
|
||||
clientAddress = SimpleString(myQueue),
|
||||
id = RPCApi.RpcRequestId(random63BitValue()),
|
||||
methodName = SlowConsumerRPCOps::streamAtInterval.name,
|
||||
arguments = listOf(10.millis, 123456)
|
||||
serialisedArguments = listOf(10.millis, 123456).serialize(context = SerializationDefaults.RPC_SERVER_CONTEXT).bytes
|
||||
)
|
||||
request.writeToClientMessage(SerializationDefaults.RPC_SERVER_CONTEXT, message)
|
||||
request.writeToClientMessage(message)
|
||||
producer.send(message)
|
||||
session.commit()
|
||||
|
||||
|
@ -1,86 +1,104 @@
|
||||
package net.corda.client.rpc
|
||||
|
||||
import net.corda.client.rpc.internal.KryoClientSerializationScheme
|
||||
import net.corda.client.rpc.internal.RPCClient
|
||||
import net.corda.client.rpc.internal.RPCClientConfiguration
|
||||
import net.corda.client.rpc.serialization.KryoClientSerializationScheme
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.serialization.SerializationDefaults
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.nodeapi.ArtemisTcpTransport.Companion.tcpTransport
|
||||
import net.corda.nodeapi.ConnectionDirection
|
||||
import net.corda.nodeapi.config.SSLConfiguration
|
||||
import net.corda.nodeapi.internal.serialization.AMQPClientSerializationScheme
|
||||
import net.corda.nodeapi.internal.serialization.KRYO_P2P_CONTEXT
|
||||
import net.corda.nodeapi.internal.serialization.KRYO_RPC_CLIENT_CONTEXT
|
||||
import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl
|
||||
import java.time.Duration
|
||||
|
||||
/** @see RPCClient.RPCConnection */
|
||||
class CordaRPCConnection internal constructor(
|
||||
connection: RPCClient.RPCConnection<CordaRPCOps>
|
||||
) : RPCClient.RPCConnection<CordaRPCOps> by connection
|
||||
/**
|
||||
* This class is essentially just a wrapper for an RPCConnection<CordaRPCOps> and can be treated identically.
|
||||
*
|
||||
* @see RPCConnection
|
||||
*/
|
||||
class CordaRPCConnection internal constructor(connection: RPCConnection<CordaRPCOps>) : RPCConnection<CordaRPCOps> by connection
|
||||
|
||||
/** @see RPCClientConfiguration */
|
||||
data class CordaRPCClientConfiguration(
|
||||
val connectionMaxRetryInterval: Duration
|
||||
) {
|
||||
/**
|
||||
* Can be used to configure the RPC client connection.
|
||||
*
|
||||
* @property connectionMaxRetryInterval How much time to wait between connection retries if the server goes down. This
|
||||
* time will be reached via exponential backoff.
|
||||
*/
|
||||
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
|
||||
)
|
||||
/**
|
||||
* Returns the default configuration we recommend you use.
|
||||
*/
|
||||
@JvmField
|
||||
val DEFAULT = CordaRPCClientConfiguration(connectionMaxRetryInterval = RPCClientConfiguration.default.connectionMaxRetryInterval)
|
||||
}
|
||||
}
|
||||
|
||||
/** @see RPCClient */
|
||||
class CordaRPCClient(
|
||||
/**
|
||||
* An RPC client connects to the specified server and allows you to make calls to the server that perform various
|
||||
* useful tasks. Please see the Client RPC section of docs.corda.net to learn more about how this API works. A brief
|
||||
* description is provided here.
|
||||
*
|
||||
* Calling [start] 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 [CordaRPCOps] defines what client RPCs are available. If an RPC returns an [rx.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 sending messages immediately that will be buffered on the
|
||||
* client, you are expected 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 hostAndPort The network address to connect to.
|
||||
* @param configuration An optional configuration used to tweak client behaviour.
|
||||
*/
|
||||
class CordaRPCClient @JvmOverloads constructor(
|
||||
hostAndPort: NetworkHostAndPort,
|
||||
sslConfiguration: SSLConfiguration? = null,
|
||||
configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.default,
|
||||
initialiseSerialization: Boolean = true
|
||||
configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT
|
||||
) {
|
||||
init {
|
||||
// Init serialization. It's plausible there are multiple clients in a single JVM, so be tolerant of
|
||||
// others having registered first.
|
||||
// TODO: allow clients to have serialization factory etc injected and align with RPC protocol version?
|
||||
if (initialiseSerialization) {
|
||||
initialiseSerialization()
|
||||
}
|
||||
KryoClientSerializationScheme.initialiseSerialization()
|
||||
}
|
||||
|
||||
private val rpcClient = RPCClient<CordaRPCOps>(
|
||||
tcpTransport(ConnectionDirection.Outbound(), hostAndPort, sslConfiguration),
|
||||
tcpTransport(ConnectionDirection.Outbound(), hostAndPort, config = null),
|
||||
configuration.toRpcClientConfiguration(),
|
||||
KRYO_RPC_CLIENT_CONTEXT
|
||||
)
|
||||
|
||||
/**
|
||||
* Logs in to the target server and returns an active connection. The returned connection is a [java.io.Closeable]
|
||||
* and can be used with a try-with-resources statement. If you don't use that, you should use the
|
||||
* [RPCConnection.notifyServerAndClose] or [RPCConnection.forceClose] methods to dispose of the connection object
|
||||
* when done.
|
||||
*
|
||||
* @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 a reasonable timeout.
|
||||
*/
|
||||
fun start(username: String, password: String): CordaRPCConnection {
|
||||
return CordaRPCConnection(rpcClient.start(CordaRPCOps::class.java, username, password))
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper for Kotlin users that simply closes the connection after the block has executed. Be careful not to
|
||||
* over-use this, as setting up and closing connections takes time.
|
||||
*/
|
||||
inline fun <A> use(username: String, password: String, block: (CordaRPCConnection) -> A): A {
|
||||
return start(username, password).use(block)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun initialiseSerialization() {
|
||||
try {
|
||||
SerializationDefaults.SERIALIZATION_FACTORY = SerializationFactoryImpl().apply {
|
||||
registerScheme(KryoClientSerializationScheme())
|
||||
registerScheme(AMQPClientSerializationScheme())
|
||||
}
|
||||
SerializationDefaults.P2P_CONTEXT = KRYO_P2P_CONTEXT
|
||||
SerializationDefaults.RPC_CLIENT_CONTEXT = KRYO_RPC_CLIENT_CONTEXT
|
||||
} catch(e: IllegalStateException) {
|
||||
// Check that it's registered as we expect
|
||||
check(SerializationDefaults.SERIALIZATION_FACTORY is SerializationFactoryImpl) { "RPC client encountered conflicting configuration of serialization subsystem." }
|
||||
check((SerializationDefaults.SERIALIZATION_FACTORY as SerializationFactoryImpl).alreadyRegisteredSchemes.any { it is KryoClientSerializationScheme }) { "RPC client encountered conflicting configuration of serialization subsystem." }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package net.corda.client.rpc
|
||||
|
||||
import net.corda.core.CordaRuntimeException
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
|
||||
/**
|
||||
* Thrown to indicate that the calling user does not have permission for something they have requested (for example
|
||||
* calling a method).
|
||||
*/
|
||||
class PermissionException(msg: String) : CordaRuntimeException(msg)
|
@ -0,0 +1,38 @@
|
||||
package net.corda.client.rpc
|
||||
|
||||
import net.corda.core.messaging.RPCOps
|
||||
import java.io.Closeable
|
||||
|
||||
/**
|
||||
* Holds a [proxy] object implementing [I] that forwards requests to the RPC server. The server version can be queried
|
||||
* via this interface.
|
||||
*
|
||||
* [Closeable.close] may be used to shut down the connection and release associated resources. It is an
|
||||
* alias for [notifyServerAndClose].
|
||||
*/
|
||||
interface RPCConnection<out I : RPCOps> : Closeable {
|
||||
/**
|
||||
* Holds a synthetic class that automatically forwards method calls to the server, and returns the response.
|
||||
*/
|
||||
val proxy: I
|
||||
|
||||
/** The RPC protocol version reported by the server. */
|
||||
val serverProtocolVersion: Int
|
||||
|
||||
/**
|
||||
* Closes this client gracefully by sending a notification to the server, so it can immediately clean up resources.
|
||||
* If the server is not available this method may block for a short period until it's clear the server is not
|
||||
* coming back.
|
||||
*/
|
||||
fun notifyServerAndClose()
|
||||
|
||||
/**
|
||||
* Closes this client without notifying the server.
|
||||
*
|
||||
* The server will eventually clear out the RPC message queue and disconnect subscribed observers,
|
||||
* but this may take longer than desired, so to conserve resources you should normally use [notifyServerAndClose].
|
||||
* This method is helpful when the node may be shutting down or have already shut down and you don't want to
|
||||
* block waiting for it to come back, which typically happens in integration tests and demos rather than production.
|
||||
*/
|
||||
fun forceClose()
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package net.corda.client.rpc
|
||||
|
||||
import net.corda.core.CordaRuntimeException
|
||||
|
||||
/**
|
||||
* Thrown to indicate a fatal error in the RPC system itself, as opposed to an error generated by the invoked
|
||||
* method.
|
||||
*/
|
||||
open class RPCException(message: String?, cause: Throwable?) : CordaRuntimeException(message, cause) {
|
||||
constructor(msg: String) : this(msg, null)
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
package net.corda.client.rpc
|
||||
|
||||
/** Records the protocol version in which this RPC was added. */
|
||||
@Target(AnnotationTarget.FUNCTION)
|
||||
@MustBeDocumented
|
||||
annotation class RPCSinceVersion(val version: Int)
|
@ -0,0 +1,48 @@
|
||||
package net.corda.client.rpc.internal
|
||||
|
||||
import com.esotericsoftware.kryo.pool.KryoPool
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.SerializationDefaults
|
||||
import net.corda.core.utilities.ByteSequence
|
||||
import net.corda.nodeapi.internal.serialization.*
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
class KryoClientSerializationScheme : AbstractKryoSerializationScheme() {
|
||||
override fun canDeserializeVersion(byteSequence: ByteSequence, target: SerializationContext.UseCase): Boolean {
|
||||
return byteSequence == KryoHeaderV0_1 && (target == SerializationContext.UseCase.RPCClient || target == SerializationContext.UseCase.P2P)
|
||||
}
|
||||
|
||||
override fun rpcClientKryoPool(context: SerializationContext): KryoPool {
|
||||
return KryoPool.Builder {
|
||||
DefaultKryoCustomizer.customize(RPCKryo(RpcClientObservableSerializer, context)).apply {
|
||||
classLoader = context.deserializationClassLoader
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
// We're on the client and don't have access to server classes.
|
||||
override fun rpcServerKryoPool(context: SerializationContext): KryoPool = throw UnsupportedOperationException()
|
||||
|
||||
companion object {
|
||||
val isInitialised = AtomicBoolean(false)
|
||||
fun initialiseSerialization() {
|
||||
if (!isInitialised.compareAndSet(false, true)) return
|
||||
try {
|
||||
SerializationDefaults.SERIALIZATION_FACTORY = SerializationFactoryImpl().apply {
|
||||
registerScheme(KryoClientSerializationScheme())
|
||||
registerScheme(AMQPClientSerializationScheme())
|
||||
}
|
||||
SerializationDefaults.P2P_CONTEXT = KRYO_P2P_CONTEXT
|
||||
SerializationDefaults.RPC_CLIENT_CONTEXT = KRYO_RPC_CLIENT_CONTEXT
|
||||
} catch (e: IllegalStateException) {
|
||||
// Check that it's registered as we expect
|
||||
val factory = SerializationDefaults.SERIALIZATION_FACTORY
|
||||
val checkedFactory = factory as? SerializationFactoryImpl
|
||||
?: throw IllegalStateException("RPC client encountered conflicting configuration of serialization subsystem: $factory")
|
||||
check(checkedFactory.alreadyRegisteredSchemes.any { it is KryoClientSerializationScheme }) {
|
||||
"RPC client encountered conflicting configuration of serialization subsystem."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,10 @@
|
||||
package net.corda.client.rpc.internal
|
||||
|
||||
import net.corda.client.rpc.RPCConnection
|
||||
import net.corda.client.rpc.RPCException
|
||||
import net.corda.core.crypto.random63BitValue
|
||||
import net.corda.core.internal.logElapsedTime
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.messaging.RPCOps
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.SerializationDefaults
|
||||
@ -12,12 +15,10 @@ import net.corda.core.utilities.seconds
|
||||
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
|
||||
|
||||
@ -79,12 +80,6 @@ data class RPCClientConfiguration(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
@ -101,54 +96,6 @@ class RPCClient<I : RPCOps>(
|
||||
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
|
||||
|
||||
/**
|
||||
* Closes this client without notifying the server.
|
||||
* The server will eventually clear out the RPC message queue and disconnect subscribed observers,
|
||||
* but this may take longer than desired, so to conserve resources you should normally use [notifyServerAndClose].
|
||||
* This method is helpful when the node may be shutting down or
|
||||
* have already shut down and you don't want to block waiting for it to come back.
|
||||
*/
|
||||
fun forceClose()
|
||||
|
||||
/**
|
||||
* Closes this client gracefully by sending a notification to the server, so it can immediately clean up resources.
|
||||
* If the server is not available this method may block for a short period until it's clear the server is not coming back.
|
||||
*/
|
||||
fun notifyServerAndClose()
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 sending messages immediately that will be buffered on the
|
||||
* client, you are expected 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,
|
||||
@ -168,10 +115,7 @@ class RPCClient<I : RPCOps>(
|
||||
val proxyHandler = RPCClientProxyHandler(rpcConfiguration, username, password, serverLocator, clientAddress, rpcOpsClass, serializationContext)
|
||||
try {
|
||||
proxyHandler.start()
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val ops = Proxy.newProxyInstance(rpcOpsClass.classLoader, arrayOf(rpcOpsClass), proxyHandler) as I
|
||||
|
||||
val ops: I = uncheckedCast(Proxy.newProxyInstance(rpcOpsClass.classLoader, arrayOf(rpcOpsClass), proxyHandler))
|
||||
val serverProtocolVersion = ops.protocolVersion
|
||||
if (serverProtocolVersion < rpcConfiguration.minimumServerProtocolVersion) {
|
||||
throw RPCException("Requested minimum protocol version (${rpcConfiguration.minimumServerProtocolVersion}) is higher" +
|
||||
|
@ -10,6 +10,8 @@ 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.client.rpc.RPCException
|
||||
import net.corda.client.rpc.RPCSinceVersion
|
||||
import net.corda.core.crypto.random63BitValue
|
||||
import net.corda.core.internal.LazyPool
|
||||
import net.corda.core.internal.LazyStickyPool
|
||||
@ -17,6 +19,7 @@ import net.corda.core.internal.LifeCycle
|
||||
import net.corda.core.internal.ThreadBox
|
||||
import net.corda.core.messaging.RPCOps
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.Try
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
@ -76,6 +79,7 @@ class RPCClientProxyHandler(
|
||||
STARTED,
|
||||
FINISHED
|
||||
}
|
||||
|
||||
private val lifeCycle = LifeCycle(State.UNSTARTED)
|
||||
|
||||
private companion object {
|
||||
@ -206,11 +210,12 @@ class RPCClientProxyHandler(
|
||||
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 serialisedArguments = (arguments?.toList() ?: emptyList()).serialize(context = serializationContextWithObservableContext)
|
||||
val request = RPCApi.ClientToServer.RpcRequest(clientAddress, rpcId, method.name, serialisedArguments.bytes)
|
||||
val replyFuture = SettableFuture.create<Any>()
|
||||
sessionAndProducerPool.run {
|
||||
val message = it.session.createMessage(false)
|
||||
request.writeToClientMessage(serializationContextWithObservableContext, message)
|
||||
request.writeToClientMessage(message)
|
||||
|
||||
log.debug {
|
||||
val argumentsString = arguments?.joinToString() ?: ""
|
||||
|
@ -1,27 +0,0 @@
|
||||
package net.corda.client.rpc.serialization
|
||||
|
||||
import com.esotericsoftware.kryo.pool.KryoPool
|
||||
import net.corda.client.rpc.internal.RpcClientObservableSerializer
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.utilities.ByteSequence
|
||||
import net.corda.nodeapi.RPCKryo
|
||||
import net.corda.nodeapi.internal.serialization.AbstractKryoSerializationScheme
|
||||
import net.corda.nodeapi.internal.serialization.DefaultKryoCustomizer
|
||||
import net.corda.nodeapi.internal.serialization.KryoHeaderV0_1
|
||||
|
||||
class KryoClientSerializationScheme : AbstractKryoSerializationScheme() {
|
||||
override fun canDeserializeVersion(byteSequence: ByteSequence, target: SerializationContext.UseCase): Boolean {
|
||||
return byteSequence == KryoHeaderV0_1 && (target == SerializationContext.UseCase.RPCClient || target == SerializationContext.UseCase.P2P)
|
||||
}
|
||||
|
||||
override fun rpcClientKryoPool(context: SerializationContext): KryoPool {
|
||||
return KryoPool.Builder {
|
||||
DefaultKryoCustomizer.customize(RPCKryo(RpcClientObservableSerializer, context)).apply { classLoader = context.deserializationClassLoader }
|
||||
}.build()
|
||||
}
|
||||
|
||||
// We're on the client and don't have access to server classes.
|
||||
override fun rpcServerKryoPool(context: SerializationContext): KryoPool {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
}
|
@ -6,7 +6,6 @@ import net.corda.core.identity.CordaX500Name;
|
||||
import net.corda.core.identity.Party;
|
||||
import net.corda.core.messaging.CordaRPCOps;
|
||||
import net.corda.core.messaging.FlowHandle;
|
||||
import net.corda.core.node.NodeInfo;
|
||||
import net.corda.core.utilities.OpaqueBytes;
|
||||
import net.corda.finance.flows.AbstractCashFlow;
|
||||
import net.corda.finance.flows.CashIssueFlow;
|
||||
@ -41,7 +40,6 @@ public class StandaloneCordaRPCJavaClientTest {
|
||||
private NodeProcess notary;
|
||||
private CordaRPCOps rpcProxy;
|
||||
private CordaRPCConnection connection;
|
||||
private NodeInfo notaryNode;
|
||||
private Party notaryNodeIdentity;
|
||||
|
||||
private NodeConfig notaryConfig = new NodeConfig(
|
||||
@ -49,8 +47,8 @@ public class StandaloneCordaRPCJavaClientTest {
|
||||
port.getAndIncrement(),
|
||||
port.getAndIncrement(),
|
||||
port.getAndIncrement(),
|
||||
Collections.singletonList("corda.notary.validating"),
|
||||
Arrays.asList(rpcUser),
|
||||
true,
|
||||
Collections.singletonList(rpcUser),
|
||||
null
|
||||
);
|
||||
|
||||
@ -61,7 +59,6 @@ public class StandaloneCordaRPCJavaClientTest {
|
||||
notary = factory.create(notaryConfig);
|
||||
connection = notary.connect();
|
||||
rpcProxy = connection.getProxy();
|
||||
notaryNode = fetchNotaryIdentity();
|
||||
notaryNodeIdentity = rpcProxy.nodeInfo().getLegalIdentities().get(0);
|
||||
}
|
||||
|
||||
@ -70,7 +67,7 @@ public class StandaloneCordaRPCJavaClientTest {
|
||||
try {
|
||||
connection.close();
|
||||
} finally {
|
||||
if(notary != null) {
|
||||
if (notary != null) {
|
||||
notary.close();
|
||||
}
|
||||
}
|
||||
@ -98,11 +95,6 @@ public class StandaloneCordaRPCJavaClientTest {
|
||||
}
|
||||
}
|
||||
|
||||
private NodeInfo fetchNotaryIdentity() {
|
||||
List<NodeInfo> nodeDataSnapshot = rpcProxy.networkMapSnapshot();
|
||||
return nodeDataSnapshot.get(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCashBalances() throws NoSuchFieldException, ExecutionException, InterruptedException {
|
||||
Amount<Currency> dollars123 = new Amount<>(123, Currency.getInstance("USD"));
|
||||
|
@ -64,7 +64,7 @@ class StandaloneCordaRPClientTest {
|
||||
p2pPort = port.andIncrement,
|
||||
rpcPort = port.andIncrement,
|
||||
webPort = port.andIncrement,
|
||||
extraServices = listOf("corda.notary.validating"),
|
||||
isNotary = true,
|
||||
users = listOf(user)
|
||||
)
|
||||
|
||||
@ -114,14 +114,14 @@ class StandaloneCordaRPClientTest {
|
||||
@Test
|
||||
fun `test starting flow`() {
|
||||
rpcProxy.startFlow(::CashIssueFlow, 127.POUNDS, OpaqueBytes.of(0), notaryNodeIdentity)
|
||||
.returnValue.getOrThrow(timeout)
|
||||
.returnValue.getOrThrow(timeout)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test starting tracked flow`() {
|
||||
var trackCount = 0
|
||||
val handle = rpcProxy.startTrackedFlow(
|
||||
::CashIssueFlow, 429.DOLLARS, OpaqueBytes.of(0), notaryNodeIdentity
|
||||
::CashIssueFlow, 429.DOLLARS, OpaqueBytes.of(0), notaryNodeIdentity
|
||||
)
|
||||
val updateLatch = CountDownLatch(1)
|
||||
handle.progress.subscribe { msg ->
|
||||
@ -156,7 +156,7 @@ class StandaloneCordaRPClientTest {
|
||||
|
||||
// Now issue some cash
|
||||
rpcProxy.startFlow(::CashIssueFlow, 513.SWISS_FRANCS, OpaqueBytes.of(0), notaryNodeIdentity)
|
||||
.returnValue.getOrThrow(timeout)
|
||||
.returnValue.getOrThrow(timeout)
|
||||
updateLatch.await()
|
||||
assertEquals(1, updateCount.get())
|
||||
}
|
||||
|
@ -20,10 +20,13 @@ open class AbstractRPCTest {
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic @Parameterized.Parameters(name = "Mode = {0}")
|
||||
@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
|
||||
|
||||
|
@ -7,7 +7,6 @@ import net.corda.core.internal.concurrent.thenMatch
|
||||
import net.corda.core.messaging.RPCOps
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
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
|
||||
|
@ -26,9 +26,11 @@ import java.util.concurrent.TimeUnit
|
||||
@RunWith(Parameterized::class)
|
||||
class RPCPerformanceTests : AbstractRPCTest() {
|
||||
companion object {
|
||||
@JvmStatic @Parameterized.Parameters(name = "Mode = {0}")
|
||||
@JvmStatic
|
||||
@Parameterized.Parameters(name = "Mode = {0}")
|
||||
fun modes() = modes(RPCTestMode.Netty)
|
||||
}
|
||||
|
||||
private interface TestOps : RPCOps {
|
||||
fun simpleReply(input: ByteArray, sizeOfReply: Int): ByteArray
|
||||
}
|
||||
@ -60,7 +62,7 @@ class RPCPerformanceTests : AbstractRPCTest() {
|
||||
val executor = Executors.newFixedThreadPool(4)
|
||||
val N = 10000
|
||||
val latch = CountDownLatch(N)
|
||||
for (i in 1 .. N) {
|
||||
for (i in 1..N) {
|
||||
executor.submit {
|
||||
proxy.ops.simpleReply(ByteArray(1024), 1024)
|
||||
latch.countDown()
|
||||
@ -155,10 +157,12 @@ class RPCPerformanceTests : AbstractRPCTest() {
|
||||
data class BigMessagesResult(
|
||||
val Mbps: Double
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `big messages`() {
|
||||
warmup()
|
||||
measure(listOf(1)) { clientParallelism -> // TODO this hangs with more parallelism
|
||||
measure(listOf(1)) { clientParallelism ->
|
||||
// TODO this hangs with more parallelism
|
||||
rpcDriver {
|
||||
val proxy = testProxy(
|
||||
RPCClientConfiguration.default,
|
||||
|
@ -3,7 +3,6 @@ package net.corda.client.rpc
|
||||
import net.corda.core.messaging.RPCOps
|
||||
import net.corda.node.services.messaging.getRpcContext
|
||||
import net.corda.node.services.messaging.requirePermission
|
||||
import net.corda.nodeapi.PermissionException
|
||||
import net.corda.nodeapi.User
|
||||
import net.corda.testing.RPCDriverExposedDSLInterface
|
||||
import net.corda.testing.rpcDriver
|
||||
@ -80,7 +79,7 @@ class RPCPermissionsTests : AbstractRPCTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `check ALL is implemented the correct way round` () {
|
||||
fun `check ALL is implemented the correct way round`() {
|
||||
rpcDriver {
|
||||
val joeUser = userOf("joe", setOf(DUMMY_FLOW))
|
||||
val proxy = testProxyFor(joeUser)
|
||||
|
@ -13,12 +13,13 @@ class RepeatingBytesInputStream(val bytesToRepeat: ByteArray, val numberOfBytes:
|
||||
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) {
|
||||
val lastIdx = Math.min(Math.min(offset + length, byteArray.size), offset + bytesLeft)
|
||||
for (i in offset until lastIdx) {
|
||||
byteArray[i] = bytesToRepeat[(numberOfBytes - bytesLeft + i - offset) % bytesToRepeat.size]
|
||||
}
|
||||
val bytesRead = until - offset
|
||||
val bytesRead = lastIdx - offset
|
||||
bytesLeft -= bytesRead
|
||||
return if (bytesRead == 0 && bytesLeft == 0) -1 else bytesRead
|
||||
}
|
||||
|
39
confidential-identities/build.gradle
Normal file
39
confidential-identities/build.gradle
Normal file
@ -0,0 +1,39 @@
|
||||
// Experimental Confidential Identities support for 1.0
|
||||
// This contains the prototype SwapIdentitiesFlow and SwapIdentitiesHandler, which can be used
|
||||
// for exchanging confidential identities as part of a flow, until a permanent solution is prepared.
|
||||
// Expect this module to be removed and merged into core in a later release.
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: CanonicalizerPlugin
|
||||
apply plugin: 'net.corda.plugins.publish-utils'
|
||||
apply plugin: 'net.corda.plugins.quasar-utils'
|
||||
apply plugin: 'com.jfrog.artifactory'
|
||||
|
||||
description 'Corda Experimental Confidential Identities'
|
||||
|
||||
dependencies {
|
||||
compile project(':core')
|
||||
|
||||
// Quasar, for suspendable fibres.
|
||||
compileOnly "co.paralleluniverse:quasar-core:$quasar_version:jdk8"
|
||||
|
||||
testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
||||
testCompile "junit:junit:$junit_version"
|
||||
|
||||
// Guava: Google test library (collections test suite)
|
||||
testCompile "com.google.guava:guava-testlib:$guava_version"
|
||||
|
||||
// Bring in the MockNode infrastructure for writing protocol unit tests.
|
||||
testCompile project(":node")
|
||||
testCompile project(":node-driver")
|
||||
|
||||
// AssertJ: for fluent assertions for testing
|
||||
testCompile "org.assertj:assertj-core:$assertj_version"
|
||||
}
|
||||
|
||||
jar {
|
||||
baseName 'corda-confidential-identities'
|
||||
}
|
||||
|
||||
publish {
|
||||
name jar.baseName
|
||||
}
|
@ -1,9 +1,10 @@
|
||||
package net.corda.core.flows
|
||||
package net.corda.confidential
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.identity.PartyAndCertificate
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
@ -11,20 +12,18 @@ import net.corda.core.utilities.unwrap
|
||||
|
||||
object IdentitySyncFlow {
|
||||
/**
|
||||
* Flow for ensuring that one or more counterparties to a transaction have the full certificate paths of confidential
|
||||
* identities used in the transaction. This is intended for use as a subflow of another flow, typically between
|
||||
* Flow for ensuring that our counterparties in a transaction have the full certificate paths for *our* confidential
|
||||
* identities used in states present in the transaction. This is intended for use as a subflow of another flow, typically between
|
||||
* transaction assembly and signing. An example of where this is useful is where a recipient of a [Cash] state wants
|
||||
* to know that it is being paid by the correct party, and the owner of the state is a confidential identity of that
|
||||
* party. This flow would send a copy of the confidential identity path to the recipient, enabling them to verify that
|
||||
* identity.
|
||||
*
|
||||
* @return a mapping of well known identities to the confidential identities used in the transaction.
|
||||
*/
|
||||
// TODO: Can this be triggered automatically from [SendTransactionFlow]
|
||||
class Send(val otherSides: Set<Party>,
|
||||
class Send(val otherSideSessions: Set<FlowSession>,
|
||||
val tx: WireTransaction,
|
||||
override val progressTracker: ProgressTracker) : FlowLogic<Unit>() {
|
||||
constructor(otherSide: Party, tx: WireTransaction) : this(setOf(otherSide), tx, tracker())
|
||||
constructor(otherSide: FlowSession, tx: WireTransaction) : this(setOf(otherSide), tx, tracker())
|
||||
|
||||
companion object {
|
||||
object SYNCING_IDENTITIES : ProgressTracker.Step("Syncing identities")
|
||||
@ -35,18 +34,11 @@ object IdentitySyncFlow {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
progressTracker.currentStep = SYNCING_IDENTITIES
|
||||
val states: List<ContractState> = (tx.inputs.map { serviceHub.loadState(it) }.requireNoNulls().map { it.data } + tx.outputs.map { it.data })
|
||||
val identities: Set<AbstractParty> = states.flatMap { it.participants }.toSet()
|
||||
// Filter participants down to the set of those not in the network map (are not well known)
|
||||
val confidentialIdentities = identities
|
||||
.filter { serviceHub.networkMapCache.getNodesByLegalIdentityKey(it.owningKey).isEmpty() }
|
||||
.toList()
|
||||
val identityCertificates: Map<AbstractParty, PartyAndCertificate?> = identities
|
||||
.map { Pair(it, serviceHub.identityService.certificateFromKey(it.owningKey)) }.toMap()
|
||||
val identityCertificates: Map<AbstractParty, PartyAndCertificate?> = extractOurConfidentialIdentities()
|
||||
|
||||
otherSides.forEach { otherSide ->
|
||||
val requestedIdentities: List<AbstractParty> = sendAndReceive<List<AbstractParty>>(otherSide, confidentialIdentities).unwrap { req ->
|
||||
require(req.all { it in identityCertificates.keys }) { "${otherSide} requested a confidential identity not part of transaction: ${tx.id}" }
|
||||
otherSideSessions.forEach { otherSideSession ->
|
||||
val requestedIdentities: List<AbstractParty> = otherSideSession.sendAndReceive<List<AbstractParty>>(identityCertificates.keys.toList()).unwrap { req ->
|
||||
require(req.all { it in identityCertificates.keys }) { "${otherSideSession.counterparty} requested a confidential identity not part of transaction: ${tx.id}" }
|
||||
req
|
||||
}
|
||||
val sendIdentities: List<PartyAndCertificate?> = requestedIdentities.map {
|
||||
@ -56,17 +48,31 @@ object IdentitySyncFlow {
|
||||
else
|
||||
throw IllegalStateException("Counterparty requested a confidential identity for which we do not have the certificate path: ${tx.id}")
|
||||
}
|
||||
send(otherSide, sendIdentities)
|
||||
otherSideSession.send(sendIdentities)
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractOurConfidentialIdentities(): Map<AbstractParty, PartyAndCertificate?> {
|
||||
val states: List<ContractState> = (tx.inputs.map { serviceHub.loadState(it) }.requireNoNulls().map { it.data } + tx.outputs.map { it.data })
|
||||
val identities: Set<AbstractParty> = states.flatMap(ContractState::participants).toSet()
|
||||
// Filter participants down to the set of those not in the network map (are not well known)
|
||||
val confidentialIdentities = identities
|
||||
.filter { serviceHub.networkMapCache.getNodesByLegalIdentityKey(it.owningKey).isEmpty() }
|
||||
.toList()
|
||||
return confidentialIdentities
|
||||
.map { Pair(it, serviceHub.identityService.certificateFromKey(it.owningKey)) }
|
||||
// Filter down to confidential identities of our well known identity
|
||||
// TODO: Consider if this too restrictive - we perhaps should be checking the name on the signing certificate in the certificate path instead
|
||||
.filter { it.second?.name == ourIdentity.name }
|
||||
.toMap()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an offer to provide proof of identity (in the form of certificate paths) for confidential identities which
|
||||
* we do not yet know about.
|
||||
*/
|
||||
class Receive(val otherSide: Party) : FlowLogic<Unit>() {
|
||||
class Receive(val otherSideSession: FlowSession) : FlowLogic<Unit>() {
|
||||
companion object {
|
||||
object RECEIVING_IDENTITIES : ProgressTracker.Step("Receiving confidential identities")
|
||||
object RECEIVING_CERTIFICATES : ProgressTracker.Step("Receiving certificates for unknown identities")
|
||||
@ -77,10 +83,10 @@ object IdentitySyncFlow {
|
||||
@Suspendable
|
||||
override fun call(): Unit {
|
||||
progressTracker.currentStep = RECEIVING_IDENTITIES
|
||||
val allIdentities = receive<List<AbstractParty>>(otherSide).unwrap { it }
|
||||
val unknownIdentities = allIdentities.filter { serviceHub.identityService.partyFromAnonymous(it) == null }
|
||||
val allIdentities = otherSideSession.receive<List<AbstractParty>>().unwrap { it }
|
||||
val unknownIdentities = allIdentities.filter { serviceHub.identityService.wellKnownPartyFromAnonymous(it) == null }
|
||||
progressTracker.currentStep = RECEIVING_CERTIFICATES
|
||||
val missingIdentities = sendAndReceive<List<PartyAndCertificate>>(otherSide, unknownIdentities)
|
||||
val missingIdentities = otherSideSession.sendAndReceive<List<PartyAndCertificate>>(unknownIdentities)
|
||||
|
||||
// Batch verify the identities we've received, so we know they're all correct before we start storing them in
|
||||
// the identity service
|
@ -0,0 +1,121 @@
|
||||
package net.corda.confidential
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.crypto.DigitalSignature
|
||||
import net.corda.core.flows.FlowException
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.identity.PartyAndCertificate
|
||||
import net.corda.core.internal.toX509CertHolder
|
||||
import net.corda.core.node.services.IdentityService
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.unwrap
|
||||
import org.bouncycastle.asn1.DERSet
|
||||
import org.bouncycastle.asn1.pkcs.CertificationRequestInfo
|
||||
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.nio.charset.Charset
|
||||
import java.security.PublicKey
|
||||
import java.security.SignatureException
|
||||
import java.security.cert.CertPath
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Very basic flow which generates new confidential identities for parties in a transaction and exchanges the transaction
|
||||
* key and certificate paths between the parties. This is intended for use as a subflow of another flow which builds a
|
||||
* transaction.
|
||||
*/
|
||||
@StartableByRPC
|
||||
@InitiatingFlow
|
||||
class SwapIdentitiesFlow(private val otherParty: Party,
|
||||
private val revocationEnabled: Boolean,
|
||||
override val progressTracker: ProgressTracker) : FlowLogic<LinkedHashMap<Party, AnonymousParty>>() {
|
||||
constructor(otherParty: Party) : this(otherParty, false, tracker())
|
||||
|
||||
companion object {
|
||||
object AWAITING_KEY : ProgressTracker.Step("Awaiting key")
|
||||
|
||||
fun tracker() = ProgressTracker(AWAITING_KEY)
|
||||
/**
|
||||
* Generate the determinstic data blob the confidential identity's key holder signs to indicate they want to
|
||||
* represent the subject named in the X.509 certificate. Note that this is never actually sent between nodes,
|
||||
* but only the signature is sent. The blob is built independently on each node and the received signature
|
||||
* verified against the expected blob, rather than exchanging the blob.
|
||||
*/
|
||||
fun buildDataToSign(confidentialIdentity: PartyAndCertificate): ByteArray {
|
||||
val certOwnerAssert = CertificateOwnershipAssertion(confidentialIdentity.name, confidentialIdentity.owningKey)
|
||||
return certOwnerAssert.serialize().bytes
|
||||
}
|
||||
|
||||
@Throws(SwapIdentitiesException::class)
|
||||
fun validateAndRegisterIdentity(identityService: IdentityService,
|
||||
otherSide: Party,
|
||||
anonymousOtherSideBytes: PartyAndCertificate,
|
||||
sigBytes: DigitalSignature): PartyAndCertificate {
|
||||
val anonymousOtherSide: PartyAndCertificate = anonymousOtherSideBytes
|
||||
if (anonymousOtherSide.name != otherSide.name) {
|
||||
throw SwapIdentitiesException("Certificate subject must match counterparty's well known identity.")
|
||||
}
|
||||
val signature = DigitalSignature.WithKey(anonymousOtherSide.owningKey, sigBytes.bytes)
|
||||
try {
|
||||
signature.verify(buildDataToSign(anonymousOtherSideBytes))
|
||||
} catch(ex: SignatureException) {
|
||||
throw SwapIdentitiesException("Signature does not match the expected identity ownership assertion.", ex)
|
||||
}
|
||||
// Validate then store their identity so that we can prove the key in the transaction is owned by the
|
||||
// counterparty.
|
||||
identityService.verifyAndRegisterIdentity(anonymousOtherSide)
|
||||
return anonymousOtherSide
|
||||
}
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call(): LinkedHashMap<Party, AnonymousParty> {
|
||||
progressTracker.currentStep = AWAITING_KEY
|
||||
val legalIdentityAnonymous = serviceHub.keyManagementService.freshKeyAndCert(ourIdentityAndCert, revocationEnabled)
|
||||
val serializedIdentity = SerializedBytes<PartyAndCertificate>(legalIdentityAnonymous.serialize().bytes)
|
||||
|
||||
// Special case that if we're both parties, a single identity is generated
|
||||
val identities = LinkedHashMap<Party, AnonymousParty>()
|
||||
if (serviceHub.myInfo.isLegalIdentity(otherParty)) {
|
||||
identities.put(otherParty, legalIdentityAnonymous.party.anonymise())
|
||||
} else {
|
||||
val otherSession = initiateFlow(otherParty)
|
||||
val data = buildDataToSign(legalIdentityAnonymous)
|
||||
val ourSig: DigitalSignature.WithKey = serviceHub.keyManagementService.sign(data, legalIdentityAnonymous.owningKey)
|
||||
val ourIdentWithSig = IdentityWithSignature(serializedIdentity, ourSig.withoutKey())
|
||||
val anonymousOtherSide = otherSession.sendAndReceive<IdentityWithSignature>(ourIdentWithSig)
|
||||
.unwrap { (confidentialIdentityBytes, theirSigBytes) ->
|
||||
val confidentialIdentity: PartyAndCertificate = confidentialIdentityBytes.bytes.deserialize()
|
||||
validateAndRegisterIdentity(serviceHub.identityService, otherParty, confidentialIdentity, theirSigBytes)
|
||||
}
|
||||
identities.put(ourIdentity, legalIdentityAnonymous.party.anonymise())
|
||||
identities.put(otherParty, anonymousOtherSide.party.anonymise())
|
||||
}
|
||||
return identities
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
data class IdentityWithSignature(val identity: SerializedBytes<PartyAndCertificate>, val signature: DigitalSignature)
|
||||
}
|
||||
|
||||
/**
|
||||
* Data class used only in the context of asserting the owner of the private key for the listed key wants to use it
|
||||
* to represent the named entity. This is pairs with an X.509 certificate (which asserts the signing identity says
|
||||
* the key represents the named entity), but protects against a certificate authority incorrectly claiming others'
|
||||
* keys.
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class CertificateOwnershipAssertion(val x500Name: CordaX500Name,
|
||||
val publicKey: PublicKey)
|
||||
|
||||
open class SwapIdentitiesException @JvmOverloads constructor(message: String, cause: Throwable? = null)
|
||||
: FlowException(message, cause)
|
@ -0,0 +1,36 @@
|
||||
package net.corda.confidential
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.identity.PartyAndCertificate
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.unwrap
|
||||
|
||||
class SwapIdentitiesHandler(val otherSideSession: FlowSession, val revocationEnabled: Boolean) : FlowLogic<Unit>() {
|
||||
constructor(otherSideSession: FlowSession) : this(otherSideSession, false)
|
||||
|
||||
companion object {
|
||||
object SENDING_KEY : ProgressTracker.Step("Sending key")
|
||||
}
|
||||
|
||||
override val progressTracker: ProgressTracker = ProgressTracker(SENDING_KEY)
|
||||
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val revocationEnabled = false
|
||||
progressTracker.currentStep = SENDING_KEY
|
||||
val ourConfidentialIdentity = serviceHub.keyManagementService.freshKeyAndCert(ourIdentityAndCert, revocationEnabled)
|
||||
val serializedIdentity = SerializedBytes<PartyAndCertificate>(ourConfidentialIdentity.serialize().bytes)
|
||||
val data = SwapIdentitiesFlow.buildDataToSign(ourConfidentialIdentity)
|
||||
val ourSig = serviceHub.keyManagementService.sign(data, ourConfidentialIdentity.owningKey)
|
||||
otherSideSession.sendAndReceive<SwapIdentitiesFlow.IdentityWithSignature>(SwapIdentitiesFlow.IdentityWithSignature(serializedIdentity, ourSig.withoutKey()))
|
||||
.unwrap { (theirConfidentialIdentityBytes, theirSigBytes) ->
|
||||
val theirConfidentialIdentity = theirConfidentialIdentityBytes.deserialize()
|
||||
SwapIdentitiesFlow.validateAndRegisterIdentity(serviceHub.identityService, otherSideSession.counterparty, theirConfidentialIdentity, theirSigBytes)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,129 @@
|
||||
package net.corda.confidential
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.flows.InitiatedBy
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.finance.DOLLARS
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import net.corda.finance.flows.CashIssueAndPaymentFlow
|
||||
import net.corda.finance.flows.CashPaymentFlow
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertNull
|
||||
|
||||
class IdentitySyncFlowTests {
|
||||
lateinit var mockNet: MockNetwork
|
||||
|
||||
@Before
|
||||
fun before() {
|
||||
setCordappPackages("net.corda.finance.contracts.asset")
|
||||
// We run this in parallel threads to help catch any race conditions that may exist.
|
||||
mockNet = MockNetwork(networkSendManuallyPumped = false, threadPerNode = true)
|
||||
}
|
||||
|
||||
@After
|
||||
fun cleanUp() {
|
||||
mockNet.stopNodes()
|
||||
unsetCordappPackages()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sync confidential identities`() {
|
||||
// Set up values we'll need
|
||||
mockNet.createNotaryNode()
|
||||
val aliceNode = mockNet.createPartyNode(ALICE.name)
|
||||
val bobNode = mockNet.createPartyNode(BOB.name)
|
||||
val alice: Party = aliceNode.services.myInfo.chooseIdentity()
|
||||
val bob: Party = bobNode.services.myInfo.chooseIdentity()
|
||||
val notary = aliceNode.services.getDefaultNotary()
|
||||
bobNode.internals.registerInitiatedFlow(Receive::class.java)
|
||||
|
||||
// Alice issues then pays some cash to a new confidential identity that Bob doesn't know about
|
||||
val anonymous = true
|
||||
val ref = OpaqueBytes.of(0x01)
|
||||
val issueFlow = aliceNode.services.startFlow(CashIssueAndPaymentFlow(1000.DOLLARS, ref, alice, anonymous, notary))
|
||||
val issueTx = issueFlow.resultFuture.getOrThrow().stx
|
||||
val confidentialIdentity = issueTx.tx.outputs.map { it.data }.filterIsInstance<Cash.State>().single().owner
|
||||
assertNull(bobNode.database.transaction { bobNode.services.identityService.wellKnownPartyFromAnonymous(confidentialIdentity) })
|
||||
|
||||
// Run the flow to sync up the identities
|
||||
aliceNode.services.startFlow(Initiator(bob, issueTx.tx)).resultFuture.getOrThrow()
|
||||
val expected = aliceNode.database.transaction {
|
||||
aliceNode.services.identityService.wellKnownPartyFromAnonymous(confidentialIdentity)
|
||||
}
|
||||
val actual = bobNode.database.transaction {
|
||||
bobNode.services.identityService.wellKnownPartyFromAnonymous(confidentialIdentity)
|
||||
}
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `don't offer other's identities confidential identities`() {
|
||||
// Set up values we'll need
|
||||
val notaryNode = mockNet.createNotaryNode()
|
||||
val aliceNode = mockNet.createPartyNode(ALICE.name)
|
||||
val bobNode = mockNet.createPartyNode(BOB.name)
|
||||
val charlieNode = mockNet.createPartyNode(CHARLIE.name)
|
||||
val alice: Party = aliceNode.services.myInfo.chooseIdentity()
|
||||
val bob: Party = bobNode.services.myInfo.chooseIdentity()
|
||||
val charlie: Party = charlieNode.services.myInfo.chooseIdentity()
|
||||
val notary = notaryNode.services.getDefaultNotary()
|
||||
bobNode.internals.registerInitiatedFlow(Receive::class.java)
|
||||
|
||||
// Charlie issues then pays some cash to a new confidential identity
|
||||
val anonymous = true
|
||||
val ref = OpaqueBytes.of(0x01)
|
||||
val issueFlow = charlieNode.services.startFlow(CashIssueAndPaymentFlow(1000.DOLLARS, ref, charlie, anonymous, notary))
|
||||
val issueTx = issueFlow.resultFuture.getOrThrow().stx
|
||||
val confidentialIdentity = issueTx.tx.outputs.map { it.data }.filterIsInstance<Cash.State>().single().owner
|
||||
val confidentialIdentCert = charlieNode.services.identityService.certificateFromKey(confidentialIdentity.owningKey)!!
|
||||
|
||||
// Manually inject this identity into Alice's database so the node could leak it, but we prove won't
|
||||
aliceNode.database.transaction { aliceNode.services.identityService.verifyAndRegisterIdentity(confidentialIdentCert) }
|
||||
assertNotNull(aliceNode.database.transaction { aliceNode.services.identityService.wellKnownPartyFromAnonymous(confidentialIdentity) })
|
||||
|
||||
// Generate a payment from Charlie to Alice, including the confidential state
|
||||
val payTx = charlieNode.services.startFlow(CashPaymentFlow(1000.DOLLARS, alice, anonymous)).resultFuture.getOrThrow().stx
|
||||
|
||||
// Run the flow to sync up the identities, and confirm Charlie's confidential identity doesn't leak
|
||||
assertNull(bobNode.database.transaction { bobNode.services.identityService.wellKnownPartyFromAnonymous(confidentialIdentity) })
|
||||
aliceNode.services.startFlow(Initiator(bob, payTx.tx)).resultFuture.getOrThrow()
|
||||
assertNull(bobNode.database.transaction { bobNode.services.identityService.wellKnownPartyFromAnonymous(confidentialIdentity) })
|
||||
}
|
||||
|
||||
/**
|
||||
* Very lightweight wrapping flow to trigger the counterparty flow that receives the identities.
|
||||
*/
|
||||
@InitiatingFlow
|
||||
class Initiator(val otherSide: Party, val tx: WireTransaction): FlowLogic<Boolean>() {
|
||||
@Suspendable
|
||||
override fun call(): Boolean {
|
||||
val session = initiateFlow(otherSide)
|
||||
subFlow(IdentitySyncFlow.Send(session, tx))
|
||||
// Wait for the counterparty to indicate they're done
|
||||
return session.receive<Boolean>().unwrap { it }
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(IdentitySyncFlowTests.Initiator::class)
|
||||
class Receive(val otherSideSession: FlowSession): FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
subFlow(IdentitySyncFlow.Receive(otherSideSession))
|
||||
// Notify the initiator that we've finished syncing
|
||||
otherSideSession.send(true)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,114 @@
|
||||
package net.corda.confidential
|
||||
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import org.junit.Test
|
||||
import kotlin.test.*
|
||||
|
||||
class SwapIdentitiesFlowTests {
|
||||
@Test
|
||||
fun `issue key`() {
|
||||
// We run this in parallel threads to help catch any race conditions that may exist.
|
||||
val mockNet = MockNetwork(false, true)
|
||||
|
||||
// Set up values we'll need
|
||||
val notaryNode = mockNet.createNotaryNode()
|
||||
val aliceNode = mockNet.createPartyNode(ALICE.name)
|
||||
val bobNode = mockNet.createPartyNode(BOB.name)
|
||||
val alice: Party = aliceNode.services.myInfo.chooseIdentity()
|
||||
val bob: Party = bobNode.services.myInfo.chooseIdentity()
|
||||
|
||||
// Run the flows
|
||||
val requesterFlow = aliceNode.services.startFlow(SwapIdentitiesFlow(bob))
|
||||
|
||||
// Get the results
|
||||
val actual: Map<Party, AnonymousParty> = requesterFlow.resultFuture.getOrThrow().toMap()
|
||||
assertEquals(2, actual.size)
|
||||
// Verify that the generated anonymous identities do not match the well known identities
|
||||
val aliceAnonymousIdentity = actual[alice] ?: throw IllegalStateException()
|
||||
val bobAnonymousIdentity = actual[bob] ?: throw IllegalStateException()
|
||||
assertNotEquals<AbstractParty>(alice, aliceAnonymousIdentity)
|
||||
assertNotEquals<AbstractParty>(bob, bobAnonymousIdentity)
|
||||
|
||||
// Verify that the anonymous identities look sane
|
||||
assertEquals(alice.name, aliceNode.database.transaction { aliceNode.services.identityService.wellKnownPartyFromAnonymous(aliceAnonymousIdentity)!!.name })
|
||||
assertEquals(bob.name, bobNode.database.transaction { bobNode.services.identityService.wellKnownPartyFromAnonymous(bobAnonymousIdentity)!!.name })
|
||||
|
||||
// Verify that the nodes have the right anonymous identities
|
||||
assertTrue { aliceAnonymousIdentity.owningKey in aliceNode.services.keyManagementService.keys }
|
||||
assertTrue { bobAnonymousIdentity.owningKey in bobNode.services.keyManagementService.keys }
|
||||
assertFalse { aliceAnonymousIdentity.owningKey in bobNode.services.keyManagementService.keys }
|
||||
assertFalse { bobAnonymousIdentity.owningKey in aliceNode.services.keyManagementService.keys }
|
||||
|
||||
mockNet.stopNodes()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that flow is actually validating the name on the certificate presented by the counterparty.
|
||||
*/
|
||||
@Test
|
||||
fun `verifies identity name`() {
|
||||
// We run this in parallel threads to help catch any race conditions that may exist.
|
||||
val mockNet = MockNetwork(false, true)
|
||||
|
||||
// Set up values we'll need
|
||||
val notaryNode = mockNet.createNotaryNode(DUMMY_NOTARY.name)
|
||||
val aliceNode = mockNet.createPartyNode(ALICE.name)
|
||||
val bobNode = mockNet.createPartyNode(BOB.name)
|
||||
val bob: Party = bobNode.services.myInfo.chooseIdentity()
|
||||
val notBob = notaryNode.database.transaction {
|
||||
notaryNode.services.keyManagementService.freshKeyAndCert(notaryNode.services.myInfo.chooseIdentityAndCert(), false)
|
||||
}
|
||||
val sigData = SwapIdentitiesFlow.buildDataToSign(notBob)
|
||||
val signature = notaryNode.services.keyManagementService.sign(sigData, notBob.owningKey)
|
||||
assertFailsWith<SwapIdentitiesException>("Certificate subject must match counterparty's well known identity.") {
|
||||
SwapIdentitiesFlow.validateAndRegisterIdentity(aliceNode.services.identityService, bob, notBob, signature.withoutKey())
|
||||
}
|
||||
|
||||
mockNet.stopNodes()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that flow is actually validating its the signature presented by the counterparty.
|
||||
*/
|
||||
@Test
|
||||
fun `verifies signature`() {
|
||||
// We run this in parallel threads to help catch any race conditions that may exist.
|
||||
val mockNet = MockNetwork(false, true)
|
||||
|
||||
// Set up values we'll need
|
||||
val notaryNode = mockNet.createNotaryNode(DUMMY_NOTARY.name)
|
||||
val aliceNode = mockNet.createPartyNode(ALICE.name)
|
||||
val bobNode = mockNet.createPartyNode(BOB.name)
|
||||
val bob: Party = bobNode.services.myInfo.chooseIdentity()
|
||||
// Check that the wrong signature is rejected
|
||||
notaryNode.database.transaction {
|
||||
notaryNode.services.keyManagementService.freshKeyAndCert(notaryNode.services.myInfo.chooseIdentityAndCert(), false)
|
||||
}.let { anonymousNotary ->
|
||||
val sigData = SwapIdentitiesFlow.buildDataToSign(anonymousNotary)
|
||||
val signature = notaryNode.services.keyManagementService.sign(sigData, anonymousNotary.owningKey)
|
||||
assertFailsWith<SwapIdentitiesException>("Signature does not match the given identity and nonce") {
|
||||
SwapIdentitiesFlow.validateAndRegisterIdentity(aliceNode.services.identityService, bob, anonymousNotary, signature.withoutKey())
|
||||
}
|
||||
}
|
||||
// Check that the right signing key, but wrong identity is rejected
|
||||
val anonymousAlice = aliceNode.database.transaction {
|
||||
aliceNode.services.keyManagementService.freshKeyAndCert(aliceNode.services.myInfo.chooseIdentityAndCert(), false)
|
||||
}
|
||||
bobNode.database.transaction {
|
||||
bobNode.services.keyManagementService.freshKeyAndCert(bobNode.services.myInfo.chooseIdentityAndCert(), false)
|
||||
}.let { anonymousBob ->
|
||||
val sigData = SwapIdentitiesFlow.buildDataToSign(anonymousAlice)
|
||||
val signature = bobNode.services.keyManagementService.sign(sigData, anonymousBob.owningKey)
|
||||
assertFailsWith<SwapIdentitiesException>("Signature does not match the given identity and nonce.") {
|
||||
SwapIdentitiesFlow.validateAndRegisterIdentity(aliceNode.services.identityService, bob, anonymousBob, signature.withoutKey())
|
||||
}
|
||||
}
|
||||
|
||||
mockNet.stopNodes()
|
||||
}
|
||||
}
|
@ -4,7 +4,6 @@ trustStorePassword : "trustpass"
|
||||
p2pAddress : "localhost:10002"
|
||||
rpcAddress : "localhost:10003"
|
||||
webAddress : "localhost:10004"
|
||||
extraAdvertisedServiceIds : [ "corda.interest_rates" ]
|
||||
networkMapService : {
|
||||
address : "localhost:10000"
|
||||
legalName : "O=Network Map Service,OU=corda,L=London,C=GB"
|
||||
|
@ -4,7 +4,6 @@ trustStorePassword : "trustpass"
|
||||
p2pAddress : "localhost:10005"
|
||||
rpcAddress : "localhost:10006"
|
||||
webAddress : "localhost:10007"
|
||||
extraAdvertisedServiceIds : [ "corda.interest_rates" ]
|
||||
networkMapService : {
|
||||
address : "localhost:10000"
|
||||
legalName : "O=Network Map Service,OU=corda,L=London,C=GB"
|
||||
|
@ -3,5 +3,7 @@ keyStorePassword : "cordacadevpass"
|
||||
trustStorePassword : "trustpass"
|
||||
p2pAddress : "localhost:10000"
|
||||
webAddress : "localhost:10001"
|
||||
extraAdvertisedServiceIds : [ "corda.notary.validating" ]
|
||||
notary : {
|
||||
validating : true
|
||||
}
|
||||
useHTTPS : false
|
||||
|
@ -1,5 +1,5 @@
|
||||
gradlePluginsVersion=0.16.5
|
||||
kotlinVersion=1.1.4
|
||||
gradlePluginsVersion=2.0.1
|
||||
kotlinVersion=1.1.50
|
||||
guavaVersion=21.0
|
||||
bouncycastleVersion=1.57
|
||||
typesafeConfigVersion=1.3.1
|
||||
|
@ -2,6 +2,7 @@ apply plugin: 'kotlin'
|
||||
apply plugin: 'kotlin-jpa'
|
||||
apply plugin: 'net.corda.plugins.quasar-utils'
|
||||
apply plugin: 'net.corda.plugins.publish-utils'
|
||||
apply plugin: 'net.corda.plugins.api-scanner'
|
||||
apply plugin: 'com.jfrog.artifactory'
|
||||
|
||||
description 'Corda core'
|
||||
@ -94,6 +95,13 @@ jar {
|
||||
baseName 'corda-core'
|
||||
}
|
||||
|
||||
scanApi {
|
||||
excludeClasses = [
|
||||
// Kotlin should probably have declared this class as "synthetic".
|
||||
"net.corda.core.Utils\$toFuture\$1\$subscription\$1"
|
||||
]
|
||||
}
|
||||
|
||||
publish {
|
||||
name jar.baseName
|
||||
}
|
||||
|
@ -15,10 +15,11 @@ interface CordaThrowable {
|
||||
open class CordaException internal constructor(override var originalExceptionClassName: String? = null,
|
||||
private var _message: String? = null,
|
||||
private var _cause: Throwable? = null) : Exception(null, null, true, true), CordaThrowable {
|
||||
|
||||
constructor(message: String?,
|
||||
cause: Throwable?) : this(null, message, cause)
|
||||
|
||||
constructor(message: String?) : this(null, message, null)
|
||||
|
||||
override val message: String?
|
||||
get() = if (originalExceptionClassName == null) originalMessage else {
|
||||
if (originalMessage == null) "$originalExceptionClassName" else "$originalExceptionClassName: $originalMessage"
|
||||
@ -59,10 +60,12 @@ open class CordaException internal constructor(override var originalExceptionCla
|
||||
}
|
||||
|
||||
open class CordaRuntimeException(override var originalExceptionClassName: String?,
|
||||
private var _message: String? = null,
|
||||
private var _cause: Throwable? = null) : RuntimeException(null, null, true, true), CordaThrowable {
|
||||
private var _message: String?,
|
||||
private var _cause: Throwable?) : RuntimeException(null, null, true, true), CordaThrowable {
|
||||
constructor(message: String?, cause: Throwable?) : this(null, message, cause)
|
||||
|
||||
constructor(message: String?) : this(null, message, null)
|
||||
|
||||
override val message: String?
|
||||
get() = if (originalExceptionClassName == null) originalMessage else {
|
||||
if (originalMessage == null) "$originalExceptionClassName" else "$originalExceptionClassName: $originalMessage"
|
||||
|
@ -1,4 +1,5 @@
|
||||
@file:JvmName("ConcurrencyUtils")
|
||||
|
||||
package net.corda.core.concurrent
|
||||
|
||||
import net.corda.core.internal.concurrent.openFuture
|
||||
@ -28,7 +29,7 @@ fun <V, W> firstOf(vararg futures: CordaFuture<out V>, handler: (CordaFuture<out
|
||||
|
||||
private val defaultLog = LoggerFactory.getLogger("net.corda.core.concurrent")
|
||||
@VisibleForTesting
|
||||
internal val shortCircuitedTaskFailedMessage = "Short-circuited task failed:"
|
||||
internal const val shortCircuitedTaskFailedMessage = "Short-circuited task failed:"
|
||||
|
||||
internal fun <V, W> firstOf(futures: Array<out CordaFuture<out V>>, log: Logger, handler: (CordaFuture<out V>) -> W): CordaFuture<W> {
|
||||
val resultFuture = openFuture<W>()
|
||||
|
@ -21,7 +21,8 @@ interface TokenizableAssetInfo {
|
||||
* Amount represents a positive quantity of some token (currency, asset, etc.), measured in quantity of the smallest
|
||||
* 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.
|
||||
* 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
|
||||
@ -29,10 +30,11 @@ interface TokenizableAssetInfo {
|
||||
* multiplication are overflow checked and will throw [ArithmeticException] if the operation would have caused integer
|
||||
* overflow.
|
||||
*
|
||||
* @property quantity the number of tokens as a Long value.
|
||||
* @property displayTokenSize the nominal display unit size of a single token, potentially with trailing decimal display places if the scale parameter is non-zero.
|
||||
* @property token an instance of type T, usually a singleton.
|
||||
* @param T the type of the token, for example [Currency]. T should implement TokenizableAssetInfo if automatic conversion to/from a display format is required.
|
||||
* @property quantity the number of tokens as a long value.
|
||||
* @property displayTokenSize the nominal display unit size of a single token, potentially with trailing decimal display
|
||||
* places if the scale parameter is non-zero.
|
||||
* @property token the type of token this is an amount of. This is usually a singleton.
|
||||
* @param T the type of the token, for example [Currency]. T should implement [TokenizableAssetInfo] if automatic conversion to/from a display format is required.
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal, val token: T) : Comparable<Amount<T>> {
|
||||
@ -40,11 +42,10 @@ data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal,
|
||||
companion object {
|
||||
/**
|
||||
* 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.
|
||||
* returns an amount with a quantity of "1234" tokens. The function [getDisplayTokenSize] is used to determine the
|
||||
* conversion scaling, for example bonds might be in nominal amounts of 100, currencies in 0.01 penny units.
|
||||
*
|
||||
* @see Amount<Currency>.toDecimal
|
||||
* @see Amount.toDecimal
|
||||
* @throws ArithmeticException if the intermediate calculations cannot be converted to an unsigned 63-bit token amount.
|
||||
*/
|
||||
@JvmStatic
|
||||
@ -166,7 +167,7 @@ data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal,
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch(e: Exception) {
|
||||
} catch (e: Exception) {
|
||||
throw IllegalArgumentException("Could not parse $input as a currency", e)
|
||||
}
|
||||
throw IllegalArgumentException("Did not recognise the currency in $input or could not parse")
|
||||
@ -195,6 +196,7 @@ data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal,
|
||||
* Mixing non-identical token types will throw [IllegalArgumentException].
|
||||
*
|
||||
* @throws ArithmeticException if there is overflow of Amount tokens during the summation
|
||||
* @throws IllegalArgumentException if mixing non-identical token types.
|
||||
*/
|
||||
operator fun plus(other: Amount<T>): Amount<T> {
|
||||
checkToken(other)
|
||||
@ -202,11 +204,11 @@ data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal,
|
||||
}
|
||||
|
||||
/**
|
||||
* A checked addition operator is supported to simplify netting of Amounts.
|
||||
* If this leads to the Amount going negative this will throw [IllegalArgumentException].
|
||||
* Mixing non-identical token types will throw [IllegalArgumentException].
|
||||
* A checked subtraction operator is supported to simplify netting of Amounts.
|
||||
*
|
||||
* @throws ArithmeticException if there is Numeric underflow
|
||||
* @throws ArithmeticException if there is numeric underflow.
|
||||
* @throws IllegalArgumentException if this leads to the amount going negative, or would mix non-identical token
|
||||
* types.
|
||||
*/
|
||||
operator fun minus(other: Amount<T>): Amount<T> {
|
||||
checkToken(other)
|
||||
@ -222,6 +224,8 @@ data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal,
|
||||
* 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.
|
||||
*
|
||||
* @throws ArithmeticException if there is overflow of Amount tokens during the multiplication.
|
||||
*/
|
||||
operator fun times(other: Long): Amount<T> = Amount(Math.multiplyExact(quantity, other), displayTokenSize, token)
|
||||
|
||||
@ -229,13 +233,15 @@ data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal,
|
||||
* 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.
|
||||
*
|
||||
* @throws ArithmeticException if there is overflow of Amount tokens during the multiplication.
|
||||
*/
|
||||
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
|
||||
* @return '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>> {
|
||||
@ -249,8 +255,10 @@ data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal,
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* of "1234" GBP, returns "12.34". The precise representation is controlled by the display token size (
|
||||
* from [getDisplayTokenSize]), which determines the size of a single token and controls the trailing decimal
|
||||
* places via its scale property. *Note* that currencies such as the Bahraini Dinar use 3 decimal places,
|
||||
* and it must not be presumed that this converts amounts to 2 decimal places.
|
||||
*
|
||||
* @see Amount.fromDecimal
|
||||
*/
|
||||
|
@ -1,15 +1,37 @@
|
||||
package net.corda.core.contracts
|
||||
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
|
||||
/** Constrain which contract-code-containing attachments can be used with a [ContractState]. */
|
||||
/** Constrain which contract-code-containing attachment can be used with a [ContractState]. */
|
||||
@CordaSerializable
|
||||
interface AttachmentConstraint {
|
||||
/** Returns whether the given contract attachments can be used with the [ContractState] associated with this constraint object. */
|
||||
fun isSatisfiedBy(attachments: List<Attachment>): Boolean
|
||||
/** Returns whether the given contract attachment can be used with the [ContractState] associated with this constraint object. */
|
||||
fun isSatisfiedBy(attachment: Attachment): Boolean
|
||||
}
|
||||
|
||||
/** An [AttachmentConstraint] where [isSatisfiedBy] always returns true. */
|
||||
object AlwaysAcceptAttachmentConstraint : AttachmentConstraint {
|
||||
override fun isSatisfiedBy(attachments: List<Attachment>) = true
|
||||
override fun isSatisfiedBy(attachment: Attachment) = true
|
||||
}
|
||||
|
||||
/** An [AttachmentConstraint] that verifies by hash */
|
||||
data class HashAttachmentConstraint(val attachmentId: SecureHash) : AttachmentConstraint {
|
||||
override fun isSatisfiedBy(attachment: Attachment) = attachment.id == attachmentId
|
||||
}
|
||||
|
||||
/**
|
||||
* This [AttachmentConstraint] is a convenience class that will be automatically resolved to a [HashAttachmentConstraint].
|
||||
* The resolution occurs in [TransactionBuilder.toWireTransaction] and uses the [TransactionState.contract] value
|
||||
* to find a corresponding loaded [Cordapp] that contains such a contract, and then uses that [Cordapp] as the
|
||||
* [Attachment].
|
||||
*
|
||||
* If, for any reason, this class is not automatically resolved the default implementation is to fail, because the
|
||||
* intent of this class is that it should be replaced by a correct [HashAttachmentConstraint] and verify against an
|
||||
* actual [Attachment].
|
||||
*/
|
||||
object AutomaticHashConstraint : AttachmentConstraint {
|
||||
override fun isSatisfiedBy(attachment: Attachment): Boolean {
|
||||
throw UnsupportedOperationException("Contracts cannot be satisfied by an AutomaticHashConstraint placeholder")
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package net.corda.core.contracts
|
||||
|
||||
/**
|
||||
* An enum, for which each property corresponds to a transaction component group. The position in the enum class
|
||||
* declaration (ordinal) is used for component-leaf ordering when computing the Merkle tree.
|
||||
*/
|
||||
enum class ComponentGroupEnum {
|
||||
INPUTS_GROUP, // ordinal = 0.
|
||||
OUTPUTS_GROUP, // ordinal = 1.
|
||||
COMMANDS_GROUP, // ordinal = 2.
|
||||
ATTACHMENTS_GROUP, // ordinal = 3.
|
||||
NOTARY_GROUP, // ordinal = 4.
|
||||
TIMEWINDOW_GROUP // ordinal = 5.
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package net.corda.core.contracts
|
||||
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
|
||||
/**
|
||||
* Wrap an attachment in this if it is to be used as an executable contract attachment
|
||||
*
|
||||
* @property attachment The attachment representing the contract JAR
|
||||
* @property contract The contract name contained within the JAR
|
||||
*/
|
||||
@CordaSerializable
|
||||
class ContractAttachment(val attachment: Attachment, val contract: ContractClassName) : Attachment by attachment
|
@ -4,6 +4,7 @@ package net.corda.core.contracts
|
||||
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import java.security.PublicKey
|
||||
import java.util.*
|
||||
|
||||
@ -33,7 +34,7 @@ inline fun <R> requireThat(body: Requirements.() -> R) = Requirements.body()
|
||||
|
||||
/** Filters the command list by type, party and public key all at once. */
|
||||
inline fun <reified T : CommandData> Collection<CommandWithParties<CommandData>>.select(signer: PublicKey? = null,
|
||||
party: AbstractParty? = null) =
|
||||
party: AbstractParty? = null) =
|
||||
filter { it.value is T }.
|
||||
filter { if (signer == null) true else signer in it.signers }.
|
||||
filter { if (party == null) true else party in it.signingParties }.
|
||||
@ -43,7 +44,7 @@ inline fun <reified T : CommandData> Collection<CommandWithParties<CommandData>>
|
||||
|
||||
/** Filters the command list by type, parties and public keys all at once. */
|
||||
inline fun <reified T : CommandData> Collection<CommandWithParties<CommandData>>.select(signers: Collection<PublicKey>?,
|
||||
parties: Collection<Party>?) =
|
||||
parties: Collection<Party>?) =
|
||||
filter { it.value is T }.
|
||||
filter { if (signers == null) true else it.signers.containsAll(signers) }.
|
||||
filter { if (parties == null) true else it.signingParties.containsAll(parties) }.
|
||||
@ -58,7 +59,7 @@ inline fun <reified T : CommandData> Collection<CommandWithParties<CommandData>>
|
||||
|
||||
/** Ensures that a transaction has only one command that is of the given type, otherwise throws an exception. */
|
||||
fun <C : CommandData> Collection<CommandWithParties<CommandData>>.requireSingleCommand(klass: Class<C>) =
|
||||
mapNotNull { @Suppress("UNCHECKED_CAST") if (klass.isInstance(it.value)) it as CommandWithParties<C> else null }.single()
|
||||
mapNotNull { if (klass.isInstance(it.value)) uncheckedCast<CommandWithParties<CommandData>, CommandWithParties<C>>(it) else null }.single()
|
||||
|
||||
/**
|
||||
* Simple functionality for verifying a move command. Verifies that each input has a signature from its owning key.
|
||||
|
@ -4,6 +4,7 @@ package net.corda.core.contracts
|
||||
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.secureRandomBytes
|
||||
import net.corda.core.crypto.toStringShort
|
||||
import net.corda.core.flows.FlowLogicRef
|
||||
import net.corda.core.flows.FlowLogicRefFactory
|
||||
import net.corda.core.identity.AbstractParty
|
||||
@ -15,10 +16,12 @@ import net.corda.core.utilities.OpaqueBytes
|
||||
import java.security.PublicKey
|
||||
import java.time.Instant
|
||||
|
||||
// DOCSTART 1
|
||||
/** Implemented by anything that can be named by a secure hash value (e.g. transactions, attachments). */
|
||||
interface NamedByHash {
|
||||
val id: SecureHash
|
||||
}
|
||||
// DOCEND 1
|
||||
|
||||
/**
|
||||
* The [Issued] data class holds the details of an on ledger digital asset.
|
||||
@ -183,7 +186,7 @@ data class Command<T : CommandData>(val value: T, val signers: List<PublicKey>)
|
||||
constructor(data: T, 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()}"
|
||||
override fun toString() = "${commandDataToString()} with pubkeys ${signers.map { it.toStringShort() }.joinToString()}"
|
||||
}
|
||||
|
||||
/** A common move command for contract states which can change owner. */
|
||||
@ -196,9 +199,6 @@ interface MoveCommand : CommandData {
|
||||
val contract: Class<out Contract>?
|
||||
}
|
||||
|
||||
/** Indicates that this transaction replaces the inputs contract state to another contract state */
|
||||
data class UpgradeCommand(val upgradedContractClass: ContractClassName) : CommandData
|
||||
|
||||
// DOCSTART 6
|
||||
/** A [Command] where the signing parties have been looked up if they have a well known/recognised institutional key. */
|
||||
@CordaSerializable
|
||||
@ -271,7 +271,7 @@ class PrivacySalt(bytes: ByteArray) : OpaqueBytes(bytes) {
|
||||
|
||||
init {
|
||||
require(bytes.size == 32) { "Privacy salt should be 32 bytes." }
|
||||
require(!bytes.all { it == 0.toByte() }) { "Privacy salt should not be all zeros." }
|
||||
require(bytes.any { it != 0.toByte() }) { "Privacy salt should not be all zeros." }
|
||||
}
|
||||
}
|
||||
|
||||
@ -281,4 +281,4 @@ class PrivacySalt(bytes: ByteArray) : OpaqueBytes(bytes) {
|
||||
* @property state A state
|
||||
* @property contract The contract that should verify the state
|
||||
*/
|
||||
data class StateAndContract(val state: ContractState, val contract: ContractClassName)
|
||||
data class StateAndContract(val state: ContractState, val contract: ContractClassName)
|
||||
|
@ -85,6 +85,7 @@ abstract class TimeWindow {
|
||||
init {
|
||||
require(fromTime < untilTime) { "fromTime must be earlier than untilTime" }
|
||||
}
|
||||
|
||||
override val midpoint: Instant get() = fromTime + (fromTime until untilTime) / 2
|
||||
override fun contains(instant: Instant): Boolean = instant >= fromTime && instant < untilTime
|
||||
override fun toString(): String = "[$fromTime, $untilTime)"
|
||||
|
@ -15,43 +15,16 @@ data class TransactionState<out T : ContractState> @JvmOverloads constructor(
|
||||
/** The custom contract state */
|
||||
val data: T,
|
||||
/**
|
||||
* An instance of the contract class that will verify this state.
|
||||
* The contract class name that will verify this state that will be created via reflection.
|
||||
* The attachment containing this class will be automatically added to the transaction at transaction creation
|
||||
* time.
|
||||
*
|
||||
* # Discussion
|
||||
* Currently these are loaded from the classpath of the node which includes the cordapp directory - at some
|
||||
* point these will also be loaded and run from the attachment store directly, allowing contracts to be
|
||||
* sent across, and run, from the network from within a sandbox environment.
|
||||
*
|
||||
* This field is not the final design, it's just a piece of temporary scaffolding. Once the contract sandbox is
|
||||
* further along, this field will become a description of which attachments are acceptable for defining the
|
||||
* contract.
|
||||
*
|
||||
* Recall that an attachment is a zip file that can be referenced from any transaction. The contents of the
|
||||
* attachments are merged together and cannot define any overlapping files, thus for any given transaction there
|
||||
* is a miniature file system in which each file can be precisely mapped to the defining attachment.
|
||||
*
|
||||
* Attachments may contain many things (data files, legal documents, etc) but mostly they contain JVM bytecode.
|
||||
* The class files inside define not only [Contract] implementations but also the classes that define the states.
|
||||
* Within the rest of a transaction, user-providable components are referenced by name only.
|
||||
*
|
||||
* This means that a smart contract in Corda does two things:
|
||||
*
|
||||
* 1. Define the data structures that compose the ledger (the states)
|
||||
* 2. Define the rules for updating those structures
|
||||
*
|
||||
* The first is merely a utility role ... in theory contract code could manually parse byte streams by hand.
|
||||
* The second is vital to the integrity of the ledger. So this field needs to be able to express constraints like:
|
||||
*
|
||||
* - Only attachment 733c350f396a727655be1363c06635ba355036bd54a5ed6e594fd0b5d05f42f6 may be used with this state.
|
||||
* - Any attachment signed by public key 2d1ce0e330c52b8055258d776c40 may be used with this state.
|
||||
* - Attachments (1, 2, 3) may all be used with this state.
|
||||
*
|
||||
* and so on. In this way it becomes possible for the business logic governing a state to be evolved, if the
|
||||
* constraints are flexible enough.
|
||||
*
|
||||
* Because contract classes often also define utilities that generate relevant transactions, and because attachments
|
||||
* cannot know their own hashes, we will have to provide various utilities to assist with obtaining the right
|
||||
* code constraints from within the contract code itself.
|
||||
*
|
||||
* TODO: Implement the above description. See COR-226
|
||||
*/
|
||||
* TODO: Implement the contract sandbox loading of the contract attachments
|
||||
* */
|
||||
val contract: ContractClassName,
|
||||
/** Identity of the notary that ensures the state is not used as an input to a transaction more than once */
|
||||
val notary: Party,
|
||||
@ -76,5 +49,5 @@ data class TransactionState<out T : ContractState> @JvmOverloads constructor(
|
||||
/**
|
||||
* A validator for the contract attachments on the transaction.
|
||||
*/
|
||||
val constraint: AttachmentConstraint = AlwaysAcceptAttachmentConstraint)
|
||||
val constraint: AttachmentConstraint = AutomaticHashConstraint)
|
||||
// DOCEND 1
|
||||
|
@ -16,6 +16,12 @@ sealed class TransactionVerificationException(val txId: SecureHash, message: Str
|
||||
class ContractRejection(txId: SecureHash, contract: Contract, cause: Throwable)
|
||||
: TransactionVerificationException(txId, "Contract verification failed: ${cause.message}, contract: $contract", cause)
|
||||
|
||||
class ContractConstraintRejection(txId: SecureHash, contractClass: String)
|
||||
: TransactionVerificationException(txId, "Contract constraints failed for $contractClass", null)
|
||||
|
||||
class MissingAttachmentRejection(txId: SecureHash, contractClass: String)
|
||||
: TransactionVerificationException(txId, "Contract constraints failed, could not find attachment for: $contractClass", null)
|
||||
|
||||
class ContractCreationError(txId: SecureHash, contractClass: String, cause: Throwable)
|
||||
: TransactionVerificationException(txId, "Contract verification failed: ${cause.message}, could not create contract class: $contractClass", cause)
|
||||
|
||||
|
39
core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt
Normal file
39
core/src/main/kotlin/net/corda/core/cordapp/Cordapp.kt
Normal file
@ -0,0 +1,39 @@
|
||||
package net.corda.core.cordapp
|
||||
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.core.serialization.SerializationWhitelist
|
||||
import net.corda.core.serialization.SerializeAsToken
|
||||
import java.net.URL
|
||||
|
||||
/**
|
||||
* Represents a cordapp by registering the JAR that contains it and all important classes for Corda.
|
||||
* Instances of this class are generated automatically at startup of a node and can get retrieved from
|
||||
* [CordappProvider.getAppContext] from the [CordappContext] it returns.
|
||||
*
|
||||
* This will only need to be constructed manually for certain kinds of tests.
|
||||
*
|
||||
* @property name Cordapp name - derived from the base name of the Cordapp JAR (therefore may not be unique)
|
||||
* @property contractClassNames List of contracts
|
||||
* @property initiatedFlows List of initiatable flow classes
|
||||
* @property rpcFlows List of RPC initiable flows classes
|
||||
* @property serviceFlows List of [CordaService] initiable flows classes
|
||||
* @property schedulableFlows List of flows startable by the scheduler
|
||||
* @property servies List of RPC services
|
||||
* @property serializationWhitelists List of Corda plugin registries
|
||||
* @property customSchemas List of custom schemas
|
||||
* @property jarPath The path to the JAR for this CorDapp
|
||||
*/
|
||||
interface Cordapp {
|
||||
val name: String
|
||||
val contractClassNames: List<String>
|
||||
val initiatedFlows: List<Class<out FlowLogic<*>>>
|
||||
val rpcFlows: List<Class<out FlowLogic<*>>>
|
||||
val serviceFlows: List<Class<out FlowLogic<*>>>
|
||||
val schedulableFlows: List<Class<out FlowLogic<*>>>
|
||||
val services: List<Class<out SerializeAsToken>>
|
||||
val serializationWhitelists: List<SerializationWhitelist>
|
||||
val customSchemas: Set<MappedSchema>
|
||||
val jarPath: URL
|
||||
val cordappClasses: List<String>
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package net.corda.core.cordapp
|
||||
|
||||
import net.corda.core.crypto.SecureHash
|
||||
|
||||
// TODO: Add per app config
|
||||
|
||||
/**
|
||||
* An app context provides information about where an app was loaded from, access to its classloader,
|
||||
* and (in the included [Cordapp] object) lists of annotated classes discovered via scanning the JAR.
|
||||
*
|
||||
* A CordappContext is obtained from [CordappProvider.getAppContext] which resides on a [ServiceHub]. This will be
|
||||
* used primarily from within flows.
|
||||
*
|
||||
* @property cordapp The cordapp this context is about
|
||||
* @property attachmentId For CorDapps containing [Contract] or [UpgradedContract] implementations this will be populated
|
||||
* with the attachment containing those class files
|
||||
* @property classLoader the classloader used to load this cordapp's classes
|
||||
*/
|
||||
class CordappContext(val cordapp: Cordapp, val attachmentId: SecureHash?, val classLoader: ClassLoader)
|
@ -0,0 +1,28 @@
|
||||
package net.corda.core.cordapp
|
||||
|
||||
import net.corda.core.contracts.ContractClassName
|
||||
import net.corda.core.node.services.AttachmentId
|
||||
|
||||
/**
|
||||
* Provides access to what the node knows about loaded applications.
|
||||
*/
|
||||
interface CordappProvider {
|
||||
/**
|
||||
* Exposes the current CorDapp context which will contain information and configuration of the CorDapp that
|
||||
* is currently running.
|
||||
*
|
||||
* The calling application is found via stack walking and finding the first class on the stack that matches any class
|
||||
* contained within the automatically resolved [Cordapp]s loaded by the [CordappLoader]
|
||||
*
|
||||
* @throws IllegalStateException When called from a non-app context
|
||||
*/
|
||||
fun getAppContext(): CordappContext
|
||||
|
||||
/**
|
||||
* Resolve an attachment ID for a given contract name
|
||||
*
|
||||
* @param contractClassName The contract to find the attachment for
|
||||
* @return An attachment ID if it exists
|
||||
*/
|
||||
fun getContractAttachmentID(contractClassName: ContractClassName): AttachmentId?
|
||||
}
|
@ -30,7 +30,7 @@ import java.util.*
|
||||
@CordaSerializable
|
||||
class CompositeKey private constructor(val threshold: Int, children: List<NodeAndWeight>) : PublicKey {
|
||||
companion object {
|
||||
val KEY_ALGORITHM = "COMPOSITE"
|
||||
const val KEY_ALGORITHM = "COMPOSITE"
|
||||
/**
|
||||
* Build a composite key from a DER encoded form.
|
||||
*/
|
||||
@ -88,7 +88,9 @@ class CompositeKey private constructor(val threshold: Int, children: List<NodeAn
|
||||
if (node is CompositeKey) {
|
||||
val curVisitedMap = IdentityHashMap<CompositeKey, Boolean>()
|
||||
curVisitedMap.putAll(visitedMap)
|
||||
require(!curVisitedMap.contains(node)) { "Cycle detected for CompositeKey: $node" }
|
||||
// We can't print the node details, because doing so involves serializing the node, which we can't
|
||||
// do because of the cyclic graph.
|
||||
require(!curVisitedMap.contains(node)) { "Cycle detected for CompositeKey" }
|
||||
curVisitedMap.put(node, true)
|
||||
node.cycleDetection(curVisitedMap)
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
package net.corda.core.crypto
|
||||
|
||||
import net.corda.core.crypto.composite.CompositeSignaturesWithKeys
|
||||
import net.corda.core.serialization.deserialize
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.security.*
|
||||
@ -12,6 +11,7 @@ import java.security.spec.AlgorithmParameterSpec
|
||||
class CompositeSignature : Signature(SIGNATURE_ALGORITHM) {
|
||||
companion object {
|
||||
const val SIGNATURE_ALGORITHM = "COMPOSITESIG"
|
||||
@JvmStatic
|
||||
fun getService(provider: Provider) = Provider.Service(provider, "Signature", SIGNATURE_ALGORITHM, CompositeSignature::class.java.name, emptyList(), emptyMap())
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
package net.corda.core.crypto.composite
|
||||
package net.corda.core.crypto
|
||||
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
|
||||
/**
|
@ -31,6 +31,8 @@ class CordaSecurityProvider : Provider(PROVIDER_NAME, 0.1, "$PROVIDER_NAME secur
|
||||
object CordaObjectIdentifier {
|
||||
// UUID-based OID
|
||||
// TODO: Register for an OID space and issue our own shorter OID.
|
||||
@JvmField val COMPOSITE_KEY = ASN1ObjectIdentifier("2.25.30086077608615255153862931087626791002")
|
||||
@JvmField val COMPOSITE_SIGNATURE = ASN1ObjectIdentifier("2.25.30086077608615255153862931087626791003")
|
||||
@JvmField
|
||||
val COMPOSITE_KEY = ASN1ObjectIdentifier("2.25.30086077608615255153862931087626791002")
|
||||
@JvmField
|
||||
val COMPOSITE_SIGNATURE = ASN1ObjectIdentifier("2.25.30086077608615255153862931087626791003")
|
||||
}
|
||||
|
@ -1,10 +1,8 @@
|
||||
package net.corda.core.crypto
|
||||
|
||||
import net.corda.core.internal.X509EdDSAEngine
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.i2p.crypto.eddsa.EdDSAEngine
|
||||
import net.i2p.crypto.eddsa.EdDSAPrivateKey
|
||||
import net.i2p.crypto.eddsa.EdDSAPublicKey
|
||||
import net.i2p.crypto.eddsa.EdDSASecurityProvider
|
||||
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
|
||||
@ -41,6 +39,8 @@ import org.bouncycastle.pqc.jcajce.provider.sphincs.BCSphincs256PublicKey
|
||||
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.spec.InvalidKeySpecException
|
||||
import java.security.spec.PKCS8EncodedKeySpec
|
||||
import java.security.spec.X509EncodedKeySpec
|
||||
@ -200,6 +200,8 @@ object Crypto {
|
||||
|
||||
private fun getBouncyCastleProvider() = BouncyCastleProvider().apply {
|
||||
putAll(EdDSASecurityProvider())
|
||||
// Override the normal EdDSA engine with one which can handle X509 keys.
|
||||
put("Signature.${EdDSAEngine.SIGNATURE_ALGORITHM}", X509EdDSAEngine::class.qualifiedName)
|
||||
addKeyInfoConverter(EDDSA_ED25519_SHA512.signatureOID.algorithm, KeyInfoConverter(EDDSA_ED25519_SHA512))
|
||||
}
|
||||
|
||||
@ -772,9 +774,10 @@ object Crypto {
|
||||
// it forms, by itself, the new private key, which in turn is used to compute the new public key.
|
||||
val pointQ = FixedPointCombMultiplier().multiply(parameterSpec.g, deterministicD)
|
||||
// This is unlikely to happen, but we should check for point at infinity.
|
||||
if (pointQ.isInfinity)
|
||||
if (pointQ.isInfinity) {
|
||||
// Instead of throwing an exception, we retry with SHA256(seed).
|
||||
return deriveKeyPairECDSA(parameterSpec, privateKey, seed.sha256().bytes)
|
||||
}
|
||||
val publicKeySpec = ECPublicKeySpec(pointQ, parameterSpec)
|
||||
val publicKeyD = BCECPublicKey(privateKey.algorithm, publicKeySpec, BouncyCastleProvider.CONFIGURATION)
|
||||
|
||||
@ -847,6 +850,7 @@ object Crypto {
|
||||
override fun generatePublic(keyInfo: SubjectPublicKeyInfo?): PublicKey? {
|
||||
return keyInfo?.let { decodePublicKey(signatureScheme, it.encoded) }
|
||||
}
|
||||
|
||||
override fun generatePrivate(keyInfo: PrivateKeyInfo?): PrivateKey? {
|
||||
return keyInfo?.let { decodePrivateKey(signatureScheme, it.encoded) }
|
||||
}
|
||||
@ -939,7 +943,16 @@ object Crypto {
|
||||
* is inappropriate for a supported key factory to produce a private key.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun toSupportedPublicKey(key: PublicKey): PublicKey = decodePublicKey(key.encoded)
|
||||
fun toSupportedPublicKey(key: PublicKey): PublicKey {
|
||||
return when (key) {
|
||||
is BCECPublicKey -> key
|
||||
is BCRSAPublicKey -> key
|
||||
is BCSphincs256PublicKey -> key
|
||||
is EdDSAPublicKey -> key
|
||||
is CompositeKey -> key
|
||||
else -> decodePublicKey(key.encoded)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a private key to a supported implementation. This can be used to convert a SUN's EC key to an BC key.
|
||||
@ -950,5 +963,13 @@ object Crypto {
|
||||
* is inappropriate for a supported key factory to produce a private key.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun toSupportedPrivateKey(key: PrivateKey): PrivateKey = decodePrivateKey(key.encoded)
|
||||
fun toSupportedPrivateKey(key: PrivateKey): PrivateKey {
|
||||
return when (key) {
|
||||
is BCECPrivateKey -> key
|
||||
is BCRSAPrivateKey -> key
|
||||
is BCSphincs256PrivateKey -> key
|
||||
is EdDSAPrivateKey -> key
|
||||
else -> decodePrivateKey(key.encoded)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,10 +2,14 @@
|
||||
|
||||
package net.corda.core.crypto
|
||||
|
||||
import net.corda.core.contracts.PrivacySalt
|
||||
import net.corda.core.serialization.SerializationDefaults
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.toBase58
|
||||
import net.corda.core.utilities.toSHA256Bytes
|
||||
import java.math.BigInteger
|
||||
import java.nio.ByteBuffer
|
||||
import java.security.*
|
||||
|
||||
/**
|
||||
@ -31,6 +35,7 @@ fun PrivateKey.sign(bytesToSign: ByteArray, publicKey: PublicKey) = DigitalSigna
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class)
|
||||
fun KeyPair.sign(bytesToSign: ByteArray) = private.sign(bytesToSign, public)
|
||||
|
||||
fun KeyPair.sign(bytesToSign: OpaqueBytes) = sign(bytesToSign.bytes)
|
||||
/**
|
||||
* Helper function for signing a [SignableData] object.
|
||||
@ -68,18 +73,18 @@ fun PublicKey.verify(content: ByteArray, signature: DigitalSignature) = Crypto.d
|
||||
* @return whether the signature is correct for this key.
|
||||
*/
|
||||
@Throws(IllegalStateException::class, SignatureException::class, IllegalArgumentException::class)
|
||||
fun PublicKey.isValid(content: ByteArray, signature: DigitalSignature) : Boolean {
|
||||
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()
|
||||
fun PublicKey.toStringShort(): String = "DL" + this.toSHA256Bytes().toBase58()
|
||||
|
||||
val PublicKey.keys: Set<PublicKey> get() = (this as? CompositeKey)?.leafKeys ?: setOf(this)
|
||||
|
||||
fun PublicKey.isFulfilledBy(otherKey: PublicKey): Boolean = isFulfilledBy(setOf(otherKey))
|
||||
fun PublicKey.isFulfilledBy(otherKey: PublicKey): Boolean = isFulfilledBy(setOf(otherKey))
|
||||
fun PublicKey.isFulfilledBy(otherKeys: Iterable<PublicKey>): Boolean = (this as? CompositeKey)?.isFulfilledBy(otherKeys) ?: (this in otherKeys)
|
||||
|
||||
/** Checks whether any of the given [keys] matches a leaf on the [CompositeKey] tree or a single [PublicKey]. */
|
||||
@ -184,3 +189,27 @@ fun random63BitValue(): Long {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the hash of each serialised component so as to be used as Merkle tree leaf. The resultant output (leaf) is
|
||||
* calculated using the SHA256d algorithm, thus SHA256(SHA256(nonce || serializedComponent)), where nonce is computed
|
||||
* from [computeNonce].
|
||||
*/
|
||||
fun componentHash(opaqueBytes: OpaqueBytes, privacySalt: PrivacySalt, componentGroupIndex: Int, internalIndex: Int): SecureHash =
|
||||
componentHash(computeNonce(privacySalt, componentGroupIndex, internalIndex), opaqueBytes)
|
||||
|
||||
/** Return the SHA256(SHA256(nonce || serializedComponent)). */
|
||||
fun componentHash(nonce: SecureHash, opaqueBytes: OpaqueBytes): SecureHash = SecureHash.sha256Twice(nonce.bytes + opaqueBytes.bytes)
|
||||
|
||||
/** Serialise the object and return the hash of the serialized bytes. */
|
||||
fun <T : Any> serializedHash(x: T): SecureHash = x.serialize(context = SerializationDefaults.P2P_CONTEXT.withoutReferences()).bytes.sha256()
|
||||
|
||||
/**
|
||||
* Method to compute a nonce based on privacySalt, component group index and component internal index.
|
||||
* SHA256d (double SHA256) is used to prevent length extension attacks.
|
||||
* @param privacySalt a [PrivacySalt].
|
||||
* @param groupIndex the fixed index (ordinal) of this component group.
|
||||
* @param internalIndex the internal index of this object in its corresponding components list.
|
||||
* @return SHA256(SHA256(privacySalt || groupIndex || internalIndex))
|
||||
*/
|
||||
fun computeNonce(privacySalt: PrivacySalt, groupIndex: Int, internalIndex: Int) = SecureHash.sha256Twice(privacySalt.bytes + ByteBuffer.allocate(8).putInt(groupIndex).putInt(internalIndex).array())
|
||||
|
@ -23,6 +23,7 @@ open class DigitalSignature(bytes: ByteArray) : OpaqueBytes(bytes) {
|
||||
*/
|
||||
@Throws(InvalidKeyException::class, SignatureException::class)
|
||||
fun verify(content: ByteArray) = by.verify(content, this)
|
||||
|
||||
/**
|
||||
* Utility to simplify the act of verifying a signature.
|
||||
*
|
||||
@ -32,6 +33,7 @@ open class DigitalSignature(bytes: ByteArray) : OpaqueBytes(bytes) {
|
||||
*/
|
||||
@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
|
||||
@ -44,5 +46,6 @@ open class DigitalSignature(bytes: ByteArray) : OpaqueBytes(bytes) {
|
||||
*/
|
||||
@Throws(InvalidKeyException::class, SignatureException::class)
|
||||
fun isValid(content: ByteArray) = by.isValid(content, this)
|
||||
fun withoutKey() : DigitalSignature = DigitalSignature(this.bytes)
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,12 @@
|
||||
package net.corda.core.crypto
|
||||
|
||||
import net.corda.core.CordaException
|
||||
import net.corda.core.crypto.SecureHash.Companion.zeroHash
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import java.util.*
|
||||
|
||||
@CordaSerializable
|
||||
class MerkleTreeException(val reason: String) : Exception("Partial Merkle Tree exception. Reason: $reason")
|
||||
class MerkleTreeException(val reason: String) : CordaException("Partial Merkle Tree exception. Reason: $reason")
|
||||
|
||||
/**
|
||||
* Building and verification of Partial Merkle Tree.
|
||||
@ -121,6 +122,28 @@ class PartialMerkleTree(val root: PartialTree) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursive calculation of root of this partial tree.
|
||||
* Modifies usedHashes to later check for inclusion with hashes provided.
|
||||
* @param node the partial Merkle tree for which we want to calculate the Merkle root.
|
||||
* @param usedHashes a mutable list that at the end of this recursive algorithm, it will consist of the included leaves (hashes of the visible components).
|
||||
* @return the root [SecureHash] of this partial Merkle tree.
|
||||
*/
|
||||
fun rootAndUsedHashes(node: PartialTree, usedHashes: MutableList<SecureHash>): SecureHash {
|
||||
return when (node) {
|
||||
is PartialTree.IncludedLeaf -> {
|
||||
usedHashes.add(node.hash)
|
||||
node.hash
|
||||
}
|
||||
is PartialTree.Leaf -> node.hash
|
||||
is PartialTree.Node -> {
|
||||
val leftHash = rootAndUsedHashes(node.left, usedHashes)
|
||||
val rightHash = rootAndUsedHashes(node.right, usedHashes)
|
||||
return leftHash.hashConcat(rightHash)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -129,29 +152,10 @@ class PartialMerkleTree(val root: PartialTree) {
|
||||
*/
|
||||
fun verify(merkleRootHash: SecureHash, hashesToCheck: List<SecureHash>): Boolean {
|
||||
val usedHashes = ArrayList<SecureHash>()
|
||||
val verifyRoot = verify(root, usedHashes)
|
||||
val verifyRoot = rootAndUsedHashes(root, usedHashes)
|
||||
// It means that we obtained more/fewer hashes than needed or different sets of hashes.
|
||||
if (hashesToCheck.groupBy { it } != usedHashes.groupBy { it })
|
||||
return false
|
||||
return (verifyRoot == merkleRootHash)
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursive calculation of root of this partial tree.
|
||||
* Modifies usedHashes to later check for inclusion with hashes provided.
|
||||
*/
|
||||
private fun verify(node: PartialTree, usedHashes: MutableList<SecureHash>): SecureHash {
|
||||
return when (node) {
|
||||
is PartialTree.IncludedLeaf -> {
|
||||
usedHashes.add(node.hash)
|
||||
node.hash
|
||||
}
|
||||
is PartialTree.Leaf -> node.hash
|
||||
is PartialTree.Node -> {
|
||||
val leftHash = verify(node.left, usedHashes)
|
||||
val rightHash = verify(node.right, usedHashes)
|
||||
return leftHash.hashConcat(rightHash)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -34,12 +34,20 @@ sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) {
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic fun sha256(bytes: ByteArray) = SHA256(MessageDigest.getInstance("SHA-256").digest(bytes))
|
||||
@JvmStatic fun sha256Twice(bytes: ByteArray) = sha256(sha256(bytes).bytes)
|
||||
@JvmStatic fun sha256(str: String) = sha256(str.toByteArray())
|
||||
@JvmStatic
|
||||
fun sha256(bytes: ByteArray) = SHA256(MessageDigest.getInstance("SHA-256").digest(bytes))
|
||||
|
||||
@JvmStatic
|
||||
fun sha256Twice(bytes: ByteArray) = sha256(sha256(bytes).bytes)
|
||||
|
||||
@JvmStatic
|
||||
fun sha256(str: String) = sha256(str.toByteArray())
|
||||
|
||||
@JvmStatic
|
||||
fun randomSHA256() = sha256(newSecureRandom().generateSeed(32))
|
||||
|
||||
@JvmStatic fun randomSHA256() = sha256(newSecureRandom().generateSeed(32))
|
||||
val zeroHash = SecureHash.SHA256(ByteArray(32, { 0.toByte() }))
|
||||
val allOnesHash = SecureHash.SHA256(ByteArray(32, { 255.toByte() }))
|
||||
}
|
||||
|
||||
// In future, maybe SHA3, truncated hashes etc.
|
||||
|
@ -1,5 +1,6 @@
|
||||
package net.corda.core.crypto
|
||||
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.serialization.deserialize
|
||||
@ -23,8 +24,7 @@ open class SignedData<T : Any>(val raw: SerializedBytes<T>, val sig: DigitalSign
|
||||
@Throws(SignatureException::class)
|
||||
fun verified(): T {
|
||||
sig.by.verify(raw.bytes, sig)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val data = raw.deserialize<Any>() as T
|
||||
val data: T = uncheckedCast(raw.deserialize<Any>())
|
||||
verifyData(data)
|
||||
return data
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import java.util.*
|
||||
* This is similar to [DigitalSignature.WithKey], but targeted to DLT transaction signatures.
|
||||
*/
|
||||
@CordaSerializable
|
||||
class TransactionSignature(bytes: ByteArray, val by: PublicKey, val signatureMetadata: SignatureMetadata): DigitalSignature(bytes) {
|
||||
class TransactionSignature(bytes: ByteArray, val by: PublicKey, val signatureMetadata: SignatureMetadata) : DigitalSignature(bytes) {
|
||||
/**
|
||||
* Function to verify a [SignableData] object's signature.
|
||||
* Note that [SignableData] contains the id of the transaction and extra metadata, such as DLT's platform version.
|
||||
|
@ -6,13 +6,14 @@ import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.crypto.isFulfilledBy
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.excludeHostNode
|
||||
import net.corda.core.identity.groupAbstractPartyByWellKnownParty
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.UntrustworthyData
|
||||
import net.corda.core.utilities.unwrap
|
||||
import java.security.PublicKey
|
||||
|
||||
/**
|
||||
* Abstract flow to be used for replacing one state with another, for example when changing the notary of a state.
|
||||
@ -33,10 +34,8 @@ abstract class AbstractStateReplacementFlow {
|
||||
* The assembled transaction for upgrading a contract.
|
||||
*
|
||||
* @param stx signed transaction to do the upgrade.
|
||||
* @param participants the parties involved in the upgrade transaction.
|
||||
* @param myKey key
|
||||
*/
|
||||
data class UpgradeTx(val stx: SignedTransaction, val participants: Iterable<PublicKey>, val myKey: PublicKey)
|
||||
data class UpgradeTx(val stx: SignedTransaction)
|
||||
|
||||
/**
|
||||
* The [Instigator] assembles the transaction for state replacement and sends out change proposals to all participants
|
||||
@ -62,62 +61,53 @@ abstract class AbstractStateReplacementFlow {
|
||||
@Suspendable
|
||||
@Throws(StateReplacementException::class)
|
||||
override fun call(): StateAndRef<T> {
|
||||
val (stx, participantKeys, myKey) = assembleTx()
|
||||
|
||||
val (stx) = assembleTx()
|
||||
val participantSessions = getParticipantSessions()
|
||||
progressTracker.currentStep = SIGNING
|
||||
|
||||
val signatures = if (participantKeys.singleOrNull() == myKey) {
|
||||
getNotarySignatures(stx)
|
||||
} else {
|
||||
collectSignatures(participantKeys - myKey, stx)
|
||||
}
|
||||
val signatures = collectSignatures(participantSessions, stx)
|
||||
|
||||
val finalTx = stx + signatures
|
||||
serviceHub.recordTransactions(finalTx)
|
||||
|
||||
val newOutput = run {
|
||||
if (stx.isNotaryChangeTransaction()) {
|
||||
stx.resolveNotaryChangeTransaction(serviceHub).outRef<T>(0)
|
||||
} else {
|
||||
stx.tx.outRef<T>(0)
|
||||
}
|
||||
}
|
||||
|
||||
return newOutput
|
||||
return stx.resolveBaseTransaction(serviceHub).outRef<T>(0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the upgrade transaction.
|
||||
*
|
||||
* @return a triple of the transaction, the public keys of all participants, and the participating public key of
|
||||
* this node.
|
||||
* @return the transaction
|
||||
*/
|
||||
abstract protected fun assembleTx(): UpgradeTx
|
||||
|
||||
@Suspendable
|
||||
private fun collectSignatures(participants: Iterable<PublicKey>, stx: SignedTransaction): List<TransactionSignature> {
|
||||
// In identity service we record all identities we know about from network map.
|
||||
val parties = participants.map {
|
||||
serviceHub.identityService.partyFromKey(it) ?:
|
||||
throw IllegalStateException("Participant $it to state $originalState not found on the network")
|
||||
}
|
||||
/**
|
||||
* Initiate sessions with parties we want signatures from.
|
||||
*/
|
||||
open fun getParticipantSessions(): List<Pair<FlowSession, List<AbstractParty>>> {
|
||||
return excludeHostNode(serviceHub, groupAbstractPartyByWellKnownParty(serviceHub, originalState.state.data.participants)).map { initiateFlow(it.key) to it.value }
|
||||
}
|
||||
|
||||
val participantSignatures = parties.map { getParticipantSignature(it, stx) }
|
||||
@Suspendable
|
||||
private fun collectSignatures(sessions: List<Pair<FlowSession, List<AbstractParty>>>, stx: SignedTransaction): List<TransactionSignature> {
|
||||
val participantSignatures = sessions.map { getParticipantSignature(it.first, it.second, stx) }
|
||||
|
||||
val allPartySignedTx = stx + participantSignatures
|
||||
|
||||
val allSignatures = participantSignatures + getNotarySignatures(allPartySignedTx)
|
||||
parties.forEach { send(it, allSignatures) }
|
||||
sessions.forEach { it.first.send(allSignatures) }
|
||||
|
||||
return allSignatures
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun getParticipantSignature(party: Party, stx: SignedTransaction): TransactionSignature {
|
||||
private fun getParticipantSignature(session: FlowSession, party: List<AbstractParty>, stx: SignedTransaction): TransactionSignature {
|
||||
require(party.size == 1) {
|
||||
"We do not currently support multiple signatures from the same party ${session.counterparty}: $party"
|
||||
}
|
||||
val proposal = Proposal(originalState.ref, modification)
|
||||
subFlow(SendTransactionFlow(party, stx))
|
||||
return sendAndReceive<TransactionSignature>(party, proposal).unwrap {
|
||||
check(party.owningKey.isFulfilledBy(it.by)) { "Not signed by the required participant" }
|
||||
subFlow(SendTransactionFlow(session, stx))
|
||||
return session.sendAndReceive<TransactionSignature>(proposal).unwrap {
|
||||
check(party.single().owningKey.isFulfilledBy(it.by)) { "Not signed by the required participant" }
|
||||
it.verify(stx.id)
|
||||
it
|
||||
}
|
||||
@ -136,9 +126,10 @@ abstract class AbstractStateReplacementFlow {
|
||||
|
||||
// Type parameter should ideally be Unit but that prevents Java code from subclassing it (https://youtrack.jetbrains.com/issue/KT-15964).
|
||||
// We use Void? instead of Unit? as that's what you'd use in Java.
|
||||
abstract class Acceptor<in T>(val otherSide: Party,
|
||||
abstract class Acceptor<in T>(val initiatingSession: FlowSession,
|
||||
override val progressTracker: ProgressTracker = Acceptor.tracker()) : FlowLogic<Void?>() {
|
||||
constructor(otherSide: Party) : this(otherSide, Acceptor.tracker())
|
||||
constructor(initiatingSession: FlowSession) : this(initiatingSession, Acceptor.tracker())
|
||||
|
||||
companion object {
|
||||
object VERIFYING : ProgressTracker.Step("Verifying state replacement proposal")
|
||||
object APPROVING : ProgressTracker.Step("State replacement approved")
|
||||
@ -151,9 +142,9 @@ abstract class AbstractStateReplacementFlow {
|
||||
override fun call(): Void? {
|
||||
progressTracker.currentStep = VERIFYING
|
||||
// We expect stx to have insufficient signatures here
|
||||
val stx = subFlow(ReceiveTransactionFlow(otherSide, checkSufficientSignatures = false))
|
||||
val stx = subFlow(ReceiveTransactionFlow(initiatingSession, checkSufficientSignatures = false))
|
||||
checkMySignatureRequired(stx)
|
||||
val maybeProposal: UntrustworthyData<Proposal<T>> = receive(otherSide)
|
||||
val maybeProposal: UntrustworthyData<Proposal<T>> = initiatingSession.receive()
|
||||
maybeProposal.unwrap {
|
||||
verifyProposal(stx, it)
|
||||
}
|
||||
@ -166,7 +157,7 @@ abstract class AbstractStateReplacementFlow {
|
||||
progressTracker.currentStep = APPROVING
|
||||
|
||||
val mySignature = sign(stx)
|
||||
val swapSignatures = sendAndReceive<List<TransactionSignature>>(otherSide, mySignature)
|
||||
val swapSignatures = initiatingSession.sendAndReceive<List<TransactionSignature>>(mySignature)
|
||||
|
||||
// TODO: This step should not be necessary, as signatures are re-checked in verifyRequiredSignatures.
|
||||
val allSignatures = swapSignatures.unwrap { signatures ->
|
||||
@ -175,11 +166,7 @@ abstract class AbstractStateReplacementFlow {
|
||||
}
|
||||
|
||||
val finalTx = stx + allSignatures
|
||||
if (finalTx.isNotaryChangeTransaction()) {
|
||||
finalTx.resolveNotaryChangeTransaction(serviceHub).verifyRequiredSignatures()
|
||||
} else {
|
||||
finalTx.verifyRequiredSignatures()
|
||||
}
|
||||
finalTx.resolveTransactionWithSignatures(serviceHub).verifyRequiredSignatures()
|
||||
serviceHub.recordTransactions(finalTx)
|
||||
}
|
||||
|
||||
@ -196,11 +183,7 @@ abstract class AbstractStateReplacementFlow {
|
||||
// TODO Check the set of multiple identities?
|
||||
val myKey = ourIdentity.owningKey
|
||||
|
||||
val requiredKeys = if (stx.isNotaryChangeTransaction()) {
|
||||
stx.resolveNotaryChangeTransaction(serviceHub).requiredSigningKeys
|
||||
} else {
|
||||
stx.tx.requiredSigningKeys
|
||||
}
|
||||
val requiredKeys = stx.resolveTransactionWithSignatures(serviceHub).requiredSigningKeys
|
||||
|
||||
require(myKey in requiredKeys) { "Party is not a participant for any of the input states of transaction ${stx.id}" }
|
||||
}
|
||||
|
@ -1,28 +0,0 @@
|
||||
package net.corda.core.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.NonEmptySet
|
||||
|
||||
/**
|
||||
* Notify the specified parties about a transaction. The remote peers will download this transaction and its
|
||||
* dependency graph, verifying them all. The flow returns when all peers have acknowledged the transactions
|
||||
* as valid. Normally you wouldn't use this directly, it would be called via [FinalityFlow].
|
||||
*
|
||||
* @param notarisedTransaction transaction which has been notarised (if needed) and is ready to notify nodes about.
|
||||
* @param participants a list of participants involved in the transaction.
|
||||
* @return a list of participants who were successfully notified of the transaction.
|
||||
*/
|
||||
@InitiatingFlow
|
||||
class BroadcastTransactionFlow(val notarisedTransaction: SignedTransaction,
|
||||
val participants: NonEmptySet<Party>) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
// TODO: Messaging layer should handle this broadcast for us
|
||||
participants.filter { it !in serviceHub.myInfo.legalIdentities }.forEach { participant ->
|
||||
// SendTransactionFlow allows otherParty to access our data to resolve the transaction.
|
||||
subFlow(SendTransactionFlow(participant, notarisedTransaction))
|
||||
}
|
||||
}
|
||||
}
|
@ -3,9 +3,8 @@ package net.corda.core.flows
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.crypto.isFulfilledBy
|
||||
import net.corda.core.utilities.toBase58String
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.identity.groupPublicKeysByWellKnownParty
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
@ -57,21 +56,26 @@ import java.security.PublicKey
|
||||
* val stx = subFlow(CollectSignaturesFlow(ptx))
|
||||
*
|
||||
* @param partiallySignedTx Transaction to collect the remaining signatures for
|
||||
* @param sessionsToCollectFrom A session for every party we need to collect a signature from. Must be an exact match.
|
||||
* @param myOptionalKeys set of keys in the transaction which are owned by this node. This includes keys used on commands, not
|
||||
* just in the states. If null, the default well known identity of the node is used.
|
||||
*/
|
||||
// TODO: AbstractStateReplacementFlow needs updating to use this flow.
|
||||
class CollectSignaturesFlow @JvmOverloads constructor (val partiallySignedTx: SignedTransaction,
|
||||
val myOptionalKeys: Iterable<PublicKey>?,
|
||||
override val progressTracker: ProgressTracker = CollectSignaturesFlow.tracker()) : FlowLogic<SignedTransaction>() {
|
||||
@JvmOverloads constructor(partiallySignedTx: SignedTransaction, progressTracker: ProgressTracker = CollectSignaturesFlow.tracker()) : this(partiallySignedTx, null, progressTracker)
|
||||
class CollectSignaturesFlow @JvmOverloads constructor(val partiallySignedTx: SignedTransaction,
|
||||
val sessionsToCollectFrom: Collection<FlowSession>,
|
||||
val myOptionalKeys: Iterable<PublicKey>?,
|
||||
override val progressTracker: ProgressTracker = CollectSignaturesFlow.tracker()) : FlowLogic<SignedTransaction>() {
|
||||
@JvmOverloads constructor(partiallySignedTx: SignedTransaction, sessionsToCollectFrom: Collection<FlowSession>, progressTracker: ProgressTracker = CollectSignaturesFlow.tracker()) : this(partiallySignedTx, sessionsToCollectFrom, null, progressTracker)
|
||||
|
||||
companion object {
|
||||
object COLLECTING : ProgressTracker.Step("Collecting signatures from counter-parties.")
|
||||
object VERIFYING : ProgressTracker.Step("Verifying collected signatures.")
|
||||
|
||||
@JvmStatic
|
||||
fun tracker() = ProgressTracker(COLLECTING, VERIFYING)
|
||||
|
||||
// TODO: Make the progress tracker adapt to the number of counter-parties to collect from.
|
||||
|
||||
}
|
||||
|
||||
@Suspendable override fun call(): SignedTransaction {
|
||||
@ -100,8 +104,15 @@ class CollectSignaturesFlow @JvmOverloads constructor (val partiallySignedTx: Si
|
||||
// If the unsigned counter-parties list is empty then we don't need to collect any more signatures here.
|
||||
if (unsigned.isEmpty()) return partiallySignedTx
|
||||
|
||||
val partyToKeysMap = groupPublicKeysByWellKnownParty(serviceHub, unsigned)
|
||||
// Check that we have a session for all parties. No more, no less.
|
||||
require(sessionsToCollectFrom.map { it.counterparty }.toSet() == partyToKeysMap.keys) {
|
||||
"The Initiator of CollectSignaturesFlow must pass in exactly the sessions required to sign the transaction."
|
||||
}
|
||||
// Collect signatures from all counter-parties and append them to the partially signed transaction.
|
||||
val counterpartySignatures = keysToParties(unsigned).map { collectSignature(it.first, it.second) }
|
||||
val counterpartySignatures = sessionsToCollectFrom.flatMap { session ->
|
||||
subFlow(CollectSignatureFlow(partiallySignedTx, session, partyToKeysMap[session.counterparty]!!))
|
||||
}
|
||||
val stx = partiallySignedTx + counterpartySignatures
|
||||
|
||||
// Verify all but the notary's signature if the transaction requires a notary, otherwise verify all signatures.
|
||||
@ -110,40 +121,39 @@ class CollectSignaturesFlow @JvmOverloads constructor (val partiallySignedTx: Si
|
||||
|
||||
return stx
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup the [Party] object for each [PublicKey] using the [ServiceHub.identityService].
|
||||
*
|
||||
* @return a pair of the well known identity to contact for a signature, and the public key that party should sign
|
||||
* with (this may belong to a confidential identity).
|
||||
*/
|
||||
@Suspendable private fun keysToParties(keys: Collection<PublicKey>): List<Pair<Party, PublicKey>> = keys.map {
|
||||
val party = serviceHub.identityService.partyFromAnonymous(AnonymousParty(it))
|
||||
?: throw IllegalStateException("Party ${it.toBase58String()} not found on the network.")
|
||||
Pair(party, it)
|
||||
}
|
||||
// DOCSTART 1
|
||||
/**
|
||||
* Get and check the required signature.
|
||||
*
|
||||
* @param partiallySignedTx the transaction to sign.
|
||||
* @param session the [FlowSession] to connect to to get the signature.
|
||||
* @param signingKeys the list of keys the party should use to sign the transaction.
|
||||
*/
|
||||
@Suspendable
|
||||
class CollectSignatureFlow(val partiallySignedTx: SignedTransaction, val session: FlowSession, val signingKeys: List<PublicKey>) : FlowLogic<List<TransactionSignature>>() {
|
||||
constructor(partiallySignedTx: SignedTransaction, session: FlowSession, vararg signingKeys: PublicKey) :
|
||||
this(partiallySignedTx, session, listOf(*signingKeys))
|
||||
|
||||
// DOCSTART 1
|
||||
/**
|
||||
* Get and check the required signature.
|
||||
*
|
||||
* @param counterparty the party to request a signature from.
|
||||
* @param signingKey the key the party should use to sign the transaction.
|
||||
*/
|
||||
@Suspendable private fun collectSignature(counterparty: Party, signingKey: PublicKey): TransactionSignature {
|
||||
@Suspendable
|
||||
override fun call(): List<TransactionSignature> {
|
||||
// SendTransactionFlow allows counterparty to access our data to resolve the transaction.
|
||||
subFlow(SendTransactionFlow(counterparty, partiallySignedTx))
|
||||
subFlow(SendTransactionFlow(session, partiallySignedTx))
|
||||
// Send the key we expect the counterparty to sign with - this is important where they may have several
|
||||
// keys to sign with, as it makes it faster for them to identify the key to sign with, and more straight forward
|
||||
// for us to check we have the expected signature returned.
|
||||
send(counterparty, signingKey)
|
||||
return receive<TransactionSignature>(counterparty).unwrap {
|
||||
require(signingKey.isFulfilledBy(it.by)) { "Not signed by the required signing key." }
|
||||
it
|
||||
session.send(signingKeys)
|
||||
return session.receive<List<TransactionSignature>>().unwrap { signatures ->
|
||||
require(signatures.size == signingKeys.size) { "Need signature for each signing key" }
|
||||
signatures.forEachIndexed { index, signature ->
|
||||
require(signingKeys[index].isFulfilledBy(signature.by)) { "Not signed by the required signing key." }
|
||||
}
|
||||
signatures
|
||||
}
|
||||
}
|
||||
// DOCEND 1
|
||||
}
|
||||
// DOCEND 1
|
||||
|
||||
/**
|
||||
* The [SignTransactionFlow] should be called in response to the [CollectSignaturesFlow]. It automates the signing of
|
||||
@ -159,15 +169,15 @@ class CollectSignaturesFlow @JvmOverloads constructor (val partiallySignedTx: Si
|
||||
* - Subclass [SignTransactionFlow] - this can be done inside an existing flow (as shown below)
|
||||
* - Override the [checkTransaction] method to add some custom verification logic
|
||||
* - Call the flow via [FlowLogic.subFlow]
|
||||
* - The flow returns the fully signed transaction once it has been committed to the ledger
|
||||
* - The flow returns the transaction signed with the additional signature.
|
||||
*
|
||||
* Example - checking and signing a transaction involving a [net.corda.core.contracts.DummyContract], see
|
||||
* CollectSignaturesFlowTests.kt for further examples:
|
||||
*
|
||||
* class Responder(val otherParty: Party): FlowLogic<SignedTransaction>() {
|
||||
* class Responder(val otherPartySession: FlowSession): FlowLogic<SignedTransaction>() {
|
||||
* @Suspendable override fun call(): SignedTransaction {
|
||||
* // [SignTransactionFlow] sub-classed as a singleton object.
|
||||
* val flow = object : SignTransactionFlow(otherParty) {
|
||||
* val flow = object : SignTransactionFlow(otherPartySession) {
|
||||
* @Suspendable override fun checkTransaction(stx: SignedTransaction) = requireThat {
|
||||
* val tx = stx.tx
|
||||
* val magicNumberState = tx.outputs.single().data as DummyContract.MultiOwnerState
|
||||
@ -182,9 +192,9 @@ class CollectSignaturesFlow @JvmOverloads constructor (val partiallySignedTx: Si
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @param otherParty The counter-party which is providing you a transaction to sign.
|
||||
* @param otherSideSession The session which is providing you a transaction to sign.
|
||||
*/
|
||||
abstract class SignTransactionFlow(val otherParty: Party,
|
||||
abstract class SignTransactionFlow(val otherSideSession: FlowSession,
|
||||
override val progressTracker: ProgressTracker = SignTransactionFlow.tracker()) : FlowLogic<SignedTransaction>() {
|
||||
|
||||
companion object {
|
||||
@ -192,30 +202,31 @@ abstract class SignTransactionFlow(val otherParty: Party,
|
||||
object VERIFYING : ProgressTracker.Step("Verifying transaction proposal.")
|
||||
object SIGNING : ProgressTracker.Step("Signing transaction proposal.")
|
||||
|
||||
@JvmStatic
|
||||
fun tracker() = ProgressTracker(RECEIVING, VERIFYING, SIGNING)
|
||||
}
|
||||
|
||||
@Suspendable override fun call(): SignedTransaction {
|
||||
progressTracker.currentStep = RECEIVING
|
||||
// Receive transaction and resolve dependencies, check sufficient signatures is disabled as we don't have all signatures.
|
||||
val stx = subFlow(ReceiveTransactionFlow(otherParty, checkSufficientSignatures = false))
|
||||
val stx = subFlow(ReceiveTransactionFlow(otherSideSession, checkSufficientSignatures = false))
|
||||
// Receive the signing key that the party requesting the signature expects us to sign with. Having this provided
|
||||
// means we only have to check we own that one key, rather than matching all keys in the transaction against all
|
||||
// keys we own.
|
||||
val signingKey = receive<PublicKey>(otherParty).unwrap {
|
||||
val signingKeys = otherSideSession.receive<List<PublicKey>>().unwrap { keys ->
|
||||
// TODO: We should have a faster way of verifying we own a single key
|
||||
serviceHub.keyManagementService.filterMyKeys(listOf(it)).single()
|
||||
serviceHub.keyManagementService.filterMyKeys(keys)
|
||||
}
|
||||
progressTracker.currentStep = VERIFYING
|
||||
// Check that the Responder actually needs to sign.
|
||||
checkMySignatureRequired(stx, signingKey)
|
||||
checkMySignaturesRequired(stx, signingKeys)
|
||||
// Check the signatures which have already been provided. Usually the Initiators and possibly an Oracle's.
|
||||
checkSignatures(stx)
|
||||
stx.tx.toLedgerTransaction(serviceHub).verify()
|
||||
// Perform some custom verification over the transaction.
|
||||
try {
|
||||
checkTransaction(stx)
|
||||
} catch(e: Exception) {
|
||||
} catch (e: Exception) {
|
||||
if (e is IllegalStateException || e is IllegalArgumentException || e is AssertionError)
|
||||
throw FlowException(e)
|
||||
else
|
||||
@ -223,18 +234,19 @@ abstract class SignTransactionFlow(val otherParty: Party,
|
||||
}
|
||||
// Sign and send back our signature to the Initiator.
|
||||
progressTracker.currentStep = SIGNING
|
||||
val mySignature = serviceHub.createSignature(stx, signingKey)
|
||||
send(otherParty, mySignature)
|
||||
val mySignatures = signingKeys.map { key ->
|
||||
serviceHub.createSignature(stx, key)
|
||||
}
|
||||
otherSideSession.send(mySignatures)
|
||||
|
||||
// Return the fully signed transaction once it has been committed.
|
||||
return waitForLedgerCommit(stx.id)
|
||||
// Return the additionally signed transaction.
|
||||
return stx + mySignatures
|
||||
}
|
||||
|
||||
@Suspendable private fun checkSignatures(stx: SignedTransaction) {
|
||||
val signingIdentities = stx.sigs.map(TransactionSignature::by).mapNotNull(serviceHub.identityService::partyFromKey)
|
||||
val signingWellKnownIdentities = signingIdentities.mapNotNull(serviceHub.identityService::partyFromAnonymous)
|
||||
require(otherParty in signingWellKnownIdentities) {
|
||||
"The Initiator of CollectSignaturesFlow must have signed the transaction. Found ${signingWellKnownIdentities}, expected ${otherParty}"
|
||||
val signingWellKnownIdentities = groupPublicKeysByWellKnownParty(serviceHub, stx.sigs.map(TransactionSignature::by))
|
||||
require(otherSideSession.counterparty in signingWellKnownIdentities) {
|
||||
"The Initiator of CollectSignaturesFlow must have signed the transaction. Found ${signingWellKnownIdentities}, expected ${otherSideSession}"
|
||||
}
|
||||
val signed = stx.sigs.map { it.by }
|
||||
val allSigners = stx.tx.requiredSigningKeys
|
||||
@ -266,9 +278,9 @@ abstract class SignTransactionFlow(val otherParty: Party,
|
||||
@Throws(FlowException::class)
|
||||
abstract protected fun checkTransaction(stx: SignedTransaction)
|
||||
|
||||
@Suspendable private fun checkMySignatureRequired(stx: SignedTransaction, signingKey: PublicKey) {
|
||||
require(signingKey in stx.tx.requiredSigningKeys) {
|
||||
"Party is not a participant for any of the input states of transaction ${stx.id}"
|
||||
@Suspendable private fun checkMySignaturesRequired(stx: SignedTransaction, signingKeys: Iterable<PublicKey>) {
|
||||
require(signingKeys.all { it in stx.tx.requiredSigningKeys }) {
|
||||
"A signature was requested for a key that isn't part of the required signing keys for transaction ${stx.id}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,11 +2,7 @@ package net.corda.core.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import java.security.PublicKey
|
||||
import net.corda.core.internal.ContractUpgradeUtils
|
||||
|
||||
/**
|
||||
* A flow to be used for authorising and upgrading state objects of an old contract to a new contract.
|
||||
@ -17,18 +13,22 @@ import java.security.PublicKey
|
||||
* use the new updated state for future transactions.
|
||||
*/
|
||||
object ContractUpgradeFlow {
|
||||
|
||||
/**
|
||||
* Authorise a contract state upgrade.
|
||||
* This will store the upgrade authorisation in persistent store, and will be queried by [ContractUpgradeFlow.Acceptor] during contract upgrade process.
|
||||
* Invoking this flow indicates the node is willing to upgrade the [StateAndRef] using the [UpgradedContract] class.
|
||||
* This method will NOT initiate the upgrade process. To start the upgrade process, see [Initiator].
|
||||
*
|
||||
* This will store the upgrade authorisation in persistent store, and will be queried by [ContractUpgradeFlow.Acceptor]
|
||||
* during contract upgrade process. Invoking this flow indicates the node is willing to upgrade the [StateAndRef] using
|
||||
* the [UpgradedContract] class.
|
||||
*
|
||||
* This flow will NOT initiate the upgrade process. To start the upgrade process, see [Initiate].
|
||||
*/
|
||||
// DOCSTART 1
|
||||
@StartableByRPC
|
||||
class Authorise(
|
||||
val stateAndRef: StateAndRef<*>,
|
||||
private val upgradedContractClass: Class<out UpgradedContract<*, *>>
|
||||
) : FlowLogic<Void?>() {
|
||||
) : FlowLogic<Void?>() {
|
||||
// DOCEND 1
|
||||
@Suspendable
|
||||
override fun call(): Void? {
|
||||
val upgrade = upgradedContractClass.newInstance()
|
||||
@ -45,102 +45,35 @@ object ContractUpgradeFlow {
|
||||
* Deauthorise a contract state upgrade.
|
||||
* This will remove the upgrade authorisation from persistent store (and prevent any further upgrade)
|
||||
*/
|
||||
// DOCSTART 2
|
||||
@StartableByRPC
|
||||
class Deauthorise(
|
||||
val stateRef: StateRef
|
||||
) : FlowLogic< Void?>() {
|
||||
class Deauthorise(val stateRef: StateRef) : FlowLogic<Void?>() {
|
||||
@Suspendable
|
||||
override fun call(): Void? {
|
||||
//DOCEND 2
|
||||
serviceHub.contractUpgradeService.removeAuthorisedContractUpgrade(stateRef)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This flow begins the contract upgrade process.
|
||||
*/
|
||||
@InitiatingFlow
|
||||
@StartableByRPC
|
||||
class Initiator<OldState : ContractState, out NewState : ContractState>(
|
||||
class Initiate<OldState : ContractState, out NewState : ContractState>(
|
||||
originalState: StateAndRef<OldState>,
|
||||
newContractClass: Class<out UpgradedContract<OldState, NewState>>
|
||||
) : AbstractStateReplacementFlow.Instigator<OldState, NewState, Class<out UpgradedContract<OldState, NewState>>>(originalState, newContractClass) {
|
||||
|
||||
companion object {
|
||||
fun <OldState : ContractState, NewState : ContractState> assembleBareTx(
|
||||
stateRef: StateAndRef<OldState>,
|
||||
upgradedContractClass: Class<out UpgradedContract<OldState, NewState>>,
|
||||
privacySalt: PrivacySalt
|
||||
): TransactionBuilder {
|
||||
val contractUpgrade = upgradedContractClass.newInstance()
|
||||
return TransactionBuilder(stateRef.state.notary)
|
||||
.withItems(
|
||||
stateRef,
|
||||
StateAndContract(contractUpgrade.upgrade(stateRef.state.data), upgradedContractClass.name),
|
||||
Command(UpgradeCommand(upgradedContractClass.name), stateRef.state.data.participants.map { it.owningKey }),
|
||||
privacySalt
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun assembleTx(): AbstractStateReplacementFlow.UpgradeTx {
|
||||
val baseTx = assembleBareTx(originalState, modification, PrivacySalt())
|
||||
val baseTx = ContractUpgradeUtils.assembleBareTx(originalState, modification, PrivacySalt())
|
||||
val participantKeys = originalState.state.data.participants.map { it.owningKey }.toSet()
|
||||
// TODO: We need a much faster way of finding our key in the transaction
|
||||
val myKey = serviceHub.keyManagementService.filterMyKeys(participantKeys).single()
|
||||
val stx = serviceHub.signInitialTransaction(baseTx, myKey)
|
||||
return AbstractStateReplacementFlow.UpgradeTx(stx, participantKeys, myKey)
|
||||
}
|
||||
}
|
||||
|
||||
@StartableByRPC
|
||||
@InitiatedBy(ContractUpgradeFlow.Initiator::class)
|
||||
class Acceptor(otherSide: Party) : AbstractStateReplacementFlow.Acceptor<Class<out UpgradedContract<ContractState, *>>>(otherSide) {
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun verify(tx: LedgerTransaction) {
|
||||
// Contract Upgrade transaction should have 1 input, 1 output and 1 command.
|
||||
verify(tx.inputs.single().state,
|
||||
tx.outputs.single(),
|
||||
tx.commandsOfType<UpgradeCommand>().single())
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun verify(input: TransactionState<ContractState>, output: TransactionState<ContractState>, commandData: Command<UpgradeCommand>) {
|
||||
val command = commandData.value
|
||||
val participantKeys: Set<PublicKey> = input.data.participants.map { it.owningKey }.toSet()
|
||||
val keysThatSigned: Set<PublicKey> = commandData.signers.toSet()
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val upgradedContract = javaClass.classLoader.loadClass(command.upgradedContractClass).newInstance() as UpgradedContract<ContractState, *>
|
||||
requireThat {
|
||||
"The signing keys include all participant keys" using keysThatSigned.containsAll(participantKeys)
|
||||
"Inputs state reference the legacy contract" using (input.contract == upgradedContract.legacyContract)
|
||||
"Outputs state reference the upgraded contract" using (output.contract == command.upgradedContractClass)
|
||||
"Output state must be an upgraded version of the input state" using (output.data == upgradedContract.upgrade(input.data))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
@Throws(StateReplacementException::class)
|
||||
override fun verifyProposal(stx: SignedTransaction, proposal: AbstractStateReplacementFlow.Proposal<Class<out UpgradedContract<ContractState, *>>>) {
|
||||
// Retrieve signed transaction from our side, we will apply the upgrade logic to the transaction on our side, and
|
||||
// verify outputs matches the proposed upgrade.
|
||||
val ourSTX = serviceHub.validatedTransactions.getTransaction(proposal.stateRef.txhash)
|
||||
requireNotNull(ourSTX) { "We don't have a copy of the referenced state" }
|
||||
val oldStateAndRef = ourSTX!!.tx.outRef<ContractState>(proposal.stateRef.index)
|
||||
val authorisedUpgrade = serviceHub.contractUpgradeService.getAuthorisedContractUpgrade(oldStateAndRef.ref) ?:
|
||||
throw IllegalStateException("Contract state upgrade is unauthorised. State hash : ${oldStateAndRef.ref}")
|
||||
val proposedTx = stx.tx
|
||||
val expectedTx = ContractUpgradeFlow.Initiator.assembleBareTx(oldStateAndRef, proposal.modification, proposedTx.privacySalt).toWireTransaction()
|
||||
requireThat {
|
||||
"The instigator is one of the participants" using (otherSide in oldStateAndRef.state.data.participants)
|
||||
"The proposed upgrade ${proposal.modification.javaClass} is a trusted upgrade path" using (proposal.modification.name == authorisedUpgrade)
|
||||
"The proposed tx matches the expected tx for this upgrade" using (proposedTx == expectedTx)
|
||||
}
|
||||
ContractUpgradeFlow.Acceptor.verify(
|
||||
oldStateAndRef.state,
|
||||
expectedTx.outRef<ContractState>(0).state,
|
||||
expectedTx.toLedgerTransaction(serviceHub).commandsOfType<UpgradeCommand>().single())
|
||||
return AbstractStateReplacementFlow.UpgradeTx(stx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,46 +1,36 @@
|
||||
package net.corda.core.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.TransactionState
|
||||
import net.corda.core.crypto.isFulfilledBy
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.ResolveTransactionsFlow
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.identity.groupAbstractPartyByWellKnownParty
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.NonEmptySet
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.toNonEmptySet
|
||||
|
||||
/**
|
||||
* Verifies the given transactions, then sends them to the named notary. If the notary agrees that the transactions
|
||||
* are acceptable then they are from that point onwards committed to the ledger, and will be written through to the
|
||||
* vault. Additionally they will be distributed to the parties reflected in the participants list of the states.
|
||||
* Verifies the given transaction, then sends it to the named notary. If the notary agrees that the transaction
|
||||
* is acceptable then it is from that point onwards committed to the ledger, and will be written through to the
|
||||
* vault. Additionally it will be distributed to the parties reflected in the participants list of the states.
|
||||
*
|
||||
* The transactions will be topologically sorted before commitment to ensure that dependencies are committed before
|
||||
* dependers, so you don't need to do this yourself.
|
||||
* The transaction is expected to have already been resolved: if its dependencies are not available in local
|
||||
* storage, verification will fail. It must have signatures from all necessary parties other than the notary.
|
||||
*
|
||||
* The transactions are expected to have already been resolved: if their dependencies are not available in local
|
||||
* storage or within the given set, verification will fail. They must have signatures from all necessary parties
|
||||
* other than the notary.
|
||||
* If specified, the extra recipients are sent the given transaction. The base set of parties to inform are calculated
|
||||
* from the contract-given set of participants.
|
||||
*
|
||||
* If specified, the extra recipients are sent all the given transactions. The base set of parties to inform of each
|
||||
* transaction are calculated on a per transaction basis from the contract-given set of participants.
|
||||
* The flow returns the same transaction but with the additional signatures from the notary.
|
||||
*
|
||||
* The flow returns the same transactions, in the same order, with the additional signatures.
|
||||
*
|
||||
* @param transactions What to commit.
|
||||
* @param transaction What to commit.
|
||||
* @param extraRecipients A list of additional participants to inform of the transaction.
|
||||
*/
|
||||
open class FinalityFlow(val transactions: Iterable<SignedTransaction>,
|
||||
private val extraRecipients: Set<Party>,
|
||||
override val progressTracker: ProgressTracker) : FlowLogic<List<SignedTransaction>>() {
|
||||
constructor(transaction: SignedTransaction, extraParticipants: Set<Party>) : this(listOf(transaction), extraParticipants, tracker())
|
||||
constructor(transaction: SignedTransaction) : this(listOf(transaction), emptySet(), tracker())
|
||||
constructor(transaction: SignedTransaction, progressTracker: ProgressTracker) : this(listOf(transaction), emptySet(), progressTracker)
|
||||
@InitiatingFlow
|
||||
class FinalityFlow(val transaction: SignedTransaction,
|
||||
private val extraRecipients: Set<Party>,
|
||||
override val progressTracker: ProgressTracker) : FlowLogic<SignedTransaction>() {
|
||||
constructor(transaction: SignedTransaction, extraParticipants: Set<Party>) : this(transaction, extraParticipants, tracker())
|
||||
constructor(transaction: SignedTransaction) : this(transaction, emptySet(), tracker())
|
||||
constructor(transaction: SignedTransaction, progressTracker: ProgressTracker) : this(transaction, emptySet(), progressTracker)
|
||||
|
||||
companion object {
|
||||
object NOTARISING : ProgressTracker.Step("Requesting signature by notary service") {
|
||||
@ -49,52 +39,41 @@ open class FinalityFlow(val transactions: Iterable<SignedTransaction>,
|
||||
|
||||
object BROADCASTING : ProgressTracker.Step("Broadcasting transaction to participants")
|
||||
|
||||
// TODO: Make all tracker() methods @JvmStatic
|
||||
@JvmStatic
|
||||
fun tracker() = ProgressTracker(NOTARISING, BROADCASTING)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
@Throws(NotaryException::class)
|
||||
override fun call(): List<SignedTransaction> {
|
||||
override fun call(): SignedTransaction {
|
||||
// Note: this method is carefully broken up to minimize the amount of data reachable from the stack at
|
||||
// the point where subFlow is invoked, as that minimizes the checkpointing work to be done.
|
||||
//
|
||||
// Lookup the resolved transactions and use them to map each signed transaction to the list of participants.
|
||||
// Then send to the notary if needed, record locally and distribute.
|
||||
val parties = getPartiesToSend(verifyTx())
|
||||
progressTracker.currentStep = NOTARISING
|
||||
val notarisedTxns: List<Pair<SignedTransaction, List<Party>>> = resolveDependenciesOf(transactions)
|
||||
.map { (stx, ltx) -> Pair(notariseAndRecord(stx), lookupParties(ltx)) }
|
||||
val notarised = notariseAndRecord()
|
||||
|
||||
// Each transaction has its own set of recipients, but extra recipients get them all.
|
||||
progressTracker.currentStep = BROADCASTING
|
||||
for ((stx, parties) in notarisedTxns) {
|
||||
val participants = (parties + extraRecipients).filter { it != ourIdentity.party }.toSet()
|
||||
if (participants.isNotEmpty()) {
|
||||
broadcastTransaction(stx, participants.toNonEmptySet())
|
||||
for (party in parties) {
|
||||
if (!serviceHub.myInfo.isLegalIdentity(party)) {
|
||||
val session = initiateFlow(party)
|
||||
subFlow(SendTransactionFlow(session, notarised))
|
||||
}
|
||||
}
|
||||
return notarisedTxns.map { it.first }
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast a transaction to the participants. By default calls [BroadcastTransactionFlow], however can be
|
||||
* overridden for more complex transaction delivery protocols (for example where not all parties know each other).
|
||||
*
|
||||
* @param participants the participants to send the transaction to. This is expected to include extra participants
|
||||
* and exclude the local node.
|
||||
*/
|
||||
@Suspendable
|
||||
open protected fun broadcastTransaction(stx: SignedTransaction, participants: NonEmptySet<Party>) {
|
||||
subFlow(BroadcastTransactionFlow(stx, participants))
|
||||
return notarised
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun notariseAndRecord(stx: SignedTransaction): SignedTransaction {
|
||||
val notarised = if (needsNotarySignature(stx)) {
|
||||
val notarySignatures = subFlow(NotaryFlow.Client(stx))
|
||||
stx + notarySignatures
|
||||
private fun notariseAndRecord(): SignedTransaction {
|
||||
val notarised = if (needsNotarySignature(transaction)) {
|
||||
val notarySignatures = subFlow(NotaryFlow.Client(transaction))
|
||||
transaction + notarySignatures
|
||||
} else {
|
||||
stx
|
||||
transaction
|
||||
}
|
||||
serviceHub.recordTransactions(notarised)
|
||||
return notarised
|
||||
@ -109,50 +88,20 @@ open class FinalityFlow(val transactions: Iterable<SignedTransaction>,
|
||||
private fun hasNoNotarySignature(stx: SignedTransaction): Boolean {
|
||||
val notaryKey = stx.tx.notary?.owningKey
|
||||
val signers = stx.sigs.map { it.by }.toSet()
|
||||
return !(notaryKey?.isFulfilledBy(signers) ?: false)
|
||||
return notaryKey?.isFulfilledBy(signers) != true
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the parties involved in a transaction.
|
||||
*
|
||||
* The default implementation throws an exception if an unknown party is encountered.
|
||||
*/
|
||||
open protected fun lookupParties(ltx: LedgerTransaction): List<Party> {
|
||||
// Calculate who is meant to see the results based on the participants involved.
|
||||
return extractParticipants(ltx).map {
|
||||
serviceHub.identityService.partyFromAnonymous(it)
|
||||
?: throw IllegalArgumentException("Could not resolve well known identity of participant $it")
|
||||
}
|
||||
private fun getPartiesToSend(ltx: LedgerTransaction): Set<Party> {
|
||||
val participants = ltx.outputStates.flatMap { it.participants } + ltx.inputStates.flatMap { it.participants }
|
||||
return groupAbstractPartyByWellKnownParty(serviceHub, participants).keys + extraRecipients
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to extract all participants from a ledger transaction. Intended to help implement [lookupParties]
|
||||
* overriding functions.
|
||||
*/
|
||||
protected fun extractParticipants(ltx: LedgerTransaction): List<AbstractParty> {
|
||||
return ltx.outputStates.flatMap { it.participants } + ltx.inputStates.flatMap { it.participants }
|
||||
}
|
||||
|
||||
private fun resolveDependenciesOf(signedTransactions: Iterable<SignedTransaction>): List<Pair<SignedTransaction, LedgerTransaction>> {
|
||||
// Make sure the dependencies come before the dependers.
|
||||
val sorted = ResolveTransactionsFlow.topologicalSort(signedTransactions.toList())
|
||||
// Build a ServiceHub that consults the argument list as well as what's in local tx storage so uncommitted
|
||||
// transactions can depend on each other.
|
||||
val augmentedLookup = object : ServiceHub by serviceHub {
|
||||
val hashToTx = sorted.associateBy { it.id }
|
||||
override fun loadState(stateRef: StateRef): TransactionState<*> {
|
||||
val provided: TransactionState<ContractState>? = hashToTx[stateRef.txhash]?.let { it.tx.outputs[stateRef.index] }
|
||||
return provided ?: super.loadState(stateRef)
|
||||
}
|
||||
}
|
||||
// Load and verify each transaction.
|
||||
return sorted.map { stx ->
|
||||
val notary = stx.tx.notary
|
||||
// The notary signature(s) are allowed to be missing but no others.
|
||||
if (notary != null) stx.verifySignaturesExcept(notary.owningKey) else stx.verifyRequiredSignatures()
|
||||
val ltx = stx.toLedgerTransaction(augmentedLookup, false)
|
||||
ltx.verify()
|
||||
stx to ltx
|
||||
}
|
||||
private fun verifyTx(): LedgerTransaction {
|
||||
val notary = transaction.tx.notary
|
||||
// The notary signature(s) are allowed to be missing but no others.
|
||||
if (notary != null) transaction.verifySignaturesExcept(notary.owningKey) else transaction.verifyRequiredSignatures()
|
||||
val ltx = transaction.toLedgerTransaction(serviceHub, false)
|
||||
ltx.verify()
|
||||
return ltx
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ import net.corda.core.CordaRuntimeException
|
||||
/**
|
||||
* Exception which can be thrown by a [FlowLogic] at any point in its logic to unexpectedly bring it to a permanent end.
|
||||
* The exception will propagate to all counterparty flows and will be thrown on their end the next time they wait on a
|
||||
* [FlowLogic.receive] or [FlowLogic.sendAndReceive]. Any flow which no longer needs to do a receive, or has already ended,
|
||||
* [FlowSession.receive] or [FlowSession.sendAndReceive]. Any flow which no longer needs to do a receive, or has already ended,
|
||||
* will not receive the exception (if this is required then have them wait for a confirmation message).
|
||||
*
|
||||
* [FlowException] (or a subclass) can be a valid expected response from a flow, particularly ones which act as a service.
|
||||
|
@ -16,14 +16,22 @@ sealed class FlowInitiator : Principal {
|
||||
data class RPC(val username: String) : FlowInitiator() {
|
||||
override fun getName(): String = username
|
||||
}
|
||||
|
||||
/** Started when we get new session initiation request. */
|
||||
data class Peer(val party: Party) : FlowInitiator() {
|
||||
override fun getName(): String = party.name.toString()
|
||||
}
|
||||
|
||||
/** Started by a CordaService. */
|
||||
data class Service(val serviceClassName: String) : FlowInitiator() {
|
||||
override fun getName(): String = serviceClassName
|
||||
}
|
||||
|
||||
/** Started as scheduled activity. */
|
||||
data class Scheduled(val scheduledState: ScheduledStateRef) : FlowInitiator() {
|
||||
override fun getName(): String = "Scheduler"
|
||||
}
|
||||
|
||||
// TODO When proper ssh access enabled, add username/use RPC?
|
||||
object Shell : FlowInitiator() {
|
||||
override fun getName(): String = "Shell User"
|
||||
|
@ -6,7 +6,9 @@ import net.corda.core.identity.Party
|
||||
import net.corda.core.identity.PartyAndCertificate
|
||||
import net.corda.core.internal.FlowStateMachine
|
||||
import net.corda.core.internal.abbreviate
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.messaging.DataFeed
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
@ -54,14 +56,32 @@ abstract class FlowLogic<out T> {
|
||||
*/
|
||||
val serviceHub: ServiceHub get() = stateMachine.serviceHub
|
||||
|
||||
/**
|
||||
* Creates a communication session with [party]. Subsequently you may send/receive using this session object. Note
|
||||
* that this function does not communicate in itself, the counter-flow will be kicked off by the first send/receive.
|
||||
*/
|
||||
@Suspendable
|
||||
fun initiateFlow(party: Party): FlowSession = stateMachine.initiateFlow(party, flowUsedForSessions)
|
||||
|
||||
/**
|
||||
* Specifies our identity in the flow. With node's multiple identities we can choose which one to use for communication.
|
||||
* Defaults to the first one from [NodeInfo.legalIdentitiesAndCerts].
|
||||
* Specifies the identity, with certificate, to use for this flow. This will be one of the multiple identities that
|
||||
* belong to this node.
|
||||
* @see NodeInfo.legalIdentitiesAndCerts
|
||||
*
|
||||
* Note: The current implementation returns the single identity of the node. This will change once multiple identities
|
||||
* is implemented.
|
||||
*/
|
||||
val ourIdentity: PartyAndCertificate get() = stateMachine.ourIdentity
|
||||
val ourIdentityAndCert: PartyAndCertificate get() = stateMachine.ourIdentityAndCert
|
||||
|
||||
/**
|
||||
* Specifies the identity to use for this flow. This will be one of the multiple identities that belong to this node.
|
||||
* This is the same as calling `ourIdentityAndCert.party`.
|
||||
* @see NodeInfo.legalIdentities
|
||||
*
|
||||
* Note: The current implementation returns the single identity of the node. This will change once multiple identities
|
||||
* is implemented.
|
||||
*/
|
||||
val ourIdentity: Party get() = ourIdentityAndCert.party
|
||||
|
||||
/**
|
||||
* Returns a [FlowInfo] object describing the flow [otherParty] is using. With [FlowInfo.flowVersion] it
|
||||
@ -85,7 +105,7 @@ abstract class FlowLogic<out T> {
|
||||
* Note that this function is not just a simple send+receive pair: it is more efficient and more correct to
|
||||
* use this when you expect to do a message swap than do use [send] and then [receive] in turn.
|
||||
*
|
||||
* @returns an [UntrustworthyData] wrapper around the received object.
|
||||
* @return an [UntrustworthyData] wrapper around the received object.
|
||||
*/
|
||||
@Deprecated("Use FlowSession.sendAndReceive()", level = DeprecationLevel.WARNING)
|
||||
inline fun <reified R : Any> sendAndReceive(otherParty: Party, payload: Any): UntrustworthyData<R> {
|
||||
@ -101,7 +121,7 @@ abstract class FlowLogic<out T> {
|
||||
* Note that this function is not just a simple send+receive pair: it is more efficient and more correct to
|
||||
* use this when you expect to do a message swap than do use [send] and then [receive] in turn.
|
||||
*
|
||||
* @returns an [UntrustworthyData] wrapper around the received object.
|
||||
* @return an [UntrustworthyData] wrapper around the received object.
|
||||
*/
|
||||
@Deprecated("Use FlowSession.sendAndReceive()", level = DeprecationLevel.WARNING)
|
||||
@Suspendable
|
||||
@ -122,11 +142,17 @@ abstract class FlowLogic<out T> {
|
||||
internal inline fun <reified R : Any> sendAndReceiveWithRetry(otherParty: Party, payload: Any): UntrustworthyData<R> {
|
||||
return stateMachine.sendAndReceive(R::class.java, otherParty, payload, flowUsedForSessions, retrySend = true)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
internal fun <R : Any> FlowSession.sendAndReceiveWithRetry(receiveType: Class<R>, payload: Any): UntrustworthyData<R> {
|
||||
return stateMachine.sendAndReceive(receiveType, counterparty, payload, flowUsedForSessions, retrySend = true)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
internal inline fun <reified R : Any> FlowSession.sendAndReceiveWithRetry(payload: Any): UntrustworthyData<R> {
|
||||
return stateMachine.sendAndReceive(R::class.java, counterparty, payload, flowUsedForSessions, retrySend = true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Suspends until the specified [otherParty] sends us a message of type [R].
|
||||
*
|
||||
@ -144,7 +170,7 @@ abstract class FlowLogic<out T> {
|
||||
* verified for consistency and that all expectations are satisfied, as a malicious peer may send you subtly
|
||||
* corrupted data in order to exploit your code.
|
||||
*
|
||||
* @returns an [UntrustworthyData] wrapper around the received object.
|
||||
* @return an [UntrustworthyData] wrapper around the received object.
|
||||
*/
|
||||
@Deprecated("Use FlowSession.receive()", level = DeprecationLevel.WARNING)
|
||||
@Suspendable
|
||||
@ -152,6 +178,38 @@ abstract class FlowLogic<out T> {
|
||||
return stateMachine.receive(receiveType, otherParty, flowUsedForSessions)
|
||||
}
|
||||
|
||||
/** Suspends until a message has been received for each session in the specified [sessions].
|
||||
*
|
||||
* Consider [receiveAll(receiveType: Class<R>, sessions: List<FlowSession>): List<UntrustworthyData<R>>] when the same type is expected from all sessions.
|
||||
*
|
||||
* Remember that when receiving data from other parties the data should not be trusted until it's been thoroughly
|
||||
* verified for consistency and that all expectations are satisfied, as a malicious peer may send you subtly
|
||||
* corrupted data in order to exploit your code.
|
||||
*
|
||||
* @returns a [Map] containing the objects received, wrapped in an [UntrustworthyData], by the [FlowSession]s who sent them.
|
||||
*/
|
||||
@Suspendable
|
||||
open fun receiveAll(sessions: Map<FlowSession, Class<out Any>>): Map<FlowSession, UntrustworthyData<Any>> {
|
||||
return stateMachine.receiveAll(sessions, this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Suspends until a message has been received for each session in the specified [sessions].
|
||||
*
|
||||
* Consider [sessions: Map<FlowSession, Class<out Any>>): Map<FlowSession, UntrustworthyData<Any>>] when sessions are expected to receive different types.
|
||||
*
|
||||
* Remember that when receiving data from other parties the data should not be trusted until it's been thoroughly
|
||||
* verified for consistency and that all expectations are satisfied, as a malicious peer may send you subtly
|
||||
* corrupted data in order to exploit your code.
|
||||
*
|
||||
* @returns a [List] containing the objects received, wrapped in an [UntrustworthyData], with the same order of [sessions].
|
||||
*/
|
||||
@Suspendable
|
||||
open fun <R : Any> receiveAll(receiveType: Class<R>, sessions: List<FlowSession>): List<UntrustworthyData<R>> {
|
||||
enforceNoDuplicates(sessions)
|
||||
return castMapValuesToKnownType(receiveAll(associateSessionsToReceiveType(receiveType, sessions)))
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues the given [payload] for sending to the [otherParty] and continues without suspending.
|
||||
*
|
||||
@ -206,7 +264,6 @@ abstract class FlowLogic<out T> {
|
||||
stateMachine.checkFlowPermission(permissionName, extraAuditData)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Flows can call this method to record application level flow audit events
|
||||
* @param eventType is a string representing the type of event. Each flow is given a distinct namespace for these names.
|
||||
@ -309,6 +366,18 @@ abstract class FlowLogic<out T> {
|
||||
ours.setChildProgressTracker(ours.currentStep, theirs)
|
||||
}
|
||||
}
|
||||
|
||||
private fun enforceNoDuplicates(sessions: List<FlowSession>) {
|
||||
require(sessions.size == sessions.toSet().size) { "A flow session can only appear once as argument." }
|
||||
}
|
||||
|
||||
private fun <R> associateSessionsToReceiveType(receiveType: Class<R>, sessions: List<FlowSession>): Map<FlowSession, Class<R>> {
|
||||
return sessions.associateByTo(LinkedHashMap(), { it }, { receiveType })
|
||||
}
|
||||
|
||||
private fun <R> castMapValuesToKnownType(map: Map<FlowSession, UntrustworthyData<Any>>): List<UntrustworthyData<R>> {
|
||||
return map.values.map { uncheckedCast<Any, UntrustworthyData<R>>(it) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -326,4 +395,4 @@ data class FlowInfo(
|
||||
* to deduplicate it from other releases of the same CorDapp, typically a version string. See the
|
||||
* [CorDapp JAR format](https://docs.corda.net/cordapp-build-systems.html#cordapp-jar-format) for more details.
|
||||
*/
|
||||
val appName: String)
|
||||
val appName: String)
|
@ -24,16 +24,4 @@ class IllegalFlowLogicException(type: Class<*>, msg: String) : IllegalArgumentEx
|
||||
*/
|
||||
// TODO: align this with the existing [FlowRef] in the bank-side API (probably replace some of the API classes)
|
||||
@CordaSerializable
|
||||
interface FlowLogicRef
|
||||
|
||||
|
||||
/**
|
||||
* This is just some way to track what attachments need to be in the class loader, but may later include some app
|
||||
* properties loaded from the attachments. And perhaps the authenticated user for an API call?
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class AppContext(val attachments: List<SecureHash>) {
|
||||
// TODO: build a real [AttachmentsClassLoader] etc
|
||||
val classLoader: ClassLoader
|
||||
get() = this.javaClass.classLoader
|
||||
}
|
||||
interface FlowLogicRef
|
@ -5,7 +5,18 @@ import net.corda.core.identity.Party
|
||||
import net.corda.core.utilities.UntrustworthyData
|
||||
|
||||
/**
|
||||
* To port existing flows:
|
||||
*
|
||||
* A [FlowSession] is a handle on a communication sequence between two paired flows, possibly running on separate nodes.
|
||||
* It is used to send and receive messages between the flows as well as to query information about the counter-flow.
|
||||
*
|
||||
* There are two ways of obtaining such a session:
|
||||
*
|
||||
* 1. Calling [FlowLogic.initiateFlow]. This will create a [FlowSession] object on which the first send/receive
|
||||
* operation will attempt to kick off a corresponding [InitiatedBy] flow on the counterparty's node.
|
||||
* 2. As constructor parameter to [InitiatedBy] flows. This session is the one corresponding to the initiating flow and
|
||||
* may be used for replies.
|
||||
*
|
||||
* To port flows using the old Party-based API:
|
||||
*
|
||||
* Look for [Deprecated] usages of send/receive/sendAndReceive/getFlowInfo.
|
||||
*
|
||||
@ -23,14 +34,18 @@ import net.corda.core.utilities.UntrustworthyData
|
||||
*
|
||||
* If it's an InitiatedBy flow:
|
||||
*
|
||||
* Change the constructor to take an initiatingSession: FlowSession instead of a counterparty: Party
|
||||
* Change the constructor to take an otherSideSession: FlowSession instead of a counterparty: Party
|
||||
* Then look for usages of the deprecated functions and change them to use the FlowSession
|
||||
* For example:
|
||||
* send(counterparty, something)
|
||||
* will become
|
||||
* initiatingSession.send(something)
|
||||
* otherSideSession.send(something)
|
||||
*/
|
||||
abstract class FlowSession {
|
||||
/**
|
||||
* The [Party] on the other side of this session. In the case of a session created by [FlowLogic.initiateFlow]
|
||||
* [counterparty] is the same Party as the one passed to that function.
|
||||
*/
|
||||
abstract val counterparty: Party
|
||||
|
||||
/**
|
||||
@ -54,12 +69,13 @@ abstract class FlowSession {
|
||||
* Note that this function is not just a simple send+receive pair: it is more efficient and more correct to
|
||||
* use this when you expect to do a message swap than do use [send] and then [receive] in turn.
|
||||
*
|
||||
* @returns an [UntrustworthyData] wrapper around the received object.
|
||||
* @return an [UntrustworthyData] wrapper around the received object.
|
||||
*/
|
||||
@Suspendable
|
||||
inline fun <reified R : Any> sendAndReceive(payload: Any): UntrustworthyData<R> {
|
||||
return sendAndReceive(R::class.java, payload)
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes and queues the given [payload] object for sending to the [counterparty]. Suspends until a response
|
||||
* is received, which must be of the given [receiveType]. Remember that when receiving data from other parties the data
|
||||
@ -69,7 +85,7 @@ abstract class FlowSession {
|
||||
* Note that this function is not just a simple send+receive pair: it is more efficient and more correct to
|
||||
* use this when you expect to do a message swap than do use [send] and then [receive] in turn.
|
||||
*
|
||||
* @returns an [UntrustworthyData] wrapper around the received object.
|
||||
* @return an [UntrustworthyData] wrapper around the received object.
|
||||
*/
|
||||
@Suspendable
|
||||
abstract fun <R : Any> sendAndReceive(receiveType: Class<R>, payload: Any): UntrustworthyData<R>
|
||||
@ -85,6 +101,7 @@ abstract class FlowSession {
|
||||
inline fun <reified R : Any> receive(): UntrustworthyData<R> {
|
||||
return receive(R::class.java)
|
||||
}
|
||||
|
||||
/**
|
||||
* Suspends until [counterparty] sends us a message of type [receiveType].
|
||||
*
|
||||
@ -92,7 +109,7 @@ abstract class FlowSession {
|
||||
* verified for consistency and that all expectations are satisfied, as a malicious peer may send you subtly
|
||||
* corrupted data in order to exploit your code.
|
||||
*
|
||||
* @returns an [UntrustworthyData] wrapper around the received object.
|
||||
* @return an [UntrustworthyData] wrapper around the received object.
|
||||
*/
|
||||
@Suspendable
|
||||
abstract fun <R : Any> receive(receiveType: Class<R>): UntrustworthyData<R>
|
||||
|
@ -1,20 +0,0 @@
|
||||
package net.corda.core.flows
|
||||
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
|
||||
/**
|
||||
* Alternative finality flow which only does not attempt to take participants from the transaction, but instead all
|
||||
* participating parties must be provided manually.
|
||||
*
|
||||
* @param transactions What to commit.
|
||||
* @param recipients List of participants to inform of the transaction.
|
||||
*/
|
||||
class ManualFinalityFlow(transactions: Iterable<SignedTransaction>,
|
||||
recipients: Set<Party>,
|
||||
progressTracker: ProgressTracker) : FinalityFlow(transactions, recipients, progressTracker) {
|
||||
constructor(transaction: SignedTransaction, extraParticipants: Set<Party>) : this(listOf(transaction), extraParticipants, tracker())
|
||||
override fun lookupParties(ltx: LedgerTransaction): List<Party> = emptyList()
|
||||
}
|
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