mirror of
https://github.com/corda/corda.git
synced 2025-01-14 16:59:52 +00:00
Merge remote-tracking branch 'corda-public/master'
This commit is contained in:
commit
f2df9d36fa
@ -3,7 +3,7 @@
|
||||
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
|
||||
<option name="MAIN_CLASS_NAME" value="net.corda.attachmentdemo.AttachmentDemoKt" />
|
||||
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " />
|
||||
<option name="PROGRAM_PARAMETERS" value="--role=RECIPIENT" />
|
||||
<option name="PROGRAM_PARAMETERS" value="--role=RECIPIENT --certificates="build/attachment-demo-nodes/Bank B/certificates"" />
|
||||
<option name="WORKING_DIRECTORY" value="" />
|
||||
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
|
||||
<option name="ALTERNATIVE_JRE_PATH" />
|
||||
@ -12,4 +12,4 @@
|
||||
<envs />
|
||||
<method />
|
||||
</configuration>
|
||||
</component>
|
||||
</component>
|
@ -3,7 +3,7 @@
|
||||
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
|
||||
<option name="MAIN_CLASS_NAME" value="net.corda.attachmentdemo.AttachmentDemoKt" />
|
||||
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " />
|
||||
<option name="PROGRAM_PARAMETERS" value="--role SENDER" />
|
||||
<option name="PROGRAM_PARAMETERS" value="--role SENDER --certificates="build/attachment-demo-nodes/Bank A/certificates"" />
|
||||
<option name="WORKING_DIRECTORY" value="" />
|
||||
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
|
||||
<option name="ALTERNATIVE_JRE_PATH" />
|
||||
@ -12,4 +12,4 @@
|
||||
<envs />
|
||||
<method />
|
||||
</configuration>
|
||||
</component>
|
||||
</component>
|
15
.idea/runConfigurations/Explorer___demo_nodes.xml
generated
Normal file
15
.idea/runConfigurations/Explorer___demo_nodes.xml
generated
Normal file
@ -0,0 +1,15 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Explorer - demo nodes" type="JetRunConfigurationType" factoryName="Kotlin" singleton="true">
|
||||
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
|
||||
<option name="MAIN_CLASS_NAME" value="net.corda.explorer.MainKt" />
|
||||
<option name="VM_PARAMETERS" value="" />
|
||||
<option name="PROGRAM_PARAMETERS" value="" />
|
||||
<option name="WORKING_DIRECTORY" value="" />
|
||||
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="true" />
|
||||
<option name="ALTERNATIVE_JRE_PATH" value="1.8" />
|
||||
<option name="PASS_PARENT_ENVS" value="true" />
|
||||
<module name="explorer_main" />
|
||||
<envs />
|
||||
<method />
|
||||
</configuration>
|
||||
</component>
|
15
.idea/runConfigurations/Explorer___demo_nodes__simulation_.xml
generated
Normal file
15
.idea/runConfigurations/Explorer___demo_nodes__simulation_.xml
generated
Normal file
@ -0,0 +1,15 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Explorer - demo nodes (simulation)" type="JetRunConfigurationType" factoryName="Kotlin" singleton="true">
|
||||
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
|
||||
<option name="MAIN_CLASS_NAME" value="net.corda.explorer.MainKt" />
|
||||
<option name="VM_PARAMETERS" value="" />
|
||||
<option name="PROGRAM_PARAMETERS" value="-S" />
|
||||
<option name="WORKING_DIRECTORY" value="" />
|
||||
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="true" />
|
||||
<option name="ALTERNATIVE_JRE_PATH" value="1.8" />
|
||||
<option name="PASS_PARENT_ENVS" value="true" />
|
||||
<module name="explorer_main" />
|
||||
<envs />
|
||||
<method />
|
||||
</configuration>
|
||||
</component>
|
15
.idea/runConfigurations/Notary_Demo__Run_Nodes.xml
generated
15
.idea/runConfigurations/Notary_Demo__Run_Nodes.xml
generated
@ -1,15 +0,0 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Notary Demo: Run Nodes" type="JetRunConfigurationType" factoryName="Kotlin" singleton="true">
|
||||
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
|
||||
<option name="MAIN_CLASS_NAME" value="net.corda.notarydemo.MainKt" />
|
||||
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " />
|
||||
<option name="PROGRAM_PARAMETERS" value="" />
|
||||
<option name="WORKING_DIRECTORY" value="" />
|
||||
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
|
||||
<option name="ALTERNATIVE_JRE_PATH" />
|
||||
<option name="PASS_PARENT_ENVS" value="true" />
|
||||
<module name="raft-notary-demo_main" />
|
||||
<envs />
|
||||
<method />
|
||||
</configuration>
|
||||
</component>
|
@ -1,15 +0,0 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Notary Demo: Run Notarisation" type="JetRunConfigurationType" factoryName="Kotlin" singleton="true">
|
||||
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
|
||||
<option name="MAIN_CLASS_NAME" value="net.corda.notarydemo.NotaryDemoKt" />
|
||||
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " />
|
||||
<option name="PROGRAM_PARAMETERS" value="" />
|
||||
<option name="WORKING_DIRECTORY" value="" />
|
||||
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
|
||||
<option name="ALTERNATIVE_JRE_PATH" />
|
||||
<option name="PASS_PARENT_ENVS" value="true" />
|
||||
<module name="raft-notary-demo_main" />
|
||||
<envs />
|
||||
<method />
|
||||
</configuration>
|
||||
</component>
|
15
.idea/runConfigurations/Raft_Notary_Demo__Run_Nodes.xml
generated
Normal file
15
.idea/runConfigurations/Raft_Notary_Demo__Run_Nodes.xml
generated
Normal file
@ -0,0 +1,15 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Raft Notary Demo: Run Nodes" type="JetRunConfigurationType" factoryName="Kotlin" singleton="true">
|
||||
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
|
||||
<option name="MAIN_CLASS_NAME" value="net.corda.notarydemo.MainKt" />
|
||||
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " />
|
||||
<option name="PROGRAM_PARAMETERS" value="" />
|
||||
<option name="WORKING_DIRECTORY" value="" />
|
||||
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
|
||||
<option name="ALTERNATIVE_JRE_PATH" />
|
||||
<option name="PASS_PARENT_ENVS" value="true" />
|
||||
<module name="raft-notary-demo_main" />
|
||||
<envs />
|
||||
<method />
|
||||
</configuration>
|
||||
</component>
|
15
.idea/runConfigurations/Raft_Notary_Demo__Run_Notarisation.xml
generated
Normal file
15
.idea/runConfigurations/Raft_Notary_Demo__Run_Notarisation.xml
generated
Normal file
@ -0,0 +1,15 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Raft Notary Demo: Run Notarisation" type="JetRunConfigurationType" factoryName="Kotlin" singleton="true">
|
||||
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
|
||||
<option name="MAIN_CLASS_NAME" value="net.corda.notarydemo.NotaryDemoKt" />
|
||||
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " />
|
||||
<option name="PROGRAM_PARAMETERS" value="" />
|
||||
<option name="WORKING_DIRECTORY" value="" />
|
||||
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
|
||||
<option name="ALTERNATIVE_JRE_PATH" />
|
||||
<option name="PASS_PARENT_ENVS" value="true" />
|
||||
<module name="raft-notary-demo_main" />
|
||||
<envs />
|
||||
<method />
|
||||
</configuration>
|
||||
</component>
|
@ -3,7 +3,7 @@
|
||||
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
|
||||
<option name="MAIN_CLASS_NAME" value="net.corda.traderdemo.TraderDemoKt" />
|
||||
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " />
|
||||
<option name="PROGRAM_PARAMETERS" value="--role BUYER" />
|
||||
<option name="PROGRAM_PARAMETERS" value="--role BUYER --certificates="build/trader-demo-nodes/Bank A/certificates"" />
|
||||
<option name="WORKING_DIRECTORY" value="" />
|
||||
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
|
||||
<option name="ALTERNATIVE_JRE_PATH" />
|
||||
@ -12,4 +12,4 @@
|
||||
<envs />
|
||||
<method />
|
||||
</configuration>
|
||||
</component>
|
||||
</component>
|
@ -3,7 +3,7 @@
|
||||
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
|
||||
<option name="MAIN_CLASS_NAME" value="net.corda.traderdemo.TraderDemoKt" />
|
||||
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " />
|
||||
<option name="PROGRAM_PARAMETERS" value="--role SELLER" />
|
||||
<option name="PROGRAM_PARAMETERS" value="--role SELLER --certificates="build/trader-demo-nodes/Bank B/certificates"" />
|
||||
<option name="WORKING_DIRECTORY" value="" />
|
||||
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
|
||||
<option name="ALTERNATIVE_JRE_PATH" />
|
||||
@ -12,4 +12,4 @@
|
||||
<envs />
|
||||
<method />
|
||||
</configuration>
|
||||
</component>
|
||||
</component>
|
16
README.md
16
README.md
@ -19,7 +19,13 @@ Read our full and planned feature list [here](https://docs.corda.net/inthebox.ht
|
||||
|
||||
Firstly, read the [Getting started](https://docs.corda.net/getting-set-up.html) documentation.
|
||||
|
||||
Watching the following webinars will give you a great introduction to Corda:
|
||||
Next, use the following guides to set up your dev environment:
|
||||
|
||||
* If you are on **Windows** [use this getting started guide](https://www.corda.net/wp-content/uploads/2017/01/Corda-Windows-Quick-start-guide-1.pdf) which also explains through how to run the sample apps.
|
||||
|
||||
* Alternatively if you are on **Mac/Linux**, [watch this brief Webinar](https://vimeo.com/200167665) which walks through getting Corda, installing it, building it, running nodes and opening projects in IntelliJ.
|
||||
|
||||
After the above, watching the following webinars will give you a great introduction to Corda:
|
||||
|
||||
### Webinar 1 – [Introduction to Corda](https://vimeo.com/192757743/c2ec39c1e1)
|
||||
|
||||
@ -72,3 +78,11 @@ Please read [here](./CONTRIBUTING.md).
|
||||
## License
|
||||
|
||||
[Apache 2.0](./LICENCE)
|
||||
|
||||
## 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.
|
||||
|
63
build.gradle
63
build.gradle
@ -4,19 +4,32 @@ buildscript {
|
||||
file("publish.properties").withInputStream { props.load(it) }
|
||||
|
||||
// Our version: bump this on release.
|
||||
ext.corda_version = "0.7-SNAPSHOT"
|
||||
ext.corda_version = "0.8-SNAPSHOT"
|
||||
ext.gradle_plugins_version = props.getProperty("gradlePluginsVersion")
|
||||
|
||||
// Dependency versions. Can run 'gradle dependencyUpdates' to find new versions of things.
|
||||
//
|
||||
// TODO: Sort this alphabetically.
|
||||
ext.kotlin_version = '1.0.5-2'
|
||||
ext.quasar_version = '0.7.6'
|
||||
ext.quasar_version = '0.7.6' // TODO: Upgrade to 0.7.7+ when Quasar bug 238 is resolved.
|
||||
ext.asm_version = '0.5.3'
|
||||
ext.artemis_version = '1.4.0'
|
||||
ext.jackson_version = '2.8.0.rc2'
|
||||
ext.artemis_version = '1.5.1'
|
||||
ext.jackson_version = '2.8.5'
|
||||
ext.jetty_version = '9.3.9.v20160517'
|
||||
ext.jersey_version = '2.23.1'
|
||||
ext.jolokia_version = '2.0.0-M1'
|
||||
ext.assertj_version = '3.5.1'
|
||||
ext.log4j_version = '2.6.2'
|
||||
ext.bouncycastle_version = '1.54'
|
||||
ext.jersey_version = '2.25'
|
||||
ext.jolokia_version = '2.0.0-M3'
|
||||
ext.assertj_version = '3.6.1'
|
||||
ext.log4j_version = '2.7'
|
||||
ext.bouncycastle_version = '1.56'
|
||||
ext.guava_version = '19.0'
|
||||
ext.quickcheck_version = '0.7'
|
||||
ext.okhttp_version = '3.5.0'
|
||||
ext.typesafe_config_version = '1.3.1'
|
||||
ext.junit_version = '4.12'
|
||||
ext.jopt_simple_version = '5.0.2'
|
||||
ext.jansi_version = '1.14'
|
||||
ext.hibernate_version = '5.2.6.Final'
|
||||
ext.dokka_version = '0.9.13'
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
@ -33,9 +46,8 @@ 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"
|
||||
|
||||
// Can run 'gradle dependencyUpdates' to find new versions of things.
|
||||
classpath 'com.github.ben-manes:gradle-versions-plugin:0.12.0'
|
||||
classpath 'com.github.ben-manes:gradle-versions-plugin:0.13.0'
|
||||
classpath "org.jetbrains.dokka:dokka-gradle-plugin:${dokka_version}"
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,6 +63,7 @@ apply plugin: 'com.github.ben-manes.versions'
|
||||
apply plugin: 'net.corda.plugins.publish-utils'
|
||||
apply plugin: 'net.corda.plugins.quasar-utils'
|
||||
apply plugin: 'net.corda.plugins.cordformation'
|
||||
apply plugin: 'org.jetbrains.dokka'
|
||||
|
||||
// We need the following three lines even though they're inside an allprojects {} block below because otherwise
|
||||
// IntelliJ gets confused when importing the project and ends up erasing and recreating the .idea directory, along
|
||||
@ -95,7 +108,7 @@ repositories {
|
||||
// Required for building out the fat JAR.
|
||||
dependencies {
|
||||
compile project(':node')
|
||||
compile "com.google.guava:guava:19.0"
|
||||
compile "com.google.guava:guava:$guava_version"
|
||||
runtime project(path: ":node:capsule", configuration: 'runtimeArtifacts')
|
||||
}
|
||||
|
||||
@ -129,7 +142,6 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['build']) {
|
||||
networkMap "Controller"
|
||||
node {
|
||||
name "Controller"
|
||||
dirName "controller"
|
||||
nearestCity "London"
|
||||
advertisedServices = ["corda.notary.validating"]
|
||||
artemisPort 10002
|
||||
@ -138,7 +150,6 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['build']) {
|
||||
}
|
||||
node {
|
||||
name "Bank A"
|
||||
dirName "nodea"
|
||||
nearestCity "London"
|
||||
advertisedServices = []
|
||||
artemisPort 10004
|
||||
@ -147,7 +158,6 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['build']) {
|
||||
}
|
||||
node {
|
||||
name "Bank B"
|
||||
dirName "nodeb"
|
||||
nearestCity "New York"
|
||||
advertisedServices = []
|
||||
artemisPort 10006
|
||||
@ -177,4 +187,23 @@ bintrayConfig {
|
||||
name = 'R3'
|
||||
email = 'dev@corda.net'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// API docs
|
||||
|
||||
dokka {
|
||||
moduleName = 'corda'
|
||||
outputDirectory = 'docs/build/html/api/kotlin'
|
||||
processConfigurations = ['compile']
|
||||
sourceDirs = files('core/src/main/kotlin', 'client/src/main/kotlin', 'node/src/main/kotlin', 'finance/src/main/kotlin')
|
||||
}
|
||||
|
||||
task dokkaJavadoc(type: org.jetbrains.dokka.gradle.DokkaTask) {
|
||||
moduleName = 'corda'
|
||||
outputFormat = "javadoc"
|
||||
outputDirectory = 'docs/build/html/api/javadoc'
|
||||
processConfigurations = ['compile']
|
||||
sourceDirs = files('core/src/main/kotlin', 'client/src/main/kotlin', 'node/src/main/kotlin', 'finance/src/main/kotlin')
|
||||
}
|
||||
|
||||
task apidocs(dependsOn: ['dokka', 'dokkaJavadoc'])
|
@ -5,5 +5,6 @@ repositories {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile "com.google.guava:guava:19.0"
|
||||
// Cannot use ext.guava_version here :(
|
||||
compile "com.google.guava:guava:20.0"
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ dependencies {
|
||||
compile "org.apache.logging.log4j:log4j-slf4j-impl:${log4j_version}"
|
||||
compile "org.apache.logging.log4j:log4j-core:${log4j_version}"
|
||||
|
||||
compile "com.google.guava:guava:19.0"
|
||||
compile "com.google.guava:guava:$guava_version"
|
||||
|
||||
// ReactFX: Functional reactive UI programming.
|
||||
compile 'org.reactfx:reactfx:2.0-M5'
|
||||
@ -61,16 +61,16 @@ dependencies {
|
||||
compile "org.apache.activemq:artemis-core-client:${artemis_version}"
|
||||
|
||||
// Unit testing helpers.
|
||||
testCompile 'junit:junit:4.12'
|
||||
testCompile "junit:junit:$junit_version"
|
||||
testCompile "org.assertj:assertj-core:${assertj_version}"
|
||||
|
||||
testCompile project(':test-utils')
|
||||
|
||||
// Integration test helpers
|
||||
integrationTestCompile 'junit:junit:4.12'
|
||||
integrationTestCompile "junit:junit:$junit_version"
|
||||
}
|
||||
|
||||
task integrationTest(type: Test) {
|
||||
testClassesDir = sourceSets.integrationTest.output.classesDir
|
||||
classpath = sourceSets.integrationTest.runtimeClasspath
|
||||
}
|
||||
}
|
||||
|
@ -8,48 +8,26 @@ import net.corda.core.random63BitValue
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.flows.CashCommand
|
||||
import net.corda.flows.CashFlow
|
||||
import net.corda.node.driver.NodeInfoAndConfig
|
||||
import net.corda.node.driver.DriverBasedTest
|
||||
import net.corda.node.driver.NodeHandle
|
||||
import net.corda.node.driver.driver
|
||||
import net.corda.node.services.User
|
||||
import net.corda.node.services.config.configureTestSSL
|
||||
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.toHostAndPort
|
||||
import net.corda.node.services.messaging.CordaRPCClient
|
||||
import net.corda.node.services.startFlowPermission
|
||||
import net.corda.node.services.transactions.ValidatingNotaryService
|
||||
import org.apache.activemq.artemis.api.core.ActiveMQSecurityException
|
||||
import org.assertj.core.api.Assertions.assertThatExceptionOfType
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
class CordaRPCClientTest {
|
||||
|
||||
class CordaRPCClientTest : DriverBasedTest() {
|
||||
private val rpcUser = User("user1", "test", permissions = setOf(startFlowPermission<CashFlow>()))
|
||||
private val stopDriver = CountDownLatch(1)
|
||||
private var driverThread: Thread? = null
|
||||
private lateinit var node: NodeHandle
|
||||
private lateinit var client: CordaRPCClient
|
||||
private lateinit var driverInfo: NodeInfoAndConfig
|
||||
|
||||
@Before
|
||||
fun start() {
|
||||
val driverStarted = CountDownLatch(1)
|
||||
driverThread = thread {
|
||||
driver(isDebug = true) {
|
||||
driverInfo = startNode(rpcUsers = listOf(rpcUser), advertisedServices = setOf(ServiceInfo(ValidatingNotaryService.type))).getOrThrow()
|
||||
client = CordaRPCClient(toHostAndPort(driverInfo.nodeInfo.address), configureTestSSL())
|
||||
driverStarted.countDown()
|
||||
stopDriver.await()
|
||||
}
|
||||
}
|
||||
driverStarted.await()
|
||||
}
|
||||
|
||||
@After
|
||||
fun stop() {
|
||||
stopDriver.countDown()
|
||||
driverThread?.join()
|
||||
override fun setup() = driver(isDebug = true) {
|
||||
node = startNode(rpcUsers = listOf(rpcUser), advertisedServices = setOf(ServiceInfo(ValidatingNotaryService.type))).getOrThrow()
|
||||
client = node.rpcClientToNode()
|
||||
runTest()
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -78,7 +56,9 @@ class CordaRPCClientTest {
|
||||
println("Creating proxy")
|
||||
val proxy = client.proxy()
|
||||
println("Starting flow")
|
||||
val flowHandle = proxy.startFlow(::CashFlow, CashCommand.IssueCash(20.DOLLARS, OpaqueBytes.of(0), driverInfo.nodeInfo.legalIdentity, driverInfo.nodeInfo.legalIdentity))
|
||||
val flowHandle = proxy.startFlow(
|
||||
::CashFlow,
|
||||
CashCommand.IssueCash(20.DOLLARS, OpaqueBytes.of(0), node.nodeInfo.legalIdentity, node.nodeInfo.legalIdentity))
|
||||
println("Started flow, waiting on result")
|
||||
flowHandle.progress.subscribe {
|
||||
println("PROGRESS $it")
|
||||
|
@ -9,7 +9,9 @@ import net.corda.core.contracts.PartyAndReference
|
||||
import net.corda.core.contracts.USD
|
||||
import net.corda.core.flows.StateMachineRunId
|
||||
import net.corda.core.getOrThrow
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.messaging.StateMachineUpdate
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.services.NetworkMapCache
|
||||
import net.corda.core.node.services.ServiceInfo
|
||||
@ -19,6 +21,7 @@ import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.flows.CashCommand
|
||||
import net.corda.flows.CashFlow
|
||||
import net.corda.node.driver.DriverBasedTest
|
||||
import net.corda.node.driver.driver
|
||||
import net.corda.node.services.User
|
||||
import net.corda.node.services.config.configureTestSSL
|
||||
@ -29,63 +32,42 @@ import net.corda.node.services.transactions.SimpleNotaryService
|
||||
import net.corda.testing.expect
|
||||
import net.corda.testing.expectEvents
|
||||
import net.corda.testing.sequence
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import rx.Observable
|
||||
import rx.Observer
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
class NodeMonitorModelTest {
|
||||
class NodeMonitorModelTest : DriverBasedTest() {
|
||||
lateinit var aliceNode: NodeInfo
|
||||
lateinit var notaryNode: NodeInfo
|
||||
val stopDriver = CountDownLatch(1)
|
||||
var driverThread: Thread? = null
|
||||
|
||||
lateinit var rpc: CordaRPCOps
|
||||
lateinit var stateMachineTransactionMapping: Observable<StateMachineTransactionMapping>
|
||||
lateinit var stateMachineUpdates: Observable<StateMachineUpdate>
|
||||
lateinit var progressTracking: Observable<ProgressTrackingEvent>
|
||||
lateinit var transactions: Observable<SignedTransaction>
|
||||
lateinit var vaultUpdates: Observable<Vault.Update>
|
||||
lateinit var networkMapUpdates: Observable<NetworkMapCache.MapChange>
|
||||
lateinit var clientToService: Observer<CashCommand>
|
||||
lateinit var newNode: (String) -> NodeInfo
|
||||
|
||||
@Before
|
||||
fun start() {
|
||||
val driverStarted = CountDownLatch(1)
|
||||
driverThread = thread {
|
||||
driver {
|
||||
val cashUser = User("user1", "test", permissions = setOf(startFlowPermission<CashFlow>()))
|
||||
val aliceNodeFuture = startNode("Alice", rpcUsers = listOf(cashUser))
|
||||
val notaryNodeFuture = startNode("Notary", advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)))
|
||||
override fun setup() = driver {
|
||||
val cashUser = User("user1", "test", permissions = setOf(startFlowPermission<CashFlow>()))
|
||||
val aliceNodeFuture = startNode("Alice", rpcUsers = listOf(cashUser))
|
||||
val notaryNodeFuture = startNode("Notary", advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)))
|
||||
|
||||
aliceNode = aliceNodeFuture.getOrThrow().nodeInfo
|
||||
notaryNode = notaryNodeFuture.getOrThrow().nodeInfo
|
||||
newNode = { nodeName -> startNode(nodeName).getOrThrow().nodeInfo }
|
||||
val monitor = NodeMonitorModel()
|
||||
aliceNode = aliceNodeFuture.getOrThrow().nodeInfo
|
||||
notaryNode = notaryNodeFuture.getOrThrow().nodeInfo
|
||||
newNode = { nodeName -> startNode(nodeName).getOrThrow().nodeInfo }
|
||||
val monitor = NodeMonitorModel()
|
||||
|
||||
stateMachineTransactionMapping = monitor.stateMachineTransactionMapping.bufferUntilSubscribed()
|
||||
stateMachineUpdates = monitor.stateMachineUpdates.bufferUntilSubscribed()
|
||||
progressTracking = monitor.progressTracking.bufferUntilSubscribed()
|
||||
transactions = monitor.transactions.bufferUntilSubscribed()
|
||||
vaultUpdates = monitor.vaultUpdates.bufferUntilSubscribed()
|
||||
networkMapUpdates = monitor.networkMap.bufferUntilSubscribed()
|
||||
clientToService = monitor.clientToService
|
||||
stateMachineTransactionMapping = monitor.stateMachineTransactionMapping.bufferUntilSubscribed()
|
||||
stateMachineUpdates = monitor.stateMachineUpdates.bufferUntilSubscribed()
|
||||
progressTracking = monitor.progressTracking.bufferUntilSubscribed()
|
||||
transactions = monitor.transactions.bufferUntilSubscribed()
|
||||
vaultUpdates = monitor.vaultUpdates.bufferUntilSubscribed()
|
||||
networkMapUpdates = monitor.networkMap.bufferUntilSubscribed()
|
||||
|
||||
monitor.register(ArtemisMessagingComponent.toHostAndPort(aliceNode.address), configureTestSSL(), cashUser.username, cashUser.password)
|
||||
driverStarted.countDown()
|
||||
stopDriver.await()
|
||||
}
|
||||
}
|
||||
driverStarted.await()
|
||||
}
|
||||
|
||||
@After
|
||||
fun stop() {
|
||||
stopDriver.countDown()
|
||||
driverThread?.join()
|
||||
monitor.register(ArtemisMessagingComponent.toHostAndPort(aliceNode.address), configureTestSSL(), cashUser.username, cashUser.password)
|
||||
rpc = monitor.proxyObservable.value!!
|
||||
runTest()
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -112,7 +94,7 @@ class NodeMonitorModelTest {
|
||||
|
||||
@Test
|
||||
fun `cash issue works end to end`() {
|
||||
clientToService.onNext(CashCommand.IssueCash(
|
||||
rpc.startFlow(::CashFlow, CashCommand.IssueCash(
|
||||
amount = Amount(100, USD),
|
||||
issueRef = OpaqueBytes(ByteArray(1, { 1 })),
|
||||
recipient = aliceNode.legalIdentity,
|
||||
@ -137,14 +119,14 @@ class NodeMonitorModelTest {
|
||||
|
||||
@Test
|
||||
fun `cash issue and move`() {
|
||||
clientToService.onNext(CashCommand.IssueCash(
|
||||
rpc.startFlow(::CashFlow, CashCommand.IssueCash(
|
||||
amount = Amount(100, USD),
|
||||
issueRef = OpaqueBytes(ByteArray(1, { 1 })),
|
||||
recipient = aliceNode.legalIdentity,
|
||||
notary = notaryNode.notaryIdentity
|
||||
))
|
||||
|
||||
clientToService.onNext(CashCommand.PayCash(
|
||||
rpc.startFlow(::CashFlow, CashCommand.PayCash(
|
||||
amount = Amount(100, Issued(PartyAndReference(aliceNode.legalIdentity, OpaqueBytes(ByteArray(1, { 1 }))), USD)),
|
||||
recipient = aliceNode.legalIdentity
|
||||
))
|
||||
|
@ -7,6 +7,7 @@ import javafx.collections.FXCollections
|
||||
import javafx.collections.ObservableList
|
||||
import javafx.collections.ObservableMap
|
||||
import rx.Observable
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Simple utilities for converting an [rx.Observable] into a javafx [ObservableValue]/[ObservableList]
|
||||
@ -29,76 +30,46 @@ fun <A, B> Observable<A>.foldToObservableValue(initial: B, folderFun: (A, B) ->
|
||||
}
|
||||
|
||||
/**
|
||||
* [foldToObservableList] takes an [rx.Observable] stream and creates an [ObservableList] out of it, while maintaining
|
||||
* an accumulator.
|
||||
* @param initialAccumulator The initial value of the accumulator.
|
||||
* [fold] takes an [rx.Observable] stream and applies fold function on it, and collects all elements using the accumulator.
|
||||
* @param accumulator The accumulator for accumulating elements.
|
||||
* @param folderFun The transformation function to be called on the observable list when a new element is emitted on
|
||||
* the stream, which should modify the list as needed.
|
||||
*/
|
||||
fun <A, B, C> Observable<A>.foldToObservableList(
|
||||
initialAccumulator: C, folderFun: (A, C, ObservableList<B>) -> C
|
||||
): ObservableList<B> {
|
||||
val result = FXCollections.observableArrayList<B>()
|
||||
fun <T, R> Observable<T>.fold(accumulator: R, folderFun: (R, T) -> Unit): R {
|
||||
/**
|
||||
* This capture is fine, as [Platform.runLater] runs closures in order
|
||||
* This capture is fine, as [Platform.runLater] runs closures in order.
|
||||
* The buffer is to avoid flooding FX thread with runnable.
|
||||
*/
|
||||
var currentAccumulator = initialAccumulator
|
||||
subscribe {
|
||||
Platform.runLater {
|
||||
currentAccumulator = folderFun(it, currentAccumulator, result)
|
||||
buffer(1, TimeUnit.SECONDS).subscribe {
|
||||
if (it.isNotEmpty()) {
|
||||
Platform.runLater {
|
||||
it.fold(accumulator) { list, item ->
|
||||
folderFun.invoke(list, item)
|
||||
list
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
return accumulator
|
||||
}
|
||||
|
||||
/**
|
||||
* [recordInSequence] records incoming events on the [rx.Observable] in sequence.
|
||||
*/
|
||||
fun <A> Observable<A>.recordInSequence(): ObservableList<A> {
|
||||
return foldToObservableList(Unit) { newElement, _unit, list ->
|
||||
return fold(FXCollections.observableArrayList()) { list, newElement ->
|
||||
list.add(newElement)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [foldToObservableMap] takes an [rx.Observable] stream and creates an [ObservableMap] out of it, while maintaining
|
||||
* an accumulator.
|
||||
* @param initialAccumulator The initial value of the accumulator.
|
||||
* @param folderFun The transformation function to be called on the observable map when a new element is emitted on
|
||||
* the stream, which should modify the map as needed.
|
||||
*/
|
||||
fun <A, B, K, C> Observable<A>.foldToObservableMap(
|
||||
initialAccumulator: C, folderFun: (A, C, ObservableMap<K, B>) -> C
|
||||
): ObservableMap<K, out B> {
|
||||
val result = FXCollections.observableHashMap<K, B>()
|
||||
/**
|
||||
* This capture is fine, as [Platform.runLater] runs closures in order
|
||||
*/
|
||||
var currentAccumulator = initialAccumulator
|
||||
subscribe {
|
||||
Platform.runLater {
|
||||
currentAccumulator = folderFun(it, currentAccumulator, result)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* This variant simply associates each event with its key.
|
||||
* @param toKey Function retrieving the key to associate with.
|
||||
* @param merge The function to be called if there is an existing element at the key.
|
||||
*/
|
||||
fun <A, K> Observable<A>.recordAsAssociation(
|
||||
toKey: (A) -> K,
|
||||
merge: (K, oldValue: A, newValue: A) -> A = { _key, _oldValue, newValue -> newValue }
|
||||
): ObservableMap<K, out A> {
|
||||
return foldToObservableMap(Unit) { newElement, _unit, map ->
|
||||
val key = toKey(newElement)
|
||||
val oldValue = map.get(key)
|
||||
if (oldValue != null) {
|
||||
map.set(key, merge(key, oldValue, newElement))
|
||||
} else {
|
||||
map.set(key, newElement)
|
||||
}
|
||||
fun <A, K> Observable<A>.recordAsAssociation(toKey: (A) -> K, merge: (K, oldValue: A, newValue: A) -> A = { _key, _oldValue, newValue -> newValue }): ObservableMap<K, A> {
|
||||
return fold(FXCollections.observableHashMap<K, A>()) { map, item ->
|
||||
val key = toKey(item)
|
||||
map[key] = map[key]?.let { merge(key, it, item) } ?: item
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import net.corda.core.crypto.Party
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.flows.CashCommand
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* [Generator]s for incoming/outgoing events to/from the [WalletMonitorService]. Internally it keeps track of owned
|
||||
@ -13,15 +14,15 @@ import net.corda.flows.CashCommand
|
||||
*/
|
||||
class EventGenerator(
|
||||
val parties: List<Party>,
|
||||
val notary: Party
|
||||
val notary: Party,
|
||||
val currencies: List<Currency> = listOf(USD, GBP, CHF),
|
||||
val issuers: List<Party> = parties
|
||||
) {
|
||||
|
||||
private var vault = listOf<StateAndRef<Cash.State>>()
|
||||
|
||||
val issuerGenerator =
|
||||
Generator.pickOne(parties).combine(Generator.intRange(0, 1)) { party, ref -> party.ref(ref.toByte()) }
|
||||
Generator.pickOne(issuers).combine(Generator.intRange(0, 1)) { party, ref -> party.ref(ref.toByte()) }
|
||||
|
||||
val currencies = setOf(USD, GBP, CHF).toList() // + Currency.getAvailableCurrencies().toList().subList(0, 3).toSet()).toList()
|
||||
val currencyGenerator = Generator.pickOne(currencies)
|
||||
|
||||
val issuedGenerator = issuerGenerator.combine(currencyGenerator) { issuer, currency -> Issued(issuer, currency) }
|
||||
@ -93,8 +94,11 @@ class EventGenerator(
|
||||
1.0 to moveCashGenerator
|
||||
)
|
||||
|
||||
val bankOfCordaCommandGenerator = Generator.frequency(
|
||||
0.6 to issueCashGenerator,
|
||||
val bankOfCordaExitGenerator = Generator.frequency(
|
||||
0.4 to exitCashGenerator
|
||||
)
|
||||
|
||||
val bankOfCordaIssueGenerator = Generator.frequency(
|
||||
0.6 to issueCashGenerator
|
||||
)
|
||||
}
|
@ -1,19 +1,19 @@
|
||||
package net.corda.client.model
|
||||
|
||||
import javafx.collections.FXCollections
|
||||
import javafx.collections.ObservableList
|
||||
import kotlinx.support.jdk8.collections.removeIf
|
||||
import net.corda.client.fxutils.foldToObservableList
|
||||
import net.corda.client.fxutils.fold
|
||||
import net.corda.client.fxutils.map
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.node.services.Vault
|
||||
import rx.Observable
|
||||
|
||||
data class Diff<out T : ContractState>(
|
||||
val added: Collection<StateAndRef<T>>,
|
||||
val removed: Collection<StateRef>
|
||||
val removed: Collection<StateAndRef<T>>
|
||||
)
|
||||
|
||||
/**
|
||||
@ -26,14 +26,12 @@ class ContractStateModel {
|
||||
Diff(it.produced, it.consumed)
|
||||
}
|
||||
private val cashStatesDiff: Observable<Diff<Cash.State>> = contractStatesDiff.map {
|
||||
// We can't filter removed hashes here as we don't have type info
|
||||
Diff(it.added.filterCashStateAndRefs(), it.removed)
|
||||
Diff(it.added.filterCashStateAndRefs(), it.removed.filterCashStateAndRefs())
|
||||
}
|
||||
val cashStates: ObservableList<StateAndRef<Cash.State>> = cashStatesDiff.fold(FXCollections.observableArrayList()) { list, statesDiff ->
|
||||
list.removeIf { it in statesDiff.removed }
|
||||
list.addAll(statesDiff.added)
|
||||
}
|
||||
val cashStates: ObservableList<StateAndRef<Cash.State>> =
|
||||
cashStatesDiff.foldToObservableList(Unit) { statesDiff, _accumulator, observableList ->
|
||||
observableList.removeIf { it.ref in statesDiff.removed }
|
||||
observableList.addAll(statesDiff.added)
|
||||
}
|
||||
|
||||
val cash = cashStates.map { it.state.data.amount }
|
||||
|
||||
|
@ -1,115 +0,0 @@
|
||||
package net.corda.client.model
|
||||
|
||||
import javafx.beans.property.SimpleObjectProperty
|
||||
import javafx.beans.value.ObservableValue
|
||||
import javafx.collections.ObservableList
|
||||
import javafx.collections.ObservableMap
|
||||
import net.corda.client.fxutils.*
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.StateMachineRunId
|
||||
import net.corda.core.messaging.StateMachineUpdate
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import org.fxmisc.easybind.EasyBind
|
||||
|
||||
data class GatheredTransactionData(
|
||||
val transaction: PartiallyResolvedTransaction,
|
||||
val stateMachines: ObservableList<out StateMachineData>
|
||||
)
|
||||
|
||||
/**
|
||||
* [PartiallyResolvedTransaction] holds a [SignedTransaction] that has zero or more inputs resolved. The intent is
|
||||
* to prepare clients for cases where an input can only be resolved in the future/cannot be resolved at all (for example
|
||||
* because of permissioning)
|
||||
*/
|
||||
data class PartiallyResolvedTransaction(
|
||||
val transaction: SignedTransaction,
|
||||
val inputs: List<ObservableValue<InputResolution>>) {
|
||||
val id = transaction.id
|
||||
|
||||
sealed class InputResolution(val stateRef: StateRef) {
|
||||
class Unresolved(stateRef: StateRef) : InputResolution(stateRef)
|
||||
class Resolved(val stateAndRef: StateAndRef<ContractState>) : InputResolution(stateAndRef.ref)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromSignedTransaction(
|
||||
transaction: SignedTransaction,
|
||||
transactions: ObservableMap<SecureHash, SignedTransaction>
|
||||
) = PartiallyResolvedTransaction(
|
||||
transaction = transaction,
|
||||
inputs = transaction.tx.inputs.map { stateRef ->
|
||||
EasyBind.map(transactions.getObservableValue(stateRef.txhash)) {
|
||||
if (it == null) {
|
||||
InputResolution.Unresolved(stateRef)
|
||||
} else {
|
||||
InputResolution.Resolved(it.tx.outRef(stateRef.index))
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
sealed class TransactionCreateStatus(val message: String?) {
|
||||
class Started(message: String?) : TransactionCreateStatus(message)
|
||||
class Failed(message: String?) : TransactionCreateStatus(message)
|
||||
|
||||
override fun toString(): String = message ?: javaClass.simpleName
|
||||
}
|
||||
|
||||
data class FlowStatus(
|
||||
val status: String
|
||||
)
|
||||
|
||||
sealed class StateMachineStatus(val stateMachineName: String) {
|
||||
class Added(stateMachineName: String) : StateMachineStatus(stateMachineName)
|
||||
class Removed(stateMachineName: String) : StateMachineStatus(stateMachineName)
|
||||
|
||||
override fun toString(): String = "${javaClass.simpleName}($stateMachineName)"
|
||||
}
|
||||
|
||||
data class StateMachineData(
|
||||
val id: StateMachineRunId,
|
||||
val flowStatus: ObservableValue<FlowStatus?>,
|
||||
val stateMachineStatus: ObservableValue<StateMachineStatus>
|
||||
)
|
||||
|
||||
/**
|
||||
* This model provides an observable list of transactions and what state machines/flows recorded them
|
||||
*/
|
||||
class GatheredTransactionDataModel {
|
||||
private val transactions by observable(NodeMonitorModel::transactions)
|
||||
private val stateMachineUpdates by observable(NodeMonitorModel::stateMachineUpdates)
|
||||
private val progressTracking by observable(NodeMonitorModel::progressTracking)
|
||||
private val stateMachineTransactionMapping by observable(NodeMonitorModel::stateMachineTransactionMapping)
|
||||
|
||||
private val collectedTransactions = transactions.recordInSequence()
|
||||
private val transactionMap = collectedTransactions.associateBy(SignedTransaction::id)
|
||||
private val progressEvents = progressTracking.recordAsAssociation(ProgressTrackingEvent::stateMachineId)
|
||||
private val stateMachineStatus = stateMachineUpdates.foldToObservableMap(Unit) { update, _unit, map: ObservableMap<StateMachineRunId, SimpleObjectProperty<StateMachineStatus>> ->
|
||||
when (update) {
|
||||
is StateMachineUpdate.Added -> {
|
||||
val added: SimpleObjectProperty<StateMachineStatus> =
|
||||
SimpleObjectProperty(StateMachineStatus.Added(update.stateMachineInfo.flowLogicClassName))
|
||||
map[update.id] = added
|
||||
}
|
||||
is StateMachineUpdate.Removed -> {
|
||||
val added = map[update.id]
|
||||
added ?: throw Exception("State machine removed with unknown id ${update.id}")
|
||||
added.set(StateMachineStatus.Removed(added.value.stateMachineName))
|
||||
}
|
||||
}
|
||||
}
|
||||
private val stateMachineDataList = LeftOuterJoinedMap(stateMachineStatus, progressEvents) { id, status, progress ->
|
||||
StateMachineData(id, progress.map { it?.let { FlowStatus(it.message) } }, status)
|
||||
}.getObservableValues()
|
||||
// TODO : Create a new screen for state machines.
|
||||
private val stateMachineDataMap = stateMachineDataList.associateBy(StateMachineData::id)
|
||||
private val smTxMappingList = stateMachineTransactionMapping.recordInSequence()
|
||||
val partiallyResolvedTransactions = collectedTransactions.map {
|
||||
PartiallyResolvedTransaction.fromSignedTransaction(it, transactionMap)
|
||||
}
|
||||
}
|
@ -1,11 +1,12 @@
|
||||
package net.corda.client.model
|
||||
|
||||
import javafx.beans.value.ObservableValue
|
||||
import javafx.collections.FXCollections
|
||||
import javafx.collections.ObservableList
|
||||
import kotlinx.support.jdk8.collections.removeIf
|
||||
import net.corda.client.fxutils.firstOrDefault
|
||||
import net.corda.client.fxutils.firstOrNullObservable
|
||||
import net.corda.client.fxutils.foldToObservableList
|
||||
import net.corda.client.fxutils.fold
|
||||
import net.corda.client.fxutils.map
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.node.NodeInfo
|
||||
@ -17,15 +18,15 @@ class NetworkIdentityModel {
|
||||
private val networkIdentityObservable by observable(NodeMonitorModel::networkMap)
|
||||
|
||||
val networkIdentities: ObservableList<NodeInfo> =
|
||||
networkIdentityObservable.foldToObservableList(Unit) { update, _accumulator, observableList ->
|
||||
observableList.removeIf {
|
||||
networkIdentityObservable.fold(FXCollections.observableArrayList()) { list, update ->
|
||||
list.removeIf {
|
||||
when (update) {
|
||||
is MapChange.Removed -> it == update.node
|
||||
is MapChange.Modified -> it == update.previousNode
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
observableList.addAll(update.node)
|
||||
list.addAll(update.node)
|
||||
}
|
||||
|
||||
private val rpcProxy by observableValue(NodeMonitorModel::proxyObservable)
|
||||
|
@ -6,14 +6,11 @@ import net.corda.core.flows.StateMachineRunId
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.messaging.StateMachineInfo
|
||||
import net.corda.core.messaging.StateMachineUpdate
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.node.services.NetworkMapCache.MapChange
|
||||
import net.corda.core.node.services.StateMachineTransactionMapping
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.flows.CashCommand
|
||||
import net.corda.flows.CashFlow
|
||||
import net.corda.node.services.config.NodeSSLConfiguration
|
||||
import net.corda.node.services.config.SSLConfiguration
|
||||
import net.corda.node.services.messaging.CordaRPCClient
|
||||
import rx.Observable
|
||||
import rx.subjects.PublishSubject
|
||||
@ -48,16 +45,13 @@ class NodeMonitorModel {
|
||||
val progressTracking: Observable<ProgressTrackingEvent> = progressTrackingSubject
|
||||
val networkMap: Observable<MapChange> = networkMapSubject
|
||||
|
||||
private val clientToServiceSource = PublishSubject.create<CashCommand>()
|
||||
val clientToService: PublishSubject<CashCommand> = clientToServiceSource
|
||||
|
||||
val proxyObservable = SimpleObjectProperty<CordaRPCOps?>()
|
||||
|
||||
/**
|
||||
* Register for updates to/from a given vault.
|
||||
* TODO provide an unsubscribe mechanism
|
||||
*/
|
||||
fun register(nodeHostAndPort: HostAndPort, sslConfig: NodeSSLConfiguration, username: String, password: String) {
|
||||
fun register(nodeHostAndPort: HostAndPort, sslConfig: SSLConfiguration, username: String, password: String) {
|
||||
val client = CordaRPCClient(nodeHostAndPort, sslConfig)
|
||||
client.start(username, password)
|
||||
val proxy = client.proxy()
|
||||
@ -98,10 +92,6 @@ class NodeMonitorModel {
|
||||
val (parties, futurePartyUpdate) = proxy.networkMapUpdates()
|
||||
futurePartyUpdate.startWith(parties.map { MapChange.Added(it) }).subscribe(networkMapSubject)
|
||||
|
||||
// Client -> Service
|
||||
clientToServiceSource.subscribe {
|
||||
proxy.startFlow(::CashFlow, it)
|
||||
}
|
||||
proxyObservable.set(proxy)
|
||||
}
|
||||
}
|
@ -0,0 +1,116 @@
|
||||
package net.corda.client.model
|
||||
|
||||
import javafx.beans.property.SimpleObjectProperty
|
||||
import javafx.beans.value.ObservableValue
|
||||
import javafx.collections.FXCollections
|
||||
import javafx.collections.ObservableList
|
||||
import javafx.collections.ObservableMap
|
||||
import net.corda.client.fxutils.*
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.StateMachineRunId
|
||||
import net.corda.core.messaging.StateMachineUpdate
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import org.fxmisc.easybind.EasyBind
|
||||
|
||||
data class GatheredTransactionData(
|
||||
val transaction: PartiallyResolvedTransaction,
|
||||
val stateMachines: ObservableList<out StateMachineData>
|
||||
)
|
||||
|
||||
/**
|
||||
* [PartiallyResolvedTransaction] holds a [SignedTransaction] that has zero or more inputs resolved. The intent is
|
||||
* to prepare clients for cases where an input can only be resolved in the future/cannot be resolved at all (for example
|
||||
* because of permissioning)
|
||||
*/
|
||||
data class PartiallyResolvedTransaction(
|
||||
val transaction: SignedTransaction,
|
||||
val inputs: List<ObservableValue<InputResolution>>) {
|
||||
val id = transaction.id
|
||||
|
||||
sealed class InputResolution(val stateRef: StateRef) {
|
||||
class Unresolved(stateRef: StateRef) : InputResolution(stateRef)
|
||||
class Resolved(val stateAndRef: StateAndRef<ContractState>) : InputResolution(stateAndRef.ref)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromSignedTransaction(
|
||||
transaction: SignedTransaction,
|
||||
transactions: ObservableMap<SecureHash, SignedTransaction>
|
||||
) = PartiallyResolvedTransaction(
|
||||
transaction = transaction,
|
||||
inputs = transaction.tx.inputs.map { stateRef ->
|
||||
EasyBind.map(transactions.getObservableValue(stateRef.txhash)) {
|
||||
if (it == null) {
|
||||
InputResolution.Unresolved(stateRef)
|
||||
} else {
|
||||
InputResolution.Resolved(it.tx.outRef(stateRef.index))
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
sealed class TransactionCreateStatus(val message: String?) {
|
||||
class Started(message: String?) : TransactionCreateStatus(message)
|
||||
class Failed(message: String?) : TransactionCreateStatus(message)
|
||||
|
||||
override fun toString(): String = message ?: javaClass.simpleName
|
||||
}
|
||||
|
||||
data class FlowStatus(
|
||||
val status: String
|
||||
)
|
||||
|
||||
sealed class StateMachineStatus(val stateMachineName: String) {
|
||||
class Added(stateMachineName: String) : StateMachineStatus(stateMachineName)
|
||||
class Removed(stateMachineName: String) : StateMachineStatus(stateMachineName)
|
||||
|
||||
override fun toString(): String = "${javaClass.simpleName}($stateMachineName)"
|
||||
}
|
||||
|
||||
data class StateMachineData(
|
||||
val id: StateMachineRunId,
|
||||
val flowStatus: ObservableValue<FlowStatus?>,
|
||||
val stateMachineStatus: ObservableValue<StateMachineStatus>
|
||||
)
|
||||
|
||||
/**
|
||||
* This model provides an observable list of transactions and what state machines/flows recorded them
|
||||
*/
|
||||
class TransactionDataModel {
|
||||
private val transactions by observable(NodeMonitorModel::transactions)
|
||||
private val stateMachineUpdates by observable(NodeMonitorModel::stateMachineUpdates)
|
||||
private val progressTracking by observable(NodeMonitorModel::progressTracking)
|
||||
private val stateMachineTransactionMapping by observable(NodeMonitorModel::stateMachineTransactionMapping)
|
||||
|
||||
private val collectedTransactions = transactions.recordInSequence()
|
||||
private val transactionMap = collectedTransactions.associateBy(SignedTransaction::id)
|
||||
private val progressEvents = progressTracking.recordAsAssociation(ProgressTrackingEvent::stateMachineId)
|
||||
private val stateMachineStatus = stateMachineUpdates.fold(FXCollections.observableHashMap<StateMachineRunId, SimpleObjectProperty<StateMachineStatus>>()) { map, update ->
|
||||
when (update) {
|
||||
is StateMachineUpdate.Added -> {
|
||||
val added: SimpleObjectProperty<StateMachineStatus> =
|
||||
SimpleObjectProperty(StateMachineStatus.Added(update.stateMachineInfo.flowLogicClassName))
|
||||
map[update.id] = added
|
||||
}
|
||||
is StateMachineUpdate.Removed -> {
|
||||
val added = map[update.id]
|
||||
added ?: throw Exception("State machine removed with unknown id ${update.id}")
|
||||
added.set(StateMachineStatus.Removed(added.value.stateMachineName))
|
||||
}
|
||||
}
|
||||
}
|
||||
private val stateMachineDataList = LeftOuterJoinedMap(stateMachineStatus, progressEvents) { id, status, progress ->
|
||||
StateMachineData(id, progress.map { it?.let { FlowStatus(it.message) } }, status)
|
||||
}.getObservableValues()
|
||||
// TODO : Create a new screen for state machines.
|
||||
private val stateMachineDataMap = stateMachineDataList.associateBy(StateMachineData::id)
|
||||
private val smTxMappingList = stateMachineTransactionMapping.recordInSequence()
|
||||
val partiallyResolvedTransactions = collectedTransactions.map {
|
||||
PartiallyResolvedTransaction.fromSignedTransaction(it, transactionMap)
|
||||
}
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
basedir : "./nodea"
|
||||
myLegalName : "Bank A"
|
||||
nearestCity : "London"
|
||||
keyStorePassword : "cordacadevpass"
|
||||
@ -6,5 +5,8 @@ trustStorePassword : "trustpass"
|
||||
artemisAddress : "localhost:31337"
|
||||
webAddress : "localhost:31339"
|
||||
extraAdvertisedServiceIds: "corda.interest_rates"
|
||||
networkMapAddress : "localhost:12345"
|
||||
networkMapService : {
|
||||
address : "localhost:12345"
|
||||
legalName : "Network Map Service"
|
||||
}
|
||||
useHTTPS : false
|
||||
|
@ -1,4 +1,3 @@
|
||||
basedir : "./nodeb"
|
||||
myLegalName : "Bank B"
|
||||
nearestCity : "London"
|
||||
keyStorePassword : "cordacadevpass"
|
||||
@ -6,5 +5,8 @@ trustStorePassword : "trustpass"
|
||||
artemisAddress : "localhost:31338"
|
||||
webAddress : "localhost:31340"
|
||||
extraAdvertisedServiceIds: "corda.interest_rates"
|
||||
networkMapAddress : "localhost:12345"
|
||||
networkMapService : {
|
||||
address : "localhost:12345"
|
||||
legalName : "Network Map Service"
|
||||
}
|
||||
useHTTPS : false
|
||||
|
@ -1,4 +1,3 @@
|
||||
basedir : "./nameserver"
|
||||
myLegalName : "Notary Service"
|
||||
nearestCity : "London"
|
||||
keyStorePassword : "cordacadevpass"
|
||||
|
@ -31,11 +31,11 @@ sourceSets {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testCompile 'junit:junit:4.12'
|
||||
testCompile "junit:junit:$junit_version"
|
||||
testCompile "commons-fileupload:commons-fileupload:1.3.2"
|
||||
|
||||
// Guava: Google test library (collections test suite)
|
||||
testCompile "com.google.guava:guava-testlib:19.0"
|
||||
testCompile "com.google.guava:guava-testlib:$guava_version"
|
||||
|
||||
// Bring in the MockNode infrastructure for writing protocol unit tests.
|
||||
testCompile project(":node")
|
||||
@ -43,7 +43,7 @@ dependencies {
|
||||
|
||||
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
compile "org.jetbrains.kotlinx:kotlinx-support-jdk8:0.2"
|
||||
compile "org.jetbrains.kotlinx:kotlinx-support-jdk8:0.3"
|
||||
compile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
||||
|
||||
// Thread safety annotations
|
||||
@ -56,18 +56,18 @@ dependencies {
|
||||
// AssertJ: for fluent assertions for testing
|
||||
testCompile "org.assertj:assertj-core:${assertj_version}"
|
||||
|
||||
compile 'com.pholser:junit-quickcheck-core:0.6'
|
||||
compile 'com.pholser:junit-quickcheck-generators:0.6'
|
||||
compile "com.pholser:junit-quickcheck-core:$quickcheck_version"
|
||||
compile "com.pholser:junit-quickcheck-generators:$quickcheck_version"
|
||||
|
||||
// Guava: Google utilities library.
|
||||
compile "com.google.guava:guava:19.0"
|
||||
compile "com.google.guava:guava:$guava_version"
|
||||
|
||||
// RxJava: observable streams of events.
|
||||
compile "io.reactivex:rxjava:1.1.6"
|
||||
compile "io.reactivex:rxjava:1.2.4"
|
||||
|
||||
// Kryo: object graph serialization.
|
||||
compile "com.esotericsoftware:kryo:4.0.0"
|
||||
compile "de.javakaffee:kryo-serializers:0.38"
|
||||
compile "de.javakaffee:kryo-serializers:0.41"
|
||||
|
||||
// Apache JEXL: An embeddable expression evaluation library.
|
||||
// This may be temporary until we experiment with other ways to do on-the-fly contract specialisation via an API.
|
||||
@ -88,4 +88,4 @@ dependencies {
|
||||
|
||||
// RS API: Response type and codes for ApiUtils.
|
||||
compile "javax.ws.rs:javax.ws.rs-api:2.0"
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
// TODO Move out the Kotlin specific stuff into a separate file
|
||||
@file:JvmName("Utils")
|
||||
|
||||
package net.corda.core
|
||||
@ -26,10 +27,7 @@ import java.nio.file.*
|
||||
import java.nio.file.attribute.FileAttribute
|
||||
import java.time.Duration
|
||||
import java.time.temporal.Temporal
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.ExecutionException
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.Future
|
||||
import java.util.concurrent.*
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import java.util.function.BiConsumer
|
||||
import java.util.stream.Stream
|
||||
@ -67,9 +65,9 @@ infix fun Long.checkedAdd(b: Long) = Math.addExact(this, b)
|
||||
fun random63BitValue(): Long = Math.abs(newSecureRandom().nextLong())
|
||||
|
||||
/** Same as [Future.get] but with a more descriptive name, and doesn't throw [ExecutionException], instead throwing its cause */
|
||||
fun <T> Future<T>.getOrThrow(): T {
|
||||
try {
|
||||
return get()
|
||||
fun <T> Future<T>.getOrThrow(timeout: Duration? = null): T {
|
||||
return try {
|
||||
if (timeout == null) get() else get(timeout.toNanos(), TimeUnit.NANOSECONDS)
|
||||
} catch (e: ExecutionException) {
|
||||
throw e.cause!!
|
||||
}
|
||||
@ -204,19 +202,31 @@ fun <T> List<T>.randomOrNull(predicate: (T) -> Boolean) = filter(predicate).rand
|
||||
// An alias that can sometimes make code clearer to read.
|
||||
val RunOnCallerThread: Executor = MoreExecutors.directExecutor()
|
||||
|
||||
inline fun elapsedTime(block: () -> Unit): Duration {
|
||||
val start = System.nanoTime()
|
||||
block()
|
||||
val end = System.nanoTime()
|
||||
return Duration.ofNanos(end-start)
|
||||
}
|
||||
|
||||
// TODO: Add inline back when a new Kotlin version is released and check if the java.lang.VerifyError
|
||||
// returns in the IRSSimulationTest. If not, commit the inline back.
|
||||
fun <T> logElapsedTime(label: String, logger: Logger? = null, body: () -> T): T {
|
||||
val now = System.currentTimeMillis()
|
||||
val r = body()
|
||||
val elapsed = System.currentTimeMillis() - now
|
||||
if (logger != null)
|
||||
logger.info("$label took $elapsed msec")
|
||||
else
|
||||
println("$label took $elapsed msec")
|
||||
return r
|
||||
// Use nanoTime as it's monotonic.
|
||||
val now = System.nanoTime()
|
||||
try {
|
||||
return body()
|
||||
} finally {
|
||||
val elapsed = Duration.ofNanos(System.nanoTime() - now).toMillis()
|
||||
if (logger != null)
|
||||
logger.info("$label took $elapsed msec")
|
||||
else
|
||||
println("$label took $elapsed msec")
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> Logger.logElapsedTime(label: String, body: () -> T): T = logElapsedTime(label, this, body)
|
||||
|
||||
/**
|
||||
* A threadbox is a simple utility that makes it harder to forget to take a lock before accessing some shared state.
|
||||
* Simply define a private class to hold the data that must be grouped under the same lock, and then pass the only
|
||||
@ -271,19 +281,17 @@ class TransientProperty<out T>(private val initializer: () -> T) {
|
||||
/**
|
||||
* Given a path to a zip file, extracts it to the given directory.
|
||||
*/
|
||||
fun extractZipFile(zipPath: Path, toPath: Path) {
|
||||
val normalisedToPath = toPath.normalize()
|
||||
normalisedToPath.createDirectories()
|
||||
fun extractZipFile(zipFile: Path, toDirectory: Path) {
|
||||
val normalisedDirectory = toDirectory.normalize().createDirectories()
|
||||
|
||||
zipPath.read {
|
||||
zipFile.read {
|
||||
val zip = ZipInputStream(BufferedInputStream(it))
|
||||
while (true) {
|
||||
val e = zip.nextEntry ?: break
|
||||
val outPath = normalisedToPath / e.name
|
||||
val outPath = (normalisedDirectory / e.name).normalize()
|
||||
|
||||
// Security checks: we should reject a zip that contains tricksy paths that try to escape toPath.
|
||||
if (!outPath.normalize().startsWith(normalisedToPath))
|
||||
throw IllegalStateException("ZIP contained a path that resolved incorrectly: ${e.name}")
|
||||
// Security checks: we should reject a zip that contains tricksy paths that try to escape toDirectory.
|
||||
check(outPath.startsWith(normalisedDirectory)) { "ZIP contained a path that resolved incorrectly: ${e.name}" }
|
||||
|
||||
if (e.isDirectory) {
|
||||
outPath.createDirectories()
|
||||
@ -355,8 +363,7 @@ data class ErrorOr<out A> private constructor(val value: A?, val error: Throwabl
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable that buffers events until subscribed.
|
||||
*
|
||||
* Returns an Observable that buffers events until subscribed.
|
||||
* @see UnicastSubject
|
||||
*/
|
||||
fun <T> Observable<T>.bufferUntilSubscribed(): Observable<T> {
|
||||
@ -375,5 +382,18 @@ fun <T> Observer<T>.tee(vararg teeTo: Observer<T>): Observer<T> {
|
||||
return subject
|
||||
}
|
||||
|
||||
/** Allows summing big decimals that are in iterable collections */
|
||||
/**
|
||||
* Returns a [ListenableFuture] bound to the *first* item emitted by this Observable. The future will complete with a
|
||||
* NoSuchElementException if no items are emitted or any other error thrown by the Observable.
|
||||
*/
|
||||
fun <T> Observable<T>.toFuture(): ListenableFuture<T> {
|
||||
val future = SettableFuture.create<T>()
|
||||
first().subscribe(
|
||||
{ future.set(it) },
|
||||
{ future.setException(it) }
|
||||
)
|
||||
return future
|
||||
}
|
||||
|
||||
/** Return the sum of an Iterable of [BigDecimal]s. */
|
||||
fun Iterable<BigDecimal>.sum(): BigDecimal = fold(BigDecimal.ZERO) { a, b -> a + b }
|
||||
|
@ -119,7 +119,7 @@ inline fun <reified T : MoveCommand> verifyMoveCommand(inputs: List<OwnableState
|
||||
val command = commands.requireSingleCommand<T>()
|
||||
val keysThatSigned = command.signers.toSet()
|
||||
requireThat {
|
||||
"the owning keys are the same as the signing keys" by keysThatSigned.containsAll(owningPubKeys)
|
||||
"the owning keys are a subset of the signing keys" by keysThatSigned.containsAll(owningPubKeys)
|
||||
}
|
||||
return command.value
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize
|
||||
import com.google.common.annotations.VisibleForTesting
|
||||
import java.math.BigDecimal
|
||||
import java.math.BigInteger
|
||||
import java.time.DayOfWeek
|
||||
import java.time.LocalDate
|
||||
import java.time.format.DateTimeFormatter
|
||||
@ -34,6 +35,19 @@ import java.util.*
|
||||
* @param T the type of the token, for example [Currency].
|
||||
*/
|
||||
data class Amount<T>(val quantity: Long, val token: T) : Comparable<Amount<T>> {
|
||||
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".
|
||||
*
|
||||
* @see Amount<Currency>.toDecimal
|
||||
*/
|
||||
fun fromDecimal(quantity: BigDecimal, currency: Currency) : Amount<Currency> {
|
||||
val longQuantity = quantity.movePointRight(currency.defaultFractionDigits).toLong()
|
||||
return Amount(longQuantity, currency)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
// Negative amounts are of course a vital part of any ledger, but negative values are only valid in certain
|
||||
// contexts: you cannot send a negative amount of cash, but you can (sometimes) have a negative balance.
|
||||
@ -41,7 +55,12 @@ data class Amount<T>(val quantity: Long, val token: T) : Comparable<Amount<T>> {
|
||||
require(quantity >= 0) { "Negative amounts are not allowed: $quantity" }
|
||||
}
|
||||
|
||||
constructor(amount: BigDecimal, currency: T) : this(amount.toLong(), currency)
|
||||
/**
|
||||
* Construct the amount using the given decimal value as quantity. Any fractional part
|
||||
* is discarded. To convert and use the fractional part, see [fromDecimal].
|
||||
*/
|
||||
constructor(quantity: BigDecimal, token: T) : this(quantity.toLong(), token)
|
||||
constructor(quantity: BigInteger, token: T) : this(quantity.toLong(), token)
|
||||
|
||||
operator fun plus(other: Amount<T>): Amount<T> {
|
||||
checkCurrency(other)
|
||||
@ -70,6 +89,14 @@ data class Amount<T>(val quantity: Long, val token: T) : Comparable<Amount<T>> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a currency [Amount] to a decimal representation. For example, with an amount with a quantity
|
||||
* of "1234" GBP, returns "12.34".
|
||||
*
|
||||
* @see Amount.Companion.fromDecimal
|
||||
*/
|
||||
fun Amount<Currency>.toDecimal() : BigDecimal = BigDecimal(quantity).movePointLeft(token.defaultFractionDigits)
|
||||
|
||||
fun <T> Iterable<Amount<T>>.sumOrNull() = if (!iterator().hasNext()) null else sumOrThrow()
|
||||
fun <T> Iterable<Amount<T>>.sumOrThrow() = reduce { left, right -> left + right }
|
||||
fun <T> Iterable<Amount<T>>.sumOrZero(currency: T) = if (iterator().hasNext()) sumOrThrow() else Amount(0, currency)
|
||||
|
@ -114,42 +114,41 @@ interface ContractState {
|
||||
* list should just contain the owner.
|
||||
*/
|
||||
val participants: List<CompositeKey>
|
||||
|
||||
/**
|
||||
* All contract states may be _encumbered_ by up to one other state.
|
||||
*
|
||||
* The encumbrance state, if present, forces additional controls over the encumbered state, since the platform checks
|
||||
* that the encumbrance state is present as an input in the same transaction that consumes the encumbered state, and
|
||||
* the contract code and rules of the encumbrance state will also be verified during the execution of the transaction.
|
||||
* For example, a cash contract state could be encumbered with a time-lock contract state; the cash state is then only
|
||||
* processable in a transaction that verifies that the time specified in the encumbrance time-lock has passed.
|
||||
*
|
||||
* The encumbered state refers to another by index, and the referred encumbrance state
|
||||
* is an output state in a particular position on the same transaction that created the encumbered state. An alternative
|
||||
* implementation would be encumber by reference to a StateRef., which would allow the specification of encumbrance
|
||||
* by a state created in a prior transaction.
|
||||
*
|
||||
* Note that an encumbered state that is being consumed must have its encumbrance consumed in the same transaction,
|
||||
* otherwise the transaction is not valid.
|
||||
*/
|
||||
val encumbrance: Int? get() = null
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper for [ContractState] containing additional platform-level state information.
|
||||
* This is the definitive state that is stored on the ledger and used in transaction outputs.
|
||||
*/
|
||||
data class TransactionState<out T : ContractState>(
|
||||
data class TransactionState<out T : ContractState> @JvmOverloads constructor(
|
||||
/** The custom contract state */
|
||||
val data: T,
|
||||
/** Identity of the notary that ensures the state is not used as an input to a transaction more than once */
|
||||
val notary: Party) {
|
||||
val notary: Party,
|
||||
/**
|
||||
* All contract states may be _encumbered_ by up to one other state.
|
||||
*
|
||||
* The encumbrance state, if present, forces additional controls over the encumbered state, since the platform checks
|
||||
* that the encumbrance state is present as an input in the same transaction that consumes the encumbered state, and
|
||||
* the contract code and rules of the encumbrance state will also be verified during the execution of the transaction.
|
||||
* For example, a cash contract state could be encumbered with a time-lock contract state; the cash state is then only
|
||||
* processable in a transaction that verifies that the time specified in the encumbrance time-lock has passed.
|
||||
*
|
||||
* The encumbered state refers to another by index, and the referred encumbrance state
|
||||
* is an output state in a particular position on the same transaction that created the encumbered state. An alternative
|
||||
* implementation would be encumbering by reference to a [StateRef], which would allow the specification of encumbrance
|
||||
* by a state created in a prior transaction.
|
||||
*
|
||||
* Note that an encumbered state that is being consumed must have its encumbrance consumed in the same transaction,
|
||||
* otherwise the transaction is not valid.
|
||||
*/
|
||||
val encumbrance: Int? = null) {
|
||||
|
||||
/**
|
||||
* Copies the underlying state, replacing the notary field with the new value.
|
||||
* To replace the notary, we need an approval (signature) from _all_ participants of the [ContractState].
|
||||
*/
|
||||
fun withNotary(newNotary: Party) = TransactionState(this.data, newNotary)
|
||||
fun withNotary(newNotary: Party) = TransactionState(this.data, newNotary, encumbrance)
|
||||
}
|
||||
|
||||
/** Wraps the [ContractState] in a [TransactionState] object */
|
||||
|
@ -19,6 +19,8 @@ sealed class TransactionType {
|
||||
*/
|
||||
fun verify(tx: LedgerTransaction) {
|
||||
require(tx.notary != null || tx.timestamp == null) { "Transactions with timestamps must be notarised." }
|
||||
val duplicates = detectDuplicateInputs(tx)
|
||||
if (duplicates.isNotEmpty()) throw TransactionVerificationException.DuplicateInputStates(tx, duplicates)
|
||||
val missing = verifySigners(tx)
|
||||
if (missing.isNotEmpty()) throw TransactionVerificationException.SignersMissing(tx, missing.toList())
|
||||
verifyTransaction(tx)
|
||||
@ -35,6 +37,19 @@ sealed class TransactionType {
|
||||
return missing
|
||||
}
|
||||
|
||||
/** Check that the inputs are unique. */
|
||||
private fun detectDuplicateInputs(tx: LedgerTransaction): Set<StateRef> {
|
||||
var seenInputs = emptySet<StateRef>()
|
||||
var duplicates = emptySet<StateRef>()
|
||||
tx.inputs.forEach { state ->
|
||||
if (seenInputs.contains(state.ref)) {
|
||||
duplicates += state.ref
|
||||
}
|
||||
seenInputs += state.ref
|
||||
}
|
||||
return duplicates
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of public keys that that require signatures for the transaction type.
|
||||
* Note: the notary key is checked separately for all transactions and need not be included.
|
||||
@ -74,14 +89,14 @@ sealed class TransactionType {
|
||||
|
||||
private fun verifyEncumbrances(tx: LedgerTransaction) {
|
||||
// Validate that all encumbrances exist within the set of input states.
|
||||
val encumberedInputs = tx.inputs.filter { it.state.data.encumbrance != null }
|
||||
val encumberedInputs = tx.inputs.filter { it.state.encumbrance != null }
|
||||
encumberedInputs.forEach { encumberedInput ->
|
||||
val encumbranceStateExists = tx.inputs.any {
|
||||
it.ref.txhash == encumberedInput.ref.txhash && it.ref.index == encumberedInput.state.data.encumbrance
|
||||
it.ref.txhash == encumberedInput.ref.txhash && it.ref.index == encumberedInput.state.encumbrance
|
||||
}
|
||||
if (!encumbranceStateExists) {
|
||||
throw TransactionVerificationException.TransactionMissingEncumbranceException(
|
||||
tx, encumberedInput.state.data.encumbrance!!,
|
||||
tx, encumberedInput.state.encumbrance!!,
|
||||
TransactionVerificationException.Direction.INPUT
|
||||
)
|
||||
}
|
||||
@ -90,7 +105,7 @@ sealed class TransactionType {
|
||||
// Check that, in the outputs, an encumbered state does not refer to itself as the encumbrance,
|
||||
// and that the number of outputs can contain the encumbrance.
|
||||
for ((i, output) in tx.outputs.withIndex()) {
|
||||
val encumbranceIndex = output.data.encumbrance ?: continue
|
||||
val encumbranceIndex = output.encumbrance ?: continue
|
||||
if (encumbranceIndex == i || encumbranceIndex >= tx.outputs.size) {
|
||||
throw TransactionVerificationException.TransactionMissingEncumbranceException(
|
||||
tx, encumbranceIndex,
|
||||
|
@ -97,6 +97,9 @@ sealed class TransactionVerificationException(val tx: LedgerTransaction, cause:
|
||||
class SignersMissing(tx: LedgerTransaction, val missing: List<CompositeKey>) : TransactionVerificationException(tx, null) {
|
||||
override fun toString() = "Signers missing: ${missing.joinToString()}"
|
||||
}
|
||||
class DuplicateInputStates(tx: LedgerTransaction, val duplicates: Set<StateRef>) : TransactionVerificationException(tx, null) {
|
||||
override fun toString() = "Duplicate inputs: ${duplicates.joinToString()}"
|
||||
}
|
||||
|
||||
class InvalidNotaryChange(tx: LedgerTransaction) : TransactionVerificationException(tx, null)
|
||||
class NotaryChangeInWrongTransactionType(tx: LedgerTransaction, val outputNotary: Party) : TransactionVerificationException(tx, null) {
|
||||
|
@ -1,39 +1,10 @@
|
||||
package net.corda.core.contracts.clauses
|
||||
|
||||
import net.corda.core.contracts.AuthenticatedObject
|
||||
import net.corda.core.contracts.CommandData
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.TransactionForContract
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Compose a number of clauses, such that all of the clauses must run for verification to pass.
|
||||
*/
|
||||
// TODO: Rename to AllOf
|
||||
class AllComposition<S : ContractState, C : CommandData, K : Any>(firstClause: Clause<S, C, K>, vararg remainingClauses: Clause<S, C, K>) : CompositeClause<S, C, K>() {
|
||||
override val clauses = ArrayList<Clause<S, C, K>>()
|
||||
|
||||
init {
|
||||
clauses.add(firstClause)
|
||||
clauses.addAll(remainingClauses)
|
||||
}
|
||||
|
||||
override fun matchedClauses(commands: List<AuthenticatedObject<C>>): List<Clause<S, C, K>> {
|
||||
clauses.forEach { clause ->
|
||||
check(clause.matches(commands)) { "Failed to match clause ${clause}" }
|
||||
}
|
||||
return clauses
|
||||
}
|
||||
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<S>,
|
||||
outputs: List<S>,
|
||||
commands: List<AuthenticatedObject<C>>,
|
||||
groupingKey: K?): Set<C> {
|
||||
return matchedClauses(commands).flatMapTo(HashSet<C>()) { clause ->
|
||||
clause.verify(tx, inputs, outputs, commands, groupingKey)
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString() = "All: $clauses.toList()"
|
||||
}
|
||||
@Deprecated("Use AllOf")
|
||||
class AllComposition<S : ContractState, C : CommandData, K : Any>(firstClause: Clause<S, C, K>, vararg remainingClauses: Clause<S, C, K>) : AllOf<S, C, K>(firstClause, *remainingClauses)
|
@ -0,0 +1,38 @@
|
||||
package net.corda.core.contracts.clauses
|
||||
|
||||
import net.corda.core.contracts.AuthenticatedObject
|
||||
import net.corda.core.contracts.CommandData
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.TransactionForContract
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Compose a number of clauses, such that all of the clauses must run for verification to pass.
|
||||
*/
|
||||
open class AllOf<S : ContractState, C : CommandData, K : Any>(firstClause: Clause<S, C, K>, vararg remainingClauses: Clause<S, C, K>) : CompositeClause<S, C, K>() {
|
||||
override val clauses = ArrayList<Clause<S, C, K>>()
|
||||
|
||||
init {
|
||||
clauses.add(firstClause)
|
||||
clauses.addAll(remainingClauses)
|
||||
}
|
||||
|
||||
override fun matchedClauses(commands: List<AuthenticatedObject<C>>): List<Clause<S, C, K>> {
|
||||
clauses.forEach { clause ->
|
||||
check(clause.matches(commands)) { "Failed to match clause ${clause}" }
|
||||
}
|
||||
return clauses
|
||||
}
|
||||
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<S>,
|
||||
outputs: List<S>,
|
||||
commands: List<AuthenticatedObject<C>>,
|
||||
groupingKey: K?): Set<C> {
|
||||
return matchedClauses(commands).flatMapTo(HashSet<C>()) { clause ->
|
||||
clause.verify(tx, inputs, outputs, commands, groupingKey)
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString() = "All: $clauses.toList()"
|
||||
}
|
@ -1,25 +1,10 @@
|
||||
package net.corda.core.contracts.clauses
|
||||
|
||||
import net.corda.core.contracts.AuthenticatedObject
|
||||
import net.corda.core.contracts.CommandData
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.TransactionForContract
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Compose a number of clauses, such that any number of the clauses can run.
|
||||
*/
|
||||
// TODO: Rename to AnyOf
|
||||
class AnyComposition<in S : ContractState, C : CommandData, in K : Any>(vararg rawClauses: Clause<S, C, K>) : CompositeClause<S, C, K>() {
|
||||
override val clauses: List<Clause<S, C, K>> = rawClauses.asList()
|
||||
|
||||
override fun matchedClauses(commands: List<AuthenticatedObject<C>>): List<Clause<S, C, K>> = clauses.filter { it.matches(commands) }
|
||||
|
||||
override fun verify(tx: TransactionForContract, inputs: List<S>, outputs: List<S>, commands: List<AuthenticatedObject<C>>, groupingKey: K?): Set<C> {
|
||||
return matchedClauses(commands).flatMapTo(HashSet<C>()) { clause ->
|
||||
clause.verify(tx, inputs, outputs, commands, groupingKey)
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String = "Or: ${clauses.toList()}"
|
||||
}
|
||||
@Deprecated("Use AnyOf instead, although note that any of requires at least one matched clause")
|
||||
class AnyComposition<in S : ContractState, C : CommandData, in K : Any>(vararg rawClauses: Clause<S, C, K>) : AnyOf<S, C, K>(*rawClauses)
|
@ -0,0 +1,28 @@
|
||||
package net.corda.core.contracts.clauses
|
||||
|
||||
import net.corda.core.contracts.AuthenticatedObject
|
||||
import net.corda.core.contracts.CommandData
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.TransactionForContract
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Compose a number of clauses, such that one or more of the clauses can run.
|
||||
*/
|
||||
open class AnyOf<in S : ContractState, C : CommandData, in K : Any>(vararg rawClauses: Clause<S, C, K>) : CompositeClause<S, C, K>() {
|
||||
override val clauses: List<Clause<S, C, K>> = rawClauses.toList()
|
||||
|
||||
override fun matchedClauses(commands: List<AuthenticatedObject<C>>): List<Clause<S, C, K>> {
|
||||
val matched = clauses.filter { it.matches(commands) }
|
||||
require(matched.isNotEmpty()) { "At least one clause must match" }
|
||||
return matched
|
||||
}
|
||||
|
||||
override fun verify(tx: TransactionForContract, inputs: List<S>, outputs: List<S>, commands: List<AuthenticatedObject<C>>, groupingKey: K?): Set<C> {
|
||||
return matchedClauses(commands).flatMapTo(HashSet<C>()) { clause ->
|
||||
clause.verify(tx, inputs, outputs, commands, groupingKey)
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String = "Any: ${clauses.toList()}"
|
||||
}
|
@ -26,7 +26,11 @@ abstract class Clause<in S : ContractState, C : CommandData, in K : Any> {
|
||||
|
||||
/**
|
||||
* Determine the subclauses which will be verified as a result of verifying this clause.
|
||||
*
|
||||
* @throws IllegalStateException if the given commands do not result in a valid execution (for example no match
|
||||
* with [FirstOf]).
|
||||
*/
|
||||
@Throws(IllegalStateException::class)
|
||||
open fun getExecutionPath(commands: List<AuthenticatedObject<C>>): List<Clause<*, *, *>>
|
||||
= listOf(this)
|
||||
|
||||
|
@ -14,6 +14,12 @@ abstract class CompositeClause<in S : ContractState, C : CommandData, in K : Any
|
||||
override fun getExecutionPath(commands: List<AuthenticatedObject<C>>): List<Clause<*, *, *>>
|
||||
= matchedClauses(commands).flatMap { it.getExecutionPath(commands) }
|
||||
|
||||
/** Determine which clauses are matched by the supplied commands */
|
||||
/**
|
||||
* Determine which clauses are matched by the supplied commands.
|
||||
*
|
||||
* @throws IllegalStateException if the given commands do not result in a valid execution (for example no match
|
||||
* with [FirstOf]).
|
||||
*/
|
||||
@Throws(IllegalStateException::class)
|
||||
abstract fun matchedClauses(commands: List<AuthenticatedObject<C>>): List<Clause<S, C, K>>
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ import java.util.*
|
||||
/**
|
||||
* Compose a number of clauses, such that the first match is run, and it errors if none is run.
|
||||
*/
|
||||
// TODO: Rename to FirstOf
|
||||
@Deprecated("Use FirstOf instead")
|
||||
class FirstComposition<S : ContractState, C : CommandData, K : Any>(val firstClause: Clause<S, C, K>, vararg remainingClauses: Clause<S, C, K>) : CompositeClause<S, C, K>() {
|
||||
companion object {
|
||||
val logger = loggerFor<FirstComposition<*, *, *>>()
|
||||
|
@ -0,0 +1,41 @@
|
||||
package net.corda.core.contracts.clauses
|
||||
|
||||
import net.corda.core.contracts.AuthenticatedObject
|
||||
import net.corda.core.contracts.CommandData
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.TransactionForContract
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Compose a number of clauses, such that the first match is run, and it errors if none is run.
|
||||
*/
|
||||
class FirstOf<S : ContractState, C : CommandData, K : Any>(val firstClause: Clause<S, C, K>, vararg remainingClauses: Clause<S, C, K>) : CompositeClause<S, C, K>() {
|
||||
companion object {
|
||||
val logger = loggerFor<FirstOf<*, *, *>>()
|
||||
}
|
||||
|
||||
override val clauses = ArrayList<Clause<S, C, K>>()
|
||||
|
||||
/**
|
||||
* Get the single matched clause from the set this composes, based on the given commands. This is provided as
|
||||
* helper method for internal use, rather than using the exposed [matchedClauses] function which unnecessarily
|
||||
* wraps the clause in a list.
|
||||
*/
|
||||
private fun matchedClause(commands: List<AuthenticatedObject<C>>): Clause<S, C, K> {
|
||||
return clauses.firstOrNull { it.matches(commands) } ?: throw IllegalStateException("No delegate clause matched in first composition")
|
||||
}
|
||||
|
||||
override fun matchedClauses(commands: List<AuthenticatedObject<C>>) = listOf(matchedClause(commands))
|
||||
|
||||
init {
|
||||
clauses.add(firstClause)
|
||||
clauses.addAll(remainingClauses)
|
||||
}
|
||||
|
||||
override fun verify(tx: TransactionForContract, inputs: List<S>, outputs: List<S>, commands: List<AuthenticatedObject<C>>, groupingKey: K?): Set<C> {
|
||||
return matchedClause(commands).verify(tx, inputs, outputs, commands, groupingKey)
|
||||
}
|
||||
|
||||
override fun toString() = "First: ${clauses.toList()}"
|
||||
}
|
@ -1,185 +0,0 @@
|
||||
package net.corda.core.crypto
|
||||
|
||||
import sun.security.util.HostnameChecker
|
||||
import java.net.InetAddress
|
||||
import java.net.Socket
|
||||
import java.net.UnknownHostException
|
||||
import java.security.KeyStore
|
||||
import java.security.Provider
|
||||
import java.security.Security
|
||||
import java.security.cert.CertificateException
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.net.ssl.*
|
||||
|
||||
/**
|
||||
* Call this to change the default verification algorithm and this use the WhitelistTrustManager
|
||||
* implementation. This is a work around to the fact that ArtemisMQ and probably many other libraries
|
||||
* don't correctly configure the SSLParameters with setEndpointIdentificationAlgorithm and thus don't check
|
||||
* that the certificate matches with the DNS entry requested. This exposes us to man in the middle attacks.
|
||||
* The issue has been raised with ArtemisMQ: https://issues.apache.org/jira/browse/ARTEMIS-656
|
||||
*/
|
||||
fun registerWhitelistTrustManager() {
|
||||
if (Security.getProvider("WhitelistTrustManager") == null) {
|
||||
WhitelistTrustManagerProvider.register()
|
||||
}
|
||||
|
||||
// Forcibly change the TrustManagerFactory defaultAlgorithm to be us
|
||||
// This will apply to all code using TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
|
||||
// Which includes the standard HTTPS implementation and most other SSL code
|
||||
// TrustManagerFactory.getInstance(WhitelistTrustManagerProvider.originalTrustProviderAlgorithm)) will
|
||||
// allow access to the original implementation which is normally "PKIX"
|
||||
Security.setProperty("ssl.TrustManagerFactory.algorithm", "whitelistTrustManager")
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom Security Provider that forces the TrustManagerFactory to be our custom one.
|
||||
* Also holds the identity of the original TrustManager algorithm so
|
||||
* that we can delegate most of the checking to the proper Java code. We simply add some more checks.
|
||||
*
|
||||
* The whitelist automatically includes the local server DNS name and IP address
|
||||
*
|
||||
*/
|
||||
object WhitelistTrustManagerProvider : Provider("WhitelistTrustManager",
|
||||
1.0,
|
||||
"Provider for custom trust manager that always validates certificate names") {
|
||||
|
||||
val originalTrustProviderAlgorithm = Security.getProperty("ssl.TrustManagerFactory.algorithm")
|
||||
|
||||
private val _whitelist = ConcurrentHashMap.newKeySet<String>()
|
||||
val whitelist: Set<String> get() = _whitelist.toSet() // The acceptable IP and DNS names for clients and servers.
|
||||
|
||||
init {
|
||||
// Add ourselves to whitelist as currently we have to connect to a local ArtemisMQ broker
|
||||
val host = InetAddress.getLocalHost()
|
||||
addWhitelistEntry(host.hostName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Security provider registration function for WhitelistTrustManagerProvider
|
||||
*/
|
||||
fun register() {
|
||||
Security.addProvider(WhitelistTrustManagerProvider)
|
||||
|
||||
// Register our custom TrustManagerFactorySpi
|
||||
put("TrustManagerFactory.whitelistTrustManager", "net.corda.core.crypto.WhitelistTrustManagerSpi")
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an extra name to the whitelist if not already present
|
||||
* If this is a new entry it will internally request a DNS lookup which may block the calling thread.
|
||||
*/
|
||||
fun addWhitelistEntry(serverName: String) {
|
||||
if (!_whitelist.contains(serverName)) { // Safe as we never delete from the set
|
||||
addWhitelistEntries(listOf(serverName))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a list of servers to the whitelist and also adds their fully resolved name/ip address after DNS lookup
|
||||
* If the server name is not an actual DNS name this is silently ignored.
|
||||
* The DNS request may block the calling thread.
|
||||
*/
|
||||
fun addWhitelistEntries(serverNames: List<String>) {
|
||||
_whitelist.addAll(serverNames)
|
||||
for (name in serverNames) {
|
||||
try {
|
||||
val addresses = InetAddress.getAllByName(name).toList()
|
||||
_whitelist.addAll(addresses.map { y -> y.canonicalHostName })
|
||||
_whitelist.addAll(addresses.map { y -> y.hostAddress })
|
||||
} catch (ex: UnknownHostException) {
|
||||
// Ignore if the server name is not resolvable e.g. for wildcard addresses, or addresses that can only be resolved externally
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registered TrustManagerFactorySpi
|
||||
*/
|
||||
class WhitelistTrustManagerSpi : TrustManagerFactorySpi() {
|
||||
// Get the original implementation to delegate to (can't use Kotlin delegation on abstract classes unfortunately).
|
||||
val originalProvider = TrustManagerFactory.getInstance(WhitelistTrustManagerProvider.originalTrustProviderAlgorithm)
|
||||
|
||||
override fun engineInit(keyStore: KeyStore?) {
|
||||
originalProvider.init(keyStore)
|
||||
}
|
||||
|
||||
override fun engineInit(managerFactoryParameters: ManagerFactoryParameters?) {
|
||||
originalProvider.init(managerFactoryParameters)
|
||||
}
|
||||
|
||||
override fun engineGetTrustManagers(): Array<out TrustManager> {
|
||||
val parent = originalProvider.trustManagers.first() as X509ExtendedTrustManager
|
||||
// Wrap original provider in ours and return
|
||||
return arrayOf(WhitelistTrustManager(parent))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Our TrustManager extension takes the standard certificate checker and first delegates all the
|
||||
* chain checking to that. If everything is well formed we then simply add a check against our whitelist
|
||||
*/
|
||||
class WhitelistTrustManager(val originalProvider: X509ExtendedTrustManager) : X509ExtendedTrustManager() {
|
||||
// Use same Helper class as standard HTTPS library validator
|
||||
val checker = HostnameChecker.getInstance(HostnameChecker.TYPE_TLS)
|
||||
|
||||
private fun checkIdentity(hostname: String?, cert: X509Certificate) {
|
||||
// Based on standard code in sun.security.ssl.X509TrustManagerImpl.checkIdentity
|
||||
// if IPv6 strip off the "[]"
|
||||
if ((hostname != null) && hostname.startsWith("[") && hostname.endsWith("]")) {
|
||||
checker.match(hostname.substring(1, hostname.length - 1), cert)
|
||||
} else {
|
||||
checker.match(hostname, cert)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* scan whitelist and confirm the certificate matches at least one entry
|
||||
*/
|
||||
private fun checkWhitelist(cert: X509Certificate) {
|
||||
for (whiteListEntry in WhitelistTrustManagerProvider.whitelist) {
|
||||
try {
|
||||
checkIdentity(whiteListEntry, cert)
|
||||
return // if we get here without throwing we had a match
|
||||
} catch(ex: CertificateException) {
|
||||
// Ignore and check the next entry until we find a match, or exhaust the whitelist
|
||||
}
|
||||
}
|
||||
throw CertificateException("Certificate not on whitelist ${cert.subjectDN}")
|
||||
}
|
||||
|
||||
override fun checkClientTrusted(chain: Array<out X509Certificate>, authType: String, socket: Socket?) {
|
||||
originalProvider.checkClientTrusted(chain, authType, socket)
|
||||
checkWhitelist(chain[0])
|
||||
}
|
||||
|
||||
override fun checkClientTrusted(chain: Array<out X509Certificate>, authType: String, engine: SSLEngine?) {
|
||||
originalProvider.checkClientTrusted(chain, authType, engine)
|
||||
checkWhitelist(chain[0])
|
||||
}
|
||||
|
||||
override fun checkClientTrusted(chain: Array<out X509Certificate>, authType: String) {
|
||||
originalProvider.checkClientTrusted(chain, authType)
|
||||
checkWhitelist(chain[0])
|
||||
}
|
||||
|
||||
override fun checkServerTrusted(chain: Array<out X509Certificate>, authType: String, socket: Socket?) {
|
||||
originalProvider.checkServerTrusted(chain, authType, socket)
|
||||
checkWhitelist(chain[0])
|
||||
}
|
||||
|
||||
override fun checkServerTrusted(chain: Array<out X509Certificate>, authType: String, engine: SSLEngine?) {
|
||||
originalProvider.checkServerTrusted(chain, authType, engine)
|
||||
checkWhitelist(chain[0])
|
||||
}
|
||||
|
||||
override fun checkServerTrusted(chain: Array<out X509Certificate>, authType: String) {
|
||||
originalProvider.checkServerTrusted(chain, authType)
|
||||
checkWhitelist(chain[0])
|
||||
}
|
||||
|
||||
override fun getAcceptedIssuers(): Array<out X509Certificate> {
|
||||
return originalProvider.acceptedIssuers
|
||||
}
|
||||
}
|
@ -26,21 +26,18 @@ import rx.Observable
|
||||
* it to the [subFlow] method. It will return the result of that flow when it completes.
|
||||
*/
|
||||
abstract class FlowLogic<out T> {
|
||||
|
||||
/** Reference to the [Fiber] instance that is the top level controller for the entire flow. */
|
||||
lateinit var fsm: FlowStateMachine<*>
|
||||
|
||||
/** This is where you should log things to. */
|
||||
val logger: Logger get() = fsm.logger
|
||||
val logger: Logger get() = stateMachine.logger
|
||||
|
||||
/** Returns a wrapped [UUID] object that identifies this state machine run (i.e. subflows have the same identifier as their parents). */
|
||||
val runId: StateMachineRunId get() = stateMachine.id
|
||||
|
||||
/**
|
||||
* Provides access to big, heavy classes that may be reconstructed from time to time, e.g. across restarts. It is
|
||||
* only available once the flow has started, which means it cannnot be accessed in the constructor. Either
|
||||
* access this lazily or from inside [call].
|
||||
*/
|
||||
val serviceHub: ServiceHub get() = fsm.serviceHub
|
||||
|
||||
private var sessionFlow: FlowLogic<*> = this
|
||||
val serviceHub: ServiceHub get() = stateMachine.serviceHub
|
||||
|
||||
/**
|
||||
* Return the marker [Class] which [party] has used to register the counterparty flow that is to execute on the
|
||||
@ -49,35 +46,73 @@ abstract class FlowLogic<out T> {
|
||||
*/
|
||||
open fun getCounterpartyMarker(party: Party): Class<*> = javaClass
|
||||
|
||||
// Kotlin helpers that allow the use of generic types.
|
||||
inline fun <reified T : Any> sendAndReceive(otherParty: Party, payload: Any): UntrustworthyData<T> {
|
||||
return sendAndReceive(otherParty, payload, T::class.java)
|
||||
}
|
||||
|
||||
// TODO: Move the receiveType param to first position for readability
|
||||
/**
|
||||
* Serializes and queues the given [payload] object for sending to the [otherParty]. Suspends until a response
|
||||
* is received, which must be of the given [R] type.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
inline fun <reified R : Any> sendAndReceive(otherParty: Party, payload: Any) = sendAndReceive(R::class.java, otherParty, payload)
|
||||
|
||||
/**
|
||||
* Serializes and queues the given [payload] object for sending to the [otherParty]. Suspends until a response
|
||||
* is received, which must be of the given [receiveType]. 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.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
@Suspendable
|
||||
fun <T : Any> sendAndReceive(otherParty: Party, payload: Any, receiveType: Class<T>): UntrustworthyData<T> {
|
||||
return fsm.sendAndReceive(otherParty, payload, receiveType, sessionFlow)
|
||||
}
|
||||
|
||||
inline fun <reified T : Any> receive(otherParty: Party): UntrustworthyData<T> = receive(otherParty, T::class.java)
|
||||
|
||||
// TODO: Move the receiveType param to first position for readability
|
||||
|
||||
@Suspendable
|
||||
fun <T : Any> receive(otherParty: Party, receiveType: Class<T>): UntrustworthyData<T> {
|
||||
return fsm.receive(otherParty, receiveType, sessionFlow)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
fun send(otherParty: Party, payload: Any) {
|
||||
fsm.send(otherParty, payload, sessionFlow)
|
||||
open fun <T : Any> sendAndReceive(receiveType: Class<T>, otherParty: Party, payload: Any): UntrustworthyData<T> {
|
||||
return stateMachine.sendAndReceive(receiveType, otherParty, payload, sessionFlow)
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the given subflow by simply passing through this [FlowLogic]s reference to the
|
||||
* [FlowStateMachine] and then calling the [call] method.
|
||||
* Suspends until the specified [otherParty] sends us a message of type [R].
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
inline fun <reified R : Any> receive(otherParty: Party): UntrustworthyData<R> = receive(R::class.java, otherParty)
|
||||
|
||||
/**
|
||||
* Suspends until the specified [otherParty] sends us a message of type [receiveType].
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
@Suspendable
|
||||
open fun <T : Any> receive(receiveType: Class<T>, otherParty: Party): UntrustworthyData<T> {
|
||||
return stateMachine.receive(receiveType, otherParty, sessionFlow)
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues the given [payload] for sending to the [otherParty] and continues without suspending.
|
||||
*
|
||||
* Note that the other party may receive the message at some arbitrary later point or not at all: if [otherParty]
|
||||
* is offline then message delivery will be retried until it comes back or until the message is older than the
|
||||
* network's event horizon time.
|
||||
*/
|
||||
@Suspendable
|
||||
open fun send(otherParty: Party, payload: Any) = stateMachine.send(otherParty, payload, sessionFlow)
|
||||
|
||||
/**
|
||||
* Invokes the given subflow. This function returns once the subflow completes successfully with the result
|
||||
* returned by that subflows [call] method. If the subflow has a progress tracker, it is attached to the
|
||||
* current step in this flow's progress tracker.
|
||||
*
|
||||
* @param shareParentSessions In certain situations the need arises to use the same sessions the parent flow has
|
||||
* already established. However this also prevents the subflow from creating new sessions with those parties.
|
||||
* For this reason the default value is false.
|
||||
@ -85,8 +120,9 @@ abstract class FlowLogic<out T> {
|
||||
// TODO Rethink the default value for shareParentSessions
|
||||
// TODO shareParentSessions is a bit too low-level and perhaps can be expresed in a better way
|
||||
@Suspendable
|
||||
fun <R> subFlow(subLogic: FlowLogic<R>, shareParentSessions: Boolean = false): R {
|
||||
subLogic.fsm = fsm
|
||||
@JvmOverloads
|
||||
open fun <R> subFlow(subLogic: FlowLogic<R>, shareParentSessions: Boolean = false): R {
|
||||
subLogic.stateMachine = stateMachine
|
||||
maybeWireUpProgressTracking(subLogic)
|
||||
if (shareParentSessions) {
|
||||
subLogic.sessionFlow = this
|
||||
@ -97,6 +133,52 @@ abstract class FlowLogic<out T> {
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Override this to provide a [ProgressTracker]. If one is provided and stepped, the framework will do something
|
||||
* helpful with the progress reports. If this flow is invoked as a subflow of another, then the
|
||||
* tracker will be made a child of the current step in the parent. If it's null, this flow doesn't track
|
||||
* progress.
|
||||
*
|
||||
* Note that this has to return a tracker before the flow is invoked. You can't change your mind half way
|
||||
* through.
|
||||
*/
|
||||
open val progressTracker: ProgressTracker? = null
|
||||
|
||||
/**
|
||||
* This is where you fill out your business logic. The returned object will usually be ignored, but can be
|
||||
* helpful if this flow is meant to be used as a subflow.
|
||||
*/
|
||||
@Suspendable
|
||||
abstract fun call(): T
|
||||
|
||||
/**
|
||||
* Returns a pair of the current progress step, as a string, and an observable of stringified changes to the
|
||||
* [progressTracker].
|
||||
*
|
||||
* @return Returns null if this flow has no progress tracker.
|
||||
*/
|
||||
fun track(): Pair<String, Observable<String>>? {
|
||||
// TODO this is not threadsafe, needs an atomic get-step-and-subscribe
|
||||
return progressTracker?.let {
|
||||
Pair(it.currentStep.toString(), it.changes.map { it.toString() })
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private var _stateMachine: FlowStateMachine<*>? = null
|
||||
/**
|
||||
* Internal only. Reference to the [Fiber] instance that is the top level controller for the entire flow. When
|
||||
* inside a flow this is equivalent to [Strand.currentStrand]. This is public only because it must be accessed
|
||||
* across module boundaries.
|
||||
*/
|
||||
var stateMachine: FlowStateMachine<*>
|
||||
get() = _stateMachine ?: throw IllegalStateException("This can only be done after the flow has been started.")
|
||||
set(value) { _stateMachine = value }
|
||||
|
||||
// This points to the outermost flow and is changed when a subflow is invoked.
|
||||
private var sessionFlow: FlowLogic<*> = this
|
||||
|
||||
private fun maybeWireUpProgressTracking(subLogic: FlowLogic<*>) {
|
||||
val ours = progressTracker
|
||||
|
||||
@ -109,27 +191,4 @@ abstract class FlowLogic<out T> {
|
||||
ours.setChildProgressTracker(ours.currentStep, theirs)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Override this to provide a [ProgressTracker]. If one is provided and stepped, the framework will do something
|
||||
* helpful with the progress reports. If this flow is invoked as a sub-flow of another, then the
|
||||
* tracker will be made a child of the current step in the parent. If it's null, this flow doesn't track
|
||||
* progress.
|
||||
*
|
||||
* Note that this has to return a tracker before the flow is invoked. You can't change your mind half way
|
||||
* through.
|
||||
*/
|
||||
open val progressTracker: ProgressTracker? = null
|
||||
|
||||
/** This is where you fill out your business logic. */
|
||||
@Suspendable
|
||||
abstract fun call(): T
|
||||
|
||||
// TODO this is not threadsafe, needs an atomic get-step-and-subscribe
|
||||
fun track(): Pair<String, Observable<String>>? {
|
||||
return progressTracker?.let {
|
||||
Pair(it.currentStep.toString(), it.changes.map { it.toString() })
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -8,8 +8,11 @@ import net.corda.core.utilities.UntrustworthyData
|
||||
import org.slf4j.Logger
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* A unique identifier for a single state machine run, valid across node restarts. Note that a single run always
|
||||
* has at least one flow, but that flow may also invoke sub-flows: they all share the same run id.
|
||||
*/
|
||||
data class StateMachineRunId private constructor(val uuid: UUID) {
|
||||
|
||||
companion object {
|
||||
fun createRandom(): StateMachineRunId = StateMachineRunId(UUID.randomUUID())
|
||||
fun wrap(uuid: UUID): StateMachineRunId = StateMachineRunId(uuid)
|
||||
@ -18,34 +21,24 @@ data class StateMachineRunId private constructor(val uuid: UUID) {
|
||||
override fun toString(): String = "[$uuid]"
|
||||
}
|
||||
|
||||
/**
|
||||
* A FlowStateMachine instance is a suspendable fiber that delegates all actual logic to a [FlowLogic] instance.
|
||||
* For any given flow there is only one PSM, even if that flow invokes subflows.
|
||||
*
|
||||
* These classes are created by the [StateMachineManager] when a new flow is started at the topmost level. If
|
||||
* a flow invokes a sub-flow, then it will pass along the PSM to the child. The call method of the topmost
|
||||
* logic element gets to return the value that the entire state machine resolves to.
|
||||
*/
|
||||
/** This is an internal interface that is implemented by code in the node module. You should look at [FlowLogic]. */
|
||||
interface FlowStateMachine<R> {
|
||||
@Suspendable
|
||||
fun <T : Any> sendAndReceive(otherParty: Party,
|
||||
fun <T : Any> sendAndReceive(receiveType: Class<T>,
|
||||
otherParty: Party,
|
||||
payload: Any,
|
||||
receiveType: Class<T>,
|
||||
sessionFlow: FlowLogic<*>): UntrustworthyData<T>
|
||||
|
||||
@Suspendable
|
||||
fun <T : Any> receive(otherParty: Party, receiveType: Class<T>, sessionFlow: FlowLogic<*>): UntrustworthyData<T>
|
||||
fun <T : Any> receive(receiveType: Class<T>, otherParty: Party, sessionFlow: FlowLogic<*>): UntrustworthyData<T>
|
||||
|
||||
@Suspendable
|
||||
fun send(otherParty: Party, payload: Any, sessionFlow: FlowLogic<*>)
|
||||
|
||||
val serviceHub: ServiceHub
|
||||
val logger: Logger
|
||||
|
||||
/** Unique ID for this machine run, valid across restarts */
|
||||
val id: StateMachineRunId
|
||||
/** This future will complete when the call method returns. */
|
||||
val resultFuture: ListenableFuture<R>
|
||||
}
|
||||
|
||||
class FlowSessionException(message: String) : Exception(message)
|
||||
class FlowException(message: String) : RuntimeException(message)
|
||||
|
@ -23,8 +23,12 @@ data class StateMachineInfo(
|
||||
)
|
||||
|
||||
sealed class StateMachineUpdate(val id: StateMachineRunId) {
|
||||
class Added(val stateMachineInfo: StateMachineInfo) : StateMachineUpdate(stateMachineInfo.id)
|
||||
class Removed(id: StateMachineRunId) : StateMachineUpdate(id)
|
||||
class Added(val stateMachineInfo: StateMachineInfo) : StateMachineUpdate(stateMachineInfo.id) {
|
||||
override fun toString() = "Added($id, ${stateMachineInfo.flowLogicClassName})"
|
||||
}
|
||||
class Removed(id: StateMachineRunId) : StateMachineUpdate(id) {
|
||||
override fun toString() = "Removed($id)"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -172,5 +176,6 @@ inline fun <T : Any, A, B, C, D, reified R : FlowLogic<T>> CordaRPCOps.startFlow
|
||||
data class FlowHandle<A>(
|
||||
val id: StateMachineRunId,
|
||||
val progress: Observable<String>,
|
||||
// TODO This should be ListenableFuture<A>
|
||||
val returnValue: Observable<A>
|
||||
)
|
||||
|
@ -4,6 +4,7 @@ import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import net.corda.core.catch
|
||||
import net.corda.core.node.services.DEFAULT_SESSION_ID
|
||||
import net.corda.core.node.services.PartyInfo
|
||||
import net.corda.core.serialization.DeserializeAsKotlinObjectDef
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
@ -79,6 +80,9 @@ interface MessagingService {
|
||||
*/
|
||||
fun createMessage(topicSession: TopicSession, data: ByteArray, uuid: UUID = UUID.randomUUID()): Message
|
||||
|
||||
/** Given information about either a specific node or a service returns its corresponding address */
|
||||
fun getAddressOfParty(partyInfo: PartyInfo): MessageRecipients
|
||||
|
||||
/** Returns an address that refers to this node. */
|
||||
val myAddress: SingleMessageRecipient
|
||||
}
|
||||
@ -127,7 +131,7 @@ inline fun MessagingService.runOnNextMessage(topicSession: TopicSession, crossin
|
||||
|
||||
/**
|
||||
* Returns a [ListenableFuture] of the next message payload ([Message.data]) which is received on the given topic and sessionId.
|
||||
* The payload is deserilaized to an object of type [M]. Any exceptions thrown will be captured by the future.
|
||||
* The payload is deserialized to an object of type [M]. Any exceptions thrown will be captured by the future.
|
||||
*/
|
||||
fun <M : Any> MessagingService.onNext(topic: String, sessionId: Long): ListenableFuture<M> {
|
||||
val messageFuture = SettableFuture.create<M>()
|
||||
|
@ -58,6 +58,7 @@ interface ServiceHub {
|
||||
|
||||
/**
|
||||
* Will check [logicType] and [args] against a whitelist and if acceptable then construct and initiate the flow.
|
||||
* Note that you must be on the server thread to call this method.
|
||||
*
|
||||
* @throws IllegalFlowLogicException or IllegalArgumentException if there are problems with the [logicType] or [args].
|
||||
*/
|
||||
@ -81,7 +82,6 @@ interface ServiceHub {
|
||||
* Typical use is during signing in flows and for unit test signing.
|
||||
*/
|
||||
val notaryIdentityKey: KeyPair get() = this.keyManagementService.toKeyPair(this.myInfo.notaryIdentity.owningKey.keys)
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -5,9 +5,11 @@ import com.google.common.util.concurrent.ListenableFuture
|
||||
import net.corda.core.contracts.Contract
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.messaging.MessageRecipients
|
||||
import net.corda.core.messaging.MessagingService
|
||||
import net.corda.core.messaging.SingleMessageRecipient
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.ServiceEntry
|
||||
import net.corda.core.randomOrNull
|
||||
import rx.Observable
|
||||
|
||||
@ -63,31 +65,27 @@ interface NetworkMapCache {
|
||||
/** Look up the node info for a legal name. */
|
||||
fun getNodeByLegalName(name: String): NodeInfo? = partyNodes.singleOrNull { it.legalIdentity.name == name }
|
||||
|
||||
/** Look up the node info for a composite key. */
|
||||
fun getNodeByCompositeKey(compositeKey: CompositeKey): NodeInfo? {
|
||||
/**
|
||||
* In general, nodes can advertise multiple identities: a legal identity, and separate identities for each of
|
||||
* the services it provides. In case of a distributed service – run by multiple nodes – each participant advertises
|
||||
* the identity of the *whole group*.
|
||||
*/
|
||||
|
||||
/** Look up the node info for a specific peer key. */
|
||||
fun getNodeByLegalIdentityKey(compositeKey: CompositeKey): NodeInfo? {
|
||||
// Although we should never have more than one match, it is theoretically possible. Report an error if it happens.
|
||||
val candidates = partyNodes.filter {
|
||||
(it.legalIdentity.owningKey == compositeKey)
|
||||
|| it.advertisedServices.any { it.identity.owningKey == compositeKey }
|
||||
}
|
||||
val candidates = partyNodes.filter { it.legalIdentity.owningKey == compositeKey }
|
||||
check(candidates.size <= 1) { "Found more than one match for key $compositeKey" }
|
||||
return candidates.singleOrNull()
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a [party], returns a node advertising it as an identity. If more than one node found the result
|
||||
* is chosen at random.
|
||||
*
|
||||
* In general, nodes can advertise multiple identities: a legal identity, and separate identities for each of
|
||||
* the services it provides. In case of a distributed service – run by multiple nodes – each participant advertises
|
||||
* the identity of the *whole group*. If the provided [party] is a group identity, multiple nodes advertising it
|
||||
* will be found, and this method will return a randomly chosen one. If [party] is an individual (legal) identity,
|
||||
* we currently assume that it will be advertised by one node only, which will be returned as the result.
|
||||
*/
|
||||
fun getRepresentativeNode(party: Party): NodeInfo? {
|
||||
return partyNodes.randomOrNull { it.legalIdentity == party || it.advertisedServices.any { it.identity == party } }
|
||||
/** Look up all nodes advertising the service owned by [compositeKey] */
|
||||
fun getNodesByAdvertisedServiceIdentityKey(compositeKey: CompositeKey): List<NodeInfo> {
|
||||
return partyNodes.filter { it.advertisedServices.any { it.identity.owningKey == compositeKey } }
|
||||
}
|
||||
|
||||
/** Returns information about the party, which may be a specific node or a service */
|
||||
fun getPartyInfo(party: Party): PartyInfo?
|
||||
|
||||
/** Gets a notary identity by the given name. */
|
||||
fun getNotary(name: String): Party? {
|
||||
val notaryNode = notaryNodes.randomOrNull {
|
||||
|
@ -0,0 +1,18 @@
|
||||
package net.corda.core.node.services
|
||||
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.ServiceEntry
|
||||
|
||||
/**
|
||||
* Holds information about a [Party], which may refer to either a specific node or a service.
|
||||
*/
|
||||
sealed class PartyInfo() {
|
||||
abstract val party: Party
|
||||
class Node(val node: NodeInfo) : PartyInfo() {
|
||||
override val party = node.legalIdentity
|
||||
}
|
||||
class Service(val service: ServiceEntry) : PartyInfo() {
|
||||
override val party = service.identity
|
||||
}
|
||||
}
|
@ -11,15 +11,9 @@ sealed class ServiceType(val id: String) {
|
||||
//
|
||||
// * IDs must start with a lower case letter
|
||||
// * IDs can only contain alphanumeric, full stop and underscore ASCII characters
|
||||
require(id.matches(Regex("[a-z][a-zA-Z0-9._]+")))
|
||||
}
|
||||
|
||||
private class ServiceTypeImpl(baseId: String, subTypeId: String) : ServiceType("$baseId.$subTypeId") {
|
||||
init {
|
||||
// only allowed one level of subtype
|
||||
require(subTypeId.matches(Regex("[a-z]\\w+")))
|
||||
}
|
||||
require(id.matches(Regex("[a-z][a-zA-Z0-9._]+"))) { id }
|
||||
}
|
||||
private class ServiceTypeImpl(baseId: String, subTypeId: String) : ServiceType("$baseId.$subTypeId")
|
||||
|
||||
private class ServiceTypeDirect(id: String) : ServiceType(id)
|
||||
|
||||
|
@ -1,12 +1,12 @@
|
||||
package net.corda.core.node.services
|
||||
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.toStringShort
|
||||
import net.corda.core.toFuture
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import rx.Observable
|
||||
@ -38,7 +38,7 @@ val DEFAULT_SESSION_ID = 0L
|
||||
* Active means they haven't been consumed yet (or we don't know about it).
|
||||
* Relevant means they contain at least one of our pubkeys.
|
||||
*/
|
||||
class Vault(val states: Iterable<StateAndRef<ContractState>>) {
|
||||
class Vault(val states: List<StateAndRef<ContractState>>) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
inline fun <reified T : ContractState> statesOfType() = states.filter { it.state.data is T } as List<StateAndRef<T>>
|
||||
|
||||
@ -50,7 +50,10 @@ class Vault(val states: Iterable<StateAndRef<ContractState>>) {
|
||||
* If the vault observes multiple transactions simultaneously, where some transactions consume the outputs of some of the
|
||||
* other transactions observed, then the changes are observed "net" of those.
|
||||
*/
|
||||
data class Update(val consumed: Set<StateRef>, val produced: Set<StateAndRef<ContractState>>) {
|
||||
data class Update(val consumed: Set<StateAndRef<ContractState>>, val produced: Set<StateAndRef<ContractState>>) {
|
||||
/** Checks whether the update contains a state of the specified type. */
|
||||
inline fun <reified T : ContractState> containsType() = consumed.any { it.state.data is T } || produced.any { it.state.data is T }
|
||||
|
||||
/**
|
||||
* Combine two updates into a single update with the combined inputs and outputs of the two updates but net
|
||||
* any outputs of the left-hand-side (this) that are consumed by the inputs of the right-hand-side (rhs).
|
||||
@ -58,12 +61,10 @@ class Vault(val states: Iterable<StateAndRef<ContractState>>) {
|
||||
* i.e. the net effect in terms of state live-ness of receiving the combined update is the same as receiving this followed by rhs.
|
||||
*/
|
||||
operator fun plus(rhs: Update): Update {
|
||||
val previouslyProduced = produced.map { it.ref }
|
||||
val previouslyConsumed = consumed
|
||||
val combined = Vault.Update(
|
||||
previouslyConsumed + (rhs.consumed - previouslyProduced),
|
||||
consumed + (rhs.consumed - produced),
|
||||
// The ordering below matters to preserve ordering of consumed/produced Sets when they are insertion order dependent implementations.
|
||||
produced.filter { it.ref !in rhs.consumed }.toSet() + rhs.produced)
|
||||
produced.filter { it !in rhs.consumed }.toSet() + rhs.produced)
|
||||
return combined
|
||||
}
|
||||
|
||||
@ -120,15 +121,7 @@ interface VaultService {
|
||||
* Returns a map of how much cash we have in each currency, ignoring details like issuer. Note: currencies for
|
||||
* which we have no cash evaluate to null (not present in map), not 0.
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val cashBalances: Map<Currency, Amount<Currency>>
|
||||
get() = currentVault.states.
|
||||
// Select the states we own which are cash, ignore the rest, take the amounts.
|
||||
mapNotNull { (it.state.data as? FungibleAsset<Currency>)?.amount }.
|
||||
// Turn into a Map<Currency, List<Amount>> like { GBP -> (£100, £500, etc), USD -> ($2000, $50) }
|
||||
groupBy { it.token.product }.
|
||||
// Collapse to Map<Currency, Amount> by summing all the amounts of the same currency together.
|
||||
mapValues { it.value.map { Amount(it.quantity, it.token.product) }.sumOrThrow() }
|
||||
|
||||
/**
|
||||
* Atomically get the current vault and a stream of updates. Note that the Observable buffers updates until the
|
||||
@ -158,24 +151,18 @@ interface VaultService {
|
||||
* Possibly update the vault by marking as spent states that these transactions consume, and adding any relevant
|
||||
* new states that they create. You should only insert transactions that have been successfully verified here!
|
||||
*
|
||||
* Returns the new vault that resulted from applying the transactions (note: it may quickly become out of date).
|
||||
*
|
||||
* TODO: Consider if there's a good way to enforce the must-be-verified requirement in the type system.
|
||||
*/
|
||||
fun notifyAll(txns: Iterable<WireTransaction>): Vault
|
||||
fun notifyAll(txns: Iterable<WireTransaction>)
|
||||
|
||||
/** Same as notifyAll but with a single transaction. */
|
||||
fun notify(tx: WireTransaction): Vault = notifyAll(listOf(tx))
|
||||
fun notify(tx: WireTransaction) = notifyAll(listOf(tx))
|
||||
|
||||
/**
|
||||
* Provide a [Future] for when a [StateRef] is consumed, which can be very useful in building tests.
|
||||
*/
|
||||
fun whenConsumed(ref: StateRef): ListenableFuture<Vault.Update> {
|
||||
val future = SettableFuture.create<Vault.Update>()
|
||||
updates.filter { ref in it.consumed }.first().subscribe {
|
||||
future.set(it)
|
||||
}
|
||||
return future
|
||||
return updates.filter { it.consumed.any { it.ref == ref } }.toFuture()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -410,10 +410,8 @@ fun createKryo(k: Kryo = Kryo()): Kryo {
|
||||
}
|
||||
})
|
||||
|
||||
// Some things where the JRE provides an efficient custom serialisation.
|
||||
val keyPair = generateKeyPair()
|
||||
register(keyPair.public.javaClass, Ed25519PublicKeySerializer)
|
||||
register(keyPair.private.javaClass, Ed25519PrivateKeySerializer)
|
||||
register(EdDSAPublicKey::class.java, Ed25519PublicKeySerializer)
|
||||
register(EdDSAPrivateKey::class.java, Ed25519PrivateKeySerializer)
|
||||
register(Instant::class.java, ReferencesAwareJavaSerializer)
|
||||
|
||||
// Using a custom serializer for compactness
|
||||
|
@ -159,11 +159,12 @@ open class TransactionBuilder(
|
||||
return outputs.size - 1
|
||||
}
|
||||
|
||||
fun addOutputState(state: ContractState, notary: Party) = addOutputState(TransactionState(state, notary))
|
||||
@JvmOverloads
|
||||
fun addOutputState(state: ContractState, notary: Party, encumbrance: Int? = null) = addOutputState(TransactionState(state, notary, encumbrance))
|
||||
|
||||
/** A default notary must be specified during builder construction to use this method */
|
||||
fun addOutputState(state: ContractState): Int {
|
||||
checkNotNull(notary) { "Need to specify a Notary for the state, or set a default one on TransactionBuilder initialisation" }
|
||||
checkNotNull(notary) { "Need to specify a notary for the state, or set a default one on TransactionBuilder initialisation" }
|
||||
return addOutputState(state, notary!!)
|
||||
}
|
||||
|
||||
|
@ -5,8 +5,9 @@ package net.corda.core.utilities
|
||||
*/
|
||||
object Emoji {
|
||||
// Unfortunately only Apple has a terminal that can do colour emoji AND an emoji font installed by default.
|
||||
val hasEmojiTerminal by lazy { System.getenv("TERM_PROGRAM") == "Apple_Terminal" }
|
||||
val hasEmojiTerminal by lazy { listOf("Apple_Terminal", "iTerm.app").contains(System.getenv("TERM_PROGRAM")) }
|
||||
|
||||
const val CODE_SANTA_CLAUS = "\ud83c\udf85"
|
||||
const val CODE_DIAMOND = "\ud83d\udd37"
|
||||
const val CODE_BAG_OF_CASH = "\ud83d\udcb0"
|
||||
const val CODE_NEWSPAPER = "\ud83d\udcf0"
|
||||
@ -22,6 +23,7 @@ object Emoji {
|
||||
*/
|
||||
val emojiMode = ThreadLocal<Any>()
|
||||
|
||||
val santaClaus: String get() = if (emojiMode.get() != null) "$CODE_SANTA_CLAUS " else ""
|
||||
val diamond: String get() = if (emojiMode.get() != null) "$CODE_DIAMOND " else ""
|
||||
val bagOfCash: String get() = if (emojiMode.get() != null) "$CODE_BAG_OF_CASH " else ""
|
||||
val newspaper: String get() = if (emojiMode.get() != null) "$CODE_NEWSPAPER " else ""
|
||||
|
@ -67,12 +67,12 @@ abstract class AbstractStateReplacementFlow<T> {
|
||||
}
|
||||
|
||||
abstract protected fun assembleProposal(stateRef: StateRef, modification: T, stx: SignedTransaction): Proposal<T>
|
||||
abstract protected fun assembleTx(): Pair<SignedTransaction, List<CompositeKey>>
|
||||
abstract protected fun assembleTx(): Pair<SignedTransaction, Iterable<CompositeKey>>
|
||||
|
||||
@Suspendable
|
||||
private fun collectSignatures(participants: List<CompositeKey>, stx: SignedTransaction): List<DigitalSignature.WithKey> {
|
||||
private fun collectSignatures(participants: Iterable<CompositeKey>, stx: SignedTransaction): List<DigitalSignature.WithKey> {
|
||||
val parties = participants.map {
|
||||
val participantNode = serviceHub.networkMapCache.getNodeByCompositeKey(it) ?:
|
||||
val participantNode = serviceHub.networkMapCache.getNodeByLegalIdentityKey(it) ?:
|
||||
throw IllegalStateException("Participant $it to state $originalState not found on the network")
|
||||
participantNode.legalIdentity
|
||||
}
|
||||
|
@ -1,13 +1,11 @@
|
||||
package net.corda.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.TransactionType
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.UntrustworthyData
|
||||
import net.corda.flows.NotaryChangeFlow.Acceptor
|
||||
@ -36,17 +34,66 @@ object NotaryChangeFlow : AbstractStateReplacementFlow<Party>() {
|
||||
override fun assembleProposal(stateRef: StateRef, modification: Party, stx: SignedTransaction): AbstractStateReplacementFlow.Proposal<Party>
|
||||
= Proposal(stateRef, modification, stx)
|
||||
|
||||
override fun assembleTx(): Pair<SignedTransaction, List<CompositeKey>> {
|
||||
override fun assembleTx(): Pair<SignedTransaction, Iterable<CompositeKey>> {
|
||||
val state = originalState.state
|
||||
val newState = state.withNotary(modification)
|
||||
val participants = state.data.participants
|
||||
val tx = TransactionType.NotaryChange.Builder(originalState.state.notary).withItems(originalState, newState)
|
||||
val tx = TransactionType.NotaryChange.Builder(originalState.state.notary)
|
||||
|
||||
val participants: Iterable<CompositeKey>
|
||||
|
||||
if (state.encumbrance == null) {
|
||||
val modifiedState = TransactionState(state.data, modification)
|
||||
tx.addInputState(originalState)
|
||||
tx.addOutputState(modifiedState)
|
||||
participants = state.data.participants
|
||||
} else {
|
||||
participants = resolveEncumbrances(tx)
|
||||
}
|
||||
|
||||
val myKey = serviceHub.legalIdentityKey
|
||||
tx.signWith(myKey)
|
||||
|
||||
val stx = tx.toSignedTransaction(false)
|
||||
|
||||
return Pair(stx, participants)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the notary change state transitions to the [tx] builder for the [originalState] and its encumbrance
|
||||
* state chain (encumbrance states might be themselves encumbered by other states).
|
||||
*
|
||||
* @return union of all added states' participants
|
||||
*/
|
||||
private fun resolveEncumbrances(tx: TransactionBuilder): Iterable<CompositeKey> {
|
||||
val stateRef = originalState.ref
|
||||
val txId = stateRef.txhash
|
||||
val issuingTx = serviceHub.storageService.validatedTransactions.getTransaction(txId) ?: throw IllegalStateException("Transaction $txId not found")
|
||||
val outputs = issuingTx.tx.outputs
|
||||
|
||||
val participants = mutableSetOf<CompositeKey>()
|
||||
|
||||
var nextStateIndex = stateRef.index
|
||||
var newOutputPosition = tx.outputStates().size
|
||||
while (true) {
|
||||
val nextState = outputs[nextStateIndex]
|
||||
tx.addInputState(StateAndRef(nextState, StateRef(txId, nextStateIndex)))
|
||||
participants.addAll(nextState.data.participants)
|
||||
|
||||
if (nextState.encumbrance == null) {
|
||||
val modifiedState = TransactionState(nextState.data, modification)
|
||||
tx.addOutputState(modifiedState)
|
||||
break
|
||||
} else {
|
||||
val modifiedState = TransactionState(nextState.data, modification, newOutputPosition + 1)
|
||||
tx.addOutputState(modifiedState)
|
||||
nextStateIndex = nextState.encumbrance
|
||||
}
|
||||
|
||||
newOutputPosition++
|
||||
}
|
||||
|
||||
return participants
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Acceptor(otherSide: Party,
|
||||
|
@ -1,7 +1,10 @@
|
||||
package net.corda.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.crypto.DigitalSignature
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.crypto.SignedData
|
||||
import net.corda.core.crypto.signWithECDSA
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.node.services.TimestampChecker
|
||||
import net.corda.core.node.services.UniquenessException
|
||||
@ -47,7 +50,7 @@ object NotaryFlow {
|
||||
try {
|
||||
stx.verifySignatures(notaryParty.owningKey)
|
||||
} catch (ex: SignedTransaction.SignaturesMissingException) {
|
||||
throw NotaryException(NotaryError.SignaturesMissing(ex.missing))
|
||||
throw NotaryException(NotaryError.SignaturesMissing(ex))
|
||||
}
|
||||
|
||||
val response = sendAndReceive<Result>(notaryParty, SignRequest(stx))
|
||||
@ -129,13 +132,22 @@ object NotaryFlow {
|
||||
open fun beforeCommit(stx: SignedTransaction) {
|
||||
}
|
||||
|
||||
/**
|
||||
* A NotaryException is thrown if any of the states have been consumed by a different transaction. Note that
|
||||
* this method does not throw an exception when input states are present multiple times within the transaction.
|
||||
*/
|
||||
private fun commitInputStates(tx: WireTransaction) {
|
||||
try {
|
||||
uniquenessProvider.commit(tx.inputs, tx.id, otherSide)
|
||||
} catch (e: UniquenessException) {
|
||||
val conflictData = e.error.serialize()
|
||||
val signedConflict = SignedData(conflictData, sign(conflictData.bytes))
|
||||
throw NotaryException(NotaryError.Conflict(tx, signedConflict))
|
||||
val conflicts = tx.inputs.filterIndexed { i, stateRef ->
|
||||
val consumingTx = e.error.stateHistory[stateRef]
|
||||
consumingTx != null && consumingTx != UniquenessProvider.ConsumingTx(tx.id, i, otherSide)
|
||||
}
|
||||
if (conflicts.isNotEmpty()) {
|
||||
// TODO: Create a new UniquenessException that only contains the conflicts filtered above.
|
||||
throw notaryException(tx, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -143,6 +155,12 @@ object NotaryFlow {
|
||||
val mySigningKey = serviceHub.notaryIdentityKey
|
||||
return mySigningKey.signWithECDSA(bits)
|
||||
}
|
||||
|
||||
private fun notaryException(tx: WireTransaction, e: UniquenessException): NotaryException {
|
||||
val conflictData = e.error.serialize()
|
||||
val signedConflict = SignedData(conflictData, sign(conflictData.bytes))
|
||||
return NotaryException(NotaryError.Conflict(tx, signedConflict))
|
||||
}
|
||||
}
|
||||
|
||||
data class SignRequest(val tx: SignedTransaction)
|
||||
@ -166,9 +184,10 @@ sealed class NotaryError {
|
||||
/** Thrown if the time specified in the timestamp command is outside the allowed tolerance */
|
||||
class TimestampInvalid : NotaryError()
|
||||
|
||||
class TransactionInvalid : NotaryError()
|
||||
class TransactionInvalid(val msg: String) : NotaryError()
|
||||
class SignaturesInvalid(val msg: String): NotaryError()
|
||||
|
||||
class SignaturesMissing(val missingSigners: Set<CompositeKey>) : NotaryError() {
|
||||
override fun toString() = "Missing signatures from: ${missingSigners.map { it.toBase58String() }}"
|
||||
class SignaturesMissing(val cause: SignedTransaction.SignaturesMissingException) : NotaryError() {
|
||||
override fun toString() = cause.toString()
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,7 @@
|
||||
package net.corda.flows
|
||||
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import net.corda.core.messaging.MessagingService
|
||||
import net.corda.core.messaging.SingleMessageRecipient
|
||||
import net.corda.core.messaging.onNext
|
||||
import net.corda.core.messaging.send
|
||||
import net.corda.core.messaging.*
|
||||
import net.corda.core.node.services.DEFAULT_SESSION_ID
|
||||
|
||||
/**
|
||||
@ -21,7 +18,7 @@ interface ServiceRequestMessage {
|
||||
*/
|
||||
fun <R : Any> MessagingService.sendRequest(topic: String,
|
||||
request: ServiceRequestMessage,
|
||||
target: SingleMessageRecipient): ListenableFuture<R> {
|
||||
target: MessageRecipients): ListenableFuture<R> {
|
||||
val responseFuture = onNext<R>(topic, request.sessionID)
|
||||
send(topic, DEFAULT_SESSION_ID, request, target)
|
||||
return responseFuture
|
||||
|
@ -29,8 +29,8 @@ class ValidatingNotaryFlow(otherSide: Party,
|
||||
wtx.toLedgerTransaction(serviceHub).verify()
|
||||
} catch (e: Exception) {
|
||||
when (e) {
|
||||
is TransactionVerificationException,
|
||||
is SignatureException -> throw NotaryException(NotaryError.TransactionInvalid())
|
||||
is TransactionVerificationException -> NotaryException(NotaryError.TransactionInvalid(e.toString()))
|
||||
is SignatureException -> throw NotaryException(NotaryError.SignaturesInvalid(e.toString()))
|
||||
else -> throw e
|
||||
}
|
||||
}
|
||||
@ -40,7 +40,7 @@ class ValidatingNotaryFlow(otherSide: Party,
|
||||
try {
|
||||
stx.verifySignatures(serviceHub.myInfo.notaryIdentity.owningKey)
|
||||
} catch(e: SignedTransaction.SignaturesMissingException) {
|
||||
throw NotaryException(NotaryError.SignaturesMissing(e.missing))
|
||||
throw NotaryException(NotaryError.SignaturesMissing(e))
|
||||
}
|
||||
}
|
||||
|
||||
|
47
core/src/test/kotlin/net/corda/core/UtilsTest.kt
Normal file
47
core/src/test/kotlin/net/corda/core/UtilsTest.kt
Normal file
@ -0,0 +1,47 @@
|
||||
package net.corda.core
|
||||
|
||||
import org.assertj.core.api.Assertions.*
|
||||
import org.junit.Test
|
||||
import rx.subjects.PublishSubject
|
||||
import java.util.*
|
||||
|
||||
class UtilsTest {
|
||||
@Test
|
||||
fun `toFuture - single item observable`() {
|
||||
val subject = PublishSubject.create<String>()
|
||||
val future = subject.toFuture()
|
||||
subject.onNext("Hello")
|
||||
assertThat(future.getOrThrow()).isEqualTo("Hello")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toFuture - empty obserable`() {
|
||||
val subject = PublishSubject.create<String>()
|
||||
val future = subject.toFuture()
|
||||
subject.onCompleted()
|
||||
assertThatExceptionOfType(NoSuchElementException::class.java).isThrownBy {
|
||||
future.getOrThrow()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toFuture - more than one item observable`() {
|
||||
val subject = PublishSubject.create<String>()
|
||||
val future = subject.toFuture()
|
||||
subject.onNext("Hello")
|
||||
subject.onNext("World")
|
||||
subject.onCompleted()
|
||||
assertThat(future.getOrThrow()).isEqualTo("Hello")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toFuture - erroring observable`() {
|
||||
val subject = PublishSubject.create<String>()
|
||||
val future = subject.toFuture()
|
||||
val exception = Exception("Error")
|
||||
subject.onError(exception)
|
||||
assertThatThrownBy {
|
||||
future.getOrThrow()
|
||||
}.isSameAs(exception)
|
||||
}
|
||||
}
|
26
core/src/test/kotlin/net/corda/core/contracts/AmountTests.kt
Normal file
26
core/src/test/kotlin/net/corda/core/contracts/AmountTests.kt
Normal file
@ -0,0 +1,26 @@
|
||||
package net.corda.core.contracts
|
||||
|
||||
import org.junit.Test
|
||||
import java.math.BigDecimal
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
/**
|
||||
* Tests of the [Amount] class.
|
||||
*/
|
||||
class AmountTests {
|
||||
@Test
|
||||
fun basicCurrency() {
|
||||
val expected = 1000L
|
||||
val amount = Amount(expected, GBP)
|
||||
assertEquals(expected, amount.quantity)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun decimalConversion() {
|
||||
val quantity = 1234L
|
||||
val amount = Amount(quantity, GBP)
|
||||
val expected = BigDecimal("12.34")
|
||||
assertEquals(expected, amount.toDecimal())
|
||||
assertEquals(amount, Amount.fromDecimal(amount.toDecimal(), amount.token))
|
||||
}
|
||||
}
|
@ -12,32 +12,28 @@ import org.junit.Test
|
||||
import java.time.Instant
|
||||
import java.time.temporal.ChronoUnit
|
||||
|
||||
val TEST_TIMELOCK_ID = TransactionEncumbranceTests.TestTimeLock()
|
||||
val TEST_TIMELOCK_ID = TransactionEncumbranceTests.DummyTimeLock()
|
||||
|
||||
class TransactionEncumbranceTests {
|
||||
val defaultIssuer = MEGA_CORP.ref(1)
|
||||
val encumberedState = Cash.State(
|
||||
amount = 1000.DOLLARS `issued by` defaultIssuer,
|
||||
owner = DUMMY_PUBKEY_1,
|
||||
encumbrance = 1
|
||||
)
|
||||
val unencumberedState = Cash.State(
|
||||
|
||||
val state = Cash.State(
|
||||
amount = 1000.DOLLARS `issued by` defaultIssuer,
|
||||
owner = DUMMY_PUBKEY_1
|
||||
)
|
||||
val stateWithNewOwner = encumberedState.copy(owner = DUMMY_PUBKEY_2)
|
||||
val stateWithNewOwner = state.copy(owner = DUMMY_PUBKEY_2)
|
||||
|
||||
val FOUR_PM = Instant.parse("2015-04-17T16:00:00.00Z")
|
||||
val FIVE_PM = FOUR_PM.plus(1, ChronoUnit.HOURS)
|
||||
val FIVE_PM_TIMELOCK = TestTimeLock.State(FIVE_PM)
|
||||
val timeLock = DummyTimeLock.State(FIVE_PM)
|
||||
|
||||
|
||||
class TestTimeLock : Contract {
|
||||
override val legalContractReference = SecureHash.sha256("TestTimeLock")
|
||||
class DummyTimeLock : Contract {
|
||||
override val legalContractReference = SecureHash.sha256("DummyTimeLock")
|
||||
override fun verify(tx: TransactionForContract) {
|
||||
val timeLockInput = tx.inputs.filterIsInstance<State>().singleOrNull() ?: return
|
||||
val time = tx.timestamp?.before ?: throw IllegalArgumentException("Transactions containing time-locks must be timestamped")
|
||||
requireThat {
|
||||
"the time specified in the time-lock has passed" by
|
||||
(time >= tx.inputs.filterIsInstance<TestTimeLock.State>().single().validFrom)
|
||||
"the time specified in the time-lock has passed" by (time >= timeLockInput.validFrom)
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,110 +46,110 @@ class TransactionEncumbranceTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun trivial() {
|
||||
// A transaction containing an input state that is encumbered must fail if the encumbrance has not been presented
|
||||
// on the input states.
|
||||
transaction {
|
||||
input { encumberedState }
|
||||
output { unencumberedState }
|
||||
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||
this `fails with` "Missing required encumbrance 1 in INPUT"
|
||||
fun `state can be encumbered`() {
|
||||
ledger {
|
||||
transaction {
|
||||
input { state }
|
||||
output(encumbrance = 1) { stateWithNewOwner }
|
||||
output("5pm time-lock") { timeLock }
|
||||
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||
verifies()
|
||||
}
|
||||
}
|
||||
// An encumbered state must not be encumbered by itself.
|
||||
transaction {
|
||||
input { unencumberedState }
|
||||
input { unencumberedState }
|
||||
output { unencumberedState }
|
||||
// The encumbered state refers to an encumbrance in position 1, so what follows is wrong.
|
||||
output { encumberedState }
|
||||
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||
this `fails with` "Missing required encumbrance 1 in OUTPUT"
|
||||
}
|
||||
// An encumbered state must not reference an index greater than the size of the output states.
|
||||
// In this test, the output encumbered state refers to an encumbrance in position 1, but there is only one output.
|
||||
transaction {
|
||||
input { unencumberedState }
|
||||
// The encumbered state refers to an encumbrance in position 1, so there should be at least 2 outputs.
|
||||
output { encumberedState }
|
||||
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||
this `fails with` "Missing required encumbrance 1 in OUTPUT"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEncumbranceEffects() {
|
||||
// This test fails because the encumbered state is pointing to the ordinary cash state as the encumbrance,
|
||||
// instead of the timelock by mistake, so when we try and use it the transaction fails as we didn't include the
|
||||
// encumbrance cash state.
|
||||
fun `state can transition if encumbrance rules are met`() {
|
||||
ledger {
|
||||
unverifiedTransaction {
|
||||
output("state encumbered by 5pm time-lock") { encumberedState }
|
||||
output { unencumberedState }
|
||||
output("5pm time-lock") { FIVE_PM_TIMELOCK }
|
||||
}
|
||||
transaction {
|
||||
input("state encumbered by 5pm time-lock")
|
||||
input("5pm time-lock")
|
||||
output { stateWithNewOwner }
|
||||
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||
timestamp(FIVE_PM)
|
||||
this `fails with` "Missing required encumbrance 1 in INPUT"
|
||||
}
|
||||
}
|
||||
// A transaction containing an input state that is encumbered must fail if the encumbrance is not in the correct
|
||||
// transaction. In this test, the intended encumbrance is presented alongside the encumbered state for consumption,
|
||||
// although the encumbered state always refers to the encumbrance produced in the same transaction, and the in this case
|
||||
// the encumbrance was created in a separate transaction.
|
||||
ledger {
|
||||
unverifiedTransaction {
|
||||
output("state encumbered by 5pm time-lock") { encumberedState }
|
||||
output { unencumberedState }
|
||||
}
|
||||
unverifiedTransaction {
|
||||
output("5pm time-lock") { FIVE_PM_TIMELOCK }
|
||||
}
|
||||
transaction {
|
||||
input("state encumbered by 5pm time-lock")
|
||||
input("5pm time-lock")
|
||||
output { stateWithNewOwner }
|
||||
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||
timestamp(FIVE_PM)
|
||||
this `fails with` "Missing required encumbrance 1 in INPUT"
|
||||
}
|
||||
}
|
||||
// A transaction with an input state that is encumbered must succeed if the rules of the encumbrance are met.
|
||||
ledger {
|
||||
unverifiedTransaction {
|
||||
output("state encumbered by 5pm time-lock") { encumberedState }
|
||||
output("5pm time-lock") { FIVE_PM_TIMELOCK }
|
||||
output("state encumbered by 5pm time-lock") { state }
|
||||
output("5pm time-lock") { timeLock }
|
||||
}
|
||||
// Un-encumber the output if the time of the transaction is later than the timelock.
|
||||
transaction {
|
||||
input("state encumbered by 5pm time-lock")
|
||||
input("5pm time-lock")
|
||||
output { unencumberedState }
|
||||
output { stateWithNewOwner }
|
||||
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||
timestamp(FIVE_PM)
|
||||
this.verifies()
|
||||
verifies()
|
||||
}
|
||||
}
|
||||
// A transaction with an input state that is encumbered must fail if the rules of the encumbrance are not met.
|
||||
// In this test, the time-lock encumbrance is being processed in a transaction before the time allowed.
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `state cannot transition if the encumbrance contract fails to verify`() {
|
||||
ledger {
|
||||
unverifiedTransaction {
|
||||
output("state encumbered by 5pm time-lock") { encumberedState }
|
||||
output("5pm time-lock") { FIVE_PM_TIMELOCK }
|
||||
output("state encumbered by 5pm time-lock") { state }
|
||||
output("5pm time-lock") { timeLock }
|
||||
}
|
||||
// The time of the transaction is earlier than the time specified in the encumbering timelock.
|
||||
transaction {
|
||||
input("state encumbered by 5pm time-lock")
|
||||
input("5pm time-lock")
|
||||
output { unencumberedState }
|
||||
output { state }
|
||||
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||
timestamp(FOUR_PM)
|
||||
this `fails with` "the time specified in the time-lock has passed"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `state must be consumed along with its encumbrance`() {
|
||||
ledger {
|
||||
unverifiedTransaction {
|
||||
output("state encumbered by 5pm time-lock", encumbrance = 1) { state }
|
||||
output("5pm time-lock") { timeLock }
|
||||
}
|
||||
transaction {
|
||||
input("state encumbered by 5pm time-lock")
|
||||
output { stateWithNewOwner }
|
||||
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||
timestamp(FIVE_PM)
|
||||
this `fails with` "Missing required encumbrance 1 in INPUT"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `state cannot be encumbered by itself`() {
|
||||
transaction {
|
||||
input { state }
|
||||
output(encumbrance = 0) { stateWithNewOwner }
|
||||
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||
this `fails with` "Missing required encumbrance 0 in OUTPUT"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `encumbrance state index must be valid`() {
|
||||
transaction {
|
||||
input { state }
|
||||
output(encumbrance = 2) { stateWithNewOwner }
|
||||
output { timeLock }
|
||||
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||
this `fails with` "Missing required encumbrance 2 in OUTPUT"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `correct encumbrance state must be provided`() {
|
||||
ledger {
|
||||
unverifiedTransaction {
|
||||
output("state encumbered by some other state", encumbrance = 1) { state }
|
||||
output("some other state") { state }
|
||||
output("5pm time-lock") { timeLock }
|
||||
}
|
||||
transaction {
|
||||
input("state encumbered by some other state")
|
||||
input("5pm time-lock")
|
||||
output { stateWithNewOwner }
|
||||
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||
timestamp(FIVE_PM)
|
||||
this `fails with` "Missing required encumbrance 1 in INPUT"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -82,6 +82,33 @@ class TransactionTests {
|
||||
transaction.type.verify(transaction)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `transaction verification fails for duplicate inputs`() {
|
||||
val baseOutState = TransactionState(DummyContract.SingleOwnerState(0, ALICE_PUBKEY), DUMMY_NOTARY)
|
||||
val stateRef = StateRef(SecureHash.randomSHA256(), 0)
|
||||
val stateAndRef = StateAndRef(baseOutState, stateRef)
|
||||
val inputs = listOf(stateAndRef, stateAndRef)
|
||||
val outputs = listOf(baseOutState)
|
||||
val commands = emptyList<AuthenticatedObject<CommandData>>()
|
||||
val attachments = emptyList<Attachment>()
|
||||
val id = SecureHash.randomSHA256()
|
||||
val signers = listOf(DUMMY_NOTARY_KEY.public.composite)
|
||||
val timestamp: Timestamp? = null
|
||||
val transaction: LedgerTransaction = LedgerTransaction(
|
||||
inputs,
|
||||
outputs,
|
||||
commands,
|
||||
attachments,
|
||||
id,
|
||||
DUMMY_NOTARY,
|
||||
signers,
|
||||
timestamp,
|
||||
TransactionType.General()
|
||||
)
|
||||
|
||||
assertFailsWith<TransactionVerificationException.DuplicateInputStates> { transaction.type.verify(transaction) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `general transactions cannot change notary`() {
|
||||
val notary: Party = DUMMY_NOTARY
|
||||
|
@ -1,31 +0,0 @@
|
||||
package net.corda.core.contracts.clauses
|
||||
|
||||
import net.corda.core.contracts.AuthenticatedObject
|
||||
import net.corda.core.contracts.CommandData
|
||||
import net.corda.core.contracts.TransactionForContract
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import org.junit.Test
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class AllCompositionTests {
|
||||
|
||||
@Test
|
||||
fun minimal() {
|
||||
val counter = AtomicInteger(0)
|
||||
val clause = AllComposition(matchedClause(counter), matchedClause(counter))
|
||||
val tx = TransactionForContract(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256())
|
||||
verifyClause(tx, clause, emptyList<AuthenticatedObject<CommandData>>())
|
||||
|
||||
// Check that we've run the verify() function of two clauses
|
||||
assertEquals(2, counter.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `not all match`() {
|
||||
val clause = AllComposition(matchedClause(), unmatchedClause())
|
||||
val tx = TransactionForContract(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256())
|
||||
assertFailsWith<IllegalStateException> { verifyClause(tx, clause, emptyList<AuthenticatedObject<CommandData>>()) }
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package net.corda.core.contracts.clauses
|
||||
|
||||
import net.corda.core.contracts.AuthenticatedObject
|
||||
import net.corda.core.contracts.CommandData
|
||||
import net.corda.core.contracts.TransactionForContract
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import org.junit.Test
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class AllOfTests {
|
||||
|
||||
@Test
|
||||
fun minimal() {
|
||||
val counter = AtomicInteger(0)
|
||||
val clause = AllOf(matchedClause(counter), matchedClause(counter))
|
||||
val tx = TransactionForContract(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256())
|
||||
verifyClause(tx, clause, emptyList<AuthenticatedObject<CommandData>>())
|
||||
|
||||
// Check that we've run the verify() function of two clauses
|
||||
assertEquals(2, counter.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `not all match`() {
|
||||
val clause = AllOf(matchedClause(), unmatchedClause())
|
||||
val tx = TransactionForContract(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256())
|
||||
assertFailsWith<IllegalStateException> { verifyClause(tx, clause, emptyList<AuthenticatedObject<CommandData>>()) }
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
package net.corda.core.contracts.clauses
|
||||
|
||||
import net.corda.core.contracts.AuthenticatedObject
|
||||
import net.corda.core.contracts.CommandData
|
||||
import net.corda.core.contracts.TransactionForContract
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import org.junit.Test
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class AnyCompositionTests {
|
||||
@Test
|
||||
fun minimal() {
|
||||
val counter = AtomicInteger(0)
|
||||
val clause = AnyComposition(matchedClause(counter), matchedClause(counter))
|
||||
val tx = TransactionForContract(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256())
|
||||
verifyClause(tx, clause, emptyList<AuthenticatedObject<CommandData>>())
|
||||
|
||||
// Check that we've run the verify() function of two clauses
|
||||
assertEquals(2, counter.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `not all match`() {
|
||||
val counter = AtomicInteger(0)
|
||||
val clause = AnyComposition(matchedClause(counter), unmatchedClause(counter))
|
||||
val tx = TransactionForContract(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256())
|
||||
verifyClause(tx, clause, emptyList<AuthenticatedObject<CommandData>>())
|
||||
|
||||
// Check that we've run the verify() function of one clause
|
||||
assertEquals(1, counter.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `none match`() {
|
||||
val counter = AtomicInteger(0)
|
||||
val clause = AnyComposition(unmatchedClause(counter), unmatchedClause(counter))
|
||||
val tx = TransactionForContract(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256())
|
||||
verifyClause(tx, clause, emptyList<AuthenticatedObject<CommandData>>())
|
||||
|
||||
// Check that we've run the verify() function of neither clause
|
||||
assertEquals(0, counter.get())
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
package net.corda.core.contracts.clauses
|
||||
|
||||
import net.corda.core.contracts.AuthenticatedObject
|
||||
import net.corda.core.contracts.CommandData
|
||||
import net.corda.core.contracts.TransactionForContract
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import org.junit.Test
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class AnyOfTests {
|
||||
@Test
|
||||
fun minimal() {
|
||||
val counter = AtomicInteger(0)
|
||||
val clause = AnyOf(matchedClause(counter), matchedClause(counter))
|
||||
val tx = TransactionForContract(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256())
|
||||
verifyClause(tx, clause, emptyList<AuthenticatedObject<CommandData>>())
|
||||
|
||||
// Check that we've run the verify() function of two clauses
|
||||
assertEquals(2, counter.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `not all match`() {
|
||||
val counter = AtomicInteger(0)
|
||||
val clause = AnyOf(matchedClause(counter), unmatchedClause(counter))
|
||||
val tx = TransactionForContract(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256())
|
||||
verifyClause(tx, clause, emptyList<AuthenticatedObject<CommandData>>())
|
||||
|
||||
// Check that we've run the verify() function of one clause
|
||||
assertEquals(1, counter.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `none match`() {
|
||||
val counter = AtomicInteger(0)
|
||||
val clause = AnyOf(unmatchedClause(counter), unmatchedClause(counter))
|
||||
val tx = TransactionForContract(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256())
|
||||
assertFailsWith(IllegalArgumentException::class) {
|
||||
verifyClause(tx, clause, emptyList<AuthenticatedObject<CommandData>>())
|
||||
}
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ import net.corda.core.contracts.TransactionForContract
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
internal fun matchedClause(counter: AtomicInteger? = null) = object : Clause<ContractState, CommandData, Unit>() {
|
||||
override val requiredCommands: Set<Class<out CommandData>> = emptySet()
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<ContractState>,
|
||||
outputs: List<ContractState>,
|
||||
|
@ -1,205 +0,0 @@
|
||||
package net.corda.core.crypto
|
||||
|
||||
import org.junit.BeforeClass
|
||||
import org.junit.Test
|
||||
import java.net.Socket
|
||||
import java.security.KeyStore
|
||||
import java.security.cert.CertificateException
|
||||
import java.security.cert.X509Certificate
|
||||
import javax.net.ssl.SSLEngine
|
||||
import javax.net.ssl.TrustManagerFactory
|
||||
import javax.net.ssl.X509ExtendedTrustManager
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
// TODO: This suppress is needed due to KT-260, fixed in Kotlin 1.0.4 so remove after upgrading.
|
||||
@Suppress("CAST_NEVER_SUCCEEDS")
|
||||
class WhitelistTrustManagerTest {
|
||||
companion object {
|
||||
@BeforeClass
|
||||
@JvmStatic
|
||||
fun registerTrustManager() {
|
||||
// Validate original factory
|
||||
assertEquals("PKIX", TrustManagerFactory.getDefaultAlgorithm())
|
||||
|
||||
//register for all tests
|
||||
registerWhitelistTrustManager()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTrustmanagerAndCert(whitelist: String, certificateName: String): Pair<X509ExtendedTrustManager, X509Certificate> {
|
||||
WhitelistTrustManagerProvider.addWhitelistEntry(whitelist)
|
||||
|
||||
val caCertAndKey = X509Utilities.createSelfSignedCACert(certificateName)
|
||||
|
||||
val keyStore = KeyStore.getInstance(KeyStore.getDefaultType())
|
||||
keyStore.load(null, null)
|
||||
keyStore.setCertificateEntry("cacert", caCertAndKey.certificate)
|
||||
|
||||
val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
|
||||
trustManagerFactory.init(keyStore)
|
||||
|
||||
return Pair(trustManagerFactory.trustManagers.first() as X509ExtendedTrustManager, caCertAndKey.certificate)
|
||||
}
|
||||
|
||||
private fun getTrustmanagerAndUntrustedChainCert(): Pair<X509ExtendedTrustManager, X509Certificate> {
|
||||
WhitelistTrustManagerProvider.addWhitelistEntry("test.r3corda.com")
|
||||
|
||||
val otherCaCertAndKey = X509Utilities.createSelfSignedCACert("bad root")
|
||||
|
||||
val caCertAndKey = X509Utilities.createSelfSignedCACert("good root")
|
||||
|
||||
val subject = X509Utilities.getDevX509Name("test.r3corda.com")
|
||||
val serverKey = X509Utilities.generateECDSAKeyPairForSSL()
|
||||
val serverCert = X509Utilities.createServerCert(subject,
|
||||
serverKey.public,
|
||||
otherCaCertAndKey,
|
||||
listOf(),
|
||||
listOf())
|
||||
|
||||
val keyStore = KeyStore.getInstance(KeyStore.getDefaultType())
|
||||
keyStore.load(null, null)
|
||||
keyStore.setCertificateEntry("cacert", caCertAndKey.certificate)
|
||||
|
||||
val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
|
||||
trustManagerFactory.init(keyStore)
|
||||
|
||||
return Pair(trustManagerFactory.trustManagers.first() as X509ExtendedTrustManager, serverCert)
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun `getDefaultAlgorithm TrustManager is WhitelistTrustManager`() {
|
||||
registerWhitelistTrustManager() // Check double register is safe
|
||||
|
||||
assertEquals("whitelistTrustManager", TrustManagerFactory.getDefaultAlgorithm())
|
||||
|
||||
val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
|
||||
|
||||
trustManagerFactory.init(null as KeyStore?)
|
||||
|
||||
val trustManagers = trustManagerFactory.trustManagers
|
||||
|
||||
assertTrue { trustManagers.all { it is WhitelistTrustManager } }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `check certificate works for whitelisted certificate and specific domain`() {
|
||||
val (trustManager, cert) = getTrustmanagerAndCert("test.r3corda.com", "test.r3corda.com")
|
||||
|
||||
trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM)
|
||||
|
||||
trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?)
|
||||
|
||||
trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?)
|
||||
|
||||
trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM)
|
||||
|
||||
trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?)
|
||||
|
||||
trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `check certificate works for specific certificate and wildcard permitted domain`() {
|
||||
val (trustManager, cert) = getTrustmanagerAndCert("*.r3corda.com", "test.r3corda.com")
|
||||
|
||||
trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM)
|
||||
|
||||
trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?)
|
||||
|
||||
trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?)
|
||||
|
||||
trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM)
|
||||
|
||||
trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?)
|
||||
|
||||
trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `check certificate works for wildcard certificate and non wildcard domain`() {
|
||||
val (trustManager, cert) = getTrustmanagerAndCert("*.r3corda.com", "test.r3corda.com")
|
||||
|
||||
trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM)
|
||||
|
||||
trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?)
|
||||
|
||||
trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?)
|
||||
|
||||
trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM)
|
||||
|
||||
trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?)
|
||||
|
||||
trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `check unknown certificate rejected`() {
|
||||
val (trustManager, cert) = getTrustmanagerAndCert("test.r3corda.com", "test.notr3.com")
|
||||
|
||||
assertFailsWith<CertificateException> { trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM) }
|
||||
|
||||
assertFailsWith<CertificateException> { trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?) }
|
||||
|
||||
assertFailsWith<CertificateException> { trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?) }
|
||||
|
||||
assertFailsWith<CertificateException> { trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM) }
|
||||
|
||||
assertFailsWith<CertificateException> { trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?) }
|
||||
|
||||
assertFailsWith<CertificateException> { trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `check unknown wildcard certificate rejected`() {
|
||||
val (trustManager, cert) = getTrustmanagerAndCert("test.r3corda.com", "*.notr3.com")
|
||||
|
||||
assertFailsWith<CertificateException> { trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM) }
|
||||
|
||||
assertFailsWith<CertificateException> { trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?) }
|
||||
|
||||
assertFailsWith<CertificateException> { trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?) }
|
||||
|
||||
assertFailsWith<CertificateException> { trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM) }
|
||||
|
||||
assertFailsWith<CertificateException> { trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?) }
|
||||
|
||||
assertFailsWith<CertificateException> { trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `check unknown certificate rejected against mismatched wildcard`() {
|
||||
val (trustManager, cert) = getTrustmanagerAndCert("*.r3corda.com", "test.notr3.com")
|
||||
|
||||
assertFailsWith<CertificateException> { trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM) }
|
||||
|
||||
assertFailsWith<CertificateException> { trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?) }
|
||||
|
||||
assertFailsWith<CertificateException> { trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?) }
|
||||
|
||||
assertFailsWith<CertificateException> { trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM) }
|
||||
|
||||
assertFailsWith<CertificateException> { trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?) }
|
||||
|
||||
assertFailsWith<CertificateException> { trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `check certificate signed by untrusted root is still rejected, despite matched name`() {
|
||||
val (trustManager, cert) = getTrustmanagerAndUntrustedChainCert()
|
||||
|
||||
assertFailsWith<CertificateException> { trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM) }
|
||||
|
||||
assertFailsWith<CertificateException> { trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?) }
|
||||
|
||||
assertFailsWith<CertificateException> { trustManager.checkServerTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?) }
|
||||
|
||||
assertFailsWith<CertificateException> { trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM) }
|
||||
|
||||
assertFailsWith<CertificateException> { trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as Socket?) }
|
||||
|
||||
assertFailsWith<CertificateException> { trustManager.checkClientTrusted(arrayOf(cert), X509Utilities.SIGNATURE_ALGORITHM, null as SSLEngine?) }
|
||||
}
|
||||
}
|
@ -46,7 +46,7 @@ class VaultUpdateTests {
|
||||
|
||||
@Test
|
||||
fun `something plus nothing is something`() {
|
||||
val before = Vault.Update(setOf(stateRef0, stateRef1), setOf(stateAndRef2, stateAndRef3))
|
||||
val before = Vault.Update(setOf(stateAndRef0, stateAndRef1), setOf(stateAndRef2, stateAndRef3))
|
||||
val after = before + Vault.NoUpdate
|
||||
assertEquals(before, after)
|
||||
}
|
||||
@ -54,32 +54,32 @@ class VaultUpdateTests {
|
||||
@Test
|
||||
fun `nothing plus something is something`() {
|
||||
val before = Vault.NoUpdate
|
||||
val after = before + Vault.Update(setOf(stateRef0, stateRef1), setOf(stateAndRef2, stateAndRef3))
|
||||
val expected = Vault.Update(setOf(stateRef0, stateRef1), setOf(stateAndRef2, stateAndRef3))
|
||||
val after = before + Vault.Update(setOf(stateAndRef0, stateAndRef1), setOf(stateAndRef2, stateAndRef3))
|
||||
val expected = Vault.Update(setOf(stateAndRef0, stateAndRef1), setOf(stateAndRef2, stateAndRef3))
|
||||
assertEquals(expected, after)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `something plus consume state 0 is something without state 0 output`() {
|
||||
val before = Vault.Update(setOf(stateRef2, stateRef3), setOf(stateAndRef0, stateAndRef1))
|
||||
val after = before + Vault.Update(setOf(stateRef0), setOf())
|
||||
val expected = Vault.Update(setOf(stateRef2, stateRef3), setOf(stateAndRef1))
|
||||
val before = Vault.Update(setOf(stateAndRef2, stateAndRef3), setOf(stateAndRef0, stateAndRef1))
|
||||
val after = before + Vault.Update(setOf(stateAndRef0), setOf())
|
||||
val expected = Vault.Update(setOf(stateAndRef2, stateAndRef3), setOf(stateAndRef1))
|
||||
assertEquals(expected, after)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `something plus produce state 4 is something with additional state 4 output`() {
|
||||
val before = Vault.Update(setOf(stateRef2, stateRef3), setOf(stateAndRef0, stateAndRef1))
|
||||
val before = Vault.Update(setOf(stateAndRef2, stateAndRef3), setOf(stateAndRef0, stateAndRef1))
|
||||
val after = before + Vault.Update(setOf(), setOf(stateAndRef4))
|
||||
val expected = Vault.Update(setOf(stateRef2, stateRef3), setOf(stateAndRef0, stateAndRef1, stateAndRef4))
|
||||
val expected = Vault.Update(setOf(stateAndRef2, stateAndRef3), setOf(stateAndRef0, stateAndRef1, stateAndRef4))
|
||||
assertEquals(expected, after)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `something plus consume states 0 and 1, and produce state 4, is something without state 0 and 1 outputs and only state 4 output`() {
|
||||
val before = Vault.Update(setOf(stateRef2, stateRef3), setOf(stateAndRef0, stateAndRef1))
|
||||
val after = before + Vault.Update(setOf(stateRef0, stateRef1), setOf(stateAndRef4))
|
||||
val expected = Vault.Update(setOf(stateRef2, stateRef3), setOf(stateAndRef4))
|
||||
val before = Vault.Update(setOf(stateAndRef2, stateAndRef3), setOf(stateAndRef0, stateAndRef1))
|
||||
val after = before + Vault.Update(setOf(stateAndRef0, stateAndRef1), setOf(stateAndRef4))
|
||||
val expected = Vault.Update(setOf(stateAndRef2, stateAndRef3), setOf(stateAndRef4))
|
||||
assertEquals(expected, after)
|
||||
}
|
||||
}
|
||||
|
@ -69,7 +69,7 @@ class TransactionSerializationTests {
|
||||
signedTX.verifySignatures()
|
||||
|
||||
// Corrupt the data and ensure the signature catches the problem.
|
||||
signedTX.id.bytes[5] = 0
|
||||
signedTX.id.bytes[5] = signedTX.id.bytes[5].inc()
|
||||
assertFailsWith(SignatureException::class) {
|
||||
signedTX.verifySignatures()
|
||||
}
|
||||
|
BIN
docs/build/doctrees/CLI-vs-IDE.doctree
vendored
BIN
docs/build/doctrees/CLI-vs-IDE.doctree
vendored
Binary file not shown.
BIN
docs/build/doctrees/azure-vm.doctree
vendored
Normal file
BIN
docs/build/doctrees/azure-vm.doctree
vendored
Normal file
Binary file not shown.
BIN
docs/build/doctrees/building-the-docs.doctree
vendored
BIN
docs/build/doctrees/building-the-docs.doctree
vendored
Binary file not shown.
BIN
docs/build/doctrees/clauses.doctree
vendored
BIN
docs/build/doctrees/clauses.doctree
vendored
Binary file not shown.
BIN
docs/build/doctrees/clientrpc.doctree
vendored
BIN
docs/build/doctrees/clientrpc.doctree
vendored
Binary file not shown.
BIN
docs/build/doctrees/codestyle.doctree
vendored
BIN
docs/build/doctrees/codestyle.doctree
vendored
Binary file not shown.
BIN
docs/build/doctrees/consensus.doctree
vendored
BIN
docs/build/doctrees/consensus.doctree
vendored
Binary file not shown.
BIN
docs/build/doctrees/contract-catalogue.doctree
vendored
BIN
docs/build/doctrees/contract-catalogue.doctree
vendored
Binary file not shown.
BIN
docs/build/doctrees/contract-irs.doctree
vendored
BIN
docs/build/doctrees/contract-irs.doctree
vendored
Binary file not shown.
BIN
docs/build/doctrees/corda-configuration-file.doctree
vendored
BIN
docs/build/doctrees/corda-configuration-file.doctree
vendored
Binary file not shown.
Binary file not shown.
BIN
docs/build/doctrees/corda-plugins.doctree
vendored
BIN
docs/build/doctrees/corda-plugins.doctree
vendored
Binary file not shown.
BIN
docs/build/doctrees/creating-a-cordapp.doctree
vendored
BIN
docs/build/doctrees/creating-a-cordapp.doctree
vendored
Binary file not shown.
BIN
docs/build/doctrees/data-model.doctree
vendored
BIN
docs/build/doctrees/data-model.doctree
vendored
Binary file not shown.
BIN
docs/build/doctrees/environment.pickle
vendored
BIN
docs/build/doctrees/environment.pickle
vendored
Binary file not shown.
BIN
docs/build/doctrees/event-scheduling.doctree
vendored
BIN
docs/build/doctrees/event-scheduling.doctree
vendored
Binary file not shown.
BIN
docs/build/doctrees/flow-state-machines.doctree
vendored
BIN
docs/build/doctrees/flow-state-machines.doctree
vendored
Binary file not shown.
BIN
docs/build/doctrees/flow-testing.doctree
vendored
BIN
docs/build/doctrees/flow-testing.doctree
vendored
Binary file not shown.
BIN
docs/build/doctrees/further-notes-on-kotlin.doctree
vendored
BIN
docs/build/doctrees/further-notes-on-kotlin.doctree
vendored
Binary file not shown.
Binary file not shown.
BIN
docs/build/doctrees/getting-set-up.doctree
vendored
BIN
docs/build/doctrees/getting-set-up.doctree
vendored
Binary file not shown.
BIN
docs/build/doctrees/glossary.doctree
vendored
BIN
docs/build/doctrees/glossary.doctree
vendored
Binary file not shown.
BIN
docs/build/doctrees/index.doctree
vendored
BIN
docs/build/doctrees/index.doctree
vendored
Binary file not shown.
BIN
docs/build/doctrees/initial-margin-agreement.doctree
vendored
BIN
docs/build/doctrees/initial-margin-agreement.doctree
vendored
Binary file not shown.
BIN
docs/build/doctrees/initialmarginagreement.doctree
vendored
BIN
docs/build/doctrees/initialmarginagreement.doctree
vendored
Binary file not shown.
BIN
docs/build/doctrees/inthebox.doctree
vendored
BIN
docs/build/doctrees/inthebox.doctree
vendored
Binary file not shown.
BIN
docs/build/doctrees/loadtesting.doctree
vendored
BIN
docs/build/doctrees/loadtesting.doctree
vendored
Binary file not shown.
BIN
docs/build/doctrees/merkle-trees.doctree
vendored
BIN
docs/build/doctrees/merkle-trees.doctree
vendored
Binary file not shown.
BIN
docs/build/doctrees/messaging.doctree
vendored
BIN
docs/build/doctrees/messaging.doctree
vendored
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user