mirror of
https://github.com/corda/corda.git
synced 2024-12-26 16:11:12 +00:00
Merge from Corda master
This commit is contained in:
commit
db0969ebda
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
48
.ci/check-api-changes.sh
Executable file
48
.ci/check-api-changes.sh
Executable file
@ -0,0 +1,48 @@
|
||||
#!/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
|
||||
|
||||
# Remove the two header lines from the diff output.
|
||||
diffContents=`diff -u $apiCurrent $APIHOME/../build/api/api-corda-*.txt | tail --lines=+3`
|
||||
echo "Diff contents:"
|
||||
echo "$diffContents"
|
||||
echo
|
||||
|
||||
# A removed line means that an API was either deleted or modified.
|
||||
removals=$(echo "$diffContents" | grep "^-")
|
||||
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 "^+" | 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
|
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>
|
9
.idea/compiler.xml
generated
9
.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" />
|
||||
@ -19,6 +21,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" />
|
||||
@ -42,8 +46,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="intellij-plugin_main" target="1.8" />
|
||||
@ -58,6 +65,8 @@
|
||||
<module name="jfx_integrationTest" target="1.8" />
|
||||
<module name="jfx_main" target="1.8" />
|
||||
<module name="jfx_test" target="1.8" />
|
||||
<module name="kryo-hook_main" target="1.8" />
|
||||
<module name="kryo-hook_test" target="1.8" />
|
||||
<module name="loadtest_main" target="1.8" />
|
||||
<module name="loadtest_test" target="1.8" />
|
||||
<module name="mock_main" target="1.8" />
|
||||
|
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>
|
46
README.md
46
README.md
@ -1,10 +1,23 @@
|
||||
![Corda](https://www.corda.net/wp-content/uploads/2016/11/fg005_corda_b.png)
|
||||
|
||||
<a href="https://ci-master.corda.r3cev.com/viewType.html?buildTypeId=CordaEnterprise_CordaBuild&tab=buildTypeStatusDiv"><img src="https://ci.corda.r3cev.com/app/rest/builds/buildType:Corda_CordaBuild/statusIcon"/></a>
|
||||
|
||||
# Corda Enterprise
|
||||
|
||||
Corda Enterprise is R3's closed source patch set on top of Corda Open Source. It adds features and improvements that we
|
||||
plan to charge for.
|
||||
|
||||
Corda is a decentralised database system in which nodes trust each other as little as possible.
|
||||
|
||||
## Features
|
||||
|
||||
* 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
|
||||
|
||||
## Extra features
|
||||
|
||||
* Doorman
|
||||
@ -12,3 +25,36 @@ plan to charge for.
|
||||
* Flow triage screen in Explorer
|
||||
* No stupid jokes at startup
|
||||
* SGX
|
||||
|
||||
## Getting started
|
||||
|
||||
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)
|
||||
* [Documentation](https://docs.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 Courses](https://www.corda.net/corda-training/)
|
||||
|
||||
## Contributing
|
||||
|
||||
Please read [here](./CONTRIBUTING.md).
|
||||
|
||||
## License
|
||||
|
||||
[Apache 2.0](./LICENSE)
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
![YourKit](https://www.yourkit.com/images/yklogo.png)
|
||||
|
||||
YourKit supports open source projects with its full-featured Java Profiler.
|
||||
|
||||
YourKit, LLC is the creator of [YourKit Java Profiler](https://www.yourkit.com/java/profiler/) and [YourKit .NET Profiler](https://www.yourkit.com/.net/profiler/), innovative and intelligent tools for profiling Java and .NET applications.
|
||||
|
53
build.gradle
53
build.gradle
@ -40,7 +40,7 @@ buildscript {
|
||||
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 Enterprise Edition')
|
||||
attributes('Automatic-Module-Name': "net.corda.${task.project.name.replaceAll('-', '.')}")
|
||||
}
|
||||
}
|
||||
|
||||
@ -175,27 +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(':confidential-identities')
|
||||
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)
|
||||
@ -223,16 +231,14 @@ tasks.withType(Test) {
|
||||
|
||||
task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
|
||||
directory "./build/nodes"
|
||||
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
|
||||
@ -240,7 +246,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
|
||||
@ -289,12 +294,20 @@ artifactory {
|
||||
publish {
|
||||
contextUrl = 'https://ci-artifactory.corda.r3cev.com/artifactory'
|
||||
repository {
|
||||
repoKey = 'corda-releases'
|
||||
repoKey = 'enterprise-dev'
|
||||
username = 'teamcity'
|
||||
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', 'corda-confidential-identities')
|
||||
// Root project applies the plugin (for this block) but does not need to be published
|
||||
if(project != rootProject) {
|
||||
publications(project.extensions.publish.name())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@ -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.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()
|
||||
}
|
||||
}
|
||||
|
@ -14,15 +14,16 @@ 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.reflect.jvm.jvmName
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class JacksonSupportTest : TestDependencyInjectionBase() {
|
||||
companion object {
|
||||
private val SEED = BigInteger.valueOf(20170922L)
|
||||
val mapper = JacksonSupport.createNonRpcMapper()
|
||||
}
|
||||
|
||||
@ -37,15 +38,34 @@ class JacksonSupportTest : TestDependencyInjectionBase() {
|
||||
}
|
||||
|
||||
@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 = """
|
||||
@ -72,10 +92,10 @@ class JacksonSupportTest : TestDependencyInjectionBase() {
|
||||
fun writeTransaction() {
|
||||
val attachmentRef = SecureHash.randomSHA256()
|
||||
whenever(cordappProvider.getContractAttachmentID(DummyContract.PROGRAM_ID))
|
||||
.thenReturn(attachmentRef)
|
||||
.thenReturn(attachmentRef)
|
||||
fun makeDummyTx(): SignedTransaction {
|
||||
val wtx = DummyContract.generateInitial(1, DUMMY_NOTARY, MINI_CORP.ref(1))
|
||||
.toWireTransaction(services)
|
||||
.toWireTransaction(services)
|
||||
val signatures = TransactionSignature(
|
||||
ByteArray(1),
|
||||
ALICE_PUBKEY,
|
||||
|
@ -57,8 +57,11 @@ task integrationTest(type: Test) {
|
||||
|
||||
jar {
|
||||
baseName 'corda-jfx'
|
||||
manifest {
|
||||
attributes 'Automatic-Module-Name': 'net.corda.client.jfx'
|
||||
}
|
||||
}
|
||||
|
||||
publish {
|
||||
name jar.baseName
|
||||
}
|
||||
}
|
||||
|
@ -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.nodeapi.internal.ServiceInfo
|
||||
import net.corda.node.services.transactions.SimpleNotaryService
|
||||
import net.corda.nodeapi.User
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.driver.driver
|
||||
@ -59,7 +57,7 @@ class NodeMonitorModelTest : DriverBasedTest() {
|
||||
startFlowPermission<CashExitFlow>())
|
||||
)
|
||||
val aliceNodeFuture = startNode(providedName = ALICE.name, rpcUsers = listOf(cashUser))
|
||||
val notaryHandle = startNode(providedName = DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))).getOrThrow()
|
||||
val notaryHandle = startNotaryNode(DUMMY_NOTARY.name, validating = false).getOrThrow()
|
||||
val aliceNodeHandle = aliceNodeFuture.getOrThrow()
|
||||
aliceNode = aliceNodeHandle.nodeInfo
|
||||
newNode = { nodeName -> startNode(providedName = nodeName).getOrThrow().nodeInfo }
|
||||
@ -71,7 +69,7 @@ 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]
|
||||
|
||||
@ -79,7 +77,7 @@ class NodeMonitorModelTest : DriverBasedTest() {
|
||||
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()
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -38,11 +38,14 @@ class NetworkIdentityModel {
|
||||
})
|
||||
|
||||
val notaries: ObservableList<Party> = networkIdentities.map {
|
||||
it.legalIdentitiesAndCerts.find { it.name.commonName?.let { ServiceType.parse(it).isNotary() } ?: false }
|
||||
it.legalIdentitiesAndCerts.find { it.name.commonName?.let { ServiceType.parse(it).isNotary() } == true }
|
||||
}.map { it?.party }.filterNotNull()
|
||||
|
||||
val notaryNodes: ObservableList<NodeInfo> = notaries.map { rpcProxy.value?.nodeInfoFromParty(it) }.filterNotNull()
|
||||
val parties: ObservableList<NodeInfo> = networkIdentities.filtered { it.legalIdentities.all { it !in notaries } }
|
||||
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]
|
||||
|
@ -55,13 +55,12 @@ class NodeMonitorModel {
|
||||
* 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
|
||||
|
@ -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>
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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 })
|
||||
|
||||
|
@ -21,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,6 +1,7 @@
|
||||
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'
|
||||
@ -89,6 +90,9 @@ task smokeTest(type: Test) {
|
||||
|
||||
jar {
|
||||
baseName 'corda-rpc'
|
||||
manifest {
|
||||
attributes 'Automatic-Module-Name': 'net.corda.client.rpc'
|
||||
}
|
||||
}
|
||||
|
||||
publish {
|
||||
|
@ -1,6 +1,5 @@
|
||||
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;
|
||||
@ -9,11 +8,9 @@ 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.internal.ServiceInfo;
|
||||
import net.corda.nodeapi.User;
|
||||
import net.corda.testing.CoreTestUtils;
|
||||
import net.corda.testing.node.NodeBasedTest;
|
||||
@ -25,24 +22,26 @@ import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import static java.util.Collections.*;
|
||||
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.*;
|
||||
import static net.corda.testing.TestConstants.getALICE;
|
||||
|
||||
public class CordaRPCJavaClientTest extends NodeBasedTest {
|
||||
public CordaRPCJavaClientTest() {
|
||||
super(Arrays.asList("net.corda.finance.contracts", CashSchemaV1.class.getPackage().getName()));
|
||||
}
|
||||
|
||||
private List<String> perms = Arrays.asList(startFlowPermission(CashPaymentFlow.class), startFlowPermission(CashIssueFlow.class));
|
||||
private Set<String> permSet = new HashSet<>(perms);
|
||||
private User rpcUser = new User("user1", "test", permSet);
|
||||
|
||||
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,18 +51,14 @@ public class CordaRPCJavaClientTest extends NodeBasedTest {
|
||||
|
||||
@Before
|
||||
public void setUp() throws ExecutionException, InterruptedException {
|
||||
setCordappPackages("net.corda.finance.contracts");
|
||||
Set<ServiceInfo> services = new HashSet<>(singletonList(new ServiceInfo(ValidatingNotaryService.Companion.getType(), null)));
|
||||
CordaFuture<StartedNode<Node>> nodeFuture = startNode(getALICE().getName(), 1, services, singletonList(rpcUser), emptyMap());
|
||||
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()), getDefault(), false);
|
||||
client = new CordaRPCClient(requireNonNull(node.getInternals().getConfiguration().getRpcAddress()));
|
||||
}
|
||||
|
||||
@After
|
||||
public void done() throws IOException {
|
||||
connection.close();
|
||||
unsetCordappPackages();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -0,0 +1,90 @@
|
||||
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(listOf("net.corda.client.rpc")) {
|
||||
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() {
|
||||
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()
|
||||
}
|
||||
|
||||
@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,6 +2,7 @@ package net.corda.client.rpc
|
||||
|
||||
import net.corda.core.crypto.random63BitValue
|
||||
import net.corda.core.flows.FlowInitiator
|
||||
import net.corda.core.internal.packageName
|
||||
import net.corda.core.messaging.FlowProgressHandle
|
||||
import net.corda.core.messaging.StateMachineUpdate
|
||||
import net.corda.core.messaging.startFlow
|
||||
@ -19,14 +20,10 @@ 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.nodeapi.internal.ServiceInfo
|
||||
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
|
||||
@ -36,7 +33,7 @@ import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class CordaRPCClientTest : NodeBasedTest() {
|
||||
class CordaRPCClientTest : NodeBasedTest(listOf("net.corda.finance.contracts", CashSchemaV1::class.packageName)) {
|
||||
private val rpcUser = User("user1", "test", permissions = setOf(
|
||||
startFlowPermission<CashIssueFlow>(),
|
||||
startFlowPermission<CashPaymentFlow>()
|
||||
@ -51,16 +48,13 @@ class CordaRPCClientTest : NodeBasedTest() {
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
setCordappPackages("net.corda.finance.contracts")
|
||||
node = startNode(ALICE.name, rpcUsers = listOf(rpcUser), advertisedServices = setOf(ServiceInfo(ValidatingNotaryService.type))).getOrThrow()
|
||||
node.internals.registerCustomSchemas(setOf(CashSchemaV1))
|
||||
client = CordaRPCClient(node.internals.configuration.rpcAddress!!, initialiseSerialization = false)
|
||||
node = startNotaryNode(ALICE.name, rpcUsers = listOf(rpcUser)).getOrThrow()
|
||||
client = CordaRPCClient(node.internals.configuration.rpcAddress!!)
|
||||
}
|
||||
|
||||
@After
|
||||
fun done() {
|
||||
connection?.close()
|
||||
unsetCordappPackages()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -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()
|
||||
|
||||
|
@ -10,42 +10,68 @@ import net.corda.nodeapi.ConnectionDirection
|
||||
import net.corda.nodeapi.internal.serialization.KRYO_RPC_CLIENT_CONTEXT
|
||||
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 */
|
||||
//TODO Add SSL support
|
||||
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,
|
||||
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) {
|
||||
KryoClientSerializationScheme.initialiseSerialization()
|
||||
}
|
||||
KryoClientSerializationScheme.initialiseSerialization()
|
||||
}
|
||||
|
||||
private val rpcClient = RPCClient<CordaRPCOps>(
|
||||
@ -54,10 +80,24 @@ class CordaRPCClient(
|
||||
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)
|
||||
}
|
||||
|
@ -1,10 +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).
|
||||
*/
|
||||
@CordaSerializable
|
||||
class PermissionException(msg: String) : RuntimeException(msg)
|
||||
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()
|
||||
}
|
@ -5,6 +5,7 @@ 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 {
|
||||
@ -23,7 +24,9 @@ class KryoClientSerializationScheme : AbstractKryoSerializationScheme() {
|
||||
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())
|
||||
@ -31,10 +34,14 @@ class KryoClientSerializationScheme : AbstractKryoSerializationScheme() {
|
||||
}
|
||||
SerializationDefaults.P2P_CONTEXT = KRYO_P2P_CONTEXT
|
||||
SerializationDefaults.RPC_CLIENT_CONTEXT = KRYO_RPC_CLIENT_CONTEXT
|
||||
} catch(e: IllegalStateException) {
|
||||
} 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." }
|
||||
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,8 +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
|
||||
@ -17,7 +19,6 @@ 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" +
|
||||
|
@ -19,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
|
||||
@ -78,6 +79,7 @@ class RPCClientProxyHandler(
|
||||
STARTED,
|
||||
FINISHED
|
||||
}
|
||||
|
||||
private val lifeCycle = LifeCycle(State.UNSTARTED)
|
||||
|
||||
private companion object {
|
||||
@ -208,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() ?: ""
|
||||
|
@ -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,16 +67,16 @@ public class StandaloneCordaRPCJavaClientTest {
|
||||
try {
|
||||
connection.close();
|
||||
} finally {
|
||||
if(notary != null) {
|
||||
if (notary != null) {
|
||||
notary.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void copyFinanceCordapp() {
|
||||
Path pluginsDir = (factory.baseDirectory(notaryConfig).resolve("plugins"));
|
||||
Path cordappsDir = (factory.baseDirectory(notaryConfig).resolve("cordapps"));
|
||||
try {
|
||||
Files.createDirectories(pluginsDir);
|
||||
Files.createDirectories(cordappsDir);
|
||||
} catch (IOException ex) {
|
||||
fail("Failed to create directories");
|
||||
}
|
||||
@ -87,7 +84,7 @@ public class StandaloneCordaRPCJavaClientTest {
|
||||
paths.forEach(file -> {
|
||||
if (file.toString().contains("corda-finance")) {
|
||||
try {
|
||||
Files.copy(file, pluginsDir.resolve(file.getFileName()));
|
||||
Files.copy(file, cordappsDir.resolve(file.getFileName()));
|
||||
} catch (IOException ex) {
|
||||
fail("Failed to copy finance jar");
|
||||
}
|
||||
@ -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)
|
||||
)
|
||||
|
||||
@ -89,12 +89,12 @@ class StandaloneCordaRPClientTest {
|
||||
}
|
||||
|
||||
private fun copyFinanceCordapp() {
|
||||
val pluginsDir = (factory.baseDirectory(notaryConfig) / "plugins").createDirectories()
|
||||
val cordappsDir = (factory.baseDirectory(notaryConfig) / "cordapps").createDirectories()
|
||||
// Find the finance jar file for the smoke tests of this module
|
||||
val financeJar = Paths.get("build", "resources", "smokeTest").list {
|
||||
it.filter { "corda-finance" in it.toString() }.toList().single()
|
||||
}
|
||||
financeJar.copyToDirectory(pluginsDir)
|
||||
financeJar.copyToDirectory(cordappsDir)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -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
|
||||
|
||||
|
@ -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,
|
||||
|
@ -79,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,6 +13,7 @@ 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 lastIdx = Math.min(Math.min(offset + length, byteArray.size), offset + bytesLeft)
|
||||
for (i in offset until lastIdx) {
|
||||
|
@ -6,15 +6,12 @@ apply plugin: 'kotlin'
|
||||
apply plugin: CanonicalizerPlugin
|
||||
apply plugin: 'net.corda.plugins.publish-utils'
|
||||
apply plugin: 'net.corda.plugins.quasar-utils'
|
||||
apply plugin: 'net.corda.plugins.cordformation'
|
||||
apply plugin: 'com.jfrog.artifactory'
|
||||
|
||||
description 'Corda Experimental Confidential Identities'
|
||||
|
||||
dependencies {
|
||||
// Note the :confidential-identities module is a CorDapp in its own right
|
||||
// and CorDapps using :confidential-identities features should use 'cordapp' not 'compile' linkage.
|
||||
cordaCompile project(':core')
|
||||
compile project(':core')
|
||||
|
||||
// Quasar, for suspendable fibres.
|
||||
compileOnly "co.paralleluniverse:quasar-core:$quasar_version:jdk8"
|
||||
|
@ -12,14 +12,12 @@ 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 otherSideSessions: Set<FlowSession>,
|
||||
@ -36,17 +34,10 @@ 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()
|
||||
|
||||
otherSideSessions.forEach { otherSideSession ->
|
||||
val requestedIdentities: List<AbstractParty> = otherSideSession.sendAndReceive<List<AbstractParty>>(confidentialIdentities).unwrap { req ->
|
||||
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
|
||||
}
|
||||
@ -61,6 +52,20 @@ object IdentitySyncFlow {
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,15 +1,32 @@
|
||||
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
|
||||
@ -27,8 +44,32 @@ class SwapIdentitiesFlow(private val otherParty: Party,
|
||||
object AWAITING_KEY : ProgressTracker.Step("Awaiting key")
|
||||
|
||||
fun tracker() = ProgressTracker(AWAITING_KEY)
|
||||
fun validateAndRegisterIdentity(identityService: IdentityService, otherSide: Party, anonymousOtherSide: PartyAndCertificate): PartyAndCertificate {
|
||||
require(anonymousOtherSide.name == otherSide.name)
|
||||
/**
|
||||
* 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)
|
||||
@ -40,6 +81,7 @@ class SwapIdentitiesFlow(private val otherParty: Party,
|
||||
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>()
|
||||
@ -47,13 +89,33 @@ class SwapIdentitiesFlow(private val otherParty: Party,
|
||||
identities.put(otherParty, legalIdentityAnonymous.party.anonymise())
|
||||
} else {
|
||||
val otherSession = initiateFlow(otherParty)
|
||||
val anonymousOtherSide = otherSession.sendAndReceive<PartyAndCertificate>(legalIdentityAnonymous).unwrap { confidentialIdentity ->
|
||||
validateAndRegisterIdentity(serviceHub.identityService, otherSession.counterparty, confidentialIdentity)
|
||||
}
|
||||
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(otherSession.counterparty, anonymousOtherSide.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)
|
@ -4,6 +4,9 @@ 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
|
||||
|
||||
@ -20,9 +23,14 @@ class SwapIdentitiesHandler(val otherSideSession: FlowSession, val revocationEna
|
||||
override fun call() {
|
||||
val revocationEnabled = false
|
||||
progressTracker.currentStep = SENDING_KEY
|
||||
val legalIdentityAnonymous = serviceHub.keyManagementService.freshKeyAndCert(ourIdentityAndCert, revocationEnabled)
|
||||
otherSideSession.sendAndReceive<PartyAndCertificate>(legalIdentityAnonymous).unwrap { confidentialIdentity ->
|
||||
SwapIdentitiesFlow.validateAndRegisterIdentity(serviceHub.identityService, otherSideSession.counterparty, confidentialIdentity)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
@ -13,12 +13,14 @@ 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 {
|
||||
@ -26,31 +28,29 @@ class IdentitySyncFlowTests {
|
||||
|
||||
@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)
|
||||
mockNet = MockNetwork(networkSendManuallyPumped = false, threadPerNode = true, cordappPackages = listOf("net.corda.finance.contracts.asset"))
|
||||
}
|
||||
|
||||
@After
|
||||
fun cleanUp() {
|
||||
mockNet.stopNodes()
|
||||
unsetCordappPackages()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sync confidential identities`() {
|
||||
// Set up values we'll need
|
||||
val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name)
|
||||
val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name)
|
||||
val bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name)
|
||||
val alice: Party = aliceNode.services.myInfo.chooseIdentity()
|
||||
val bob: Party = bobNode.services.myInfo.chooseIdentity()
|
||||
val notaryNode = mockNet.createNotaryNode()
|
||||
val aliceNode = mockNet.createPartyNode(ALICE_NAME)
|
||||
val bobNode = mockNet.createPartyNode(BOB_NAME)
|
||||
val alice: Party = aliceNode.info.singleIdentity()
|
||||
val bob: Party = bobNode.info.singleIdentity()
|
||||
val notary = notaryNode.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 notary = aliceNode.services.getDefaultNotary()
|
||||
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
|
||||
@ -67,6 +67,40 @@ class IdentitySyncFlowTests {
|
||||
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.info.singleIdentity()
|
||||
val bob: Party = bobNode.info.singleIdentity()
|
||||
val charlie: Party = charlieNode.info.singleIdentity()
|
||||
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.
|
||||
*/
|
||||
|
@ -4,16 +4,10 @@ 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.ALICE
|
||||
import net.corda.testing.BOB
|
||||
import net.corda.testing.DUMMY_NOTARY
|
||||
import net.corda.testing.chooseIdentity
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNotEquals
|
||||
import kotlin.test.assertTrue
|
||||
import kotlin.test.*
|
||||
|
||||
class SwapIdentitiesFlowTests {
|
||||
@Test
|
||||
@ -22,12 +16,11 @@ class SwapIdentitiesFlowTests {
|
||||
val mockNet = MockNetwork(false, true)
|
||||
|
||||
// Set up values we'll need
|
||||
val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name)
|
||||
val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name)
|
||||
val bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name)
|
||||
val alice: Party = aliceNode.services.myInfo.chooseIdentity()
|
||||
val bob: Party = bobNode.services.myInfo.chooseIdentity()
|
||||
mockNet.registerIdentities()
|
||||
val notaryNode = mockNet.createNotaryNode()
|
||||
val aliceNode = mockNet.createPartyNode(ALICE.name)
|
||||
val bobNode = mockNet.createPartyNode(BOB.name)
|
||||
val alice = aliceNode.info.singleIdentity()
|
||||
val bob = bobNode.services.myInfo.singleIdentity()
|
||||
|
||||
// Run the flows
|
||||
val requesterFlow = aliceNode.services.startFlow(SwapIdentitiesFlow(bob))
|
||||
@ -53,4 +46,69 @@ class SwapIdentitiesFlowTests {
|
||||
|
||||
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.singleIdentity()
|
||||
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.singleIdentity()
|
||||
// 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,4 +1,4 @@
|
||||
gradlePluginsVersion=1.0.0
|
||||
gradlePluginsVersion=2.0.4
|
||||
kotlinVersion=1.1.50
|
||||
guavaVersion=21.0
|
||||
bouncycastleVersion=1.57
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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.
|
||||
|
@ -199,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
|
||||
|
@ -64,6 +64,17 @@ abstract class TimeWindow {
|
||||
*/
|
||||
abstract val midpoint: Instant?
|
||||
|
||||
/**
|
||||
* Returns the duration between [fromTime] and [untilTime] if both are non-null. Otherwise returns null.
|
||||
*/
|
||||
val length: Duration? get() {
|
||||
return if (fromTime == null || untilTime == null) {
|
||||
null
|
||||
} else {
|
||||
Duration.between(fromTime, untilTime)
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns true iff the given [instant] is within the time interval of this [TimeWindow]. */
|
||||
abstract operator fun contains(instant: Instant): Boolean
|
||||
|
||||
@ -85,6 +96,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)"
|
||||
|
@ -1,8 +1,8 @@
|
||||
package net.corda.core.cordapp
|
||||
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.node.CordaPluginRegistry
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.core.serialization.SerializationWhitelist
|
||||
import net.corda.core.serialization.SerializeAsToken
|
||||
import java.net.URL
|
||||
|
||||
@ -13,20 +13,26 @@ import java.net.URL
|
||||
*
|
||||
* 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 plugins List of Corda plugin registries
|
||||
* @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 plugins: List<CordaPluginRegistry>
|
||||
val serializationWhitelists: List<SerializationWhitelist>
|
||||
val customSchemas: Set<MappedSchema>
|
||||
val jarPath: URL
|
||||
val cordappClasses: List<String>
|
||||
|
@ -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.*
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -2,7 +2,10 @@ package net.corda.core.crypto
|
||||
|
||||
import net.corda.core.internal.X509EdDSAEngine
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.i2p.crypto.eddsa.*
|
||||
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.math.GroupElement
|
||||
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec
|
||||
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable
|
||||
@ -39,8 +42,6 @@ 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
|
||||
@ -148,7 +149,7 @@ object Crypto {
|
||||
"at the cost of larger key sizes and loss of compatibility."
|
||||
)
|
||||
|
||||
/** Corda composite key type */
|
||||
/** Corda composite key type. */
|
||||
@JvmField
|
||||
val COMPOSITE_KEY = SignatureScheme(
|
||||
6,
|
||||
@ -774,9 +775,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)
|
||||
|
||||
@ -822,7 +824,7 @@ object Crypto {
|
||||
@JvmStatic
|
||||
fun deriveKeyPairFromEntropy(entropy: BigInteger): KeyPair = deriveKeyPairFromEntropy(DEFAULT_SIGNATURE_SCHEME, entropy)
|
||||
|
||||
// custom key pair generator from entropy.
|
||||
// Custom key pair generator from entropy.
|
||||
private fun deriveEdDSAKeyPairFromEntropy(entropy: BigInteger): KeyPair {
|
||||
val params = EDDSA_ED25519_SHA512.algSpec as EdDSANamedCurveSpec
|
||||
val bytes = entropy.toByteArray().copyOf(params.curve.field.getb() / 8) // Need to pad the entropy to the valid seed length.
|
||||
@ -849,6 +851,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) }
|
||||
}
|
||||
@ -880,7 +883,7 @@ object Crypto {
|
||||
}
|
||||
}
|
||||
|
||||
// return true if EdDSA publicKey is point at infinity.
|
||||
// Return true if EdDSA publicKey is point at infinity.
|
||||
// For EdDSA a custom function is required as it is not supported by the I2P implementation.
|
||||
private fun isEdDSAPointAtInfinity(publicKey: EdDSAPublicKey): Boolean {
|
||||
return publicKey.a.toP3() == (EDDSA_ED25519_SHA512.algSpec as EdDSANamedCurveSpec).curve.getZero(GroupElement.Representation.P3)
|
||||
@ -892,7 +895,7 @@ object Crypto {
|
||||
return signatureScheme.schemeCodeName in signatureSchemeMap
|
||||
}
|
||||
|
||||
// validate a key, by checking its algorithmic params.
|
||||
// Validate a key, by checking its algorithmic params.
|
||||
private fun validateKey(signatureScheme: SignatureScheme, key: Key): Boolean {
|
||||
return when (key) {
|
||||
is PublicKey -> validatePublicKey(signatureScheme, key)
|
||||
@ -901,7 +904,7 @@ object Crypto {
|
||||
}
|
||||
}
|
||||
|
||||
// check if a public key satisfies algorithm specs (for ECC: key should lie on the curve and not being point-at-infinity).
|
||||
// Check if a public key satisfies algorithm specs (for ECC: key should lie on the curve and not being point-at-infinity).
|
||||
private fun validatePublicKey(signatureScheme: SignatureScheme, key: PublicKey): Boolean {
|
||||
return when (key) {
|
||||
is BCECPublicKey, is EdDSAPublicKey -> publicKeyOnCurve(signatureScheme, key)
|
||||
@ -910,7 +913,7 @@ object Crypto {
|
||||
}
|
||||
}
|
||||
|
||||
// check if a private key satisfies algorithm specs.
|
||||
// Check if a private key satisfies algorithm specs.
|
||||
private fun validatePrivateKey(signatureScheme: SignatureScheme, key: PrivateKey): Boolean {
|
||||
return when (key) {
|
||||
is BCECPrivateKey -> key.parameters == signatureScheme.algSpec
|
||||
@ -922,7 +925,6 @@ object Crypto {
|
||||
|
||||
/**
|
||||
* Convert a public key to a supported implementation.
|
||||
*
|
||||
* @param key a public key.
|
||||
* @return a supported implementation of the input public key.
|
||||
* @throws IllegalArgumentException on not supported scheme or if the given key specification
|
||||
@ -941,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.
|
||||
@ -952,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,9 +21,19 @@ import java.security.*
|
||||
* @throws InvalidKeyException if the private key is invalid.
|
||||
* @throws SignatureException if signing is not possible due to malformed data or private key.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class)
|
||||
@Throws(InvalidKeyException::class, SignatureException::class)
|
||||
fun PrivateKey.sign(bytesToSign: ByteArray): DigitalSignature = DigitalSignature(Crypto.doSign(this, bytesToSign))
|
||||
|
||||
/**
|
||||
* Utility to simplify the act of signing a byte array and return a [DigitalSignature.WithKey] object.
|
||||
* Note that there is no check if the public key matches with the signing private key.
|
||||
* @param bytesToSign the data/message to be signed in [ByteArray] form (usually the Merkle root).
|
||||
* @return the [DigitalSignature.WithKey] object on the input message [bytesToSign] and [publicKey].
|
||||
* @throws IllegalArgumentException if the signature scheme is not supported for this private key.
|
||||
* @throws InvalidKeyException if the private key is invalid.
|
||||
* @throws SignatureException if signing is not possible due to malformed data or private key.
|
||||
*/
|
||||
@Throws(InvalidKeyException::class, SignatureException::class)
|
||||
fun PrivateKey.sign(bytesToSign: ByteArray, publicKey: PublicKey) = DigitalSignature.WithKey(publicKey, this.sign(bytesToSign).bytes)
|
||||
|
||||
/**
|
||||
@ -34,9 +44,13 @@ fun PrivateKey.sign(bytesToSign: ByteArray, publicKey: PublicKey) = DigitalSigna
|
||||
* @throws InvalidKeyException if the private key is invalid.
|
||||
* @throws SignatureException if signing is not possible due to malformed data or private key.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class)
|
||||
@Throws(InvalidKeyException::class, SignatureException::class)
|
||||
fun KeyPair.sign(bytesToSign: ByteArray) = private.sign(bytesToSign, public)
|
||||
|
||||
/** Helper function to sign the bytes of [bytesToSign] with a key pair. */
|
||||
@Throws(InvalidKeyException::class, SignatureException::class)
|
||||
fun KeyPair.sign(bytesToSign: OpaqueBytes) = sign(bytesToSign.bytes)
|
||||
|
||||
/**
|
||||
* Helper function for signing a [SignableData] object.
|
||||
* @param signableData the object to be signed.
|
||||
@ -56,8 +70,8 @@ fun KeyPair.sign(signableData: SignableData): TransactionSignature = Crypto.doSi
|
||||
* @throws SignatureException if the signature is invalid (i.e. damaged), or does not match the key (incorrect).
|
||||
* @throws IllegalArgumentException if the signature scheme is not supported or if any of the clear or signature data is empty.
|
||||
*/
|
||||
// TODO: SignatureException should be used only for a damaged signature, as per `java.security.Signature.verify()`,
|
||||
@Throws(SignatureException::class, IllegalArgumentException::class, InvalidKeyException::class)
|
||||
// TODO: SignatureException should be used only for a damaged signature, as per `java.security.Signature.verify()`.
|
||||
@Throws(SignatureException::class, InvalidKeyException::class)
|
||||
fun PublicKey.verify(content: ByteArray, signature: DigitalSignature) = Crypto.doVerify(this, signature.bytes, content)
|
||||
|
||||
/**
|
||||
@ -70,21 +84,25 @@ fun PublicKey.verify(content: ByteArray, signature: DigitalSignature) = Crypto.d
|
||||
* signature).
|
||||
* @throws SignatureException if the signature is invalid (i.e. damaged).
|
||||
* @throws IllegalArgumentException if the signature scheme is not supported or if any of the clear or signature data is empty.
|
||||
* @throws IllegalStateException if this is a [CompositeKey], because verification of composite key signatures is not supported.
|
||||
* @return whether the signature is correct for this key.
|
||||
*/
|
||||
@Throws(IllegalStateException::class, SignatureException::class, IllegalArgumentException::class)
|
||||
fun PublicKey.isValid(content: ByteArray, signature: DigitalSignature) : Boolean {
|
||||
@Throws(SignatureException::class, InvalidKeyException::class)
|
||||
fun PublicKey.isValid(content: ByteArray, signature: DigitalSignature): Boolean {
|
||||
if (this is CompositeKey)
|
||||
throw IllegalStateException("Verification of CompositeKey signatures currently not supported.") // TODO CompositeSignature verification.
|
||||
return Crypto.isValid(this, signature.bytes, content)
|
||||
}
|
||||
|
||||
/** Render a public key to its hash (in Base58) of its serialised form using the DL prefix. */
|
||||
fun PublicKey.toStringShort(): String = "DL" + this.toSHA256Bytes().toBase58()
|
||||
fun PublicKey.toStringShort(): String = "DL" + this.toSHA256Bytes().toBase58()
|
||||
|
||||
/** Return a [Set] of the contained keys if this is a [CompositeKey]; otherwise, return a [Set] with a single element (this [PublicKey]). */
|
||||
val PublicKey.keys: Set<PublicKey> get() = (this as? CompositeKey)?.leafKeys ?: setOf(this)
|
||||
|
||||
fun PublicKey.isFulfilledBy(otherKey: PublicKey): Boolean = isFulfilledBy(setOf(otherKey))
|
||||
/** Return true if [otherKey] fulfils the requirements of this [PublicKey]. */
|
||||
fun PublicKey.isFulfilledBy(otherKey: PublicKey): Boolean = isFulfilledBy(setOf(otherKey))
|
||||
/** Return true if [otherKeys] fulfil the requirements of this [PublicKey]. */
|
||||
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]. */
|
||||
@ -98,8 +116,9 @@ fun Iterable<TransactionSignature>.byKeys() = map { it.by }.toSet()
|
||||
|
||||
// Allow Kotlin destructuring:
|
||||
// val (private, public) = keyPair
|
||||
/* The [PrivateKey] of this [KeyPair] .*/
|
||||
operator fun KeyPair.component1(): PrivateKey = this.private
|
||||
|
||||
/* The [PublicKey] of this [KeyPair] .*/
|
||||
operator fun KeyPair.component2(): PublicKey = this.public
|
||||
|
||||
/** A simple wrapper that will make it easier to swap out the EC algorithm we use in future. */
|
||||
@ -122,7 +141,7 @@ fun entropyToKeyPair(entropy: BigInteger): KeyPair = Crypto.deriveKeyPairFromEnt
|
||||
* if this signatureData algorithm is unable to process the input data provided, etc.
|
||||
* @throws IllegalArgumentException if the signature scheme is not supported for this private key or if any of the clear or signature data is empty.
|
||||
*/
|
||||
@Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class)
|
||||
@Throws(InvalidKeyException::class, SignatureException::class)
|
||||
fun PublicKey.verify(signatureData: ByteArray, clearData: ByteArray): Boolean = Crypto.doVerify(this, signatureData, clearData)
|
||||
|
||||
/**
|
||||
@ -135,7 +154,7 @@ fun PublicKey.verify(signatureData: ByteArray, clearData: ByteArray): Boolean =
|
||||
* if this signatureData algorithm is unable to process the input data provided, etc.
|
||||
* @throws IllegalArgumentException if the signature scheme is not supported for this private key or if any of the clear or signature data is empty.
|
||||
*/
|
||||
@Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class)
|
||||
@Throws(InvalidKeyException::class, SignatureException::class)
|
||||
fun KeyPair.verify(signatureData: ByteArray, clearData: ByteArray): Boolean = Crypto.doVerify(this.public, signatureData, clearData)
|
||||
|
||||
/**
|
||||
|
@ -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.
|
||||
|
@ -19,32 +19,87 @@ sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the hash value to an uppercase hexadecimal [String].
|
||||
*/
|
||||
override fun toString(): String = bytes.toHexString()
|
||||
|
||||
/**
|
||||
* Returns the first [prefixLen] hexadecimal digits of the [SecureHash] value.
|
||||
* @param prefixLen The number of characters in the prefix.
|
||||
*/
|
||||
fun prefixChars(prefixLen: Int = 6) = toString().substring(0, prefixLen)
|
||||
|
||||
/**
|
||||
* Append a second hash value to this hash value, and then compute the SHA-256 hash of the result.
|
||||
* @param other The hash to append to this one.
|
||||
*/
|
||||
fun hashConcat(other: SecureHash) = (this.bytes + other.bytes).sha256()
|
||||
|
||||
// Like static methods in Java, except the 'companion' is a singleton that can have state.
|
||||
companion object {
|
||||
/**
|
||||
* Converts a SHA-256 hash value represented as a hexadecimal [String] into a [SecureHash].
|
||||
* @param str A sequence of 64 hexadecimal digits that represents a SHA-256 hash value.
|
||||
* @throws IllegalArgumentException The input string does not contain 64 hexadecimal digits, or it contains incorrectly-encoded characters.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun parse(str: String) = str.toUpperCase().parseAsHex().let {
|
||||
when (it.size) {
|
||||
32 -> SHA256(it)
|
||||
else -> throw IllegalArgumentException("Provided string is ${it.size} bytes not 32 bytes in hex: $str")
|
||||
fun parse(str: String): SHA256 {
|
||||
return str.toUpperCase().parseAsHex().let {
|
||||
when (it.size) {
|
||||
32 -> SHA256(it)
|
||||
else -> throw IllegalArgumentException("Provided string is ${it.size} bytes not 32 bytes in hex: $str")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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())
|
||||
/**
|
||||
* Computes the SHA-256 hash value of the [ByteArray].
|
||||
* @param bytes The [ByteArray] to hash.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun sha256(bytes: ByteArray) = SHA256(MessageDigest.getInstance("SHA-256").digest(bytes))
|
||||
|
||||
@JvmStatic fun randomSHA256() = sha256(newSecureRandom().generateSeed(32))
|
||||
/**
|
||||
* Computes the SHA-256 hash of the [ByteArray], and then computes the SHA-256 hash of the hash.
|
||||
* @param bytes The [ByteArray] to hash.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun sha256Twice(bytes: ByteArray) = sha256(sha256(bytes).bytes)
|
||||
|
||||
/**
|
||||
* Computes the SHA-256 hash of the [String]'s UTF-8 byte contents.
|
||||
* @param str [String] whose UTF-8 contents will be hashed.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun sha256(str: String) = sha256(str.toByteArray())
|
||||
|
||||
/**
|
||||
* Generates a random SHA-256 value.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun randomSHA256() = sha256(newSecureRandom().generateSeed(32))
|
||||
|
||||
/**
|
||||
* A SHA-256 hash value consisting of 32 0x00 bytes.
|
||||
*/
|
||||
val zeroHash = SecureHash.SHA256(ByteArray(32, { 0.toByte() }))
|
||||
|
||||
/**
|
||||
* A SHA-256 hash value consisting of 32 0xFF bytes.
|
||||
*/
|
||||
val allOnesHash = SecureHash.SHA256(ByteArray(32, { 255.toByte() }))
|
||||
}
|
||||
|
||||
// In future, maybe SHA3, truncated hashes etc.
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the SHA-256 hash for the contents of the [ByteArray].
|
||||
*/
|
||||
fun ByteArray.sha256(): SecureHash.SHA256 = SecureHash.sha256(this)
|
||||
|
||||
/**
|
||||
* Compute the SHA-256 hash for the contents of the [OpaqueBytes].
|
||||
*/
|
||||
fun OpaqueBytes.sha256(): SecureHash.SHA256 = SecureHash.sha256(this.bytes)
|
||||
|
@ -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.
|
||||
|
@ -70,15 +70,7 @@ abstract class AbstractStateReplacementFlow {
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -136,7 +128,8 @@ abstract class AbstractStateReplacementFlow {
|
||||
// We use Void? instead of Unit? as that's what you'd use in Java.
|
||||
abstract class Acceptor<in T>(val initiatingSession: FlowSession,
|
||||
override val progressTracker: ProgressTracker = Acceptor.tracker()) : FlowLogic<Void?>() {
|
||||
constructor(initiatingSession: FlowSession) : this(initiatingSession, 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")
|
||||
@ -173,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)
|
||||
}
|
||||
|
||||
@ -194,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}" }
|
||||
}
|
||||
|
@ -61,11 +61,12 @@ import java.security.PublicKey
|
||||
* 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 sessionsToCollectFrom: Collection<FlowSession>,
|
||||
val myOptionalKeys: Iterable<PublicKey>?,
|
||||
override val progressTracker: ProgressTracker = CollectSignaturesFlow.tracker()) : FlowLogic<SignedTransaction>() {
|
||||
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.")
|
||||
@ -134,6 +135,7 @@ class CollectSignaturesFlow @JvmOverloads constructor (val partiallySignedTx: Si
|
||||
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))
|
||||
|
||||
@Suspendable
|
||||
override fun call(): List<TransactionSignature> {
|
||||
// SendTransactionFlow allows counterparty to access our data to resolve the transaction.
|
||||
@ -224,7 +226,7 @@ abstract class SignTransactionFlow(val otherSideSession: FlowSession,
|
||||
// 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
|
||||
|
@ -3,9 +3,6 @@ package net.corda.core.flows
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.internal.ContractUpgradeUtils
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import java.security.PublicKey
|
||||
|
||||
/**
|
||||
* A flow to be used for authorising and upgrading state objects of an old contract to a new contract.
|
||||
@ -16,30 +13,6 @@ import java.security.PublicKey
|
||||
* use the new updated state for future transactions.
|
||||
*/
|
||||
object ContractUpgradeFlow {
|
||||
|
||||
@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))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorise a contract state upgrade.
|
||||
*
|
||||
@ -49,11 +22,13 @@ object ContractUpgradeFlow {
|
||||
*
|
||||
* 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()
|
||||
@ -70,10 +45,12 @@ 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?>() {
|
||||
@Suspendable
|
||||
override fun call(): Void? {
|
||||
//DOCEND 2
|
||||
serviceHub.contractUpgradeService.removeAuthorisedContractUpgrade(stateRef)
|
||||
return null
|
||||
}
|
||||
@ -89,23 +66,6 @@ object ContractUpgradeFlow {
|
||||
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 = ContractUpgradeUtils.assembleBareTx(originalState, modification, PrivacySalt())
|
||||
|
@ -26,8 +26,8 @@ import net.corda.core.utilities.ProgressTracker
|
||||
*/
|
||||
@InitiatingFlow
|
||||
class FinalityFlow(val transaction: SignedTransaction,
|
||||
private val extraRecipients: Set<Party>,
|
||||
override val progressTracker: ProgressTracker) : FlowLogic<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)
|
||||
@ -88,7 +88,7 @@ class FinalityFlow(val transaction: 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
|
||||
}
|
||||
|
||||
private fun getPartiesToSend(ltx: LedgerTransaction): Set<Party> {
|
||||
|
@ -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"
|
||||
|
@ -1,11 +1,13 @@
|
||||
package net.corda.core.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import co.paralleluniverse.strands.Strand
|
||||
import net.corda.core.crypto.SecureHash
|
||||
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
|
||||
@ -15,6 +17,8 @@ import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.UntrustworthyData
|
||||
import net.corda.core.utilities.debug
|
||||
import org.slf4j.Logger
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* A sub-class of [FlowLogic<T>] implements a flow using direct, straight line blocking code. Thus you
|
||||
@ -42,6 +46,34 @@ abstract class FlowLogic<out T> {
|
||||
/** This is where you should log things to. */
|
||||
val logger: Logger get() = stateMachine.logger
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Return the outermost [FlowLogic] instance, or null if not in a flow.
|
||||
*/
|
||||
@JvmStatic
|
||||
val currentTopLevel: FlowLogic<*>? get() = (Strand.currentStrand() as? FlowStateMachine<*>)?.logic
|
||||
|
||||
/**
|
||||
* If on a flow, suspends the flow and only wakes it up after at least [duration] time has passed. Otherwise,
|
||||
* just sleep for [duration]. This sleep function is not designed to aid scheduling, for which you should
|
||||
* consider using [SchedulableState]. It is designed to aid with managing contention for which you have not
|
||||
* managed via another means.
|
||||
*
|
||||
* Warning: long sleeps and in general long running flows are highly discouraged, as there is currently no
|
||||
* support for flow migration! This method will throw an exception if you attempt to sleep for longer than
|
||||
* 5 minutes.
|
||||
*/
|
||||
@Suspendable
|
||||
@JvmStatic
|
||||
@Throws(FlowException::class)
|
||||
fun sleep(duration: Duration) {
|
||||
if (duration.compareTo(Duration.ofMinutes(5)) > 0) {
|
||||
throw FlowException("Attempt to sleep for longer than 5 minutes is not supported. Consider using SchedulableState.")
|
||||
}
|
||||
(Strand.currentStrand() as? FlowStateMachine<*>)?.sleepUntil(Instant.now() + duration) ?: Strand.sleep(duration.toMillis())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a wrapped [java.util.UUID] object that identifies this state machine run (i.e. subflows have the same
|
||||
* identifier as their parents).
|
||||
@ -55,6 +87,10 @@ 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)
|
||||
|
||||
@ -100,7 +136,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> {
|
||||
@ -116,7 +152,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
|
||||
@ -165,7 +201,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
|
||||
@ -173,6 +209,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.
|
||||
*
|
||||
@ -227,7 +295,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.
|
||||
@ -330,6 +397,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) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -347,4 +426,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.
|
||||
*
|
||||
@ -31,6 +42,10 @@ import net.corda.core.utilities.UntrustworthyData
|
||||
* 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>
|
||||
|
@ -13,7 +13,6 @@ import net.corda.core.node.services.NotaryService
|
||||
import net.corda.core.node.services.TrustedAuthorityNotaryService
|
||||
import net.corda.core.node.services.UniquenessProvider
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.FilteredTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.UntrustworthyData
|
||||
@ -55,11 +54,7 @@ class NotaryFlow {
|
||||
}
|
||||
|
||||
try {
|
||||
if (stx.isNotaryChangeTransaction()) {
|
||||
stx.resolveNotaryChangeTransaction(serviceHub).verifySignaturesExcept(notaryParty.owningKey)
|
||||
} else {
|
||||
stx.verifySignaturesExcept(notaryParty.owningKey)
|
||||
}
|
||||
stx.resolveTransactionWithSignatures(serviceHub).verifySignaturesExcept(notaryParty.owningKey)
|
||||
} catch (ex: SignatureException) {
|
||||
throw NotaryException(NotaryError.TransactionInvalid(ex))
|
||||
}
|
||||
@ -138,8 +133,11 @@ class NotaryFlow {
|
||||
// Check if transaction is intended to be signed by this notary.
|
||||
@Suspendable
|
||||
protected fun checkNotary(notary: Party?) {
|
||||
if (notary !in serviceHub.myInfo.legalIdentities)
|
||||
// TODO This check implies that it's OK to use the node's main identity. Shouldn't it be just limited to the
|
||||
// notary identities?
|
||||
if (notary !in serviceHub.myInfo.legalIdentities) {
|
||||
throw NotaryException(NotaryError.WrongNotary)
|
||||
}
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
@ -171,5 +169,5 @@ sealed class NotaryError {
|
||||
override fun toString() = cause.toString()
|
||||
}
|
||||
|
||||
object WrongNotary: NotaryError()
|
||||
object WrongNotary : NotaryError()
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ import java.security.SignatureException
|
||||
* This flow is a combination of [FlowSession.receive], resolve and [SignedTransaction.verify]. This flow will receive the
|
||||
* [SignedTransaction] and perform the resolution back-and-forth required to check the dependencies and download any missing
|
||||
* attachments. The flow will return the [SignedTransaction] after it is resolved and then verified using [SignedTransaction.verify].
|
||||
*
|
||||
*
|
||||
* @param otherSideSession session to the other side which is calling [SendTransactionFlow].
|
||||
* @param checkSufficientSignatures if true checks all required signatures are present. See [SignedTransaction.verify].
|
||||
*/
|
||||
|
@ -0,0 +1,12 @@
|
||||
package net.corda.core.flows
|
||||
|
||||
import kotlin.annotation.AnnotationTarget.CLASS
|
||||
|
||||
/**
|
||||
* Any [FlowLogic] which is to be started by the [AppServiceHub] interface from
|
||||
* within a [CordaService] must have this annotation. If it's missing the
|
||||
* flow will not be allowed to start and an exception will be thrown.
|
||||
*/
|
||||
@Target(CLASS)
|
||||
@MustBeDocumented
|
||||
annotation class StartableByService
|
@ -13,6 +13,7 @@ import java.security.PublicKey
|
||||
abstract class AbstractParty(val owningKey: PublicKey) {
|
||||
/** Anonymised parties do not include any detail apart from owning key, so equality is dependent solely on the key */
|
||||
override fun equals(other: Any?): Boolean = other === this || other is AbstractParty && other.owningKey == owningKey
|
||||
|
||||
override fun hashCode(): Int = owningKey.hashCode()
|
||||
abstract fun nameOrNull(): CordaX500Name?
|
||||
|
||||
|
@ -81,7 +81,7 @@ data class CordaX500Name(val commonName: String?,
|
||||
private val countryCodes: Set<String> = ImmutableSet.copyOf(Locale.getISOCountries())
|
||||
|
||||
@JvmStatic
|
||||
fun build(principal: X500Principal) : CordaX500Name {
|
||||
fun build(principal: X500Principal): CordaX500Name {
|
||||
val x500Name = X500Name.getInstance(principal.encoded)
|
||||
val attrsMap: Map<ASN1ObjectIdentifier, ASN1Encodable> = x500Name.rdNs
|
||||
.flatMap { it.typesAndValues.asList() }
|
||||
@ -109,7 +109,7 @@ data class CordaX500Name(val commonName: String?,
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun parse(name: String) : CordaX500Name = build(X500Principal(name))
|
||||
fun parse(name: String): CordaX500Name = build(X500Principal(name))
|
||||
}
|
||||
|
||||
@Transient
|
||||
|
@ -29,6 +29,7 @@ import java.security.cert.X509Certificate
|
||||
class Party(val name: CordaX500Name, owningKey: PublicKey) : AbstractParty(owningKey) {
|
||||
constructor(certificate: X509Certificate)
|
||||
: this(CordaX500Name.build(certificate.subjectX500Principal), Crypto.toSupportedPublicKey(certificate.publicKey))
|
||||
|
||||
override fun nameOrNull(): CordaX500Name = name
|
||||
fun anonymise(): AnonymousParty = AnonymousParty(owningKey)
|
||||
override fun ref(bytes: OpaqueBytes): PartyAndReference = PartyAndReference(this, bytes)
|
||||
|
@ -11,7 +11,9 @@ import java.security.cert.*
|
||||
*/
|
||||
@CordaSerializable
|
||||
class PartyAndCertificate(val certPath: CertPath) {
|
||||
@Transient val certificate: X509Certificate
|
||||
@Transient
|
||||
val certificate: X509Certificate
|
||||
|
||||
init {
|
||||
require(certPath.type == "X.509") { "Only X.509 certificates supported" }
|
||||
val certs = certPath.certificates
|
||||
@ -19,7 +21,8 @@ class PartyAndCertificate(val certPath: CertPath) {
|
||||
certificate = certs[0] as X509Certificate
|
||||
}
|
||||
|
||||
@Transient val party: Party = Party(certificate)
|
||||
@Transient
|
||||
val party: Party = Party(certificate)
|
||||
|
||||
val owningKey: PublicKey get() = party.owningKey
|
||||
val name: CordaX500Name get() = party.name
|
||||
|
@ -13,22 +13,38 @@ object Emoji {
|
||||
(System.getenv("TERM_PROGRAM") == "JediTerm" && System.getProperty("java.vendor") == "JetBrains s.r.o")
|
||||
}
|
||||
|
||||
@JvmStatic val CODE_SANTA_CLAUS: String = codePointsString(0x1F385)
|
||||
@JvmStatic val CODE_DIAMOND: String = codePointsString(0x1F537)
|
||||
@JvmStatic val CODE_BAG_OF_CASH: String = codePointsString(0x1F4B0)
|
||||
@JvmStatic val CODE_NEWSPAPER: String = codePointsString(0x1F4F0)
|
||||
@JvmStatic val CODE_RIGHT_ARROW: String = codePointsString(0x27A1, 0xFE0F)
|
||||
@JvmStatic val CODE_LEFT_ARROW: String = codePointsString(0x2B05, 0xFE0F)
|
||||
@JvmStatic val CODE_GREEN_TICK: String = codePointsString(0x2705)
|
||||
@JvmStatic val CODE_PAPERCLIP: String = codePointsString(0x1F4CE)
|
||||
@JvmStatic val CODE_COOL_GUY: String = codePointsString(0x1F60E)
|
||||
@JvmStatic val CODE_NO_ENTRY: String = codePointsString(0x1F6AB)
|
||||
@JvmStatic val CODE_SKULL_AND_CROSSBONES: String = codePointsString(0x2620)
|
||||
@JvmStatic val CODE_BOOKS: String = codePointsString(0x1F4DA)
|
||||
@JvmStatic val CODE_SLEEPING_FACE: String = codePointsString(0x1F634)
|
||||
@JvmStatic val CODE_LIGHTBULB: String = codePointsString(0x1F4A1)
|
||||
@JvmStatic val CODE_FREE: String = codePointsString(0x1F193)
|
||||
@JvmStatic val CODE_SOON: String = codePointsString(0x1F51C)
|
||||
@JvmStatic
|
||||
val CODE_SANTA_CLAUS: String = codePointsString(0x1F385)
|
||||
@JvmStatic
|
||||
val CODE_DIAMOND: String = codePointsString(0x1F537)
|
||||
@JvmStatic
|
||||
val CODE_BAG_OF_CASH: String = codePointsString(0x1F4B0)
|
||||
@JvmStatic
|
||||
val CODE_NEWSPAPER: String = codePointsString(0x1F4F0)
|
||||
@JvmStatic
|
||||
val CODE_RIGHT_ARROW: String = codePointsString(0x27A1, 0xFE0F)
|
||||
@JvmStatic
|
||||
val CODE_LEFT_ARROW: String = codePointsString(0x2B05, 0xFE0F)
|
||||
@JvmStatic
|
||||
val CODE_GREEN_TICK: String = codePointsString(0x2705)
|
||||
@JvmStatic
|
||||
val CODE_PAPERCLIP: String = codePointsString(0x1F4CE)
|
||||
@JvmStatic
|
||||
val CODE_COOL_GUY: String = codePointsString(0x1F60E)
|
||||
@JvmStatic
|
||||
val CODE_NO_ENTRY: String = codePointsString(0x1F6AB)
|
||||
@JvmStatic
|
||||
val CODE_SKULL_AND_CROSSBONES: String = codePointsString(0x2620)
|
||||
@JvmStatic
|
||||
val CODE_BOOKS: String = codePointsString(0x1F4DA)
|
||||
@JvmStatic
|
||||
val CODE_SLEEPING_FACE: String = codePointsString(0x1F634)
|
||||
@JvmStatic
|
||||
val CODE_LIGHTBULB: String = codePointsString(0x1F4A1)
|
||||
@JvmStatic
|
||||
val CODE_FREE: String = codePointsString(0x1F193)
|
||||
@JvmStatic
|
||||
val CODE_SOON: String = codePointsString(0x1F51C)
|
||||
|
||||
|
||||
/**
|
||||
|
@ -114,8 +114,7 @@ sealed class FetchDataFlow<T : NamedByHash, in W : Any>(
|
||||
|
||||
protected abstract fun load(txid: SecureHash): T?
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
protected open fun convert(wire: W): T = wire as T
|
||||
protected open fun convert(wire: W): T = uncheckedCast(wire)
|
||||
|
||||
private fun validateFetchResponse(maybeItems: UntrustworthyData<ArrayList<W>>,
|
||||
requests: List<SecureHash>): List<T> {
|
||||
|
@ -10,6 +10,7 @@ import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.UntrustworthyData
|
||||
import org.slf4j.Logger
|
||||
import java.time.Instant
|
||||
|
||||
/** This is an internal interface that is implemented by code in the node module. You should look at [FlowLogic]. */
|
||||
interface FlowStateMachine<R> {
|
||||
@ -30,25 +31,32 @@ interface FlowStateMachine<R> {
|
||||
fun <T : Any> receive(receiveType: Class<T>, otherParty: Party, sessionFlow: FlowLogic<*>): UntrustworthyData<T>
|
||||
|
||||
@Suspendable
|
||||
fun send(otherParty: Party, payload: Any, sessionFlow: FlowLogic<*>): Unit
|
||||
fun send(otherParty: Party, payload: Any, sessionFlow: FlowLogic<*>)
|
||||
|
||||
@Suspendable
|
||||
fun waitForLedgerCommit(hash: SecureHash, sessionFlow: FlowLogic<*>): SignedTransaction
|
||||
|
||||
fun checkFlowPermission(permissionName: String, extraAuditData: Map<String, String>): Unit
|
||||
@Suspendable
|
||||
fun sleepUntil(until: Instant)
|
||||
|
||||
fun recordAuditEvent(eventType: String, comment: String, extraAuditData: Map<String, String>): Unit
|
||||
fun checkFlowPermission(permissionName: String, extraAuditData: Map<String, String>)
|
||||
|
||||
fun recordAuditEvent(eventType: String, comment: String, extraAuditData: Map<String, String>)
|
||||
|
||||
@Suspendable
|
||||
fun flowStackSnapshot(flowClass: Class<out FlowLogic<*>>): FlowStackSnapshot?
|
||||
|
||||
@Suspendable
|
||||
fun persistFlowStackSnapshot(flowClass: Class<out FlowLogic<*>>): Unit
|
||||
fun persistFlowStackSnapshot(flowClass: Class<out FlowLogic<*>>)
|
||||
|
||||
val logic: FlowLogic<R>
|
||||
val serviceHub: ServiceHub
|
||||
val logger: Logger
|
||||
val id: StateMachineRunId
|
||||
val resultFuture: CordaFuture<R>
|
||||
val flowInitiator: FlowInitiator
|
||||
val ourIdentityAndCert: PartyAndCertificate
|
||||
|
||||
@Suspendable
|
||||
fun receiveAll(sessions: Map<FlowSession, Class<out Any>>, sessionFlow: FlowLogic<*>): Map<FlowSession, UntrustworthyData<Any>>
|
||||
}
|
||||
|
@ -1,7 +1,14 @@
|
||||
@file:JvmName("InternalUtils")
|
||||
|
||||
package net.corda.core.internal
|
||||
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.ServicesForResolution
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import org.bouncycastle.cert.X509CertificateHolder
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
|
||||
import org.slf4j.Logger
|
||||
@ -44,6 +51,7 @@ operator fun Duration.times(multiplicand: Long): Duration = multipliedBy(multipl
|
||||
* separator problems.
|
||||
*/
|
||||
operator fun Path.div(other: String): Path = resolve(other)
|
||||
|
||||
operator fun String.div(other: String): Path = Paths.get(this) / other
|
||||
|
||||
/**
|
||||
@ -99,6 +107,7 @@ fun Path.copyToDirectory(targetDir: Path, vararg options: CopyOption): Path {
|
||||
Files.copy(this, targetFile, *options)
|
||||
return targetFile
|
||||
}
|
||||
|
||||
fun Path.moveTo(target: Path, vararg options: CopyOption): Path = Files.move(this, target, *options)
|
||||
fun Path.isRegularFile(vararg options: LinkOption): Boolean = Files.isRegularFile(this, *options)
|
||||
fun Path.isDirectory(vararg options: LinkOption): Boolean = Files.isDirectory(this, *options)
|
||||
@ -226,25 +235,29 @@ private fun IntProgression.toSpliterator(): Spliterator.OfInt {
|
||||
|
||||
fun IntProgression.stream(parallel: Boolean = false): IntStream = StreamSupport.intStream(toSpliterator(), parallel)
|
||||
|
||||
@Suppress("UNCHECKED_CAST") // When toArray has filled in the array, the component type is no longer T? but T (that may itself be nullable).
|
||||
inline fun <reified T> Stream<out T>.toTypedArray() = toArray { size -> arrayOfNulls<T>(size) } as Array<T>
|
||||
// When toArray has filled in the array, the component type is no longer T? but T (that may itself be nullable):
|
||||
inline fun <reified T> Stream<out T>.toTypedArray(): Array<T> = uncheckedCast(toArray { size -> arrayOfNulls<T>(size) })
|
||||
|
||||
fun <T> Class<T>.castIfPossible(obj: Any): T? = if (isInstance(obj)) cast(obj) else null
|
||||
|
||||
/** Returns a [DeclaredField] wrapper around the declared (possibly non-public) static field of the receiver [Class]. */
|
||||
fun <T> Class<*>.staticField(name: String): DeclaredField<T> = DeclaredField(this, name, null)
|
||||
|
||||
/** Returns a [DeclaredField] wrapper around the declared (possibly non-public) static field of the receiver [KClass]. */
|
||||
fun <T> KClass<*>.staticField(name: String): DeclaredField<T> = DeclaredField(java, name, null)
|
||||
/** Returns a [DeclaredField] wrapper around the declared (possibly non-public) instance field of the receiver object. */
|
||||
|
||||
/** @suppress Returns a [DeclaredField] wrapper around the declared (possibly non-public) instance field of the receiver object. */
|
||||
fun <T> Any.declaredField(name: String): DeclaredField<T> = DeclaredField(javaClass, name, this)
|
||||
|
||||
/**
|
||||
* Returns a [DeclaredField] wrapper around the (possibly non-public) instance field of the receiver object, but declared
|
||||
* in its superclass [clazz].
|
||||
* @suppress
|
||||
*/
|
||||
fun <T> Any.declaredField(clazz: KClass<*>, name: String): DeclaredField<T> = DeclaredField(clazz.java, name, this)
|
||||
|
||||
/** creates a new instance if not a Kotlin object */
|
||||
fun <T: Any> KClass<T>.objectOrNewInstance(): T {
|
||||
fun <T : Any> KClass<T>.objectOrNewInstance(): T {
|
||||
return this.objectInstance ?: this.createInstance()
|
||||
}
|
||||
|
||||
@ -255,8 +268,7 @@ fun <T: Any> KClass<T>.objectOrNewInstance(): T {
|
||||
class DeclaredField<T>(clazz: Class<*>, name: String, private val receiver: Any?) {
|
||||
private val javaField = clazz.getDeclaredField(name).apply { isAccessible = true }
|
||||
var value: T
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
get() = javaField.get(receiver) as T
|
||||
get() = uncheckedCast<Any?, T>(javaField.get(receiver))
|
||||
set(value) = javaField.set(receiver, value)
|
||||
}
|
||||
|
||||
@ -273,4 +285,21 @@ annotation class VisibleForTesting
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T, U : T> uncheckedCast(obj: T) = obj as U
|
||||
|
||||
fun <K, V> Iterable<Pair<K, V>>.toMultiMap(): Map<K, List<V>> = this.groupBy({ it.first }) { it.second }
|
||||
fun <K, V> Iterable<Pair<K, V>>.toMultiMap(): Map<K, List<V>> = this.groupBy({ it.first }) { it.second }
|
||||
|
||||
/**
|
||||
* Provide access to internal method for AttachmentClassLoaderTests
|
||||
* @suppress
|
||||
*/
|
||||
fun TransactionBuilder.toWireTransaction(services: ServicesForResolution, serializationContext: SerializationContext): WireTransaction {
|
||||
return toWireTransactionWithContext(services, serializationContext)
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide access to internal method for AttachmentClassLoaderTests
|
||||
* @suppress
|
||||
*/
|
||||
fun TransactionBuilder.toLedgerTransaction(services: ServiceHub, serializationContext: SerializationContext) = toLedgerTransactionWithContext(services, serializationContext)
|
||||
|
||||
/** Convenience method to get the package name of a class literal. */
|
||||
val KClass<*>.packageName get() = java.`package`.name
|
||||
|
@ -27,6 +27,7 @@ class LazyPool<A>(
|
||||
STARTED,
|
||||
FINISHED
|
||||
}
|
||||
|
||||
private val lifeCycle = LifeCycle(State.STARTED)
|
||||
|
||||
private fun clearIfNeeded(instance: A): A {
|
||||
|
@ -18,6 +18,7 @@ class LazyStickyPool<A : Any>(
|
||||
private class InstanceBox<A> {
|
||||
var instance: LinkedBlockingQueue<A>? = null
|
||||
}
|
||||
|
||||
private val random = Random()
|
||||
private val boxes = Array(size) { InstanceBox<A>() }
|
||||
|
||||
|
@ -2,6 +2,7 @@ package net.corda.core.internal
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowException
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
@ -19,14 +20,15 @@ import java.util.*
|
||||
class ResolveTransactionsFlow(private val txHashes: Set<SecureHash>,
|
||||
private val otherSide: FlowSession) : FlowLogic<List<SignedTransaction>>() {
|
||||
/**
|
||||
* Resolves and validates the dependencies of the specified [signedTransaction]. Fetches the attachments, but does
|
||||
* *not* validate or store the [signedTransaction] itself.
|
||||
* Resolves and validates the dependencies of the specified [SignedTransaction]. Fetches the attachments, but does
|
||||
* *not* validate or store the [SignedTransaction] itself.
|
||||
*
|
||||
* @return a list of verified [SignedTransaction] objects, in a depth-first order.
|
||||
*/
|
||||
constructor(signedTransaction: SignedTransaction, otherSide: FlowSession) : this(dependencyIDs(signedTransaction), otherSide) {
|
||||
this.signedTransaction = signedTransaction
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun dependencyIDs(stx: SignedTransaction) = stx.inputs.map { it.txhash }.toSet()
|
||||
|
||||
@ -63,7 +65,7 @@ class ResolveTransactionsFlow(private val txHashes: Set<SecureHash>,
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
class ExcessivelyLargeTransactionGraph : Exception()
|
||||
class ExcessivelyLargeTransactionGraph : FlowException()
|
||||
|
||||
/** Transaction for fetch attachments for */
|
||||
private var signedTransaction: SignedTransaction? = null
|
||||
|
@ -0,0 +1,7 @@
|
||||
package net.corda.core.internal
|
||||
|
||||
import net.corda.core.contracts.CommandData
|
||||
import net.corda.core.contracts.ContractClassName
|
||||
|
||||
/** Indicates that this transaction replaces the inputs contract state to another contract state */
|
||||
data class UpgradeCommand(val upgradedContractClass: ContractClassName) : CommandData
|
@ -6,7 +6,7 @@ import kotlin.reflect.KProperty
|
||||
* A write-once property to be used as delegate for Kotlin var properties. The expectation is that this is initialised
|
||||
* prior to the spawning of any threads that may access it and so there's no need for it to be volatile.
|
||||
*/
|
||||
class WriteOnceProperty<T : Any>(private val defaultValue:T? = null) {
|
||||
class WriteOnceProperty<T : Any>(private val defaultValue: T? = null) {
|
||||
private var v: T? = defaultValue
|
||||
|
||||
operator fun getValue(thisRef: Any?, property: KProperty<*>) = v ?: throw IllegalStateException("Write-once property $property not set.")
|
||||
|
@ -46,6 +46,7 @@ class X509EdDSAEngine : Signature {
|
||||
override fun engineSetParameter(params: AlgorithmParameterSpec) = engine.setParameter(params)
|
||||
@Suppress("DEPRECATION")
|
||||
override fun engineGetParameter(param: String): Any = engine.getParameter(param)
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun engineSetParameter(param: String, value: Any?) = engine.setParameter(param, value)
|
||||
}
|
||||
|
@ -2,23 +2,28 @@ package net.corda.core.internal.cordapp
|
||||
|
||||
import net.corda.core.cordapp.Cordapp
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.node.CordaPluginRegistry
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.core.serialization.SerializationWhitelist
|
||||
import net.corda.core.serialization.SerializeAsToken
|
||||
import java.io.File
|
||||
import java.net.URL
|
||||
|
||||
data class CordappImpl(
|
||||
override val contractClassNames: List<String>,
|
||||
override val initiatedFlows: List<Class<out FlowLogic<*>>>,
|
||||
override val rpcFlows: List<Class<out FlowLogic<*>>>,
|
||||
override val serviceFlows: List<Class<out FlowLogic<*>>>,
|
||||
override val schedulableFlows: List<Class<out FlowLogic<*>>>,
|
||||
override val services: List<Class<out SerializeAsToken>>,
|
||||
override val plugins: List<CordaPluginRegistry>,
|
||||
override val serializationWhitelists: List<SerializationWhitelist>,
|
||||
override val customSchemas: Set<MappedSchema>,
|
||||
override val jarPath: URL) : Cordapp {
|
||||
override val name: String = File(jarPath.toURI()).name.removeSuffix(".jar")
|
||||
|
||||
/**
|
||||
* An exhaustive list of all classes relevant to the node within this CorDapp
|
||||
*
|
||||
* TODO: Also add [SchedulableFlow] as a Cordapp class
|
||||
*/
|
||||
override val cordappClasses = ((rpcFlows + initiatedFlows + services + plugins.map { javaClass }).map { it.name } + contractClassNames)
|
||||
override val cordappClasses = ((rpcFlows + initiatedFlows + services + serializationWhitelists.map { javaClass }).map { it.name } + contractClassNames)
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user