From f3d2a7674cc22476d162347d76cef3225afb3026 Mon Sep 17 00:00:00 2001 From: Tommy Lillehagen Date: Thu, 1 Feb 2018 16:06:12 +0000 Subject: [PATCH 01/50] Add module for end-to-end testing library --- .idea/compiler.xml | 5 +- experimental/behave/build.gradle | 107 ++++++++++++++++++ .../main/kotlin/net/corda/behave/Utility.kt | 7 ++ .../behave/src/scenario/kotlin/Scenarios.kt | 12 ++ .../corda/behave/scenarios/ScenarioHooks.kt | 17 +++ .../corda/behave/scenarios/ScenarioState.kt | 7 ++ .../net/corda/behave/scenarios/StepsBlock.kt | 3 + .../corda/behave/scenarios/StepsContainer.kt | 25 ++++ .../behave/scenarios/steps/DummySteps.kt | 18 +++ .../scenario/resources/features/dummy.feature | 6 + .../behave/src/scenario/resources/log4j2.xml | 14 +++ .../kotlin/net/corda/behave/UtilityTests.kt | 13 +++ settings.gradle | 1 + 13 files changed, 234 insertions(+), 1 deletion(-) create mode 100644 experimental/behave/build.gradle create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/Utility.kt create mode 100644 experimental/behave/src/scenario/kotlin/Scenarios.kt create mode 100644 experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioHooks.kt create mode 100644 experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioState.kt create mode 100644 experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/StepsBlock.kt create mode 100644 experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/StepsContainer.kt create mode 100644 experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/DummySteps.kt create mode 100644 experimental/behave/src/scenario/resources/features/dummy.feature create mode 100644 experimental/behave/src/scenario/resources/log4j2.xml create mode 100644 experimental/behave/src/test/kotlin/net/corda/behave/UtilityTests.kt diff --git a/.idea/compiler.xml b/.idea/compiler.xml index a33ce1e0f0..4f9145d5f4 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -10,6 +10,9 @@ + + + @@ -159,4 +162,4 @@ - \ No newline at end of file + diff --git a/experimental/behave/build.gradle b/experimental/behave/build.gradle new file mode 100644 index 0000000000..1c420488ce --- /dev/null +++ b/experimental/behave/build.gradle @@ -0,0 +1,107 @@ +buildscript { + ext.kotlin_version = '1.2.21' + ext.commonsio_version = '2.6' + ext.cucumber_version = '1.2.5' + ext.crash_version = 'cce5a00f114343c1145c1d7756e1dd6df3ea984e' + ext.docker_client_version = '8.11.0' + + repositories { + maven { + jcenter() + url 'https://jitpack.io' + } + } + + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +group 'net.corda.behave' + +apply plugin: 'java' +apply plugin: 'kotlin' + +sourceCompatibility = 1.8 + +repositories { + mavenCentral() +} + +sourceSets { + scenario { + java { + compileClasspath += main.output + runtimeClasspath += main.output + srcDir file('src/scenario/kotlin') + } + resources.srcDir file('src/scenario/resources') + } +} + +configurations { + scenarioCompile.extendsFrom testCompile + scenarioRuntime.extendsFrom testRuntime +} + +dependencies { + + // Library + + compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" + + compile("com.github.corda.crash:crash.shell:$crash_version") { + exclude group: "org.slf4j", module: "slf4j-jdk14" + exclude group: "org.bouncycastle" + } + + compile("com.github.corda.crash:crash.connectors.ssh:$crash_version") { + exclude group: "org.slf4j", module: "slf4j-jdk14" + exclude group: "org.bouncycastle" + } + + compile "com.spotify:docker-client:$docker_client_version" + + // Unit Tests + + testCompile "junit:junit:$junit_version" + + // Scenarios / End-to-End Tests + + scenarioCompile "info.cukes:cucumber-java8:$cucumber_version" + scenarioCompile "info.cukes:cucumber-junit:$cucumber_version" + scenarioCompile "info.cukes:cucumber-picocontainer:$cucumber_version" + scenarioCompile "org.assertj:assertj-core:$assertj_version" + scenarioCompile "org.slf4j:log4j-over-slf4j:$slf4j_version" + scenarioCompile "org.slf4j:jul-to-slf4j:$slf4j_version" + scenarioCompile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version" + scenarioCompile "org.apache.logging.log4j:log4j-core:$log4j_version" + scenarioCompile "commons-io:commons-io:$commonsio_version" + +} + +compileKotlin { + kotlinOptions.jvmTarget = "1.8" +} + +compileTestKotlin { + kotlinOptions.jvmTarget = "1.8" +} + +compileScenarioKotlin { + kotlinOptions.jvmTarget = "1.8" +} + +test { + testLogging.showStandardStreams = true +} + +task scenarios(type: Test) { + setTestClassesDirs sourceSets.scenario.output.getClassesDirs() + classpath = sourceSets.scenario.runtimeClasspath + outputs.upToDateWhen { false } +} + +scenarios.mustRunAfter test +scenarios.dependsOn test \ No newline at end of file diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/Utility.kt b/experimental/behave/src/main/kotlin/net/corda/behave/Utility.kt new file mode 100644 index 0000000000..b3dd28f73c --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/Utility.kt @@ -0,0 +1,7 @@ +package net.corda.behave + +object Utility { + + fun dummy() = true + +} \ No newline at end of file diff --git a/experimental/behave/src/scenario/kotlin/Scenarios.kt b/experimental/behave/src/scenario/kotlin/Scenarios.kt new file mode 100644 index 0000000000..b0c96a98ee --- /dev/null +++ b/experimental/behave/src/scenario/kotlin/Scenarios.kt @@ -0,0 +1,12 @@ +import cucumber.api.CucumberOptions +import cucumber.api.junit.Cucumber +import org.junit.runner.RunWith + +@RunWith(Cucumber::class) +@CucumberOptions( + features = arrayOf("src/scenario/resources/features"), + glue = arrayOf("net.corda.behave.scenarios"), + plugin = arrayOf("pretty") +) +@Suppress("KDocMissingDocumentation") +class CucumberTest diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioHooks.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioHooks.kt new file mode 100644 index 0000000000..745ef851b6 --- /dev/null +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioHooks.kt @@ -0,0 +1,17 @@ +package net.corda.behave.scenarios + +import cucumber.api.java.After +import cucumber.api.java.Before + +@Suppress("KDocMissingDocumentation") +class ScenarioHooks(private val state: ScenarioState) { + + @Before + fun beforeScenario() { + } + + @After + fun afterScenario() { + } + +} \ No newline at end of file diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioState.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioState.kt new file mode 100644 index 0000000000..a21ab59a10 --- /dev/null +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioState.kt @@ -0,0 +1,7 @@ +package net.corda.behave.scenarios + +class ScenarioState { + + var count: Int = 0 + +} \ No newline at end of file diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/StepsBlock.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/StepsBlock.kt new file mode 100644 index 0000000000..5880c939a3 --- /dev/null +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/StepsBlock.kt @@ -0,0 +1,3 @@ +package net.corda.behave.scenarios + +typealias StepsBlock = (StepsContainer.() -> Unit) -> Unit \ No newline at end of file diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/StepsContainer.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/StepsContainer.kt new file mode 100644 index 0000000000..69ef293853 --- /dev/null +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/StepsContainer.kt @@ -0,0 +1,25 @@ +package net.corda.behave.scenarios + +import cucumber.api.java8.En +import net.corda.behave.scenarios.steps.dummySteps +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +@Suppress("KDocMissingDocumentation") +class StepsContainer(val state: ScenarioState) : En { + + val log: Logger = LoggerFactory.getLogger(StepsContainer::class.java) + + private val stepDefinitions: List<(StepsBlock) -> Unit> = listOf( + ::dummySteps + ) + + init { + stepDefinitions.forEach { it({ this.steps(it) }) } + } + + private fun steps(action: (StepsContainer.() -> Unit)) { + action(this) + } + +} diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/DummySteps.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/DummySteps.kt new file mode 100644 index 0000000000..ce86fa5186 --- /dev/null +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/DummySteps.kt @@ -0,0 +1,18 @@ +package net.corda.behave.scenarios.steps + +import net.corda.behave.scenarios.StepsBlock +import org.assertj.core.api.Assertions.assertThat + +fun dummySteps(steps: StepsBlock) = steps { + + When("^(\\d+) dumm(y|ies) exists?$") { count, _ -> + state.count = count + log.info("Checking pre-condition $count") + } + + Then("^there is a dummy$") { + assertThat(state.count).isGreaterThan(0) + log.info("Checking outcome ${state.count}") + } + +} diff --git a/experimental/behave/src/scenario/resources/features/dummy.feature b/experimental/behave/src/scenario/resources/features/dummy.feature new file mode 100644 index 0000000000..6ff1613bd5 --- /dev/null +++ b/experimental/behave/src/scenario/resources/features/dummy.feature @@ -0,0 +1,6 @@ +Feature: Dummy + Lorem ipsum + + Scenario: Noop + Given 15 dummies exist + Then there is a dummy \ No newline at end of file diff --git a/experimental/behave/src/scenario/resources/log4j2.xml b/experimental/behave/src/scenario/resources/log4j2.xml new file mode 100644 index 0000000000..43fcf63c3d --- /dev/null +++ b/experimental/behave/src/scenario/resources/log4j2.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/experimental/behave/src/test/kotlin/net/corda/behave/UtilityTests.kt b/experimental/behave/src/test/kotlin/net/corda/behave/UtilityTests.kt new file mode 100644 index 0000000000..c956cc67e1 --- /dev/null +++ b/experimental/behave/src/test/kotlin/net/corda/behave/UtilityTests.kt @@ -0,0 +1,13 @@ +package net.corda.behave + +import org.junit.Assert +import org.junit.Test + +class UtilityTests { + + @Test + fun `dummy`() { + Assert.assertEquals(true, Utility.dummy()) + } + +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index a4f87dee28..4b0a92c815 100644 --- a/settings.gradle +++ b/settings.gradle @@ -16,6 +16,7 @@ include 'client:rpc' include 'webserver' include 'webserver:webcapsule' include 'experimental' +include 'experimental:behave' include 'experimental:sandbox' include 'experimental:quasar-hook' include 'experimental:kryo-hook' From cd079de4b8aed5831c942d8b51b7b98b3fcfcce7 Mon Sep 17 00:00:00 2001 From: Tommy Lillehagen Date: Fri, 9 Feb 2018 12:58:38 +0000 Subject: [PATCH 02/50] Port library functionality from corda/behave --- experimental/behave/README.md | 11 + experimental/behave/build.gradle | 27 +- experimental/behave/deps/.gitignore | 1 + experimental/behave/deps/corda/3.0.0/.gitkeep | 0 .../behave/deps/corda/3.0.0/apps/.gitkeep | 0 experimental/behave/deps/drivers/.gitkeep | 0 experimental/behave/deps/drivers/README.md | 3 + experimental/behave/prepare.sh | 24 ++ .../main/kotlin/net/corda/behave/Utilities.kt | 28 ++ .../main/kotlin/net/corda/behave/Utility.kt | 7 - .../database/DatabaseConfigurationTemplate.kt | 13 + .../behave/database/DatabaseConnection.kt | 85 +++++ .../corda/behave/database/DatabaseSettings.kt | 59 ++++ .../net/corda/behave/database/DatabaseType.kt | 48 +++ .../configuration/H2ConfigurationTemplate.kt | 18 + .../SqlServerConfigurationTemplate.kt | 28 ++ .../net/corda/behave/file/FileUtilities.kt | 8 + .../kotlin/net/corda/behave/file/LogSource.kt | 42 +++ .../net/corda/behave/logging/LogUtilities.kt | 7 + .../behave/monitoring/ConjunctiveWatch.kt | 23 ++ .../behave/monitoring/DisjunctiveWatch.kt | 24 ++ .../corda/behave/monitoring/PatternWatch.kt | 22 ++ .../net/corda/behave/monitoring/Watch.kt | 33 ++ .../net/corda/behave/network/Network.kt | 301 ++++++++++++++++ .../net/corda/behave/node/Distribution.kt | 116 +++++++ .../main/kotlin/net/corda/behave/node/Node.kt | 321 ++++++++++++++++++ .../node/configuration/Configuration.kt | 56 +++ .../configuration/ConfigurationTemplate.kt | 9 + .../configuration/CordappConfiguration.kt | 28 ++ .../configuration/CurrencyConfiguration.kt | 18 + .../configuration/DatabaseConfiguration.kt | 17 + .../node/configuration/NetworkInterface.kt | 65 ++++ .../node/configuration/NotaryConfiguration.kt | 16 + .../behave/node/configuration/NotaryType.kt | 18 + .../node/configuration/UserConfiguration.kt | 37 ++ .../net/corda/behave/process/Command.kt | 157 +++++++++ .../net/corda/behave/process/JarCommand.kt | 34 ++ .../behave/process/output/OutputListener.kt | 9 + .../corda/behave/service/ContainerService.kt | 122 +++++++ .../net/corda/behave/service/Service.kt | 72 ++++ .../corda/behave/service/ServiceInitiator.kt | 5 + .../corda/behave/service/ServiceSettings.kt | 13 + .../behave/service/database/H2Service.kt | 18 + .../service/database/SqlServerService.kt | 58 ++++ .../corda/behave/ssh/MonitoringSSHClient.kt | 69 ++++ .../kotlin/net/corda/behave/ssh/SSHClient.kt | 161 +++++++++ .../corda/behave/scenarios/ScenarioHooks.kt | 1 + .../corda/behave/scenarios/ScenarioState.kt | 97 +++++- .../corda/behave/scenarios/StepsContainer.kt | 41 ++- .../corda/behave/scenarios/helpers/Cash.kt | 30 ++ .../behave/scenarios/helpers/Database.kt | 23 ++ .../net/corda/behave/scenarios/helpers/Ssh.kt | 43 +++ .../corda/behave/scenarios/helpers/Startup.kt | 65 ++++ .../behave/scenarios/helpers/Substeps.kt | 24 ++ .../corda/behave/scenarios/steps/CashSteps.kt | 20 ++ .../scenarios/steps/ConfigurationSteps.kt | 49 +++ .../behave/scenarios/steps/DatabaseSteps.kt | 13 + .../behave/scenarios/steps/DummySteps.kt | 18 - .../behave/scenarios/steps/NetworkSteps.kt | 11 + .../corda/behave/scenarios/steps/RpcSteps.kt | 13 + .../corda/behave/scenarios/steps/SshSteps.kt | 13 + .../behave/scenarios/steps/StartupSteps.kt | 31 ++ .../features/cash/currencies.feature | 14 + .../features/database/connection.feature | 13 + .../scenario/resources/features/dummy.feature | 6 - .../features/startup/logging.feature | 13 + .../kotlin/net/corda/behave/UtilityTests.kt | 13 - .../behave/monitoring/MonitoringTests.kt | 64 ++++ .../net/corda/behave/network/NetworkTests.kt | 39 +++ .../net/corda/behave/process/CommandTests.kt | 34 ++ .../behave/service/SqlServerServiceTests.kt | 17 + .../behave/src/test/resources/log4j2.xml | 14 + 72 files changed, 2892 insertions(+), 58 deletions(-) create mode 100644 experimental/behave/README.md create mode 100644 experimental/behave/deps/.gitignore create mode 100644 experimental/behave/deps/corda/3.0.0/.gitkeep create mode 100644 experimental/behave/deps/corda/3.0.0/apps/.gitkeep create mode 100644 experimental/behave/deps/drivers/.gitkeep create mode 100644 experimental/behave/deps/drivers/README.md create mode 100755 experimental/behave/prepare.sh create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/Utilities.kt delete mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/Utility.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/database/DatabaseConfigurationTemplate.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/database/DatabaseConnection.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/database/DatabaseSettings.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/database/DatabaseType.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/database/configuration/H2ConfigurationTemplate.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/database/configuration/SqlServerConfigurationTemplate.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/file/FileUtilities.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/file/LogSource.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/logging/LogUtilities.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/monitoring/ConjunctiveWatch.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/monitoring/DisjunctiveWatch.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/monitoring/PatternWatch.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/monitoring/Watch.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/network/Network.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/node/Distribution.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/node/Node.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/Configuration.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/ConfigurationTemplate.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/CordappConfiguration.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/CurrencyConfiguration.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/DatabaseConfiguration.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/NetworkInterface.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/NotaryConfiguration.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/NotaryType.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/UserConfiguration.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/process/Command.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/process/JarCommand.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/process/output/OutputListener.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/service/ContainerService.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/service/Service.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/service/ServiceInitiator.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/service/ServiceSettings.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/service/database/H2Service.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/service/database/SqlServerService.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/ssh/MonitoringSSHClient.kt create mode 100644 experimental/behave/src/main/kotlin/net/corda/behave/ssh/SSHClient.kt create mode 100644 experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Cash.kt create mode 100644 experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Database.kt create mode 100644 experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Ssh.kt create mode 100644 experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Startup.kt create mode 100644 experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Substeps.kt create mode 100644 experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/CashSteps.kt create mode 100644 experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/ConfigurationSteps.kt create mode 100644 experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/DatabaseSteps.kt delete mode 100644 experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/DummySteps.kt create mode 100644 experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/NetworkSteps.kt create mode 100644 experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/RpcSteps.kt create mode 100644 experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/SshSteps.kt create mode 100644 experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/StartupSteps.kt create mode 100644 experimental/behave/src/scenario/resources/features/cash/currencies.feature create mode 100644 experimental/behave/src/scenario/resources/features/database/connection.feature delete mode 100644 experimental/behave/src/scenario/resources/features/dummy.feature create mode 100644 experimental/behave/src/scenario/resources/features/startup/logging.feature delete mode 100644 experimental/behave/src/test/kotlin/net/corda/behave/UtilityTests.kt create mode 100644 experimental/behave/src/test/kotlin/net/corda/behave/monitoring/MonitoringTests.kt create mode 100644 experimental/behave/src/test/kotlin/net/corda/behave/network/NetworkTests.kt create mode 100644 experimental/behave/src/test/kotlin/net/corda/behave/process/CommandTests.kt create mode 100644 experimental/behave/src/test/kotlin/net/corda/behave/service/SqlServerServiceTests.kt create mode 100644 experimental/behave/src/test/resources/log4j2.xml diff --git a/experimental/behave/README.md b/experimental/behave/README.md new file mode 100644 index 0000000000..163c6310cc --- /dev/null +++ b/experimental/behave/README.md @@ -0,0 +1,11 @@ +# Setup + +To get started, please run the following command: + +```bash +$ ./prepare.sh +``` + +This command will download necessary database drivers and set up +the dependencies directory with copies of the Corda fat-JAR and +the network bootstrapping tool. \ No newline at end of file diff --git a/experimental/behave/build.gradle b/experimental/behave/build.gradle index 1c420488ce..569a5deb57 100644 --- a/experimental/behave/build.gradle +++ b/experimental/behave/build.gradle @@ -1,6 +1,6 @@ buildscript { - ext.kotlin_version = '1.2.21' ext.commonsio_version = '2.6' + ext.commonslogging_version = '1.2' ext.cucumber_version = '1.2.5' ext.crash_version = 'cce5a00f114343c1145c1d7756e1dd6df3ea984e' ext.docker_client_version = '8.11.0' @@ -48,7 +48,7 @@ dependencies { // Library - compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" compile("com.github.corda.crash:crash.shell:$crash_version") { @@ -61,23 +61,30 @@ dependencies { exclude group: "org.bouncycastle" } + compile "org.slf4j:log4j-over-slf4j:$slf4j_version" + compile "org.slf4j:jul-to-slf4j:$slf4j_version" + compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version" + compile "org.apache.logging.log4j:log4j-core:$log4j_version" + + compile "commons-io:commons-io:$commonsio_version" + compile "commons-logging:commons-logging:$commonslogging_version" compile "com.spotify:docker-client:$docker_client_version" + compile "io.reactivex:rxjava:$rxjava_version" + + compile project(':finance') + compile project(':node-api') + compile project(':client:rpc') // Unit Tests testCompile "junit:junit:$junit_version" + testCompile "org.assertj:assertj-core:$assertj_version" // Scenarios / End-to-End Tests scenarioCompile "info.cukes:cucumber-java8:$cucumber_version" scenarioCompile "info.cukes:cucumber-junit:$cucumber_version" scenarioCompile "info.cukes:cucumber-picocontainer:$cucumber_version" - scenarioCompile "org.assertj:assertj-core:$assertj_version" - scenarioCompile "org.slf4j:log4j-over-slf4j:$slf4j_version" - scenarioCompile "org.slf4j:jul-to-slf4j:$slf4j_version" - scenarioCompile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version" - scenarioCompile "org.apache.logging.log4j:log4j-core:$log4j_version" - scenarioCompile "commons-io:commons-io:$commonsio_version" } @@ -103,5 +110,5 @@ task scenarios(type: Test) { outputs.upToDateWhen { false } } -scenarios.mustRunAfter test -scenarios.dependsOn test \ No newline at end of file +//scenarios.mustRunAfter test +//scenarios.dependsOn test \ No newline at end of file diff --git a/experimental/behave/deps/.gitignore b/experimental/behave/deps/.gitignore new file mode 100644 index 0000000000..d392f0e82c --- /dev/null +++ b/experimental/behave/deps/.gitignore @@ -0,0 +1 @@ +*.jar diff --git a/experimental/behave/deps/corda/3.0.0/.gitkeep b/experimental/behave/deps/corda/3.0.0/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/experimental/behave/deps/corda/3.0.0/apps/.gitkeep b/experimental/behave/deps/corda/3.0.0/apps/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/experimental/behave/deps/drivers/.gitkeep b/experimental/behave/deps/drivers/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/experimental/behave/deps/drivers/README.md b/experimental/behave/deps/drivers/README.md new file mode 100644 index 0000000000..19ff783c5f --- /dev/null +++ b/experimental/behave/deps/drivers/README.md @@ -0,0 +1,3 @@ +Download and store database drivers here; for example: + - h2-1.4.196.jar + - mssql-jdbc-6.2.2.jre8.jar diff --git a/experimental/behave/prepare.sh b/experimental/behave/prepare.sh new file mode 100755 index 0000000000..62af3afb91 --- /dev/null +++ b/experimental/behave/prepare.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +VERSION=3.0.0 + +# Set up directories +mkdir -p deps/corda/${VERSION}/apps +mkdir -p deps/drivers + +# Copy Corda capsule into deps +cp ../../node/capsule/build/libs/corda-*.jar deps/corda/${VERSION}/corda.jar + +# Download database drivers +curl "https://search.maven.org/remotecontent?filepath=com/h2database/h2/1.4.196/h2-1.4.196.jar" > deps/drivers/h2-1.4.196.jar +curl -L "https://github.com/Microsoft/mssql-jdbc/releases/download/v6.2.2/mssql-jdbc-6.2.2.jre8.jar" > deps/drivers/mssql-jdbc-6.2.2.jre8.jar + +# Build required artefacts +cd ../.. +./gradlew buildBootstrapperJar +./gradlew :finance:jar + +# Copy build artefacts into deps +cd experimental/behave +cp ../../tools/bootstrapper/build/libs/*.jar deps/corda/${VERSION}/network-bootstrapper.jar +cp ../../finance/build/libs/corda-finance-*.jar deps/corda/${VERSION}/apps/corda-finance.jar diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/Utilities.kt b/experimental/behave/src/main/kotlin/net/corda/behave/Utilities.kt new file mode 100644 index 0000000000..1fdc9f9bcc --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/Utilities.kt @@ -0,0 +1,28 @@ +package net.corda.behave + +import java.time.Duration +import java.util.concurrent.CountDownLatch + +val Int.millisecond: Duration + get() = Duration.ofMillis(this.toLong()) + +val Int.milliseconds: Duration + get() = Duration.ofMillis(this.toLong()) + +val Int.second: Duration + get() = Duration.ofSeconds(this.toLong()) + +val Int.seconds: Duration + get() = Duration.ofSeconds(this.toLong()) + +val Int.minute: Duration + get() = Duration.ofMinutes(this.toLong()) + +val Int.minutes: Duration + get() = Duration.ofMinutes(this.toLong()) + +fun CountDownLatch.await(duration: Duration) = + this.await(duration.toMillis(), java.util.concurrent.TimeUnit.MILLISECONDS) + +fun Process.waitFor(duration: Duration) = + this.waitFor(duration.toMillis(), java.util.concurrent.TimeUnit.MILLISECONDS) diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/Utility.kt b/experimental/behave/src/main/kotlin/net/corda/behave/Utility.kt deleted file mode 100644 index b3dd28f73c..0000000000 --- a/experimental/behave/src/main/kotlin/net/corda/behave/Utility.kt +++ /dev/null @@ -1,7 +0,0 @@ -package net.corda.behave - -object Utility { - - fun dummy() = true - -} \ No newline at end of file diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/database/DatabaseConfigurationTemplate.kt b/experimental/behave/src/main/kotlin/net/corda/behave/database/DatabaseConfigurationTemplate.kt new file mode 100644 index 0000000000..0acff416e8 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/database/DatabaseConfigurationTemplate.kt @@ -0,0 +1,13 @@ +package net.corda.behave.database + +import net.corda.behave.node.configuration.DatabaseConfiguration + +open class DatabaseConfigurationTemplate { + + open val connectionString: (DatabaseConfiguration) -> String = { "" } + + protected open val config: (DatabaseConfiguration) -> String = { "" } + + fun generate(config: DatabaseConfiguration) = config(config).trimMargin() + +} \ No newline at end of file diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/database/DatabaseConnection.kt b/experimental/behave/src/main/kotlin/net/corda/behave/database/DatabaseConnection.kt new file mode 100644 index 0000000000..1916000bb2 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/database/DatabaseConnection.kt @@ -0,0 +1,85 @@ +package net.corda.behave.database + +import net.corda.behave.node.configuration.DatabaseConfiguration +import java.io.Closeable +import java.sql.* +import java.util.* + +class DatabaseConnection( + private val config: DatabaseConfiguration, + template: DatabaseConfigurationTemplate +) : Closeable { + + private val connectionString = template.connectionString(config) + + private var conn: Connection? = null + + fun open(): Connection { + try { + val connectionProps = Properties() + connectionProps.put("user", config.username) + connectionProps.put("password", config.password) + retry (5) { + conn = DriverManager.getConnection(connectionString, connectionProps) + } + return conn ?: throw Exception("Unable to open connection") + } catch (ex: SQLException) { + throw Exception("An error occurred whilst connecting to \"$connectionString\". " + + "Maybe the user and/or password is invalid?", ex) + } + } + + override fun close() { + val connection = conn + if (connection != null) { + try { + conn = null + connection.close() + } catch (ex: SQLException) { + throw Exception("Failed to close database connection to \"$connectionString\"", ex) + } + } + } + + private fun query(conn: Connection?, stmt: String? = null) { + var statement: Statement? = null + val resultset: ResultSet? + try { + statement = conn?.prepareStatement(stmt + ?: "SELECT name FROM sys.tables WHERE name = ?") + statement?.setString(1, "Test") + resultset = statement?.executeQuery() + + try { + while (resultset?.next() == true) { + val name = resultset.getString("name") + println(name) + } + } catch (e: Exception) { + e.printStackTrace() + } finally { + resultset?.close() + } + } catch (e: Exception) { + e.printStackTrace() + } finally { + statement?.close() + } + } + + private fun retry(numberOfTimes: Int, action: () -> Unit) { + var i = numberOfTimes + while (numberOfTimes > 0) { + Thread.sleep(2000) + try { + action() + } catch (ex: Exception) { + if (i == 1) { + throw ex + } + } + i -= 1 + } + } + +} \ No newline at end of file diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/database/DatabaseSettings.kt b/experimental/behave/src/main/kotlin/net/corda/behave/database/DatabaseSettings.kt new file mode 100644 index 0000000000..4e870f77de --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/database/DatabaseSettings.kt @@ -0,0 +1,59 @@ +package net.corda.behave.database + +import net.corda.behave.node.configuration.Configuration +import net.corda.behave.node.configuration.DatabaseConfiguration +import net.corda.behave.service.Service +import net.corda.behave.service.ServiceInitiator + +class DatabaseSettings { + + var databaseName: String = "node" + private set + + var schemaName: String = "dbo" + private set + + var userName: String = "sa" + private set + + private var databaseConfigTemplate: DatabaseConfigurationTemplate = DatabaseConfigurationTemplate() + + private val serviceInitiators = mutableListOf() + + fun withDatabase(name: String): DatabaseSettings { + databaseName = name + return this + } + + fun withSchema(name: String): DatabaseSettings { + schemaName = name + return this + } + + fun withUser(name: String): DatabaseSettings { + userName = name + return this + } + + fun withServiceInitiator(initiator: ServiceInitiator): DatabaseSettings { + serviceInitiators.add(initiator) + return this + } + + fun withConfigTemplate(configTemplate: DatabaseConfigurationTemplate): DatabaseSettings { + databaseConfigTemplate = configTemplate + return this + } + + fun config(config: DatabaseConfiguration): String { + return databaseConfigTemplate.generate(config) + } + + fun dependencies(config: Configuration): List { + return serviceInitiators.map { it(config) } + } + + val template: DatabaseConfigurationTemplate + get() = databaseConfigTemplate + +} diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/database/DatabaseType.kt b/experimental/behave/src/main/kotlin/net/corda/behave/database/DatabaseType.kt new file mode 100644 index 0000000000..851a8d1387 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/database/DatabaseType.kt @@ -0,0 +1,48 @@ +package net.corda.behave.database + +import net.corda.behave.database.configuration.H2ConfigurationTemplate +import net.corda.behave.database.configuration.SqlServerConfigurationTemplate +import net.corda.behave.node.configuration.Configuration +import net.corda.behave.node.configuration.DatabaseConfiguration +import net.corda.behave.service.database.H2Service +import net.corda.behave.service.database.SqlServerService + +enum class DatabaseType(val settings: DatabaseSettings) { + + H2(DatabaseSettings() + .withDatabase(H2Service.database) + .withSchema(H2Service.schema) + .withUser(H2Service.username) + .withConfigTemplate(H2ConfigurationTemplate()) + .withServiceInitiator { + H2Service("h2-${it.name}", it.database.port) + } + ), + + SQL_SERVER(DatabaseSettings() + .withDatabase(SqlServerService.database) + .withSchema(SqlServerService.schema) + .withUser(SqlServerService.username) + .withConfigTemplate(SqlServerConfigurationTemplate()) + .withServiceInitiator { + SqlServerService("sqlserver-${it.name}", it.database.port, it.database.password) + } + ); + + fun dependencies(config: Configuration) = settings.dependencies(config) + + fun connection(config: DatabaseConfiguration) = DatabaseConnection(config, settings.template) + + companion object { + + fun fromName(name: String): DatabaseType? = when (name.toLowerCase()) { + "h2" -> H2 + "sql_server" -> SQL_SERVER + "sql server" -> SQL_SERVER + "sqlserver" -> SQL_SERVER + else -> null + } + + } + +} \ No newline at end of file diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/database/configuration/H2ConfigurationTemplate.kt b/experimental/behave/src/main/kotlin/net/corda/behave/database/configuration/H2ConfigurationTemplate.kt new file mode 100644 index 0000000000..6bdfa2ad80 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/database/configuration/H2ConfigurationTemplate.kt @@ -0,0 +1,18 @@ +package net.corda.behave.database.configuration + +import net.corda.behave.database.DatabaseConfigurationTemplate +import net.corda.behave.node.configuration.DatabaseConfiguration + +class H2ConfigurationTemplate : DatabaseConfigurationTemplate() { + + override val connectionString: (DatabaseConfiguration) -> String + get() = { "jdbc:h2:tcp://${it.host}:${it.port}/${it.database}" } + + override val config: (DatabaseConfiguration) -> String + get() = { + """ + |h2port=${it.port} + """ + } + +} \ No newline at end of file diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/database/configuration/SqlServerConfigurationTemplate.kt b/experimental/behave/src/main/kotlin/net/corda/behave/database/configuration/SqlServerConfigurationTemplate.kt new file mode 100644 index 0000000000..370a241d20 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/database/configuration/SqlServerConfigurationTemplate.kt @@ -0,0 +1,28 @@ +package net.corda.behave.database.configuration + +import net.corda.behave.database.DatabaseConfigurationTemplate +import net.corda.behave.node.configuration.DatabaseConfiguration + +class SqlServerConfigurationTemplate : DatabaseConfigurationTemplate() { + + override val connectionString: (DatabaseConfiguration) -> String + get() = { "jdbc:sqlserver://${it.host}:${it.port};database=${it.database}" } + + override val config: (DatabaseConfiguration) -> String + get() = { + """ + |dataSourceProperties = { + | dataSourceClassName = "com.microsoft.sqlserver.jdbc.SQLServerDataSource" + | dataSource.url = "${connectionString(it)}" + | dataSource.user = "${it.username}" + | dataSource.password = "${it.password}" + |} + |database = { + | initialiseSchema=true + | transactionIsolationLevel = READ_COMMITTED + | schema="${it.schema}" + |} + """ + } + +} \ No newline at end of file diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/file/FileUtilities.kt b/experimental/behave/src/main/kotlin/net/corda/behave/file/FileUtilities.kt new file mode 100644 index 0000000000..405404fcb2 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/file/FileUtilities.kt @@ -0,0 +1,8 @@ +package net.corda.behave.file + +import java.io.File + +val currentDirectory: File + get() = File(System.getProperty("user.dir")) + +operator fun File.div(relative: String): File = this.resolve(relative) diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/file/LogSource.kt b/experimental/behave/src/main/kotlin/net/corda/behave/file/LogSource.kt new file mode 100644 index 0000000000..4649a4898a --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/file/LogSource.kt @@ -0,0 +1,42 @@ +package net.corda.behave.file + +import java.io.File + +class LogSource( + private val directory: File, + filePattern: String? = ".*\\.log", + private val filePatternUsedForExclusion: Boolean = false +) { + + private val fileRegex = Regex(filePattern ?: ".*") + + data class MatchedLogContent( + val filename: File, + val contents: String + ) + + fun find(pattern: String? = null): List { + val regex = if (pattern != null) { + Regex(pattern) + } else { + null + } + val logFiles = directory.listFiles({ file -> + (!filePatternUsedForExclusion && file.name.matches(fileRegex)) || + (filePatternUsedForExclusion && !file.name.matches(fileRegex)) + }) + val result = mutableListOf() + for (file in logFiles) { + val contents = file.readText() + if (regex != null) { + result.addAll(regex.findAll(contents).map { match -> + MatchedLogContent(file, match.value) + }) + } else { + result.add(MatchedLogContent(file, contents)) + } + } + return result + } + +} \ No newline at end of file diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/logging/LogUtilities.kt b/experimental/behave/src/main/kotlin/net/corda/behave/logging/LogUtilities.kt new file mode 100644 index 0000000000..76a66c9c4e --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/logging/LogUtilities.kt @@ -0,0 +1,7 @@ +package net.corda.behave.logging + +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +inline fun getLogger(): Logger = + LoggerFactory.getLogger(T::class.java) diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/monitoring/ConjunctiveWatch.kt b/experimental/behave/src/main/kotlin/net/corda/behave/monitoring/ConjunctiveWatch.kt new file mode 100644 index 0000000000..c0308fca1e --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/monitoring/ConjunctiveWatch.kt @@ -0,0 +1,23 @@ +package net.corda.behave.monitoring + +import net.corda.behave.await +import rx.Observable +import java.time.Duration +import java.util.concurrent.CountDownLatch + +class ConjunctiveWatch( + private val left: Watch, + private val right: Watch +) : Watch() { + + override fun await(observable: Observable, timeout: Duration): Boolean { + val latch = CountDownLatch(2) + listOf(left, right).parallelStream().forEach { + if (it.await(observable, timeout)) { + latch.countDown() + } + } + return latch.await(timeout) + } + +} \ No newline at end of file diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/monitoring/DisjunctiveWatch.kt b/experimental/behave/src/main/kotlin/net/corda/behave/monitoring/DisjunctiveWatch.kt new file mode 100644 index 0000000000..061ca1ed61 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/monitoring/DisjunctiveWatch.kt @@ -0,0 +1,24 @@ +package net.corda.behave.monitoring + +import net.corda.behave.await +import rx.Observable +import java.time.Duration +import java.util.concurrent.CountDownLatch + +class DisjunctiveWatch( + private val left: Watch, + private val right: Watch +) : Watch() { + + override fun await(observable: Observable, timeout: Duration): Boolean { + val latch = CountDownLatch(1) + listOf(left, right).parallelStream().forEach { + if (it.await(observable, timeout)) { + latch.countDown() + } + } + return latch.await(timeout) + } + +} + diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/monitoring/PatternWatch.kt b/experimental/behave/src/main/kotlin/net/corda/behave/monitoring/PatternWatch.kt new file mode 100644 index 0000000000..50a715dd79 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/monitoring/PatternWatch.kt @@ -0,0 +1,22 @@ +package net.corda.behave.monitoring + +class PatternWatch( + pattern: String, + ignoreCase: Boolean = false +) : Watch() { + + private val regularExpression = if (ignoreCase) { + Regex("^.*$pattern.*$", RegexOption.IGNORE_CASE) + } else { + Regex("^.*$pattern.*$") + } + + override fun match(data: String) = regularExpression.matches(data.trim()) + + companion object { + + val EMPTY = PatternWatch("") + + } + +} \ No newline at end of file diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/monitoring/Watch.kt b/experimental/behave/src/main/kotlin/net/corda/behave/monitoring/Watch.kt new file mode 100644 index 0000000000..c5b7d94920 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/monitoring/Watch.kt @@ -0,0 +1,33 @@ +package net.corda.behave.monitoring + +import net.corda.behave.await +import net.corda.behave.seconds +import rx.Observable +import java.time.Duration +import java.util.concurrent.CountDownLatch + +abstract class Watch { + + private val latch = CountDownLatch(1) + + open fun await( + observable: Observable, + timeout: Duration = 10.seconds + ): Boolean { + observable + .filter { match(it) } + .forEach { latch.countDown() } + return latch.await(timeout) + } + + open fun match(data: String): Boolean = false + + operator fun times(other: Watch): Watch { + return ConjunctiveWatch(this, other) + } + + operator fun div(other: Watch): Watch { + return DisjunctiveWatch(this, other) + } + +} \ No newline at end of file diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/network/Network.kt b/experimental/behave/src/main/kotlin/net/corda/behave/network/Network.kt new file mode 100644 index 0000000000..d0cabfc02c --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/network/Network.kt @@ -0,0 +1,301 @@ +package net.corda.behave.network + +import net.corda.behave.database.DatabaseType +import net.corda.behave.file.LogSource +import net.corda.behave.file.currentDirectory +import net.corda.behave.file.div +import net.corda.behave.logging.getLogger +import net.corda.behave.minutes +import net.corda.behave.node.Distribution +import net.corda.behave.node.Node +import net.corda.behave.node.configuration.NotaryType +import net.corda.behave.process.JarCommand +import org.apache.commons.io.FileUtils +import java.io.Closeable +import java.io.File +import java.time.Duration +import java.time.Instant +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +class Network private constructor( + private val nodes: Map, + private val targetDirectory: File, + private val timeout: Duration = 2.minutes +) : Closeable, Iterable { + + private val log = getLogger() + + private val latch = CountDownLatch(1) + + private var isRunning = false + + private var isStopped = false + + private var hasError = false + + class Builder internal constructor( + private val timeout: Duration + ) { + + private val nodes = mutableMapOf() + + private val startTime = DateTimeFormatter + .ofPattern("yyyyMMDD-HHmmss") + .withZone(ZoneId.of("UTC")) + .format(Instant.now()) + + private val directory = currentDirectory / "build/runs/$startTime" + + fun addNode( + name: String, + distribution: Distribution = Distribution.LATEST_MASTER, + databaseType: DatabaseType = DatabaseType.H2, + notaryType: NotaryType = NotaryType.NONE, + issuableCurrencies: List = emptyList() + ): Builder { + return addNode(Node.new() + .withName(name) + .withDistribution(distribution) + .withDatabaseType(databaseType) + .withNotaryType(notaryType) + .withIssuableCurrencies(*issuableCurrencies.toTypedArray()) + ) + } + + fun addNode(nodeBuilder: Node.Builder): Builder { + nodeBuilder + .withDirectory(directory) + .withTimeout(timeout) + val node = nodeBuilder.build() + nodes[node.config.name] = node + return this + } + + fun generate(): Network { + val network = Network(nodes, directory, timeout) + network.bootstrapNetwork() + return network + } + + } + + private fun copyDatabaseDrivers() { + val driverDirectory = targetDirectory / "libs" + FileUtils.forceMkdir(driverDirectory) + FileUtils.copyDirectory( + currentDirectory / "deps/drivers", + driverDirectory + ) + } + + private fun configureNodes(): Boolean { + var allDependenciesStarted = true + log.info("Configuring nodes ...") + for (node in nodes.values) { + node.configure() + if (!node.startDependencies()) { + allDependenciesStarted = false + break + } + } + return if (allDependenciesStarted) { + log.info("Nodes configured") + true + } else { + false + } + } + + private fun bootstrapNetwork() { + copyDatabaseDrivers() + if (!configureNodes()) { + hasError = true + return + } + val bootstrapper = nodes.values + .sortedByDescending { it.config.distribution.version } + .first() + .config.distribution.networkBootstrapper + + if (!bootstrapper.exists()) { + log.warn("Network bootstrapping tool does not exist; continuing ...") + return + } + + log.info("Bootstrapping network, please wait ...") + val command = JarCommand( + bootstrapper, + arrayOf("$targetDirectory"), + targetDirectory, + timeout + ) + log.info("Running command: {}", command) + command.output.subscribe { + if (it.contains("Exception")) { + log.warn("Found error in output; interrupting bootstrapping action ...\n{}", it) + command.interrupt() + } + } + command.start() + if (!command.waitFor()) { + hasError = true + error("Failed to bootstrap network") { + val matches = LogSource(targetDirectory) + .find(".*[Ee]xception.*") + .groupBy { it.filename.absolutePath } + for (match in matches) { + log.info("Log(${match.key}):\n${match.value.joinToString("\n") { it.contents }}") + } + } + } else { + log.info("Network set-up completed") + } + } + + private fun cleanup() { + try { + if (!hasError || CLEANUP_ON_ERROR) { + log.info("Cleaning up runtime ...") + FileUtils.deleteDirectory(targetDirectory) + } else { + log.info("Deleting temporary files, but retaining logs and config ...") + for (node in nodes.values.map { it.config.name }) { + val nodeFolder = targetDirectory / node + FileUtils.deleteDirectory(nodeFolder / "additional-node-infos") + FileUtils.deleteDirectory(nodeFolder / "artemis") + FileUtils.deleteDirectory(nodeFolder / "certificates") + FileUtils.deleteDirectory(nodeFolder / "cordapps") + FileUtils.deleteDirectory(nodeFolder / "shell-commands") + FileUtils.deleteDirectory(nodeFolder / "sshkey") + FileUtils.deleteQuietly(nodeFolder / "corda.jar") + FileUtils.deleteQuietly(nodeFolder / "network-parameters") + FileUtils.deleteQuietly(nodeFolder / "persistence.mv.db") + FileUtils.deleteQuietly(nodeFolder / "process-id") + + for (nodeInfo in nodeFolder.listFiles({ + file -> file.name.matches(Regex("nodeInfo-.*")) + })) { + FileUtils.deleteQuietly(nodeInfo) + } + } + FileUtils.deleteDirectory(targetDirectory / "libs") + FileUtils.deleteDirectory(targetDirectory / ".cache") + } + log.info("Network was shut down successfully") + } catch (e: Exception) { + log.warn("Failed to cleanup runtime environment") + e.printStackTrace() + } + } + + private fun error(message: String, ex: Throwable? = null, action: (() -> Unit)? = null) { + hasError = true + log.warn(message, ex) + action?.invoke() + stop() + throw Exception(message, ex) + } + + fun start() { + if (isRunning || hasError) { + return + } + isRunning = true + for (node in nodes.values) { + node.start() + } + } + + fun waitUntilRunning(waitDuration: Duration? = null): Boolean { + if (hasError) { + return false + } + var failedNodes = 0 + val nodesLatch = CountDownLatch(nodes.size) + nodes.values.parallelStream().forEach { + if (!it.waitUntilRunning(waitDuration ?: timeout)) { + failedNodes += 1 + } + nodesLatch.countDown() + } + nodesLatch.await() + return if (failedNodes > 0) { + error("$failedNodes node(s) did not start up as expected within the given time frame") { + signal() + keepAlive(timeout) + } + false + } else { + log.info("All nodes are running") + true + } + } + + fun signalFailure(message: String?, ex: Throwable? = null) { + error(message ?: "Signaling error to network ...", ex) { + signal() + keepAlive(timeout) + } + } + + fun signal() { + log.info("Sending termination signal ...") + latch.countDown() + } + + fun keepAlive(timeout: Duration) { + val secs = timeout.seconds + log.info("Waiting for up to {} second(s) for termination signal ...", secs) + val wasSignalled = latch.await(secs, TimeUnit.SECONDS) + log.info(if (wasSignalled) { + "Received termination signal" + } else { + "Timed out. No termination signal received during wait period" + }) + stop() + } + + fun stop() { + if (isStopped) { + return + } + log.info("Shutting down network ...") + isStopped = true + for (node in nodes.values) { + node.shutDown() + } + cleanup() + } + + fun use(action: (Network) -> Unit) { + this.start() + action(this) + close() + } + + override fun close() { + stop() + } + + override fun iterator(): Iterator { + return nodes.values.iterator() + } + + operator fun get(nodeName: String): Node? { + return nodes[nodeName] + } + + companion object { + + const val CLEANUP_ON_ERROR = false + + fun new( + timeout: Duration = 2.minutes + ): Builder = Builder(timeout) + + } + +} \ No newline at end of file diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/node/Distribution.kt b/experimental/behave/src/main/kotlin/net/corda/behave/node/Distribution.kt new file mode 100644 index 0000000000..cca3570f39 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/node/Distribution.kt @@ -0,0 +1,116 @@ +package net.corda.behave.node + +import net.corda.behave.file.div +import org.apache.commons.io.FileUtils +import java.io.File +import java.net.URL + +/** + * Corda distribution. + */ +class Distribution private constructor( + + /** + * The version string of the Corda distribution. + */ + val version: String, + + /** + * The path of the distribution fat JAR on disk, if available. + */ + file: File? = null, + + /** + * The URL of the distribution fat JAR, if available. + */ + val url: URL? = null + +) { + + /** + * The path to the distribution fat JAR. + */ + val jarFile: File = file ?: nodePrefix / "$version/corda.jar" + + /** + * The path to available Cordapps for this distribution. + */ + val cordappDirectory: File = nodePrefix / "$version/apps" + + /** + * The path to network bootstrapping tool. + */ + val networkBootstrapper: File = nodePrefix / "$version/network-bootstrapper.jar" + + /** + * Ensure that the distribution is available on disk. + */ + fun ensureAvailable() { + if (!jarFile.exists()) { + if (url != null) { + try { + FileUtils.forceMkdirParent(jarFile) + FileUtils.copyURLToFile(url, jarFile) + } catch (e: Exception) { + throw Exception("Invalid Corda version $version", e) + } + } else { + throw Exception("File not found $jarFile") + } + } + } + + /** + * Human-readable representation of the distribution. + */ + override fun toString() = "Corda(version = $version, path = $jarFile)" + + companion object { + + private val distributions = mutableListOf() + + private val directory = File(System.getProperty("user.dir")) + + private val nodePrefix = directory / "deps/corda" + + /** + * Corda Open Source, version 3.0.0 + */ + val V3 = fromJarFile("3.0.0") + + val LATEST_MASTER = V3 + + /** + * Get representation of an open source distribution based on its version string. + * @param version The version of the open source Corda distribution. + */ + fun fromOpenSourceVersion(version: String): Distribution { + val url = URL("https://dl.bintray.com/r3/corda/net/corda/corda/$version/corda-$version.jar") + val distribution = Distribution(version, url = url) + distributions.add(distribution) + return distribution + } + + /** + * Get representation of a Corda distribution based on its version string and fat JAR path. + * @param version The version of the Corda distribution. + * @param jarFile The path to the Corda fat JAR. + */ + fun fromJarFile(version: String, jarFile: File? = null): Distribution { + val distribution = Distribution(version, file = jarFile) + distributions.add(distribution) + return distribution + } + + /** + * Get registered representation of a Corda distribution based on its version string. + * @param version The version of the Corda distribution + */ + fun fromVersionString(version: String): Distribution? = when (version.toLowerCase()) { + "master" -> LATEST_MASTER + else -> distributions.firstOrNull { it.version == version } + } + + } + +} diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/node/Node.kt b/experimental/behave/src/main/kotlin/net/corda/behave/node/Node.kt new file mode 100644 index 0000000000..e5c9f69bb2 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/node/Node.kt @@ -0,0 +1,321 @@ +package net.corda.behave.node + +import net.corda.behave.database.DatabaseConnection +import net.corda.behave.database.DatabaseType +import net.corda.behave.file.LogSource +import net.corda.behave.file.currentDirectory +import net.corda.behave.file.div +import net.corda.behave.logging.getLogger +import net.corda.behave.monitoring.PatternWatch +import net.corda.behave.node.configuration.* +import net.corda.behave.process.JarCommand +import net.corda.behave.service.Service +import net.corda.behave.service.ServiceSettings +import net.corda.behave.ssh.MonitoringSSHClient +import net.corda.behave.ssh.SSHClient +import org.apache.commons.io.FileUtils +import java.io.File +import java.time.Duration +import java.util.concurrent.CountDownLatch + +/** + * Corda node. + */ +class Node( + val config: Configuration, + private val rootDirectory: File = currentDirectory, + private val settings: ServiceSettings = ServiceSettings() +) { + + private val log = getLogger() + + private val runtimeDirectory = rootDirectory / config.name + + private val logDirectory = runtimeDirectory / "logs" + + private val command = JarCommand( + config.distribution.jarFile, + arrayOf("--config", "node.conf"), + runtimeDirectory, + settings.timeout, + enableRemoteDebugging = false + ) + + private val isAliveLatch = PatternWatch("Node for \".*\" started up and registered") + + private var isConfigured = false + + private val serviceDependencies = mutableListOf() + + private var isStarted = false + + private var haveDependenciesStarted = false + + private var haveDependenciesStopped = false + + fun describe(): String { + val network = config.nodeInterface + val database = config.database + return """ + |Node Information: ${config.name} + | - P2P: ${network.host}:${network.p2pPort} + | - RPC: ${network.host}:${network.rpcPort} + | - SSH: ${network.host}:${network.sshPort} + | - DB: ${network.host}:${database.port} (${database.type}) + |""".trimMargin() + } + + fun configure() { + if (isConfigured) { return } + isConfigured = true + log.info("Configuring {} ...", this) + serviceDependencies.addAll(config.database.type.dependencies(config)) + config.distribution.ensureAvailable() + config.writeToFile(rootDirectory / "${config.name}.conf") + installApps() + } + + fun start(): Boolean { + if (!startDependencies()) { + return false + } + log.info("Starting {} ...", this) + return try { + command.start() + isStarted = true + true + } catch (e: Exception) { + log.warn("Failed to start {}", this) + e.printStackTrace() + false + } + } + + fun waitUntilRunning(waitDuration: Duration? = null): Boolean { + val ok = isAliveLatch.await(command.output, waitDuration ?: settings.timeout) + if (!ok) { + log.warn("{} did not start up as expected within the given time frame", this) + } else { + log.info("{} is running and ready for incoming connections", this) + } + return ok + } + + fun shutDown(): Boolean { + return try { + if (isStarted) { + log.info("Shutting down {} ...", this) + command.kill() + } + stopDependencies() + true + } catch (e: Exception) { + log.warn("Failed to shut down {} cleanly", this) + e.printStackTrace() + false + } + } + + val nodeInfoGenerationOutput: LogSource by lazy { + LogSource(logDirectory, "node-info-gen.log") + } + + val logOutput: LogSource by lazy { + LogSource(logDirectory, "node-info-gen.log", filePatternUsedForExclusion = true) + } + + val database: DatabaseConnection by lazy { + DatabaseConnection(config.database, config.databaseType.settings.template) + } + + fun ssh( + exitLatch: CountDownLatch? = null, + clientLogic: (MonitoringSSHClient) -> Unit + ) { + Thread(Runnable { + val network = config.nodeInterface + val user = config.users.first() + val client = SSHClient.connect(network.sshPort, user.password, username = user.username) + MonitoringSSHClient(client).use { + log.info("Connected to {} over SSH", this) + clientLogic(it) + log.info("Disconnecting from {} ...", this) + it.writeLine("bye") + exitLatch?.countDown() + } + }).start() + } + + override fun toString(): String { + return "Node(name = ${config.name}, version = ${config.distribution.version})" + } + + fun startDependencies(): Boolean { + if (haveDependenciesStarted) { return true } + haveDependenciesStarted = true + + if (serviceDependencies.isEmpty()) { return true } + + log.info("Starting dependencies for {} ...", this) + val latch = CountDownLatch(serviceDependencies.size) + var failed = false + serviceDependencies.parallelStream().forEach { + val wasStarted = it.start() + latch.countDown() + if (!wasStarted) { + failed = true + } + } + latch.await() + return if (!failed) { + log.info("Dependencies started for {}", this) + true + } else { + log.warn("Failed to start one or more dependencies for {}", this) + false + } + } + + private fun stopDependencies() { + if (haveDependenciesStopped) { return } + haveDependenciesStopped = true + + if (serviceDependencies.isEmpty()) { return } + + log.info("Stopping dependencies for {} ...", this) + val latch = CountDownLatch(serviceDependencies.size) + serviceDependencies.parallelStream().forEach { + it.stop() + latch.countDown() + } + latch.await() + log.info("Dependencies stopped for {}", this) + } + + private fun installApps() { + val version = config.distribution.version + val appDirectory = rootDirectory / "../../../deps/corda/$version/apps" + if (appDirectory.exists()) { + val targetAppDirectory = runtimeDirectory / "cordapps" + FileUtils.copyDirectory(appDirectory, targetAppDirectory) + } + } + + class Builder { + + var name: String? = null + private set + + private var distribution = Distribution.V3 + + private var databaseType = DatabaseType.H2 + + private var notaryType = NotaryType.NONE + + private val issuableCurrencies = mutableListOf() + + private var location: String = "London" + + private var country: String = "GB" + + private val apps = mutableListOf() + + private var includeFinance = false + + private var directory: File? = null + + private var timeout = Duration.ofSeconds(60) + + fun withName(newName: String): Builder { + name = newName + return this + } + + fun withDistribution(newDistribution: Distribution): Builder { + distribution = newDistribution + return this + } + + fun withDatabaseType(newDatabaseType: DatabaseType): Builder { + databaseType = newDatabaseType + return this + } + + fun withNotaryType(newNotaryType: NotaryType): Builder { + notaryType = newNotaryType + return this + } + + fun withIssuableCurrencies(vararg currencies: String): Builder { + issuableCurrencies.addAll(currencies) + return this + } + + fun withIssuableCurrencies(currencies: List): Builder { + issuableCurrencies.addAll(currencies) + return this + } + + fun withLocation(location: String, country: String): Builder { + this.location = location + this.country = country + return this + } + + fun withFinanceApp(): Builder { + includeFinance = true + return this + } + + fun withApp(app: String): Builder { + apps.add(app) + return this + } + + fun withDirectory(newDirectory: File): Builder { + directory = newDirectory + return this + } + + fun withTimeout(newTimeout: Duration): Builder { + timeout = newTimeout + return this + } + + fun build(): Node { + val name = name ?: error("Node name not set") + val directory = directory ?: error("Runtime directory not set") + return Node( + Configuration( + name, + distribution, + databaseType, + location = location, + country = country, + configElements = *arrayOf( + NotaryConfiguration(notaryType), + CurrencyConfiguration(issuableCurrencies), + CordappConfiguration( + apps = *apps.toTypedArray(), + includeFinance = includeFinance + ) + ) + ), + directory, + ServiceSettings(timeout) + ) + } + + private fun error(message: String): T { + throw IllegalArgumentException(message) + } + + } + + companion object { + + fun new() = Builder() + + } + +} \ No newline at end of file diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/Configuration.kt b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/Configuration.kt new file mode 100644 index 0000000000..ce1cdc44e2 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/Configuration.kt @@ -0,0 +1,56 @@ +package net.corda.behave.node.configuration + +import net.corda.behave.database.DatabaseType +import net.corda.behave.node.* +import org.apache.commons.io.FileUtils +import java.io.File + +class Configuration( + val name: String, + val distribution: Distribution = Distribution.LATEST_MASTER, + val databaseType: DatabaseType = DatabaseType.H2, + val location: String = "London", + val country: String = "GB", + val users: UserConfiguration = UserConfiguration().withUser("corda", DEFAULT_PASSWORD), + val nodeInterface: NetworkInterface = NetworkInterface(), + val database: DatabaseConfiguration = DatabaseConfiguration( + databaseType, + nodeInterface.host, + nodeInterface.dbPort, + password = DEFAULT_PASSWORD + ), + vararg configElements: ConfigurationTemplate +) { + + private val developerMode = true + + private val useHttps = false + + private val basicConfig = """ + |myLegalName="C=$country,L=$location,O=$name" + |keyStorePassword="cordacadevpass" + |trustStorePassword="trustpass" + |extraAdvertisedServiceIds=[ "" ] + |useHTTPS=$useHttps + |devMode=$developerMode + |jarDirs = [ "../libs" ] + """.trimMargin() + + private val extraConfig = (configElements.toList() + listOf(users, nodeInterface)) + .joinToString(separator = "\n") { it.generate(this) } + + fun writeToFile(file: File) { + FileUtils.writeStringToFile(file, this.generate(), "UTF-8") + } + + private fun generate() = listOf(basicConfig, database.config(), extraConfig) + .filter { it.isNotBlank() } + .joinToString("\n") + + companion object { + + private val DEFAULT_PASSWORD = "S0meS3cretW0rd" + + } + +} diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/ConfigurationTemplate.kt b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/ConfigurationTemplate.kt new file mode 100644 index 0000000000..332e7de951 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/ConfigurationTemplate.kt @@ -0,0 +1,9 @@ +package net.corda.behave.node.configuration + +open class ConfigurationTemplate { + + protected open val config: (Configuration) -> String = { "" } + + fun generate(config: Configuration) = config(config).trimMargin() + +} diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/CordappConfiguration.kt b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/CordappConfiguration.kt new file mode 100644 index 0000000000..e6a2c94be2 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/CordappConfiguration.kt @@ -0,0 +1,28 @@ +package net.corda.behave.node.configuration + +class CordappConfiguration(vararg apps: String, var includeFinance: Boolean = false) : ConfigurationTemplate() { + + private val applications = apps.toList() + if (includeFinance) { + listOf("net.corda:corda-finance:CORDA_VERSION") + } else { + emptyList() + } + + override val config: (Configuration) -> String + get() = { config -> + if (applications.isEmpty()) { + "" + } else { + """ + |cordapps = [ + |${applications.joinToString(", ") { formatApp(config, it) }} + |] + """ + } + } + + private fun formatApp(config: Configuration, app: String): String { + return "\"${app.replace("CORDA_VERSION", config.distribution.version)}\"" + } + +} diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/CurrencyConfiguration.kt b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/CurrencyConfiguration.kt new file mode 100644 index 0000000000..fd639141e0 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/CurrencyConfiguration.kt @@ -0,0 +1,18 @@ +package net.corda.behave.node.configuration + +class CurrencyConfiguration(private val issuableCurrencies: List) : ConfigurationTemplate() { + + override val config: (Configuration) -> String + get() = { + if (issuableCurrencies.isEmpty()) { + "" + } else { + """ + |issuableCurrencies=[ + | ${issuableCurrencies.joinToString(", ")} + |] + """ + } + } + +} \ No newline at end of file diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/DatabaseConfiguration.kt b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/DatabaseConfiguration.kt new file mode 100644 index 0000000000..ca9f3c37a8 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/DatabaseConfiguration.kt @@ -0,0 +1,17 @@ +package net.corda.behave.node.configuration + +import net.corda.behave.database.DatabaseType + +data class DatabaseConfiguration( + val type: DatabaseType, + val host: String, + val port: Int, + val username: String = type.settings.userName, + val password: String, + val database: String = type.settings.databaseName, + val schema: String = type.settings.schemaName +) { + + fun config() = type.settings.config(this) + +} diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/NetworkInterface.kt b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/NetworkInterface.kt new file mode 100644 index 0000000000..fe09792dd2 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/NetworkInterface.kt @@ -0,0 +1,65 @@ +package net.corda.behave.node.configuration + +import java.net.Socket +import java.util.concurrent.atomic.AtomicInteger + +data class NetworkInterface( + val host: String = "localhost", + val sshPort: Int = getPort(2222 + nodeIndex), + val p2pPort: Int = getPort(12001 + (nodeIndex * 5)), + val rpcPort: Int = getPort(12002 + (nodeIndex * 5)), + val rpcAdminPort: Int = getPort(12003 + (nodeIndex * 5)), + val webPort: Int = getPort(12004 + (nodeIndex * 5)), + val dbPort: Int = getPort(12005 + (nodeIndex * 5)) +) : ConfigurationTemplate() { + + init { + nodeIndex += 1 + } + + override val config: (Configuration) -> String + get() = { + """ + |sshd={ port=$sshPort } + |p2pAddress="$host:$p2pPort" + |rpcSettings = { + | useSsl = false + | standAloneBroker = false + | address = "$host:$rpcPort" + | adminAddress = "$host:$rpcAdminPort" + |} + |webAddress="$host:$webPort" + """ + } + + companion object { + + private var nodeIndex = 0 + + private var startOfBackupRange = AtomicInteger(40000) + + private fun getPort(suggestedPortNumber: Int): Int { + var portNumber = suggestedPortNumber + while (isPortInUse(portNumber)) { + portNumber = startOfBackupRange.getAndIncrement() + } + if (portNumber >= 65535) { + throw Exception("No free port found (suggested $suggestedPortNumber)") + } + return portNumber + } + + private fun isPortInUse(portNumber: Int): Boolean { + return try { + val s = Socket("localhost", portNumber) + s.close() + true + + } catch (_: Exception) { + false + } + } + + } + +} diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/NotaryConfiguration.kt b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/NotaryConfiguration.kt new file mode 100644 index 0000000000..58dff6ad6d --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/NotaryConfiguration.kt @@ -0,0 +1,16 @@ +package net.corda.behave.node.configuration + +class NotaryConfiguration(private val notaryType: NotaryType) : ConfigurationTemplate() { + + override val config: (Configuration) -> String + get() = { + when (notaryType) { + NotaryType.NONE -> "" + NotaryType.NON_VALIDATING -> + "notary { validating = false }" + NotaryType.VALIDATING -> + "notary { validating = true }" + } + } + +} diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/NotaryType.kt b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/NotaryType.kt new file mode 100644 index 0000000000..abceb4b519 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/NotaryType.kt @@ -0,0 +1,18 @@ +package net.corda.behave.node.configuration + +enum class NotaryType { + + NONE, + VALIDATING, + NON_VALIDATING + +} + +fun String.toNotaryType(): NotaryType? { + return when (this.toLowerCase()) { + "non-validating" -> NotaryType.NON_VALIDATING + "nonvalidating" -> NotaryType.NON_VALIDATING + "validating" -> NotaryType.VALIDATING + else -> null + } +} diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/UserConfiguration.kt b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/UserConfiguration.kt new file mode 100644 index 0000000000..1c44a2f967 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/UserConfiguration.kt @@ -0,0 +1,37 @@ +package net.corda.behave.node.configuration + +class UserConfiguration : ConfigurationTemplate(), Iterable { + + data class User(val username: String, val password: String, val permissions: List) + + private val users = mutableListOf() + + fun withUser(username: String, password: String, permissions: List = listOf("ALL")): UserConfiguration { + users.add(User(username, password, permissions)) + return this + } + + override fun iterator(): Iterator { + return users.iterator() + } + + override val config: (Configuration) -> String + get() = { + """ + |rpcUsers=[ + |${users.joinToString("\n") { userObject(it) }} + |] + """ + } + + private fun userObject(user: User): String { + return """ + |{ + | username="${user.username}" + | password="${user.password}" + | permissions=[${user.permissions.joinToString(", ")}] + |} + """.trimMargin() + } + +} diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/process/Command.kt b/experimental/behave/src/main/kotlin/net/corda/behave/process/Command.kt new file mode 100644 index 0000000000..a35ae287db --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/process/Command.kt @@ -0,0 +1,157 @@ +package net.corda.behave.process + +import net.corda.behave.* +import net.corda.behave.file.currentDirectory +import net.corda.behave.logging.getLogger +import net.corda.behave.process.output.OutputListener +import rx.Observable +import java.io.Closeable +import java.io.File +import java.io.IOException +import java.time.Duration +import java.util.concurrent.CountDownLatch + +open class Command( + private val command: List, + private val directory: File = currentDirectory, + private val timeout: Duration = 2.minutes +): Closeable { + + protected val log = getLogger() + + private val terminationLatch = CountDownLatch(1) + + private val outputCapturedLatch = CountDownLatch(1) + + private var isInterrupted = false + + private var process: Process? = null + + private lateinit var outputListener: OutputListener + + var exitCode = -1 + private set + + val output: Observable = Observable.create { emitter -> + outputListener = object : OutputListener { + override fun onNewLine(line: String) { + emitter.onNext(line) + } + + override fun onEndOfStream() { + emitter.onCompleted() + } + } + } + + private val thread = Thread(Runnable { + try { + val processBuilder = ProcessBuilder(command) + .directory(directory) + .redirectErrorStream(true) + processBuilder.environment().putAll(System.getenv()) + process = processBuilder.start() + val process = process!! + Thread(Runnable { + val input = process.inputStream.bufferedReader() + while (true) { + try { + val line = input.readLine()?.trimEnd() ?: break + outputListener.onNewLine(line) + } catch (_: IOException) { + break + } + } + input.close() + outputListener.onEndOfStream() + outputCapturedLatch.countDown() + }).start() + val streamIsClosed = outputCapturedLatch.await(timeout) + val timeout = if (!streamIsClosed || isInterrupted) { + 1.second + } else { + timeout + } + if (!process.waitFor(timeout)) { + process.destroy() + process.waitFor(WAIT_BEFORE_KILL) + if (process.isAlive) { + process.destroyForcibly() + process.waitFor() + } + } + exitCode = process.exitValue() + if (isInterrupted) { + log.warn("Process ended after interruption") + } else if (exitCode != 0 && exitCode != 143 /* SIGTERM */) { + log.warn("Process {} ended with exit code {}", this, exitCode) + } + } catch (e: Exception) { + log.warn("Error occurred when trying to run process", e) + } + process = null + terminationLatch.countDown() + }) + + fun start() { + output.subscribe() + thread.start() + } + + fun interrupt() { + isInterrupted = true + outputCapturedLatch.countDown() + } + + fun waitFor(): Boolean { + terminationLatch.await() + return exitCode == 0 + } + + fun kill() { + process?.destroy() + process?.waitFor(WAIT_BEFORE_KILL) + if (process?.isAlive == true) { + process?.destroyForcibly() + } + if (process != null) { + terminationLatch.await() + } + process = null + } + + override fun close() { + waitFor() + } + + fun run() = use { _ -> } + + fun use(action: (Command) -> Unit): Int { + try { + start() + action(this) + } finally { + close() + } + return exitCode + } + + fun use(action: (Command, Observable) -> Unit = { _, _ -> }): Int { + try { + start() + action(this, output) + } finally { + close() + } + return exitCode + } + + override fun toString() = "Command(${command.joinToString(" ")})" + + companion object { + + private val WAIT_BEFORE_KILL: Duration = 5.seconds + + } + +} diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/process/JarCommand.kt b/experimental/behave/src/main/kotlin/net/corda/behave/process/JarCommand.kt new file mode 100644 index 0000000000..d465170c17 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/process/JarCommand.kt @@ -0,0 +1,34 @@ +package net.corda.behave.process + +import java.io.File +import java.time.Duration + +class JarCommand( + jarFile: File, + arguments: Array, + directory: File, + timeout: Duration, + enableRemoteDebugging: Boolean = false +) : Command( + command = listOf( + "/usr/bin/java", + *extraArguments(enableRemoteDebugging), + "-jar", "$jarFile", + *arguments + ), + directory = directory, + timeout = timeout +) { + + companion object { + + private fun extraArguments(enableRemoteDebugging: Boolean) = + if (enableRemoteDebugging) { + arrayOf("-Dcapsule.jvm.args=-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005") + } else { + arrayOf() + } + + } + +} \ No newline at end of file diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/process/output/OutputListener.kt b/experimental/behave/src/main/kotlin/net/corda/behave/process/output/OutputListener.kt new file mode 100644 index 0000000000..18dcc065e6 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/process/output/OutputListener.kt @@ -0,0 +1,9 @@ +package net.corda.behave.process.output + +interface OutputListener { + + fun onNewLine(line: String) + + fun onEndOfStream() + +} \ No newline at end of file diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/service/ContainerService.kt b/experimental/behave/src/main/kotlin/net/corda/behave/service/ContainerService.kt new file mode 100644 index 0000000000..262c0fcb27 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/service/ContainerService.kt @@ -0,0 +1,122 @@ +package net.corda.behave.service + +import com.spotify.docker.client.DefaultDockerClient +import com.spotify.docker.client.DockerClient +import com.spotify.docker.client.messages.ContainerConfig +import com.spotify.docker.client.messages.HostConfig +import com.spotify.docker.client.messages.PortBinding +import net.corda.behave.monitoring.PatternWatch +import net.corda.behave.monitoring.Watch +import rx.Observable +import java.io.Closeable + +abstract class ContainerService( + name: String, + port: Int, + settings: ServiceSettings = ServiceSettings() +) : Service(name, port, settings), Closeable { + + protected val client: DockerClient = DefaultDockerClient.fromEnv().build() + + protected var id: String? = null + + protected open val baseImage: String = "" + + protected open val imageTag: String = "latest" + + protected abstract val internalPort: Int + + private var isClientOpen: Boolean = true + + private val environmentVariables: MutableList = mutableListOf() + + private var startupStatement: Watch = PatternWatch.EMPTY + + private val imageReference: String + get() = "$baseImage:$imageTag" + + override fun startService(): Boolean { + return try { + val port = "$internalPort" + val portBindings = mapOf( + port to listOf(PortBinding.of("0.0.0.0", this.port)) + ) + val hostConfig = HostConfig.builder().portBindings(portBindings).build() + val containerConfig = ContainerConfig.builder() + .hostConfig(hostConfig) + .image(imageReference) + .exposedPorts(port) + .env(*environmentVariables.toTypedArray()) + .build() + + val creation = client.createContainer(containerConfig) + id = creation.id() + client.startContainer(id) + true + } catch (e: Exception) { + id = null + e.printStackTrace() + false + } + } + + override fun stopService(): Boolean { + if (id != null) { + client.stopContainer(id, 30) + client.removeContainer(id) + id = null + } + return true + } + + protected fun addEnvironmentVariable(name: String, value: String) { + environmentVariables.add("$name=$value") + } + + protected fun setStartupStatement(statement: String) { + startupStatement = PatternWatch(statement) + } + + override fun checkPrerequisites() { + if (!client.listImages().any { true == it.repoTags()?.contains(imageReference) }) { + log.info("Pulling image $imageReference ...") + client.pull(imageReference, { _ -> + run { } + }) + log.info("Image $imageReference downloaded") + } + } + + override fun verify(): Boolean { + return true + } + + override fun waitUntilStarted(): Boolean { + try { + var timeout = settings.startupTimeout.toMillis() + while (timeout > 0) { + client.logs(id, DockerClient.LogsParam.stdout(), DockerClient.LogsParam.stderr()).use { + val contents = it.readFully() + val observable = Observable.from(contents.split("\n")) + if (startupStatement.await(observable, settings.pollInterval)) { + log.info("Found process start-up statement for {}", this) + return true + } + } + timeout -= settings.pollInterval.toMillis() + } + return false + } catch (e: Exception) { + e.printStackTrace() + return false + } + } + + override fun close() { + if (isClientOpen) { + isClientOpen = false + client.close() + } + } + +} diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/service/Service.kt b/experimental/behave/src/main/kotlin/net/corda/behave/service/Service.kt new file mode 100644 index 0000000000..64c7e869f8 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/service/Service.kt @@ -0,0 +1,72 @@ +package net.corda.behave.service + +import net.corda.behave.logging.getLogger +import java.io.Closeable + +abstract class Service( + val name: String, + val port: Int, + val settings: ServiceSettings = ServiceSettings() +) : Closeable { + + private var isRunning: Boolean = false + + protected val log = getLogger() + + fun start(): Boolean { + if (isRunning) { + log.warn("{} is already running", this) + return false + } + log.info("Starting {} ...", this) + checkPrerequisites() + if (!startService()) { + log.warn("Failed to start {}", this) + return false + } + isRunning = true + Thread.sleep(settings.startupDelay.toMillis()) + return if (!waitUntilStarted()) { + log.warn("Failed to start {}", this) + stop() + false + } else if (!verify()) { + log.warn("Failed to verify start-up of {}", this) + stop() + false + } else { + log.info("{} started and available", this) + true + } + } + + fun stop() { + if (!isRunning) { + return + } + log.info("Stopping {} ...", this) + if (stopService()) { + log.info("{} stopped", this) + isRunning = false + } else { + log.warn("Failed to stop {}", this) + } + } + + override fun close() { + stop() + } + + override fun toString() = "Service(name = $name, port = $port)" + + protected open fun checkPrerequisites() { } + + protected open fun startService() = true + + protected open fun stopService() = true + + protected open fun verify() = true + + protected open fun waitUntilStarted() = true + +} \ No newline at end of file diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/service/ServiceInitiator.kt b/experimental/behave/src/main/kotlin/net/corda/behave/service/ServiceInitiator.kt new file mode 100644 index 0000000000..eb3d042d7f --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/service/ServiceInitiator.kt @@ -0,0 +1,5 @@ +package net.corda.behave.service + +import net.corda.behave.node.configuration.Configuration + +typealias ServiceInitiator = (Configuration) -> Service diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/service/ServiceSettings.kt b/experimental/behave/src/main/kotlin/net/corda/behave/service/ServiceSettings.kt new file mode 100644 index 0000000000..f94523691e --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/service/ServiceSettings.kt @@ -0,0 +1,13 @@ +package net.corda.behave.service + +import net.corda.behave.minute +import net.corda.behave.second +import net.corda.behave.seconds +import java.time.Duration + +data class ServiceSettings( + val timeout: Duration = 1.minute, + val startupDelay: Duration = 1.second, + val startupTimeout: Duration = 15.seconds, + val pollInterval: Duration = 1.second +) diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/service/database/H2Service.kt b/experimental/behave/src/main/kotlin/net/corda/behave/service/database/H2Service.kt new file mode 100644 index 0000000000..484944ee29 --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/service/database/H2Service.kt @@ -0,0 +1,18 @@ +package net.corda.behave.service.database + +import net.corda.behave.service.Service + +class H2Service( + name: String, + port: Int +) : Service(name, port) { + + companion object { + + val database = "node" + val schema = "dbo" + val username = "sa" + + } + +} \ No newline at end of file diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/service/database/SqlServerService.kt b/experimental/behave/src/main/kotlin/net/corda/behave/service/database/SqlServerService.kt new file mode 100644 index 0000000000..6a18df586f --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/service/database/SqlServerService.kt @@ -0,0 +1,58 @@ +package net.corda.behave.service.database + +import net.corda.behave.database.DatabaseConnection +import net.corda.behave.database.DatabaseType +import net.corda.behave.database.configuration.SqlServerConfigurationTemplate +import net.corda.behave.node.configuration.DatabaseConfiguration +import net.corda.behave.service.ContainerService +import net.corda.behave.service.ServiceSettings + +class SqlServerService( + name: String, + port: Int, + private val password: String, + settings: ServiceSettings = ServiceSettings() +) : ContainerService(name, port, settings) { + + override val baseImage = "microsoft/mssql-server-linux" + + override val internalPort = 1433 + + init { + addEnvironmentVariable("ACCEPT_EULA", "Y") + addEnvironmentVariable("SA_PASSWORD", password) + setStartupStatement("SQL Server is now ready for client connections") + } + + override fun verify(): Boolean { + val config = DatabaseConfiguration( + type = DatabaseType.SQL_SERVER, + host = host, + port = port, + database = database, + schema = schema, + username = username, + password = password + ) + val connection = DatabaseConnection(config, SqlServerConfigurationTemplate()) + try { + connection.use { + return true + } + } catch (ex: Exception) { + log.warn(ex.message, ex) + ex.printStackTrace() + } + return false + } + + companion object { + + val host = "localhost" + val database = "master" + val schema = "dbo" + val username = "sa" + + } + +} \ No newline at end of file diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/ssh/MonitoringSSHClient.kt b/experimental/behave/src/main/kotlin/net/corda/behave/ssh/MonitoringSSHClient.kt new file mode 100644 index 0000000000..fcbde2d54f --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/ssh/MonitoringSSHClient.kt @@ -0,0 +1,69 @@ +package net.corda.behave.ssh + +import net.corda.behave.process.output.OutputListener +import rx.Observable +import java.io.Closeable +import java.io.InterruptedIOException + +class MonitoringSSHClient( + private val client: SSHClient +) : Closeable { + + private var isRunning = false + + private lateinit var outputListener: OutputListener + + val output: Observable = Observable.create { emitter -> + outputListener = object : OutputListener { + override fun onNewLine(line: String) { + emitter.onNext(line) + } + + override fun onEndOfStream() { + emitter.onCompleted() + } + } + } + + private val thread = Thread(Runnable { + while (isRunning) { + try { + val line = client.readLine() ?: break + outputListener.onNewLine(line) + } catch (_: InterruptedIOException) { + break + } + } + outputListener.onEndOfStream() + }) + + init { + isRunning = true + output.subscribe() + thread.start() + } + + override fun close() { + isRunning = false + thread.join(1000) + if (thread.isAlive) { + thread.interrupt() + } + client.close() + } + + fun use(action: (MonitoringSSHClient) -> Unit) { + try { + action(this) + } finally { + close() + } + } + + fun write(vararg bytes: Byte) = client.write(*bytes) + + fun write(charSequence: CharSequence) = client.write(charSequence) + + fun writeLine(string: String) = client.writeLine(string) + +} \ No newline at end of file diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/ssh/SSHClient.kt b/experimental/behave/src/main/kotlin/net/corda/behave/ssh/SSHClient.kt new file mode 100644 index 0000000000..afa530cf3c --- /dev/null +++ b/experimental/behave/src/main/kotlin/net/corda/behave/ssh/SSHClient.kt @@ -0,0 +1,161 @@ +package net.corda.behave.ssh + +import net.corda.behave.logging.getLogger +import org.apache.sshd.client.SshClient +import org.apache.sshd.client.channel.ChannelShell +import org.apache.sshd.client.session.ClientSession +import org.apache.sshd.common.channel.SttySupport +import org.crsh.util.Utils +import java.io.* +import java.time.Duration +import java.util.concurrent.TimeUnit + +open class SSHClient private constructor( + private val client: SshClient, + private val outputStream: OutputStream, + private val inputStream: InputStream, + private val session: ClientSession, + private val channel: ChannelShell +) : Closeable { + + private var isClosed = false + + fun read(): Int? { + if (isClosed) { + return null + } + val char = inputStream.read() + return if (char != -1) { + char + } else { + null + } + } + + fun readLine(): String? { + if (isClosed) { + return null + } + var ch: Int? + val lineBuffer = mutableListOf() + while (true) { + ch = read() + if (ch == null) { + if (lineBuffer.isEmpty()) { + return null + } + break + } + lineBuffer.add(ch.toChar()) + if (ch == 10) { + break + } + } + return String(lineBuffer.toCharArray()) + } + + fun write(s: CharSequence) { + if (isClosed) { + return + } + write(*s.toString().toByteArray(UTF8)) + } + + fun write(vararg bytes: Byte) { + if (isClosed) { + return + } + outputStream.write(bytes) + } + + fun writeLine(s: String) { + write("$s\n") + flush() + } + + fun flush() { + if (isClosed) { + return + } + outputStream.flush() + } + + override fun close() { + if (isClosed) { + return + } + try { + Utils.close(outputStream) + channel.close(false) + session.close(false) + client.stop() + } finally { + isClosed = true + } + } + + companion object { + + private val log = getLogger() + + fun connect( + port: Int, + password: String, + hostname: String = "localhost", + username: String = "corda", + timeout: Duration = Duration.ofSeconds(4) + ): SSHClient { + val tty = SttySupport.parsePtyModes(TTY) + val client = SshClient.setUpDefaultClient() + client.start() + + log.info("Connecting to $hostname:$port ...") + val session = client + .connect(username, hostname, port) + .verify(timeout.seconds, TimeUnit.SECONDS) + .session + + log.info("Authenticating using password identity ...") + session.addPasswordIdentity(password) + val authFuture = session.auth().verify(timeout.seconds, TimeUnit.SECONDS) + + authFuture.addListener { + log.info("Authentication completed with " + if (it.isSuccess) "success" else "failure") + } + + val channel = session.createShellChannel() + channel.ptyModes = tty + + val outputStream = PipedOutputStream() + val channelIn = PipedInputStream(outputStream) + + val channelOut = PipedOutputStream() + val inputStream = PipedInputStream(channelOut) + + channel.`in` = channelIn + channel.out = channelOut + channel.err = ByteArrayOutputStream() + channel.open() + + return SSHClient(client, outputStream, inputStream, session, channel) + } + + private const val TTY = "speed 9600 baud; 36 rows; 180 columns;\n" + + "lflags: icanon isig iexten echo echoe -echok echoke -echonl echoctl\n" + + "\t-echoprt -altwerase -noflsh -tostop -flusho pendin -nokerninfo\n" + + "\t-extproc\n" + + "iflags: -istrip icrnl -inlcr -igncr ixon -ixoff ixany imaxbel iutf8\n" + + "\t-ignbrk brkint -inpck -ignpar -parmrk\n" + + "oflags: opost onlcr -oxtabs -onocr -onlret\n" + + "cflags: cread cs8 -parenb -parodd hupcl -clocal -cstopb -crtscts -dsrflow\n" + + "\t-dtrflow -mdmbuf\n" + + "cchars: discard = ^O; dsusp = ^Y; eof = ^D; eol = ;\n" + + "\teol2 = ; erase = ^?; intr = ^C; kill = ^U; lnext = ^V;\n" + + "\tmin = 1; quit = ^\\; reprint = ^R; start = ^Q; status = ^T;\n" + + "\tstop = ^S; susp = ^Z; time = 0; werase = ^W;" + + private val UTF8 = charset("UTF-8") + + } + +} \ No newline at end of file diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioHooks.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioHooks.kt index 745ef851b6..f3fad092c9 100644 --- a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioHooks.kt +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioHooks.kt @@ -12,6 +12,7 @@ class ScenarioHooks(private val state: ScenarioState) { @After fun afterScenario() { + state.stopNetwork() } } \ No newline at end of file diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioState.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioState.kt index a21ab59a10..02e2af9c22 100644 --- a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioState.kt +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioState.kt @@ -1,7 +1,102 @@ package net.corda.behave.scenarios +import net.corda.behave.logging.getLogger +import net.corda.behave.network.Network +import net.corda.behave.node.Node +import net.corda.behave.seconds +import net.corda.client.rpc.CordaRPCClient +import net.corda.client.rpc.CordaRPCClientConfiguration +import net.corda.core.messaging.CordaRPCOps +import net.corda.core.utilities.NetworkHostAndPort +import org.assertj.core.api.Assertions.assertThat + class ScenarioState { - var count: Int = 0 + private val log = getLogger() + + private val nodes = mutableListOf() + + private var network: Network? = null + + fun fail(message: String) { + error(message) + } + + fun error(message: String, ex: Throwable? = null): T { + this.network?.signalFailure(message, ex) + if (ex != null) { + throw Exception(message, ex) + } else { + throw Exception(message) + } + } + + fun node(name: String): Node { + val network = network ?: error("Network is not running") + return network[nodeName(name)] ?: error("Node '$name' not found") + } + + fun nodeBuilder(name: String): Node.Builder { + return nodes.firstOrNull { it.name == nodeName(name) } ?: newNode(name) + } + + fun ensureNetworkIsRunning() { + if (network != null) { + // Network is already running + return + } + val networkBuilder = Network.new() + for (node in nodes) { + networkBuilder.addNode(node) + } + network = networkBuilder.generate() + network?.start() + assertThat(network?.waitUntilRunning()).isTrue() + } + + fun withNetwork(action: ScenarioState.() -> Unit) { + ensureNetworkIsRunning() + action() + } + + fun withClient(nodeName: String, action: (CordaRPCOps) -> T): T { + var result: T? = null + withNetwork { + val node = node(nodeName) + val user = node.config.users.first() + val address = node.config.nodeInterface + val targetHost = NetworkHostAndPort(address.host, address.rpcPort) + val config = CordaRPCClientConfiguration( + connectionMaxRetryInterval = 10.seconds + ) + log.info("Establishing RPC connection to ${targetHost.host} on port ${targetHost.port} ...") + CordaRPCClient(targetHost, config).use(user.username, user.password) { + log.info("RPC connection to ${targetHost.host}:${targetHost.port} established") + val client = it.proxy + result = action(client) + } + } + return result ?: error("Failed to run RPC action") + } + + fun stopNetwork() { + val network = network ?: return + for (node in network) { + val matches = node.logOutput.find("\\[ERR") + if (matches.any()) { + fail("Found errors in the log for node '${node.config.name}': ${matches.first().filename}") + } + } + network.stop() + } + + private fun nodeName(name: String) = "Entity$name" + + private fun newNode(name: String): Node.Builder { + val builder = Node.new() + .withName(nodeName(name)) + nodes.add(builder) + return builder + } } \ No newline at end of file diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/StepsContainer.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/StepsContainer.kt index 69ef293853..818c08916a 100644 --- a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/StepsContainer.kt +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/StepsContainer.kt @@ -1,23 +1,58 @@ package net.corda.behave.scenarios import cucumber.api.java8.En -import net.corda.behave.scenarios.steps.dummySteps +import net.corda.behave.scenarios.helpers.Cash +import net.corda.behave.scenarios.helpers.Database +import net.corda.behave.scenarios.helpers.Ssh +import net.corda.behave.scenarios.helpers.Startup +import net.corda.behave.scenarios.steps.* +import net.corda.core.messaging.CordaRPCOps import org.slf4j.Logger import org.slf4j.LoggerFactory @Suppress("KDocMissingDocumentation") class StepsContainer(val state: ScenarioState) : En { - val log: Logger = LoggerFactory.getLogger(StepsContainer::class.java) + private val log: Logger = LoggerFactory.getLogger(StepsContainer::class.java) private val stepDefinitions: List<(StepsBlock) -> Unit> = listOf( - ::dummySteps + ::cashSteps, + ::configurationSteps, + ::databaseSteps, + ::networkSteps, + ::rpcSteps, + ::sshSteps, + ::startupSteps ) init { stepDefinitions.forEach { it({ this.steps(it) }) } } + fun succeed() = log.info("Step succeeded") + + fun fail(message: String) = state.fail(message) + + fun error(message: String) = state.error(message) + + fun node(name: String) = state.nodeBuilder(name) + + fun withNetwork(action: ScenarioState.() -> Unit) { + state.withNetwork(action) + } + + fun withClient(nodeName: String, action: (CordaRPCOps) -> T): T { + return state.withClient(nodeName, action) + } + + val startup = Startup(state) + + val database = Database(state) + + val ssh = Ssh(state) + + val cash = Cash(state) + private fun steps(action: (StepsContainer.() -> Unit)) { action(this) } diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Cash.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Cash.kt new file mode 100644 index 0000000000..f119e5ebe0 --- /dev/null +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Cash.kt @@ -0,0 +1,30 @@ +package net.corda.behave.scenarios.helpers + +import net.corda.behave.scenarios.ScenarioState +import net.corda.core.messaging.startFlow +import net.corda.finance.flows.CashConfigDataFlow +import java.util.concurrent.TimeUnit + +class Cash(state: ScenarioState) : Substeps(state) { + + fun numberOfIssuableCurrencies(nodeName: String): Int { + return withClient(nodeName) { + for (flow in it.registeredFlows()) { + log.info(flow) + } + try { + val config = it.startFlow(::CashConfigDataFlow).returnValue.get(10, TimeUnit.SECONDS) + for (supportedCurrency in config.supportedCurrencies) { + log.info("Can use $supportedCurrency") + } + for (issuableCurrency in config.issuableCurrencies) { + log.info("Can issue $issuableCurrency") + } + return@withClient config.issuableCurrencies.size + } catch (_: Exception) { + return@withClient 0 + } + } + } + +} \ No newline at end of file diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Database.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Database.kt new file mode 100644 index 0000000000..2406aff675 --- /dev/null +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Database.kt @@ -0,0 +1,23 @@ +package net.corda.behave.scenarios.helpers + +import net.corda.behave.await +import net.corda.behave.scenarios.ScenarioState +import net.corda.behave.seconds +import org.assertj.core.api.Assertions.assertThat +import java.util.concurrent.CountDownLatch + +class Database(state: ScenarioState) : Substeps(state) { + + fun canConnectTo(nodeName: String) { + withNetwork { + val latch = CountDownLatch(1) + log.info("Connecting to the database of node '$nodeName' ...") + node(nodeName).database.use { + log.info("Connected to the database of node '$nodeName'") + latch.countDown() + } + assertThat(latch.await(10.seconds)).isTrue() + } + } + +} \ No newline at end of file diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Ssh.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Ssh.kt new file mode 100644 index 0000000000..a1e7ddd875 --- /dev/null +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Ssh.kt @@ -0,0 +1,43 @@ +package net.corda.behave.scenarios.helpers + +import net.corda.behave.scenarios.ScenarioState +import org.assertj.core.api.Assertions.assertThat +import rx.observers.TestSubscriber +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +class Ssh(state: ScenarioState) : Substeps(state) { + + fun canConnectTo(nodeName: String) { + withNetwork { + log.info("Connecting to node '$nodeName' over SSH ...") + hasSshStartupMessage(nodeName) + val latch = CountDownLatch(1) + val subscriber = TestSubscriber() + node(nodeName).ssh { + it.output.subscribe(subscriber) + assertThat(subscriber.onNextEvents).isNotEmpty + log.info("Successfully connect to node '$nodeName' over SSH") + latch.countDown() + } + if (!latch.await(15, TimeUnit.SECONDS)) { + fail("Failed to connect to node '$nodeName' over SSH") + } + } + } + + private fun hasSshStartupMessage(nodeName: String) { + var i = 5 + while (i > 0) { + Thread.sleep(2000) + if (state.node(nodeName).logOutput.find(".*SSH server listening on port.*").any()) { + break + } + i -= 1 + } + if (i == 0) { + state.fail("Unable to find SSH start-up message for node $nodeName") + } + } + +} \ No newline at end of file diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Startup.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Startup.kt new file mode 100644 index 0000000000..3520acec43 --- /dev/null +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Startup.kt @@ -0,0 +1,65 @@ +package net.corda.behave.scenarios.helpers + +import net.corda.behave.scenarios.ScenarioState + +class Startup(state: ScenarioState) : Substeps(state) { + + fun hasLoggingInformation(nodeName: String) { + withNetwork { + log.info("Retrieving logging information for node '$nodeName' ...") + if (!node(nodeName).nodeInfoGenerationOutput.find("Logs can be found in.*").any()) { + fail("Unable to find logging information for node $nodeName") + } + } + } + + fun hasDatabaseDetails(nodeName: String) { + withNetwork { + log.info("Retrieving database details for node '$nodeName' ...") + if (!node(nodeName).nodeInfoGenerationOutput.find("Database connection url is.*").any()) { + fail("Unable to find database details for node $nodeName") + } + } + } + + fun hasPlatformVersion(nodeName: String, platformVersion: Int) { + withNetwork { + log.info("Finding platform version for node '$nodeName' ...") + val logOutput = node(nodeName).logOutput + if (!logOutput.find(".*Platform Version: $platformVersion .*").any()) { + val match = logOutput.find(".*Platform Version: .*").firstOrNull() + if (match == null) { + fail("Unable to find platform version for node '$nodeName'") + } else { + val foundVersion = Regex("Platform Version: (\\d+) ") + .find(match.contents) + ?.groups?.last()?.value + fail("Expected platform version $platformVersion for node '$nodeName', " + + "but found version $foundVersion") + + } + } + } + } + + fun hasVersion(nodeName: String, version: String) { + withNetwork { + log.info("Finding version for node '$nodeName' ...") + val logOutput = node(nodeName).logOutput + if (!logOutput.find(".*Release: $version .*").any()) { + val match = logOutput.find(".*Release: .*").firstOrNull() + if (match == null) { + fail("Unable to find version for node '$nodeName'") + } else { + val foundVersion = Regex("Version: ([^ ]+) ") + .find(match.contents) + ?.groups?.last()?.value + fail("Expected version $version for node '$nodeName', " + + "but found version $foundVersion") + + } + } + } + } + +} \ No newline at end of file diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Substeps.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Substeps.kt new file mode 100644 index 0000000000..bba2f052e0 --- /dev/null +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Substeps.kt @@ -0,0 +1,24 @@ +package net.corda.behave.scenarios.helpers + +import net.corda.behave.logging.getLogger +import net.corda.behave.scenarios.ScenarioState +import net.corda.core.messaging.CordaRPCOps + +abstract class Substeps(protected val state: ScenarioState) { + + protected val log = getLogger() + + protected fun withNetwork(action: ScenarioState.() -> Unit) = + state.withNetwork(action) + + protected fun withClient(nodeName: String, action: ScenarioState.(CordaRPCOps) -> T): T { + return state.withClient(nodeName, { + return@withClient try { + action(state, it) + } catch (ex: Exception) { + state.error(ex.message ?: "Failed to execute RPC call") + } + }) + } + +} \ No newline at end of file diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/CashSteps.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/CashSteps.kt new file mode 100644 index 0000000000..e26486c351 --- /dev/null +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/CashSteps.kt @@ -0,0 +1,20 @@ +package net.corda.behave.scenarios.steps + +import net.corda.behave.scenarios.StepsBlock +import org.assertj.core.api.Assertions.assertThat + +fun cashSteps(steps: StepsBlock) = steps { + + Then("^node (\\w+) has 1 issuable currency$") { name -> + withNetwork { + assertThat(cash.numberOfIssuableCurrencies(name)).isEqualTo(1) + } + } + + Then("^node (\\w+) has (\\w+) issuable currencies$") { name, count -> + withNetwork { + assertThat(cash.numberOfIssuableCurrencies(name)).isEqualTo(count.toInt()) + } + } + +} diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/ConfigurationSteps.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/ConfigurationSteps.kt new file mode 100644 index 0000000000..27def111b8 --- /dev/null +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/ConfigurationSteps.kt @@ -0,0 +1,49 @@ +package net.corda.behave.scenarios.steps + +import net.corda.behave.database.DatabaseType +import net.corda.behave.node.Distribution +import net.corda.behave.node.configuration.toNotaryType +import net.corda.behave.scenarios.StepsBlock + +fun configurationSteps(steps: StepsBlock) = steps { + + Given("^a node (\\w+) of version ([^ ]+)$") { name, version -> + node(name) + .withDistribution(Distribution.fromVersionString(version) + ?: error("Unknown version '$version'")) + } + + Given("^a (\\w+) notary (\\w+) of version ([^ ]+)$") { type, name, version -> + node(name) + .withDistribution(Distribution.fromVersionString(version) + ?: error("Unknown version '$version'")) + .withNotaryType(type.toNotaryType() + ?: error("Unknown notary type '$type'")) + } + + Given("^node (\\w+) uses database of type (.+)$") { name, type -> + node(name) + .withDatabaseType(DatabaseType.fromName(type) + ?: error("Unknown database type '$type'")) + } + + Given("^node (\\w+) can issue (.+)$") { name, currencies -> + node(name).withIssuableCurrencies(currencies + .replace(" and ", ", ") + .split(", ") + .map { it.toUpperCase() }) + } + + Given("^node (\\w+) is located in (\\w+), (\\w+)$") { name, location, country -> + node(name).withLocation(location, country) + } + + Given("^node (\\w+) has the finance app installed$") { name -> + node(name).withFinanceApp() + } + + Given("^node (\\w+) has app installed: (.+)$") { name, app -> + node(name).withApp(app) + } + +} diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/DatabaseSteps.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/DatabaseSteps.kt new file mode 100644 index 0000000000..9b21650a50 --- /dev/null +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/DatabaseSteps.kt @@ -0,0 +1,13 @@ +package net.corda.behave.scenarios.steps + +import net.corda.behave.scenarios.StepsBlock + +fun databaseSteps(steps: StepsBlock) = steps { + + Then("^user can connect to the database of node (\\w+)$") { name -> + withNetwork { + database.canConnectTo(name) + } + } + +} diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/DummySteps.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/DummySteps.kt deleted file mode 100644 index ce86fa5186..0000000000 --- a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/DummySteps.kt +++ /dev/null @@ -1,18 +0,0 @@ -package net.corda.behave.scenarios.steps - -import net.corda.behave.scenarios.StepsBlock -import org.assertj.core.api.Assertions.assertThat - -fun dummySteps(steps: StepsBlock) = steps { - - When("^(\\d+) dumm(y|ies) exists?$") { count, _ -> - state.count = count - log.info("Checking pre-condition $count") - } - - Then("^there is a dummy$") { - assertThat(state.count).isGreaterThan(0) - log.info("Checking outcome ${state.count}") - } - -} diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/NetworkSteps.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/NetworkSteps.kt new file mode 100644 index 0000000000..fcc544de29 --- /dev/null +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/NetworkSteps.kt @@ -0,0 +1,11 @@ +package net.corda.behave.scenarios.steps + +import net.corda.behave.scenarios.StepsBlock + +fun networkSteps(steps: StepsBlock) = steps { + + When("^the network is ready$") { + state.ensureNetworkIsRunning() + } + +} diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/RpcSteps.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/RpcSteps.kt new file mode 100644 index 0000000000..9accb28398 --- /dev/null +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/RpcSteps.kt @@ -0,0 +1,13 @@ +package net.corda.behave.scenarios.steps + +import net.corda.behave.scenarios.StepsBlock + +fun rpcSteps(steps: StepsBlock) = steps { + + Then("^user can connect to node (\\w+) using RPC$") { name -> + withClient(name) { + succeed() + } + } + +} diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/SshSteps.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/SshSteps.kt new file mode 100644 index 0000000000..516732f1e7 --- /dev/null +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/SshSteps.kt @@ -0,0 +1,13 @@ +package net.corda.behave.scenarios.steps + +import net.corda.behave.scenarios.StepsBlock + +fun sshSteps(steps: StepsBlock) = steps { + + Then("^user can connect to node (\\w+) using SSH$") { name -> + withNetwork { + ssh.canConnectTo(name) + } + } + +} diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/StartupSteps.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/StartupSteps.kt new file mode 100644 index 0000000000..f78415f2c8 --- /dev/null +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/steps/StartupSteps.kt @@ -0,0 +1,31 @@ +package net.corda.behave.scenarios.steps + +import net.corda.behave.scenarios.StepsBlock + +fun startupSteps(steps: StepsBlock) = steps { + + Then("^user can retrieve database details for node (\\w+)$") { name -> + withNetwork { + startup.hasDatabaseDetails(name) + } + } + + Then("^user can retrieve logging information for node (\\w+)$") { name -> + withNetwork { + startup.hasLoggingInformation(name) + } + } + + Then("^node (\\w+) is on version ([^ ]+)$") { name, version -> + withNetwork { + startup.hasVersion(name, version) + } + } + + Then("^node (\\w+) is on platform version (\\w+)$") { name, platformVersion -> + withNetwork { + startup.hasPlatformVersion(name, platformVersion.toInt()) + } + } + +} \ No newline at end of file diff --git a/experimental/behave/src/scenario/resources/features/cash/currencies.feature b/experimental/behave/src/scenario/resources/features/cash/currencies.feature new file mode 100644 index 0000000000..b90e7fa8a1 --- /dev/null +++ b/experimental/behave/src/scenario/resources/features/cash/currencies.feature @@ -0,0 +1,14 @@ +Feature: Cash - Issuable Currencies + To have cash on ledger, certain nodes must have the ability to issue cash of various currencies. + + Scenario: Node can issue no currencies by default + Given a node A of version master + And node A has the finance app installed + When the network is ready + Then node A has 0 issuable currencies + + Scenario: Node can issue a currency + Given a node A of version master + And node A can issue USD + When the network is ready + Then node A has 1 issuable currency \ No newline at end of file diff --git a/experimental/behave/src/scenario/resources/features/database/connection.feature b/experimental/behave/src/scenario/resources/features/database/connection.feature new file mode 100644 index 0000000000..47137b8aea --- /dev/null +++ b/experimental/behave/src/scenario/resources/features/database/connection.feature @@ -0,0 +1,13 @@ +Feature: Database - Connection + For Corda to work, a database must be running and appropriately configured. + + Scenario Outline: User can connect to node's database + Given a node A of version + And node A uses database of type + When the network is ready + Then user can connect to the database of node A + + Examples: + | Node-Version | Database-Type | + | MASTER | H2 | + #| MASTER | SQL Server | \ No newline at end of file diff --git a/experimental/behave/src/scenario/resources/features/dummy.feature b/experimental/behave/src/scenario/resources/features/dummy.feature deleted file mode 100644 index 6ff1613bd5..0000000000 --- a/experimental/behave/src/scenario/resources/features/dummy.feature +++ /dev/null @@ -1,6 +0,0 @@ -Feature: Dummy - Lorem ipsum - - Scenario: Noop - Given 15 dummies exist - Then there is a dummy \ No newline at end of file diff --git a/experimental/behave/src/scenario/resources/features/startup/logging.feature b/experimental/behave/src/scenario/resources/features/startup/logging.feature new file mode 100644 index 0000000000..8cbf35eda1 --- /dev/null +++ b/experimental/behave/src/scenario/resources/features/startup/logging.feature @@ -0,0 +1,13 @@ +Feature: Startup Information - Logging + A Corda node should inform the user of important parameters during startup so that he/she can confirm the setup and + configure / connect relevant software to said node. + + Scenario: Node shows logging information on startup + Given a node A of version MASTER + And node A uses database of type H2 + And node A is located in London, GB + When the network is ready + Then node A is on platform version 2 + And node A is on version 3.0-SNAPSHOT + And user can retrieve logging information for node A + And user can retrieve database details for node A diff --git a/experimental/behave/src/test/kotlin/net/corda/behave/UtilityTests.kt b/experimental/behave/src/test/kotlin/net/corda/behave/UtilityTests.kt deleted file mode 100644 index c956cc67e1..0000000000 --- a/experimental/behave/src/test/kotlin/net/corda/behave/UtilityTests.kt +++ /dev/null @@ -1,13 +0,0 @@ -package net.corda.behave - -import org.junit.Assert -import org.junit.Test - -class UtilityTests { - - @Test - fun `dummy`() { - Assert.assertEquals(true, Utility.dummy()) - } - -} \ No newline at end of file diff --git a/experimental/behave/src/test/kotlin/net/corda/behave/monitoring/MonitoringTests.kt b/experimental/behave/src/test/kotlin/net/corda/behave/monitoring/MonitoringTests.kt new file mode 100644 index 0000000000..23e0718b6d --- /dev/null +++ b/experimental/behave/src/test/kotlin/net/corda/behave/monitoring/MonitoringTests.kt @@ -0,0 +1,64 @@ +package net.corda.behave.monitoring + +import net.corda.behave.second +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import rx.Observable + +class MonitoringTests { + + @Test + fun `watch gets triggered when pattern is observed`() { + val observable = Observable.just("first", "second", "third") + val result = PatternWatch("c.n").await(observable, 1.second) + assertThat(result).isTrue() + } + + @Test + fun `watch does not get triggered when pattern is not observed`() { + val observable = Observable.just("first", "second", "third") + val result = PatternWatch("forth").await(observable, 1.second) + assertThat(result).isFalse() + } + + @Test + fun `conjunctive watch gets triggered when all its constituents match on the input`() { + val observable = Observable.just("first", "second", "third") + val watch1 = PatternWatch("fir") + val watch2 = PatternWatch("ond") + val watch3 = PatternWatch("ird") + val aggregate = watch1 * watch2 * watch3 + assertThat(aggregate.await(observable, 1.second)).isTrue() + } + + @Test + fun `conjunctive watch does not get triggered when one or more of its constituents do not match on the input`() { + val observable = Observable.just("first", "second", "third") + val watch1 = PatternWatch("fir") + val watch2 = PatternWatch("ond") + val watch3 = PatternWatch("baz") + val aggregate = watch1 * watch2 * watch3 + assertThat(aggregate.await(observable, 1.second)).isFalse() + } + + @Test + fun `disjunctive watch gets triggered when one or more of its constituents match on the input`() { + val observable = Observable.just("first", "second", "third") + val watch1 = PatternWatch("foo") + val watch2 = PatternWatch("ond") + val watch3 = PatternWatch("bar") + val aggregate = watch1 / watch2 / watch3 + assertThat(aggregate.await(observable, 1.second)).isTrue() + } + + @Test + fun `disjunctive watch does not get triggered when none its constituents match on the input`() { + val observable = Observable.just("first", "second", "third") + val watch1 = PatternWatch("foo") + val watch2 = PatternWatch("baz") + val watch3 = PatternWatch("bar") + val aggregate = watch1 / watch2 / watch3 + assertThat(aggregate.await(observable, 1.second)).isFalse() + } + +} \ No newline at end of file diff --git a/experimental/behave/src/test/kotlin/net/corda/behave/network/NetworkTests.kt b/experimental/behave/src/test/kotlin/net/corda/behave/network/NetworkTests.kt new file mode 100644 index 0000000000..93cdaa9394 --- /dev/null +++ b/experimental/behave/src/test/kotlin/net/corda/behave/network/NetworkTests.kt @@ -0,0 +1,39 @@ +package net.corda.behave.network + +import net.corda.behave.database.DatabaseType +import net.corda.behave.node.configuration.NotaryType +import net.corda.behave.seconds +import org.junit.Test + +class NetworkTests { + + @Test + fun `network of two nodes can be spun up`() { + val network = Network + .new() + .addNode("Foo") + .addNode("Bar") + .generate() + network.use { + it.waitUntilRunning(30.seconds) + it.signal() + it.keepAlive(30.seconds) + } + } + + @Test + fun `network of three nodes and mixed databases can be spun up`() { + val network = Network + .new() + .addNode("Foo") + .addNode("Bar", databaseType = DatabaseType.SQL_SERVER) + .addNode("Baz", notaryType = NotaryType.NON_VALIDATING) + .generate() + network.use { + it.waitUntilRunning(30.seconds) + it.signal() + it.keepAlive(30.seconds) + } + } + +} \ No newline at end of file diff --git a/experimental/behave/src/test/kotlin/net/corda/behave/process/CommandTests.kt b/experimental/behave/src/test/kotlin/net/corda/behave/process/CommandTests.kt new file mode 100644 index 0000000000..a6a6b121a7 --- /dev/null +++ b/experimental/behave/src/test/kotlin/net/corda/behave/process/CommandTests.kt @@ -0,0 +1,34 @@ +package net.corda.behave.process + +import org.assertj.core.api.Assertions.* +import org.junit.Test +import rx.observers.TestSubscriber + +class CommandTests { + + @Test + fun `successful command returns zero`() { + val exitCode = Command(listOf("ls", "/")).run() + assertThat(exitCode).isEqualTo(0) + } + + @Test + fun `failed command returns non-zero`() { + val exitCode = Command(listOf("some-random-command-that-does-not-exist")).run() + assertThat(exitCode).isNotEqualTo(0) + } + + @Test + fun `output stream for command can be observed`() { + val subscriber = TestSubscriber() + val exitCode = Command(listOf("ls", "/")).use { _, output -> + output.subscribe(subscriber) + subscriber.awaitTerminalEvent() + subscriber.assertCompleted() + subscriber.assertNoErrors() + assertThat(subscriber.onNextEvents).contains("bin", "etc", "var") + } + assertThat(exitCode).isEqualTo(0) + } + +} \ No newline at end of file diff --git a/experimental/behave/src/test/kotlin/net/corda/behave/service/SqlServerServiceTests.kt b/experimental/behave/src/test/kotlin/net/corda/behave/service/SqlServerServiceTests.kt new file mode 100644 index 0000000000..79a662a617 --- /dev/null +++ b/experimental/behave/src/test/kotlin/net/corda/behave/service/SqlServerServiceTests.kt @@ -0,0 +1,17 @@ +package net.corda.behave.service + +import net.corda.behave.service.database.SqlServerService +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test + +class SqlServerServiceTests { + + @Test + fun `sql server can be started and stopped`() { + val service = SqlServerService("test-mssql", 12345, "S0meS3cretW0rd") + val didStart = service.start() + service.stop() + assertThat(didStart).isTrue() + } + +} \ No newline at end of file diff --git a/experimental/behave/src/test/resources/log4j2.xml b/experimental/behave/src/test/resources/log4j2.xml new file mode 100644 index 0000000000..43fcf63c3d --- /dev/null +++ b/experimental/behave/src/test/resources/log4j2.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file From bd707eb9c88235f48c39738d12e66b54f6a15a67 Mon Sep 17 00:00:00 2001 From: Tommy Lillehagen Date: Fri, 9 Feb 2018 19:23:56 +0000 Subject: [PATCH 03/50] Update README --- experimental/behave/README.md | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/experimental/behave/README.md b/experimental/behave/README.md index 163c6310cc..33c5517cf7 100644 --- a/experimental/behave/README.md +++ b/experimental/behave/README.md @@ -1,3 +1,29 @@ +# Introduction + +This project illustrates how one can use Cucumber / BDD to drive +and test homogeneous and heterogeneous Corda networks on a local +machine. The framework has built-in support for Dockerised node +dependencies so that you easily can spin up a Corda node locally +that, for instance, uses a 3rd party database provider such as +MS SQL Server or Postgres. + +# Structure + +The project is split into three pieces: + + * **Testing Library** (main) - This library contains auxiliary + functions that help in configuring and bootstrapping Corda + networks on a local machine. The purpose of the library is to + aid in black-box testing and automation. + + * **Unit Tests** (test) - These are various tests for the + library described above. Note that there's only limited + coverage for now. + + * **BDD Framework** (scenario) - This module shows how to use + BDD-style frameworks to control the testing of Corda networks; + more specifically, using [Cucumber](cucumber.io). + # Setup To get started, please run the following command: @@ -6,6 +32,6 @@ To get started, please run the following command: $ ./prepare.sh ``` -This command will download necessary database drivers and set up +This script will download necessary database drivers and set up the dependencies directory with copies of the Corda fat-JAR and -the network bootstrapping tool. \ No newline at end of file +the network bootstrapping tool. From d5d5b12a2db38446bc1e7c8e3d273691bbdef088 Mon Sep 17 00:00:00 2001 From: Tommy Lillehagen Date: Fri, 9 Feb 2018 19:31:03 +0000 Subject: [PATCH 04/50] Ignore tests to not impede test pipeline --- .../src/test/kotlin/net/corda/behave/network/NetworkTests.kt | 3 +++ .../kotlin/net/corda/behave/service/SqlServerServiceTests.kt | 2 ++ 2 files changed, 5 insertions(+) diff --git a/experimental/behave/src/test/kotlin/net/corda/behave/network/NetworkTests.kt b/experimental/behave/src/test/kotlin/net/corda/behave/network/NetworkTests.kt index 93cdaa9394..93801b1d8d 100644 --- a/experimental/behave/src/test/kotlin/net/corda/behave/network/NetworkTests.kt +++ b/experimental/behave/src/test/kotlin/net/corda/behave/network/NetworkTests.kt @@ -3,10 +3,12 @@ package net.corda.behave.network import net.corda.behave.database.DatabaseType import net.corda.behave.node.configuration.NotaryType import net.corda.behave.seconds +import org.junit.Ignore import org.junit.Test class NetworkTests { + @Ignore @Test fun `network of two nodes can be spun up`() { val network = Network @@ -21,6 +23,7 @@ class NetworkTests { } } + @Ignore @Test fun `network of three nodes and mixed databases can be spun up`() { val network = Network diff --git a/experimental/behave/src/test/kotlin/net/corda/behave/service/SqlServerServiceTests.kt b/experimental/behave/src/test/kotlin/net/corda/behave/service/SqlServerServiceTests.kt index 79a662a617..872574cb70 100644 --- a/experimental/behave/src/test/kotlin/net/corda/behave/service/SqlServerServiceTests.kt +++ b/experimental/behave/src/test/kotlin/net/corda/behave/service/SqlServerServiceTests.kt @@ -2,10 +2,12 @@ package net.corda.behave.service import net.corda.behave.service.database.SqlServerService import org.assertj.core.api.Assertions.assertThat +import org.junit.Ignore import org.junit.Test class SqlServerServiceTests { + @Ignore @Test fun `sql server can be started and stopped`() { val service = SqlServerService("test-mssql", 12345, "S0meS3cretW0rd") From c2503921ad063adc594618179e559a1f1a108430 Mon Sep 17 00:00:00 2001 From: Tommy Lillehagen Date: Mon, 12 Feb 2018 10:07:27 +0000 Subject: [PATCH 05/50] Update instructions --- experimental/behave/README.md | 18 ++++++++++++++---- experimental/behave/prepare.sh | 2 +- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/experimental/behave/README.md b/experimental/behave/README.md index 33c5517cf7..0e10434285 100644 --- a/experimental/behave/README.md +++ b/experimental/behave/README.md @@ -26,11 +26,21 @@ The project is split into three pieces: # Setup -To get started, please run the following command: +To get started, please follow the instructions below: -```bash -$ ./prepare.sh -``` + * Go up to the root directory and build the capsule JAR. + + ```bash + $ cd ../../ + $ ./gradlew install + ``` + + * Come back to this folder and run: + + ```bash + $ cd experimental/behave + $ ./prepare.sh + ``` This script will download necessary database drivers and set up the dependencies directory with copies of the Corda fat-JAR and diff --git a/experimental/behave/prepare.sh b/experimental/behave/prepare.sh index 62af3afb91..3912cf11be 100755 --- a/experimental/behave/prepare.sh +++ b/experimental/behave/prepare.sh @@ -7,7 +7,7 @@ mkdir -p deps/corda/${VERSION}/apps mkdir -p deps/drivers # Copy Corda capsule into deps -cp ../../node/capsule/build/libs/corda-*.jar deps/corda/${VERSION}/corda.jar +cp $(ls ../../node/capsule/build/libs/corda-*.jar | tail -n1) deps/corda/${VERSION}/corda.jar # Download database drivers curl "https://search.maven.org/remotecontent?filepath=com/h2database/h2/1.4.196/h2-1.4.196.jar" > deps/drivers/h2-1.4.196.jar From cca71e3d6eae39664f649cbe13a49fe9dd4953a0 Mon Sep 17 00:00:00 2001 From: Tommy Lillehagen Date: Mon, 12 Feb 2018 10:18:18 +0000 Subject: [PATCH 06/50] Make RPC scaffolding available for Node --- .../main/kotlin/net/corda/behave/node/Node.kt | 22 ++++++++++++++++ .../corda/behave/scenarios/ScenarioState.kt | 26 ++++--------------- 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/node/Node.kt b/experimental/behave/src/main/kotlin/net/corda/behave/node/Node.kt index e5c9f69bb2..ed6b9f097a 100644 --- a/experimental/behave/src/main/kotlin/net/corda/behave/node/Node.kt +++ b/experimental/behave/src/main/kotlin/net/corda/behave/node/Node.kt @@ -9,10 +9,15 @@ import net.corda.behave.logging.getLogger import net.corda.behave.monitoring.PatternWatch import net.corda.behave.node.configuration.* import net.corda.behave.process.JarCommand +import net.corda.behave.seconds import net.corda.behave.service.Service import net.corda.behave.service.ServiceSettings import net.corda.behave.ssh.MonitoringSSHClient import net.corda.behave.ssh.SSHClient +import net.corda.client.rpc.CordaRPCClient +import net.corda.client.rpc.CordaRPCClientConfiguration +import net.corda.core.messaging.CordaRPCOps +import net.corda.core.utilities.NetworkHostAndPort import org.apache.commons.io.FileUtils import java.io.File import java.time.Duration @@ -146,6 +151,23 @@ class Node( }).start() } + fun rpc(action: (CordaRPCOps) -> T): T { + var result: T? = null + val user = config.users.first() + val address = config.nodeInterface + val targetHost = NetworkHostAndPort(address.host, address.rpcPort) + val config = CordaRPCClientConfiguration( + connectionMaxRetryInterval = 10.seconds + ) + log.info("Establishing RPC connection to ${targetHost.host} on port ${targetHost.port} ...") + CordaRPCClient(targetHost, config).use(user.username, user.password) { + log.info("RPC connection to ${targetHost.host}:${targetHost.port} established") + val client = it.proxy + result = action(client) + } + return result ?: error("Failed to run RPC action") + } + override fun toString(): String { return "Node(name = ${config.name}, version = ${config.distribution.version})" } diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioState.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioState.kt index 02e2af9c22..f6cfb32298 100644 --- a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioState.kt +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/ScenarioState.kt @@ -3,11 +3,7 @@ package net.corda.behave.scenarios import net.corda.behave.logging.getLogger import net.corda.behave.network.Network import net.corda.behave.node.Node -import net.corda.behave.seconds -import net.corda.client.rpc.CordaRPCClient -import net.corda.client.rpc.CordaRPCClientConfiguration import net.corda.core.messaging.CordaRPCOps -import net.corda.core.utilities.NetworkHostAndPort import org.assertj.core.api.Assertions.assertThat class ScenarioState { @@ -54,29 +50,17 @@ class ScenarioState { assertThat(network?.waitUntilRunning()).isTrue() } - fun withNetwork(action: ScenarioState.() -> Unit) { + inline fun withNetwork(action: ScenarioState.() -> T): T { ensureNetworkIsRunning() - action() + return action() } - fun withClient(nodeName: String, action: (CordaRPCOps) -> T): T { - var result: T? = null + inline fun withClient(nodeName: String, crossinline action: (CordaRPCOps) -> T): T { withNetwork { - val node = node(nodeName) - val user = node.config.users.first() - val address = node.config.nodeInterface - val targetHost = NetworkHostAndPort(address.host, address.rpcPort) - val config = CordaRPCClientConfiguration( - connectionMaxRetryInterval = 10.seconds - ) - log.info("Establishing RPC connection to ${targetHost.host} on port ${targetHost.port} ...") - CordaRPCClient(targetHost, config).use(user.username, user.password) { - log.info("RPC connection to ${targetHost.host}:${targetHost.port} established") - val client = it.proxy - result = action(client) + return node(nodeName).rpc { + action(it) } } - return result ?: error("Failed to run RPC action") } fun stopNetwork() { From 74e65f392f3e3b5b85761aac8d62f152ad827f74 Mon Sep 17 00:00:00 2001 From: Tommy Lillehagen Date: Mon, 12 Feb 2018 10:36:44 +0000 Subject: [PATCH 07/50] Fix prepare script and cash config check --- experimental/behave/prepare.sh | 6 +++--- .../kotlin/net/corda/behave/scenarios/helpers/Cash.kt | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/experimental/behave/prepare.sh b/experimental/behave/prepare.sh index 3912cf11be..02c5df162b 100755 --- a/experimental/behave/prepare.sh +++ b/experimental/behave/prepare.sh @@ -7,7 +7,7 @@ mkdir -p deps/corda/${VERSION}/apps mkdir -p deps/drivers # Copy Corda capsule into deps -cp $(ls ../../node/capsule/build/libs/corda-*.jar | tail -n1) deps/corda/${VERSION}/corda.jar +cp -v $(ls ../../node/capsule/build/libs/corda-*.jar | tail -n1) deps/corda/${VERSION}/corda.jar # Download database drivers curl "https://search.maven.org/remotecontent?filepath=com/h2database/h2/1.4.196/h2-1.4.196.jar" > deps/drivers/h2-1.4.196.jar @@ -20,5 +20,5 @@ cd ../.. # Copy build artefacts into deps cd experimental/behave -cp ../../tools/bootstrapper/build/libs/*.jar deps/corda/${VERSION}/network-bootstrapper.jar -cp ../../finance/build/libs/corda-finance-*.jar deps/corda/${VERSION}/apps/corda-finance.jar +cp -v $(ls ../../tools/bootstrapper/build/libs/*.jar | tail -n1) deps/corda/${VERSION}/network-bootstrapper.jar +cp -v $(ls ../../finance/build/libs/corda-finance-*.jar | tail -n1) deps/corda/${VERSION}/apps/corda-finance.jar diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Cash.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Cash.kt index f119e5ebe0..0b59771164 100644 --- a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Cash.kt +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Cash.kt @@ -21,8 +21,9 @@ class Cash(state: ScenarioState) : Substeps(state) { log.info("Can issue $issuableCurrency") } return@withClient config.issuableCurrencies.size - } catch (_: Exception) { - return@withClient 0 + } catch (ex: Exception) { + log.warn("Failed to retrieve cash configuration data", ex) + throw ex } } } From 407885c93b66b426838384fc71eaaba5dbf6263e Mon Sep 17 00:00:00 2001 From: Tommy Lillehagen Date: Mon, 12 Feb 2018 10:43:54 +0000 Subject: [PATCH 08/50] Enable selective runs --- experimental/behave/README.md | 11 +++++++++++ experimental/behave/build.gradle | 5 +++++ experimental/behave/src/scenario/kotlin/Scenarios.kt | 1 - .../resources/features/cash/currencies.feature | 1 + .../resources/features/database/connection.feature | 1 + .../resources/features/startup/logging.feature | 12 ++++++++++-- .../kotlin/net/corda/behave/process/CommandTests.kt | 2 +- 7 files changed, 29 insertions(+), 4 deletions(-) diff --git a/experimental/behave/README.md b/experimental/behave/README.md index 0e10434285..b3b37ca2e9 100644 --- a/experimental/behave/README.md +++ b/experimental/behave/README.md @@ -45,3 +45,14 @@ To get started, please follow the instructions below: This script will download necessary database drivers and set up the dependencies directory with copies of the Corda fat-JAR and the network bootstrapping tool. + +# Selective Runs + +If you only want to run tests of a specific tag, you can append +the following parameter to the Gradle command: + +```bash +$ ../../gradlew scenario -Ptags="@cash" +# or +$ ../../gradlew scenario -Ptags="@cash,@logging" +``` \ No newline at end of file diff --git a/experimental/behave/build.gradle b/experimental/behave/build.gradle index 569a5deb57..4148d9d462 100644 --- a/experimental/behave/build.gradle +++ b/experimental/behave/build.gradle @@ -108,6 +108,11 @@ task scenarios(type: Test) { setTestClassesDirs sourceSets.scenario.output.getClassesDirs() classpath = sourceSets.scenario.runtimeClasspath outputs.upToDateWhen { false } + + if (project.hasProperty("tags")) { + systemProperty "cucumber.options", "--tags $tags" + logger.warn("Only running tests tagged with: $tags ...") + } } //scenarios.mustRunAfter test diff --git a/experimental/behave/src/scenario/kotlin/Scenarios.kt b/experimental/behave/src/scenario/kotlin/Scenarios.kt index b0c96a98ee..ac65923a00 100644 --- a/experimental/behave/src/scenario/kotlin/Scenarios.kt +++ b/experimental/behave/src/scenario/kotlin/Scenarios.kt @@ -4,7 +4,6 @@ import org.junit.runner.RunWith @RunWith(Cucumber::class) @CucumberOptions( - features = arrayOf("src/scenario/resources/features"), glue = arrayOf("net.corda.behave.scenarios"), plugin = arrayOf("pretty") ) diff --git a/experimental/behave/src/scenario/resources/features/cash/currencies.feature b/experimental/behave/src/scenario/resources/features/cash/currencies.feature index b90e7fa8a1..3085ced84e 100644 --- a/experimental/behave/src/scenario/resources/features/cash/currencies.feature +++ b/experimental/behave/src/scenario/resources/features/cash/currencies.feature @@ -1,3 +1,4 @@ +@cash @issuance Feature: Cash - Issuable Currencies To have cash on ledger, certain nodes must have the ability to issue cash of various currencies. diff --git a/experimental/behave/src/scenario/resources/features/database/connection.feature b/experimental/behave/src/scenario/resources/features/database/connection.feature index 47137b8aea..9be4fc7d2d 100644 --- a/experimental/behave/src/scenario/resources/features/database/connection.feature +++ b/experimental/behave/src/scenario/resources/features/database/connection.feature @@ -1,3 +1,4 @@ +@database @connectivity Feature: Database - Connection For Corda to work, a database must be running and appropriately configured. diff --git a/experimental/behave/src/scenario/resources/features/startup/logging.feature b/experimental/behave/src/scenario/resources/features/startup/logging.feature index 8cbf35eda1..20531237c6 100644 --- a/experimental/behave/src/scenario/resources/features/startup/logging.feature +++ b/experimental/behave/src/scenario/resources/features/startup/logging.feature @@ -1,3 +1,4 @@ +@logging @startup Feature: Startup Information - Logging A Corda node should inform the user of important parameters during startup so that he/she can confirm the setup and configure / connect relevant software to said node. @@ -7,7 +8,14 @@ Feature: Startup Information - Logging And node A uses database of type H2 And node A is located in London, GB When the network is ready + Then user can retrieve logging information for node A + + Scenario: Node shows database details on startup + Given a node A of version MASTER + When the network is ready + Then user can retrieve database details for node A + + Scenario: Node shows version information on startup + Given a node A of version MASTER Then node A is on platform version 2 And node A is on version 3.0-SNAPSHOT - And user can retrieve logging information for node A - And user can retrieve database details for node A diff --git a/experimental/behave/src/test/kotlin/net/corda/behave/process/CommandTests.kt b/experimental/behave/src/test/kotlin/net/corda/behave/process/CommandTests.kt index a6a6b121a7..4395ddb83a 100644 --- a/experimental/behave/src/test/kotlin/net/corda/behave/process/CommandTests.kt +++ b/experimental/behave/src/test/kotlin/net/corda/behave/process/CommandTests.kt @@ -14,7 +14,7 @@ class CommandTests { @Test fun `failed command returns non-zero`() { - val exitCode = Command(listOf("some-random-command-that-does-not-exist")).run() + val exitCode = Command(listOf("ls", "some-weird-path-that-does-not-exist")).run() assertThat(exitCode).isNotEqualTo(0) } From ad1be79900d60f9fa93d9a4e37141e325440d6ad Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Mon, 12 Feb 2018 17:02:34 +0000 Subject: [PATCH 09/50] Reorders params to put non-default args first. Creates simpler default constructors. --- .../core/crypto/PartialMerkleTreeTest.kt | 16 +-- .../core/flows/CollectSignaturesFlowTests.kt | 2 +- .../TransactionSerializationTests.kt | 6 +- .../LedgerTransactionQueryTests.kt | 7 +- .../TransactionEncumbranceTests.kt | 10 +- .../docs/tutorial/testdsl/TutorialTestDSL.kt | 4 +- .../corda/finance/contracts/universal/Cap.kt | 22 ++-- .../contracts/asset/CashTestsJava.java | 2 +- .../contracts/asset/ObligationTests.kt | 8 +- .../ContractAttachmentSerializerTest.kt | 2 +- .../services/vault/VaultQueryJavaTests.java | 2 +- .../node/messaging/TwoPartyTradeFlowTests.kt | 10 +- .../corda/node/services/keys/KMSUtilsTests.kt | 2 +- .../persistence/HibernateConfigurationTest.kt | 10 +- .../services/vault/NodeVaultServiceTest.kt | 12 +- .../node/services/vault/VaultQueryTests.kt | 8 +- .../node/services/vault/VaultWithCashTest.kt | 6 +- .../corda/irs/api/NodeInterestRatesTest.kt | 2 +- .../kotlin/net/corda/irs/contract/IRSTests.kt | 107 +++++++++--------- .../traderdemo/TransactionGraphSearchTests.kt | 4 +- .../net/corda/testing/node/MockServices.kt | 28 +++-- .../net/corda/loadtest/tests/NotaryTest.kt | 2 +- 22 files changed, 144 insertions(+), 128 deletions(-) diff --git a/core/src/test/kotlin/net/corda/core/crypto/PartialMerkleTreeTest.kt b/core/src/test/kotlin/net/corda/core/crypto/PartialMerkleTreeTest.kt index 725dd34c2b..88df92acf3 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/PartialMerkleTreeTest.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/PartialMerkleTreeTest.kt @@ -58,19 +58,19 @@ class PartialMerkleTreeTest { hashed = nodes.map { it.serialize().sha256() } expectedRoot = MerkleTree.getMerkleTree(hashed.toMutableList() + listOf(zeroHash, zeroHash)).hash merkleTree = MerkleTree.getMerkleTree(hashed) - testLedger = MockServices(emptyList(), rigorousMock().also { + testLedger = MockServices(emptyList(), MEGA_CORP.name, rigorousMock().also { doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY) - }, MEGA_CORP.name).ledger(DUMMY_NOTARY) { + }).ledger(DUMMY_NOTARY) { unverifiedTransaction { attachments(Cash.PROGRAM_ID) output(Cash.PROGRAM_ID, "MEGA_CORP cash", - Cash.State( - amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1), - owner = MEGA_CORP)) + Cash.State( + amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1), + owner = MEGA_CORP)) output(Cash.PROGRAM_ID, "dummy cash 1", - Cash.State( - amount = 900.DOLLARS `issued by` MEGA_CORP.ref(1, 1), - owner = MINI_CORP)) + Cash.State( + amount = 900.DOLLARS `issued by` MEGA_CORP.ref(1, 1), + owner = MINI_CORP)) } transaction { attachments(Cash.PROGRAM_ID) diff --git a/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt index 2dfbb59352..1586c9aa07 100644 --- a/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt @@ -136,7 +136,7 @@ class CollectSignaturesFlowTests { @Test fun `fails when not signed by initiator`() { val onePartyDummyContract = DummyContract.generateInitial(1337, notary, alice.ref(1)) - val miniCorpServices = MockServices(listOf("net.corda.testing.contracts"), rigorousMock(), miniCorp) + val miniCorpServices = MockServices(listOf("net.corda.testing.contracts"), miniCorp, rigorousMock()) val ptx = miniCorpServices.signInitialTransaction(onePartyDummyContract) val flow = aliceNode.services.startFlow(CollectSignaturesFlow(ptx, emptySet())) mockNet.runNetwork() diff --git a/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt b/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt index 1d326554e3..d7894ed1ee 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt @@ -62,8 +62,8 @@ class TransactionSerializationTests { val inputState = StateAndRef(TransactionState(TestCash.State(depositRef, 100.POUNDS, MEGA_CORP), TEST_CASH_PROGRAM_ID, DUMMY_NOTARY), fakeStateRef) val outputState = TransactionState(TestCash.State(depositRef, 600.POUNDS, MEGA_CORP), TEST_CASH_PROGRAM_ID, DUMMY_NOTARY) val changeState = TransactionState(TestCash.State(depositRef, 400.POUNDS, MEGA_CORP), TEST_CASH_PROGRAM_ID, DUMMY_NOTARY) - val megaCorpServices = MockServices(listOf("net.corda.core.serialization"), rigorousMock(), MEGA_CORP.name, MEGA_CORP_KEY) - val notaryServices = MockServices(listOf("net.corda.core.serialization"), rigorousMock(), DUMMY_NOTARY.name, DUMMY_NOTARY_KEY) + val megaCorpServices = MockServices(listOf("net.corda.core.serialization"), MEGA_CORP.name, rigorousMock(), MEGA_CORP_KEY) + val notaryServices = MockServices(listOf("net.corda.core.serialization"), DUMMY_NOTARY.name, rigorousMock(), DUMMY_NOTARY_KEY) lateinit var tx: TransactionBuilder @Before @@ -107,7 +107,7 @@ class TransactionSerializationTests { Command(TestCash.Commands.Move(), DUMMY_KEY_2.public)) val ptx2 = notaryServices.signInitialTransaction(tx2) - val dummyServices = MockServices(emptyList(), rigorousMock(), MEGA_CORP.name, DUMMY_KEY_2) + val dummyServices = MockServices(emptyList(), MEGA_CORP.name, rigorousMock(), DUMMY_KEY_2) val stx2 = dummyServices.addSignature(ptx2) stx.copy(sigs = stx2.sigs).verifyRequiredSignatures() diff --git a/core/src/test/kotlin/net/corda/core/transactions/LedgerTransactionQueryTests.kt b/core/src/test/kotlin/net/corda/core/transactions/LedgerTransactionQueryTests.kt index 973752a197..fc87fd23ea 100644 --- a/core/src/test/kotlin/net/corda/core/transactions/LedgerTransactionQueryTests.kt +++ b/core/src/test/kotlin/net/corda/core/transactions/LedgerTransactionQueryTests.kt @@ -29,9 +29,10 @@ class LedgerTransactionQueryTests { @JvmField val testSerialization = SerializationEnvironmentRule() private val keyPair = generateKeyPair() - private val services = MockServices(emptyList(), rigorousMock().also { - doReturn(null).whenever(it).partyFromKey(keyPair.public) - }, CordaX500Name("MegaCorp", "London", "GB"), keyPair) + private val services = MockServices(emptyList(), CordaX500Name("MegaCorp", "London", "GB"), + rigorousMock().also { + doReturn(null).whenever(it).partyFromKey(keyPair.public) + }, keyPair) private val identity: Party = services.myInfo.singleIdentity() @Before diff --git a/core/src/test/kotlin/net/corda/core/transactions/TransactionEncumbranceTests.kt b/core/src/test/kotlin/net/corda/core/transactions/TransactionEncumbranceTests.kt index 1a3878b586..6b7e98617f 100644 --- a/core/src/test/kotlin/net/corda/core/transactions/TransactionEncumbranceTests.kt +++ b/core/src/test/kotlin/net/corda/core/transactions/TransactionEncumbranceTests.kt @@ -51,7 +51,8 @@ class TransactionEncumbranceTests { class DummyTimeLock : Contract { override fun verify(tx: LedgerTransaction) { val timeLockInput = tx.inputsOfType().singleOrNull() ?: return - val time = tx.timeWindow?.untilTime ?: throw IllegalArgumentException("Transactions containing time-locks must have a time-window") + val time = tx.timeWindow?.untilTime + ?: throw IllegalArgumentException("Transactions containing time-locks must have a time-window") requireThat { "the time specified in the time-lock has passed" using (time >= timeLockInput.validFrom) } @@ -64,9 +65,10 @@ class TransactionEncumbranceTests { } } - private val ledgerServices = MockServices(emptyList(), rigorousMock().also { - doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY) - }, MEGA_CORP.name) + private val ledgerServices = MockServices(emptyList(), MEGA_CORP.name, + rigorousMock().also { + doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY) + }) @Test fun `state can be encumbered`() { diff --git a/docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt index ef7e62920b..3b3d5c003a 100644 --- a/docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt +++ b/docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt @@ -38,11 +38,11 @@ class CommercialPaperTest { @Rule @JvmField val testSerialization = SerializationEnvironmentRule() - private val ledgerServices = MockServices(emptyList(), rigorousMock().also { + private val ledgerServices = MockServices(emptyList(), MEGA_CORP.name, rigorousMock().also { doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY) doReturn(null).whenever(it).partyFromKey(BIG_CORP_PUBKEY) doReturn(null).whenever(it).partyFromKey(ALICE_PUBKEY) - }, MEGA_CORP.name) + }) // DOCSTART 1 fun getPaper(): ICommercialPaperState = CommercialPaper.State( diff --git a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Cap.kt b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Cap.kt index 55e5b7fa3a..b276650b81 100644 --- a/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Cap.kt +++ b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Cap.kt @@ -25,11 +25,12 @@ import java.time.LocalDate internal val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party fun transaction(script: TransactionDSL.() -> EnforceVerifyOrFail) = run { - MockServices(listOf("net.corda.finance.contracts.universal"), rigorousMock().also { - listOf(acmeCorp, highStreetBank, momAndPop).forEach { party -> - doReturn(null).whenever(it).partyFromKey(party.owningKey) - } - }, CordaX500Name("MegaCorp", "London", "GB")).transaction(DUMMY_NOTARY, script) + MockServices(listOf("net.corda.finance.contracts.universal"), CordaX500Name("MegaCorp", "London", "GB"), + rigorousMock().also { + listOf(acmeCorp, highStreetBank, momAndPop).forEach { party -> + doReturn(null).whenever(it).partyFromKey(party.owningKey) + } + }).transaction(DUMMY_NOTARY, script) } class Cap { @@ -312,14 +313,15 @@ class Cap { } } - @Test @Ignore + @Test + @Ignore fun `pretty print`() { - println ( prettyPrint(contractInitial) ) + println(prettyPrint(contractInitial)) - println ( prettyPrint(contractAfterFixingFirst) ) + println(prettyPrint(contractAfterFixingFirst)) - println ( prettyPrint(contractAfterExecutionFirst) ) + println(prettyPrint(contractAfterExecutionFirst)) - println ( prettyPrint(contractAfterFixingFinal) ) + println(prettyPrint(contractAfterFixingFinal)) } } diff --git a/finance/src/test/java/net/corda/finance/contracts/asset/CashTestsJava.java b/finance/src/test/java/net/corda/finance/contracts/asset/CashTestsJava.java index 034404e184..3e28b1aee1 100644 --- a/finance/src/test/java/net/corda/finance/contracts/asset/CashTestsJava.java +++ b/finance/src/test/java/net/corda/finance/contracts/asset/CashTestsJava.java @@ -38,7 +38,7 @@ public class CashTestsJava { IdentityServiceInternal identityService = rigorousMock(IdentityServiceInternal.class); doReturn(MEGA_CORP.getParty()).when(identityService).partyFromKey(MEGA_CORP.getPublicKey()); doReturn(MINI_CORP.getParty()).when(identityService).partyFromKey(MINI_CORP.getPublicKey()); - transaction(new MockServices(emptyList(), identityService, MEGA_CORP.getName()), DUMMY_NOTARY, tx -> { + transaction(new MockServices(emptyList(), MEGA_CORP.getName(), identityService), DUMMY_NOTARY, tx -> { tx.attachment(Cash.PROGRAM_ID); tx.input(Cash.PROGRAM_ID, inState); diff --git a/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt b/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt index 35a0db24c3..87f481396b 100644 --- a/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt @@ -78,8 +78,8 @@ class ObligationTests { beneficiary = CHARLIE ) private val outState = inState.copy(beneficiary = AnonymousParty(BOB_PUBKEY)) - private val miniCorpServices = MockServices(listOf("net.corda.finance.contracts.asset"), rigorousMock(), miniCorp) - private val notaryServices = MockServices(emptyList(), rigorousMock(), MEGA_CORP.name, dummyNotary.keyPair) + private val miniCorpServices = MockServices(listOf("net.corda.finance.contracts.asset"), miniCorp, rigorousMock()) + private val notaryServices = MockServices(emptyList(), MEGA_CORP.name, rigorousMock(), dummyNotary.keyPair) private val identityService = rigorousMock().also { doReturn(null).whenever(it).partyFromKey(ALICE_PUBKEY) doReturn(null).whenever(it).partyFromKey(BOB_PUBKEY) @@ -87,8 +87,8 @@ class ObligationTests { doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY) doReturn(MINI_CORP).whenever(it).partyFromKey(MINI_CORP_PUBKEY) } - private val mockService = MockServices(listOf("net.corda.finance.contracts.asset"), identityService, MEGA_CORP.name) - private val ledgerServices get() = MockServices(emptyList(), identityService, MEGA_CORP.name) + private val mockService = MockServices(listOf("net.corda.finance.contracts.asset"), MEGA_CORP.name, identityService) + private val ledgerServices get() = MockServices(emptyList(), MEGA_CORP.name, identityService) private fun cashObligationTestRoots( group: LedgerDSL ) = group.apply { diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/ContractAttachmentSerializerTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/ContractAttachmentSerializerTest.kt index e39c2e87a9..01e296324d 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/ContractAttachmentSerializerTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/ContractAttachmentSerializerTest.kt @@ -25,7 +25,7 @@ class ContractAttachmentSerializerTest { private lateinit var factory: SerializationFactory private lateinit var context: SerializationContext private lateinit var contextWithToken: SerializationContext - private val mockServices = MockServices(emptyList(), rigorousMock(), CordaX500Name("MegaCorp", "London", "GB")) + private val mockServices = MockServices(emptyList(), CordaX500Name("MegaCorp", "London", "GB"), rigorousMock()) @Before fun setup() { diff --git a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java index 6df1039035..7cd4ce468e 100644 --- a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java +++ b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java @@ -77,7 +77,7 @@ public class VaultQueryJavaTests { identitySvc, MEGA_CORP, DUMMY_NOTARY.getKeyPair()); - issuerServices = new MockServices(cordappPackages, rigorousMock(IdentityServiceInternal.class), DUMMY_CASH_ISSUER_INFO, BOC.getKeyPair()); + issuerServices = new MockServices(cordappPackages, DUMMY_CASH_ISSUER_INFO, rigorousMock(IdentityServiceInternal.class), BOC.getKeyPair()); database = databaseAndServices.getFirst(); MockServices services = databaseAndServices.getSecond(); vaultFiller = new VaultFiller(services, DUMMY_NOTARY); diff --git a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt index d9d2051e7e..6ffcdcd158 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt @@ -105,7 +105,7 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { // allow interruption half way through. mockNet = InternalMockNetwork(threadPerNode = true, cordappPackages = cordappPackages) val ledgerIdentityService = rigorousMock() - MockServices(cordappPackages, ledgerIdentityService, MEGA_CORP.name).ledger(DUMMY_NOTARY) { + MockServices(cordappPackages, MEGA_CORP.name, ledgerIdentityService).ledger(DUMMY_NOTARY) { val notaryNode = mockNet.defaultNotaryNode val aliceNode = mockNet.createPartyNode(ALICE_NAME) val bobNode = mockNet.createPartyNode(BOB_NAME) @@ -157,7 +157,7 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { fun `trade cash for commercial paper fails using soft locking`() { mockNet = InternalMockNetwork(threadPerNode = true, cordappPackages = cordappPackages) val ledgerIdentityService = rigorousMock() - MockServices(cordappPackages, ledgerIdentityService, MEGA_CORP.name).ledger(DUMMY_NOTARY) { + MockServices(cordappPackages, MEGA_CORP.name, ledgerIdentityService).ledger(DUMMY_NOTARY) { val notaryNode = mockNet.defaultNotaryNode val aliceNode = mockNet.createPartyNode(ALICE_NAME) val bobNode = mockNet.createPartyNode(BOB_NAME) @@ -215,7 +215,7 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { fun `shutdown and restore`() { mockNet = InternalMockNetwork(cordappPackages = cordappPackages) val ledgerIdentityService = rigorousMock() - MockServices(cordappPackages, ledgerIdentityService, MEGA_CORP.name).ledger(DUMMY_NOTARY) { + MockServices(cordappPackages, MEGA_CORP.name, ledgerIdentityService).ledger(DUMMY_NOTARY) { val notaryNode = mockNet.defaultNotaryNode val aliceNode = mockNet.createPartyNode(ALICE_NAME) var bobNode = mockNet.createPartyNode(BOB_NAME) @@ -508,7 +508,7 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { fun `dependency with error on buyer side`() { mockNet = InternalMockNetwork(cordappPackages = cordappPackages) val ledgerIdentityService = rigorousMock() - MockServices(cordappPackages, ledgerIdentityService, MEGA_CORP.name).ledger(DUMMY_NOTARY) { + MockServices(cordappPackages, MEGA_CORP.name, ledgerIdentityService).ledger(DUMMY_NOTARY) { runWithError(ledgerIdentityService, true, false, "at least one cash input") } } @@ -517,7 +517,7 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { fun `dependency with error on seller side`() { mockNet = InternalMockNetwork(cordappPackages = cordappPackages) val ledgerIdentityService = rigorousMock() - MockServices(cordappPackages, ledgerIdentityService, MEGA_CORP.name).ledger(DUMMY_NOTARY) { + MockServices(cordappPackages, MEGA_CORP.name, ledgerIdentityService).ledger(DUMMY_NOTARY) { runWithError(ledgerIdentityService, false, true, "Issuances have a time-window") } } diff --git a/node/src/test/kotlin/net/corda/node/services/keys/KMSUtilsTests.kt b/node/src/test/kotlin/net/corda/node/services/keys/KMSUtilsTests.kt index b76caacba4..deebbf3363 100644 --- a/node/src/test/kotlin/net/corda/node/services/keys/KMSUtilsTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/keys/KMSUtilsTests.kt @@ -19,7 +19,7 @@ class KMSUtilsTests { val alice = getTestPartyAndCertificate(ALICE_NAME, aliceKey.public) val cordappPackages = emptyList() val ledgerIdentityService = makeTestIdentityService(alice) - val mockServices = MockServices(cordappPackages, ledgerIdentityService, alice.name, aliceKey) + val mockServices = MockServices(cordappPackages, alice.name, ledgerIdentityService, aliceKey) val wellKnownIdentity = mockServices.myInfo.singleIdentityAndCert() val confidentialIdentity = mockServices.keyManagementService.freshKeyAndCert(wellKnownIdentity, false) val cert = confidentialIdentity.certificate diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt index dd724c5a6b..6e996a8d94 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt @@ -97,9 +97,9 @@ class HibernateConfigurationTest { @Before fun setUp() { val cordappPackages = listOf("net.corda.testing.internal.vault", "net.corda.finance.contracts.asset") - bankServices = MockServices(cordappPackages, rigorousMock(), BOC.name, BOC_KEY) - issuerServices = MockServices(cordappPackages, rigorousMock(), dummyCashIssuer) - notaryServices = MockServices(cordappPackages, rigorousMock(), dummyNotary) + bankServices = MockServices(cordappPackages, BOC.name, rigorousMock(), BOC_KEY) + issuerServices = MockServices(cordappPackages, dummyCashIssuer, rigorousMock()) + notaryServices = MockServices(cordappPackages, dummyNotary, rigorousMock()) notary = notaryServices.myInfo.singleIdentity() val dataSourceProps = makeTestDataSourceProperties() val identityService = rigorousMock().also { mock -> @@ -115,9 +115,9 @@ class HibernateConfigurationTest { hibernateConfig = database.hibernateConfig // `consumeCash` expects we can self-notarise transactions - services = object : MockServices(cordappPackages, rigorousMock().also { + services = object : MockServices(cordappPackages, BOB_NAME, rigorousMock().also { doNothing().whenever(it).justVerifyAndRegisterIdentity(argThat { name == BOB_NAME }) - }, BOB_NAME, generateKeyPair(), dummyNotary.keyPair) { + }, generateKeyPair(), dummyNotary.keyPair) { override val vaultService = NodeVaultService(Clock.systemUTC(), keyManagementService, validatedTransactions, hibernateConfig) override fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable) { for (stx in txs) { diff --git a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt index b7ecaa8fbf..d9aa1248af 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt @@ -96,8 +96,8 @@ class NodeVaultServiceTest { vaultFiller = VaultFiller(services, dummyNotary) // This is safe because MockServices only ever have a single identity identity = services.myInfo.singleIdentityAndCert() - issuerServices = MockServices(cordappPackages, rigorousMock(), dummyCashIssuer) - bocServices = MockServices(cordappPackages, rigorousMock(), bankOfCorda) + issuerServices = MockServices(cordappPackages, dummyCashIssuer, rigorousMock()) + bocServices = MockServices(cordappPackages, bankOfCorda, rigorousMock()) services.identityService.verifyAndRegisterIdentity(DUMMY_CASH_ISSUER_IDENTITY) services.identityService.verifyAndRegisterIdentity(BOC_IDENTITY) } @@ -137,7 +137,7 @@ class NodeVaultServiceTest { assertThat(w1).hasSize(3) val originalVault = vaultService - val services2 = object : MockServices(emptyList(), rigorousMock(), MEGA_CORP.name) { + val services2 = object : MockServices(emptyList(), MEGA_CORP.name, rigorousMock()) { override val vaultService: NodeVaultService get() = originalVault override fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable) { for (stx in txs) { @@ -480,7 +480,7 @@ class NodeVaultServiceTest { @Test fun addNoteToTransaction() { - val megaCorpServices = MockServices(cordappPackages, rigorousMock(), MEGA_CORP.name, MEGA_CORP_KEY) + val megaCorpServices = MockServices(cordappPackages, MEGA_CORP.name, rigorousMock(), MEGA_CORP_KEY) database.transaction { val freshKey = identity.owningKey @@ -587,9 +587,9 @@ class NodeVaultServiceTest { val identity = services.myInfo.singleIdentityAndCert() assertEquals(services.identityService.partyFromKey(identity.owningKey), identity.party) val anonymousIdentity = services.keyManagementService.freshKeyAndCert(identity, false) - val thirdPartyServices = MockServices(emptyList(), rigorousMock().also { + val thirdPartyServices = MockServices(emptyList(), MEGA_CORP.name, rigorousMock().also { doNothing().whenever(it).justVerifyAndRegisterIdentity(argThat { name == MEGA_CORP.name }) - }, MEGA_CORP.name) + }) val thirdPartyIdentity = thirdPartyServices.keyManagementService.freshKeyAndCert(thirdPartyServices.myInfo.singleIdentityAndCert(), false) val amount = Amount(1000, Issued(BOC.ref(1), GBP)) diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt index db72541f8b..c75bf3f447 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt @@ -115,7 +115,7 @@ class VaultQueryTests { services = databaseAndServices.second vaultFiller = VaultFiller(services, dummyNotary) vaultFillerCashNotary = VaultFiller(services, dummyNotary, CASH_NOTARY) - notaryServices = MockServices(cordappPackages, rigorousMock(), dummyNotary, dummyCashIssuer.keyPair, BOC_KEY, MEGA_CORP_KEY) + notaryServices = MockServices(cordappPackages, dummyNotary, rigorousMock(), dummyCashIssuer.keyPair, BOC_KEY, MEGA_CORP_KEY) identitySvc = services.identityService // Register all of the identities we're going to use (notaryServices.myInfo.legalIdentitiesAndCerts + BOC_IDENTITY + CASH_NOTARY_IDENTITY + MINI_CORP_IDENTITY + MEGA_CORP_IDENTITY).forEach { identity -> @@ -1344,15 +1344,15 @@ class VaultQueryTests { fun `unconsumed fungible assets for selected issuer parties`() { // GBP issuer val gbpCashIssuerName = CordaX500Name(organisation = "British Pounds Cash Issuer", locality = "London", country = "GB") - val gbpCashIssuerServices = MockServices(cordappPackages, rigorousMock(), gbpCashIssuerName, generateKeyPair()) + val gbpCashIssuerServices = MockServices(cordappPackages, gbpCashIssuerName, rigorousMock(), generateKeyPair()) val gbpCashIssuer = gbpCashIssuerServices.myInfo.singleIdentityAndCert() // USD issuer val usdCashIssuerName = CordaX500Name(organisation = "US Dollars Cash Issuer", locality = "New York", country = "US") - val usdCashIssuerServices = MockServices(cordappPackages, rigorousMock(), usdCashIssuerName, generateKeyPair()) + val usdCashIssuerServices = MockServices(cordappPackages, usdCashIssuerName, rigorousMock(), generateKeyPair()) val usdCashIssuer = usdCashIssuerServices.myInfo.singleIdentityAndCert() // CHF issuer val chfCashIssuerName = CordaX500Name(organisation = "Swiss Francs Cash Issuer", locality = "Zurich", country = "CH") - val chfCashIssuerServices = MockServices(cordappPackages, rigorousMock(), chfCashIssuerName, generateKeyPair()) + val chfCashIssuerServices = MockServices(cordappPackages, chfCashIssuerName, rigorousMock(), generateKeyPair()) val chfCashIssuer = chfCashIssuerServices.myInfo.singleIdentityAndCert() listOf(gbpCashIssuer, usdCashIssuer, chfCashIssuer).forEach { identity -> services.identityService.verifyAndRegisterIdentity(identity) diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt index 19772b9193..1dcb7cb984 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt @@ -78,8 +78,8 @@ class VaultWithCashTest { database = databaseAndServices.first services = databaseAndServices.second vaultFiller = VaultFiller(services, dummyNotary) - issuerServices = MockServices(cordappPackages, rigorousMock(), dummyCashIssuer, MEGA_CORP_KEY) - notaryServices = MockServices(cordappPackages, rigorousMock(), dummyNotary) + issuerServices = MockServices(cordappPackages, dummyCashIssuer, rigorousMock(), MEGA_CORP_KEY) + notaryServices = MockServices(cordappPackages, dummyNotary, rigorousMock()) notary = notaryServices.myInfo.legalIdentitiesAndCerts.single().party } @@ -109,7 +109,7 @@ class VaultWithCashTest { @Test fun `issue and spend total correctly and irrelevant ignored`() { - val megaCorpServices = MockServices(cordappPackages, rigorousMock(), MEGA_CORP.name, MEGA_CORP_KEY) + val megaCorpServices = MockServices(cordappPackages, MEGA_CORP.name, rigorousMock(), MEGA_CORP_KEY) val freshKey = services.keyManagementService.freshKey() val usefulTX = diff --git a/samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt b/samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt index bc35a1e7ad..81bc7e254a 100644 --- a/samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt +++ b/samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt @@ -56,7 +56,7 @@ class NodeInterestRatesTest { EURIBOR 2016-03-15 2M = 0.111 """.trimIndent()) private val dummyCashIssuer = TestIdentity(CordaX500Name("Cash issuer", "London", "GB")) - private val services = MockServices(listOf("net.corda.finance.contracts.asset"), rigorousMock(), dummyCashIssuer, MEGA_CORP_KEY) + private val services = MockServices(listOf("net.corda.finance.contracts.asset"), dummyCashIssuer, rigorousMock(), MEGA_CORP_KEY) // This is safe because MockServices only ever have a single identity private val identity = services.myInfo.singleIdentity() diff --git a/samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/contract/IRSTests.kt b/samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/contract/IRSTests.kt index 7234ee5d72..9dd8a7debf 100644 --- a/samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/contract/IRSTests.kt +++ b/samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/contract/IRSTests.kt @@ -237,14 +237,14 @@ class IRSTests { @Rule @JvmField val testSerialization = SerializationEnvironmentRule() - private val megaCorpServices = MockServices(listOf("net.corda.irs.contract"), rigorousMock(), MEGA_CORP.name, MEGA_CORP_KEY) - private val miniCorpServices = MockServices(listOf("net.corda.irs.contract"), rigorousMock(), MINI_CORP.name, MINI_CORP_KEY) - private val notaryServices = MockServices(listOf("net.corda.irs.contract"), rigorousMock(), DUMMY_NOTARY.name, DUMMY_NOTARY_KEY) + private val megaCorpServices = MockServices(listOf("net.corda.irs.contract"), MEGA_CORP.name, rigorousMock(), MEGA_CORP_KEY) + private val miniCorpServices = MockServices(listOf("net.corda.irs.contract"), MINI_CORP.name, rigorousMock(), MINI_CORP_KEY) + private val notaryServices = MockServices(listOf("net.corda.irs.contract"), DUMMY_NOTARY.name, rigorousMock(), DUMMY_NOTARY_KEY) private val ledgerServices - get() = MockServices(emptyList(), rigorousMock().also { + get() = MockServices(emptyList(), MEGA_CORP.name, rigorousMock().also { doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY) doReturn(null).whenever(it).partyFromKey(ORACLE_PUBKEY) - }, MEGA_CORP.name) + }) @Test fun ok() { @@ -341,11 +341,12 @@ class IRSTests { */ @Test fun generateIRSandFixSome() { - val services = MockServices(listOf("net.corda.irs.contract"), rigorousMock().also { - listOf(MEGA_CORP, MINI_CORP).forEach { party -> - doReturn(party).whenever(it).partyFromKey(party.owningKey) - } - }, MEGA_CORP.name) + val services = MockServices(listOf("net.corda.irs.contract"), MEGA_CORP.name, + rigorousMock().also { + listOf(MEGA_CORP, MINI_CORP).forEach { party -> + doReturn(party).whenever(it).partyFromKey(party.owningKey) + } + }) var previousTXN = generateIRSTxn(1) previousTXN.toLedgerTransaction(services).verify() services.recordTransactions(previousTXN) @@ -429,13 +430,13 @@ class IRSTests { input("irs post agreement") val postAgreement = "irs post agreement".output() output(IRS_PROGRAM_ID, "irs post first fixing", - postAgreement.copy( - postAgreement.fixedLeg, - postAgreement.floatingLeg, - postAgreement.calculation.applyFixing(ld, FixedRate(RatioUnit(bd))), - postAgreement.common)) + postAgreement.copy( + postAgreement.fixedLeg, + postAgreement.floatingLeg, + postAgreement.calculation.applyFixing(ld, FixedRate(RatioUnit(bd))), + postAgreement.common)) command(ORACLE_PUBKEY, - InterestRateSwap.Commands.Refix(Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd))) + InterestRateSwap.Commands.Refix(Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd))) timeWindow(TEST_TX_TIME) this.verifies() } @@ -620,7 +621,7 @@ class IRSTests { // Templated tweak for reference. A corrent fixing applied should be ok tweak { command(ORACLE_PUBKEY, - InterestRateSwap.Commands.Refix(Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd))) + InterestRateSwap.Commands.Refix(Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd))) timeWindow(TEST_TX_TIME) output(IRS_PROGRAM_ID, newIRS) this.verifies() @@ -643,12 +644,12 @@ class IRSTests { val firstResetValue = newIRS.calculation.floatingLegPaymentSchedule[firstResetKey] val modifiedFirstResetValue = firstResetValue!!.copy(notional = Amount(firstResetValue.notional.quantity, Currency.getInstance("JPY"))) output(IRS_PROGRAM_ID, - newIRS.copy( - newIRS.fixedLeg, - newIRS.floatingLeg, - newIRS.calculation.copy(floatingLegPaymentSchedule = newIRS.calculation.floatingLegPaymentSchedule.plus( - Pair(firstResetKey, modifiedFirstResetValue))), - newIRS.common)) + newIRS.copy( + newIRS.fixedLeg, + newIRS.floatingLeg, + newIRS.calculation.copy(floatingLegPaymentSchedule = newIRS.calculation.floatingLegPaymentSchedule.plus( + Pair(firstResetKey, modifiedFirstResetValue))), + newIRS.common)) this `fails with` "There is only one change in the IRS floating leg payment schedule" } @@ -660,12 +661,12 @@ class IRSTests { val latestReset = newIRS.calculation.floatingLegPaymentSchedule.filter { it.value.rate is FixedRate }.maxBy { it.key } val modifiedLatestResetValue = latestReset!!.value.copy(notional = Amount(latestReset.value.notional.quantity, Currency.getInstance("JPY"))) output(IRS_PROGRAM_ID, - newIRS.copy( - newIRS.fixedLeg, - newIRS.floatingLeg, - newIRS.calculation.copy(floatingLegPaymentSchedule = newIRS.calculation.floatingLegPaymentSchedule.plus( - Pair(latestReset.key, modifiedLatestResetValue))), - newIRS.common)) + newIRS.copy( + newIRS.fixedLeg, + newIRS.floatingLeg, + newIRS.calculation.copy(floatingLegPaymentSchedule = newIRS.calculation.floatingLegPaymentSchedule.plus( + Pair(latestReset.key, modifiedLatestResetValue))), + newIRS.common)) this `fails with` "The fix payment has the same currency as the notional" } } @@ -687,11 +688,11 @@ class IRSTests { transaction("Agreement") { attachments(IRS_PROGRAM_ID) output(IRS_PROGRAM_ID, "irs post agreement1", - irs.copy( - irs.fixedLeg, - irs.floatingLeg, - irs.calculation, - irs.common.copy(tradeID = "t1"))) + irs.copy( + irs.fixedLeg, + irs.floatingLeg, + irs.calculation, + irs.common.copy(tradeID = "t1"))) command(MEGA_CORP_PUBKEY, InterestRateSwap.Commands.Agree()) timeWindow(TEST_TX_TIME) this.verifies() @@ -700,12 +701,12 @@ class IRSTests { transaction("Agreement") { attachments(IRS_PROGRAM_ID) output(IRS_PROGRAM_ID, "irs post agreement2", - irs.copy( - linearId = UniqueIdentifier("t2"), - fixedLeg = irs.fixedLeg, - floatingLeg = irs.floatingLeg, - calculation = irs.calculation, - common = irs.common.copy(tradeID = "t2"))) + irs.copy( + linearId = UniqueIdentifier("t2"), + fixedLeg = irs.fixedLeg, + floatingLeg = irs.floatingLeg, + calculation = irs.calculation, + common = irs.common.copy(tradeID = "t2"))) command(MEGA_CORP_PUBKEY, InterestRateSwap.Commands.Agree()) timeWindow(TEST_TX_TIME) this.verifies() @@ -717,25 +718,23 @@ class IRSTests { input("irs post agreement2") val postAgreement1 = "irs post agreement1".output() output(IRS_PROGRAM_ID, "irs post first fixing1", - postAgreement1.copy( - postAgreement1.fixedLeg, - postAgreement1.floatingLeg, - postAgreement1.calculation.applyFixing(ld1, FixedRate(RatioUnit(bd1))), - postAgreement1.common.copy(tradeID = "t1"))) + postAgreement1.copy( + postAgreement1.fixedLeg, + postAgreement1.floatingLeg, + postAgreement1.calculation.applyFixing(ld1, FixedRate(RatioUnit(bd1))), + postAgreement1.common.copy(tradeID = "t1"))) val postAgreement2 = "irs post agreement2".output() output(IRS_PROGRAM_ID, "irs post first fixing2", - postAgreement2.copy( - postAgreement2.fixedLeg, - postAgreement2.floatingLeg, - postAgreement2.calculation.applyFixing(ld1, FixedRate(RatioUnit(bd1))), - postAgreement2.common.copy(tradeID = "t2"))) + postAgreement2.copy( + postAgreement2.fixedLeg, + postAgreement2.floatingLeg, + postAgreement2.calculation.applyFixing(ld1, FixedRate(RatioUnit(bd1))), + postAgreement2.common.copy(tradeID = "t2"))) command(ORACLE_PUBKEY, - InterestRateSwap.Commands.Refix(Fix(FixOf("ICE LIBOR", ld1, Tenor("3M")), bd1))) + InterestRateSwap.Commands.Refix(Fix(FixOf("ICE LIBOR", ld1, Tenor("3M")), bd1))) timeWindow(TEST_TX_TIME) this.verifies() } } } -} - - +} \ No newline at end of file diff --git a/samples/trader-demo/src/test/kotlin/net/corda/traderdemo/TransactionGraphSearchTests.kt b/samples/trader-demo/src/test/kotlin/net/corda/traderdemo/TransactionGraphSearchTests.kt index a7dc44fb28..b501cb7d52 100644 --- a/samples/trader-demo/src/test/kotlin/net/corda/traderdemo/TransactionGraphSearchTests.kt +++ b/samples/trader-demo/src/test/kotlin/net/corda/traderdemo/TransactionGraphSearchTests.kt @@ -46,8 +46,8 @@ class TransactionGraphSearchTests { * @param signer signer for the two transactions and their commands. */ fun buildTransactions(command: CommandData): GraphTransactionStorage { - val megaCorpServices = MockServices(listOf("net.corda.testing.contracts"), rigorousMock(), megaCorp) - val notaryServices = MockServices(listOf("net.corda.testing.contracts"), rigorousMock(), dummyNotary) + val megaCorpServices = MockServices(listOf("net.corda.testing.contracts"), megaCorp, rigorousMock()) + val notaryServices = MockServices(listOf("net.corda.testing.contracts"), dummyNotary, rigorousMock()) val originBuilder = TransactionBuilder(dummyNotary.party) .addOutputState(DummyState(random31BitValue()), DummyContract.PROGRAM_ID) .addCommand(command, megaCorp.publicKey) diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt index 51de7c6549..a86f4e380b 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -136,37 +136,42 @@ open class MockServices private constructor( * (you can get one from [makeTestIdentityService]) and represents the given identity. */ @JvmOverloads - constructor(cordappPackages: List, identityService: IdentityService = makeTestIdentityService(), initialIdentity: TestIdentity, vararg moreKeys: KeyPair) : this(CordappLoader.createWithTestPackages(cordappPackages), identityService, initialIdentity, moreKeys) + constructor(cordappPackages: List, initialIdentity: TestIdentity, identityService: IdentityService = makeTestIdentityService(), vararg moreKeys: KeyPair) : this(CordappLoader.createWithTestPackages(cordappPackages), identityService, initialIdentity, moreKeys) /** * Create a mock [ServiceHub] that looks for app code in the given package names, uses the provided identity service * (you can get one from [makeTestIdentityService]) and represents the given identity. */ @JvmOverloads - constructor(cordappPackages: List, identityService: IdentityService = makeTestIdentityService(), initialIdentityName: CordaX500Name, key: KeyPair, vararg moreKeys: KeyPair) : this(cordappPackages, identityService, TestIdentity(initialIdentityName, key), *moreKeys) + constructor(cordappPackages: List, initialIdentityName: CordaX500Name, identityService: IdentityService = makeTestIdentityService(), key: KeyPair, vararg moreKeys: KeyPair) : this(cordappPackages, TestIdentity(initialIdentityName, key), identityService, *moreKeys) /** * Create a mock [ServiceHub] that can't load CorDapp code, which uses the provided identity service * (you can get one from [makeTestIdentityService]) and which represents the given identity. */ @JvmOverloads - constructor(cordappPackages: List, identityService: IdentityService = makeTestIdentityService(), initialIdentityName: CordaX500Name) : this(cordappPackages, identityService, TestIdentity(initialIdentityName)) + constructor(cordappPackages: List, initialIdentityName: CordaX500Name, identityService: IdentityService = makeTestIdentityService()) : this(cordappPackages, TestIdentity(initialIdentityName), identityService) + + /** + * Create a mock [ServiceHub] that can't load CorDapp code, and which uses a default service identity. + */ + constructor(cordappPackages: List): this(cordappPackages, CordaX500Name("TestIdentity", "", "GB"), makeTestIdentityService()) /** * Create a mock [ServiceHub] which uses the package of the caller to find CorDapp code. It uses the provided identity service * (you can get one from [makeTestIdentityService]) and which represents the given identity. */ @JvmOverloads - constructor(identityService: IdentityService = makeTestIdentityService(), initialIdentityName: CordaX500Name, key: KeyPair, vararg moreKeys: KeyPair) - : this(listOf(getCallerPackage()), identityService, TestIdentity(initialIdentityName, key), *moreKeys) + constructor(initialIdentityName: CordaX500Name, identityService: IdentityService = makeTestIdentityService(), key: KeyPair, vararg moreKeys: KeyPair) + : this(listOf(getCallerPackage()), TestIdentity(initialIdentityName, key), identityService, *moreKeys) /** * Create a mock [ServiceHub] which uses the package of the caller to find CorDapp code. It uses the provided identity service * (you can get one from [makeTestIdentityService]) and which represents the given identity. It has no keys. */ @JvmOverloads - constructor(identityService: IdentityService = makeTestIdentityService(), initialIdentityName: CordaX500Name) - : this(listOf(getCallerPackage()), identityService, TestIdentity(initialIdentityName)) + constructor(initialIdentityName: CordaX500Name, identityService: IdentityService = makeTestIdentityService()) + : this(listOf(getCallerPackage()), TestIdentity(initialIdentityName), identityService) /** * A helper constructor that requires at least one test identity to be registered, and which takes the package of @@ -176,10 +181,17 @@ open class MockServices private constructor( */ constructor(firstIdentity: TestIdentity, vararg moreIdentities: TestIdentity) : this( listOf(getCallerPackage()), + firstIdentity, makeTestIdentityService(*listOf(firstIdentity, *moreIdentities).map { it.identity }.toTypedArray()), - firstIdentity, firstIdentity.keyPair + firstIdentity.keyPair ) + /** + * Create a mock [ServiceHub] which uses the package of the caller to find CorDapp code. It uses a default service + * identity. + */ + constructor(): this(listOf(getCallerPackage()), CordaX500Name("TestIdentity", "", "GB"), makeTestIdentityService()) + override fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable) { txs.forEach { validatedTransactions.addTransaction(it) diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/NotaryTest.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/NotaryTest.kt index 87a0b2589c..a8cff6abb9 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/NotaryTest.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/NotaryTest.kt @@ -28,7 +28,7 @@ data class NotariseCommand(val issueTx: SignedTransaction, val moveTx: SignedTra val dummyNotarisationTest = LoadTest( "Notarising dummy transactions", generate = { _, _ -> - val issuerServices = MockServices(emptyList(), makeTestIdentityService(megaCorp.identity, miniCorp.identity, dummyCashIssuer.identity, dummyNotary.identity), megaCorp.name, dummyCashIssuer.keyPair) + val issuerServices = MockServices(emptyList(), megaCorp.name, makeTestIdentityService(megaCorp.identity, miniCorp.identity, dummyCashIssuer.identity, dummyNotary.identity), dummyCashIssuer.keyPair) val generateTx = Generator.pickOne(simpleNodes).flatMap { node -> Generator.int().map { val issueBuilder = DummyContract.generateInitial(it, notary.info.legalIdentities[0], DUMMY_CASH_ISSUER) // TODO notary choice From e5857b8e45e6169e7727c358fda2f724efc83063 Mon Sep 17 00:00:00 2001 From: Maksymilian Pawlak <120831+m4ksio@users.noreply.github.com> Date: Mon, 12 Feb 2018 18:39:20 +0100 Subject: [PATCH 10/50] Windows curl warning (#2324) * Windows curl warning (cherry picked from commit abb48b1) * Enhanced message (cherry picked from commit 5b0fe7c) * Ninja-character removal (cherry picked from commit 821a672) --- samples/simm-valuation-demo/README.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/samples/simm-valuation-demo/README.md b/samples/simm-valuation-demo/README.md index 8237872b33..b6160dc5d5 100644 --- a/samples/simm-valuation-demo/README.md +++ b/samples/simm-valuation-demo/README.md @@ -47,8 +47,15 @@ Initial Margin Agreement Process - Agree on the calculation of the above with the other party - In practice, pay (or receive) this margin (omitted for the sake of complexity for this example) -## Demo execution (step by step) +## Requirements +This document assumes you have cURL (curl) installed and ready to use. It is usually installed by default in many Linux +distributions and MacOS. +On Windows, there are numerous ways of installation, including [Cygwin](https://www.cygwin.com), [official distribution](https://curl.haxx.se), +package managers like [Chocolatey](https://chocolatey.org), [NuGet](https://www.nuget.org/), or [Windows Linux subsystem](https://docs.microsoft.com/en-us/windows/wsl/about). +Please refer to installation documents of your chosen source. + +## Demo execution (step by step) **Setting up the Corda infrastructure** @@ -66,7 +73,7 @@ To run from the command line in Windows: From the command line run - curl http://localhost:10005/api/simmvaluationdemo/whoami + curl http://localhost:10005/api/simmvaluationdemo/whoami The response should be something like From c3455053acbe56c67b831d4a7e42e0073f28702e Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Mon, 12 Feb 2018 17:50:35 +0000 Subject: [PATCH 11/50] Adds constructors for creating mock nodes by passing params. --- .../net/corda/testing/node/MockNetwork.kt | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetwork.kt index 070fec8ef9..66a930ce3e 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetwork.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetwork.kt @@ -152,9 +152,45 @@ open class MockNetwork( /** Create a started node with the given parameters. **/ fun createNode(parameters: MockNodeParameters = MockNodeParameters()): StartedMockNode = StartedMockNode.create(internalMockNetwork.createNode(parameters)) + /** Create a started node with the given parameters. + * @param legalName the node's legal name. + * @param forcedID a unique identifier for the node. + * @param entropyRoot the initial entropy value to use when generating keys. Defaults to an (insecure) random value, + * but can be overridden to cause nodes to have stable or colliding identity/service keys. + * @param configOverrides add/override behaviour of the [NodeConfiguration] mock object. + * @param version the mock node's platform, release, revision and vendor versions. + */ + @JvmOverloads + fun createNode(legalName: CordaX500Name? = null, + forcedID: Int? = null, + entropyRoot: BigInteger = BigInteger.valueOf(random63BitValue()), + configOverrides: (NodeConfiguration) -> Any? = {}, + version: VersionInfo = MockServices.MOCK_VERSION_INFO): StartedMockNode { + val parameters = MockNodeParameters(forcedID, legalName, entropyRoot, configOverrides, version) + return StartedMockNode.create(internalMockNetwork.createNode(parameters)) + } + /** Create an unstarted node with the given parameters. **/ fun createUnstartedNode(parameters: MockNodeParameters = MockNodeParameters()): UnstartedMockNode = UnstartedMockNode.create(internalMockNetwork.createUnstartedNode(parameters)) + /** Create an unstarted node with the given parameters. + * @param legalName the node's legal name. + * @param forcedID a unique identifier for the node. + * @param entropyRoot the initial entropy value to use when generating keys. Defaults to an (insecure) random value, + * but can be overridden to cause nodes to have stable or colliding identity/service keys. + * @param configOverrides add/override behaviour of the [NodeConfiguration] mock object. + * @param version the mock node's platform, release, revision and vendor versions. + */ + @JvmOverloads + fun createUnstartedNode(legalName: CordaX500Name? = null, + forcedID: Int? = null, + entropyRoot: BigInteger = BigInteger.valueOf(random63BitValue()), + configOverrides: (NodeConfiguration) -> Any? = {}, + version: VersionInfo = MockServices.MOCK_VERSION_INFO): UnstartedMockNode { + val parameters = MockNodeParameters(forcedID, legalName, entropyRoot, configOverrides, version) + return UnstartedMockNode.create(internalMockNetwork.createUnstartedNode(parameters)) + } + /** Start all nodes that aren't already started. **/ fun startNodes() = internalMockNetwork.startNodes() From 949d4fd7fb23a896d8526b3e9c44ce7ebc6e767d Mon Sep 17 00:00:00 2001 From: CaisR3 <30469399+CaisR3@users.noreply.github.com> Date: Tue, 13 Feb 2018 10:24:06 +0000 Subject: [PATCH 12/50] Fixing a small mistake added/uploaded, only one required. --- docs/source/tutorial-attachments.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/tutorial-attachments.rst b/docs/source/tutorial-attachments.rst index cbb88cf92b..df5e4b5a5e 100644 --- a/docs/source/tutorial-attachments.rst +++ b/docs/source/tutorial-attachments.rst @@ -11,7 +11,7 @@ locally so they are not re-requested if encountered again. Attachments typically * Metadata about a transaction, such as PDF version of an invoice being settled * Shared information to be permanently recorded on the ledger -To add attachments the file must first be added to uploaded to the node, which returns a unique ID that can be added +To add attachments the file must first be uploaded to the node, which returns a unique ID that can be added using ``TransactionBuilder.addAttachment()``. Attachments can be uploaded and downloaded via RPC and the Corda :doc:`shell`. From e5118fedaf43b6c4826b1e7bbe88317c3954e896 Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Tue, 13 Feb 2018 11:21:48 +0000 Subject: [PATCH 13/50] Updates API stability file. --- .ci/api-current.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.ci/api-current.txt b/.ci/api-current.txt index 7232c8431c..445b5673c1 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -4002,9 +4002,9 @@ public final class net.corda.testing.node.MockNodeParameters extends java.lang.O ## public class net.corda.testing.node.MockServices extends java.lang.Object implements net.corda.core.node.StateLoader, net.corda.core.node.ServiceHub public (List, net.corda.core.identity.CordaX500Name) - public (List, net.corda.core.node.services.IdentityService, net.corda.core.identity.CordaX500Name) + public (List, net.corda.core.identity.CordaX500Name, net.corda.core.node.services.IdentityService) public (net.corda.core.identity.CordaX500Name) - public (net.corda.core.node.services.IdentityService, net.corda.core.identity.CordaX500Name) + public (net.corda.core.identity.CordaX500Name, net.corda.core.node.services.IdentityService) @org.jetbrains.annotations.NotNull public net.corda.core.transactions.SignedTransaction addSignature(net.corda.core.transactions.SignedTransaction) @org.jetbrains.annotations.NotNull public net.corda.core.transactions.SignedTransaction addSignature(net.corda.core.transactions.SignedTransaction, java.security.PublicKey) @org.jetbrains.annotations.NotNull public net.corda.core.serialization.SerializeAsToken cordaService(Class) From b91dd43a834ae08e3830476eee7563a7eef0a828 Mon Sep 17 00:00:00 2001 From: Maksymilian Pawlak <120831+m4ksio@users.noreply.github.com> Date: Tue, 13 Feb 2018 13:15:05 +0000 Subject: [PATCH 14/50] Test driver default parameters removal (#2519) * Removed long parameter list in test driver, replaced with DriverParameters object --- .ci/api-current.txt | 1 - .../corda/client/jfx/NodeMonitorModelTest.kt | 3 +- .../client/rpc/BlacklistKotlinClosureTest.kt | 3 +- .../client/rpc/FlowsExecutionModeRpcTest.kt | 3 +- .../corda/docs/IntegrationTestingTutorial.kt | 5 +- .../net/corda/docs/ClientRpcTutorial.kt | 3 +- .../kotlin/net/corda/node/BootTests.kt | 3 +- .../net/corda/node/NodeKeystoreCheckTest.kt | 5 +- .../net/corda/node/NodePerformanceTests.kt | 3 +- .../net/corda/node/NodeUnloadHandlerTests.kt | 3 +- .../kotlin/net/corda/node/SSHServerTest.kt | 5 +- .../draining/P2PFlowsDrainingModeTest.kt | 3 +- .../draining/RpcFlowsDrainingModeTest.kt | 3 +- .../node/services/DistributedServiceTests.kt | 5 +- .../node/services/RaftNotaryServiceTests.kt | 5 +- .../net/corda/node/services/rpc/RpcSslTest.kt | 5 +- .../statemachine/LargeTransactionsTest.kt | 3 +- .../services/messaging/P2PMessagingTest.kt | 3 +- .../test/node/NodeStatePersistenceTests.kt | 5 +- .../services/schema/NodeSchemaServiceTest.kt | 9 +-- .../attachmentdemo/AttachmentDemoTest.kt | 3 +- .../kotlin/net/corda/attachmentdemo/Main.kt | 3 +- .../corda/bank/BankOfCordaRPCClientTest.kt | 3 +- .../src/test/kotlin/net/corda/irs/Main.kt | 3 +- .../kotlin/net/corda/irs/IRSDemoTest.kt | 5 +- .../net/corda/test/spring/SpringDriver.kt | 22 -------- .../system-test/kotlin/IRSDemoDockerTest.kt | 6 ++ .../net/corda/vega/SimmValuationTest.kt | 3 +- .../src/test/kotlin/net/corda/vega/Main.kt | 3 +- .../net/corda/traderdemo/TraderDemoTest.kt | 3 +- .../test/kotlin/net/corda/traderdemo/Main.kt | 3 +- .../net/corda/testing/driver/DriverTests.kt | 6 +- .../testing/node/FlowStackSnapshotTest.kt | 11 ++-- .../kotlin/net/corda/testing/driver/Driver.kt | 55 +++++-------------- .../testing/node/internal/DriverDSLImpl.kt | 47 ++++++---------- .../corda/testing/node/internal/RPCDriver.kt | 2 +- .../net/corda/explorer/ExplorerSimulation.kt | 7 +-- .../net/corda/verifier/VerifierDriver.kt | 2 +- 38 files changed, 116 insertions(+), 149 deletions(-) diff --git a/.ci/api-current.txt b/.ci/api-current.txt index 445b5673c1..8ae7805a7a 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -3641,7 +3641,6 @@ public static final class net.corda.client.jackson.StringToMethodCallParser$Unpa ## public final class net.corda.testing.driver.Driver extends java.lang.Object public static final Object driver(net.corda.testing.driver.DriverParameters, kotlin.jvm.functions.Function1) - public static final Object driver(net.corda.testing.driver.DriverParameters, boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, Map, boolean, boolean, boolean, boolean, List, List, net.corda.testing.driver.JmxPolicy, int, kotlin.jvm.functions.Function1) ## @net.corda.core.DoNotImplement public interface net.corda.testing.driver.DriverDSL @org.jetbrains.annotations.NotNull public abstract java.nio.file.Path baseDirectory(net.corda.core.identity.CordaX500Name) diff --git a/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt b/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt index 157fa8b4b1..640107c565 100644 --- a/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt +++ b/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt @@ -29,6 +29,7 @@ import net.corda.finance.flows.CashPaymentFlow import net.corda.node.services.Permissions.Companion.invokeRpc import net.corda.node.services.Permissions.Companion.startFlow import net.corda.testing.core.* +import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.driver import net.corda.testing.node.User import org.junit.Test @@ -51,7 +52,7 @@ class NodeMonitorModelTest { private lateinit var newNode: (CordaX500Name) -> NodeInfo private fun setup(runTest: () -> Unit) { - driver(extraCordappPackagesToScan = listOf("net.corda.finance")) { + driver(DriverParameters(extraCordappPackagesToScan = listOf("net.corda.finance"))) { val cashUser = User("user1", "test", permissions = setOf( startFlow(), startFlow(), diff --git a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/BlacklistKotlinClosureTest.kt b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/BlacklistKotlinClosureTest.kt index f8c2e3fcc5..a223cf7210 100644 --- a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/BlacklistKotlinClosureTest.kt +++ b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/BlacklistKotlinClosureTest.kt @@ -8,6 +8,7 @@ import net.corda.core.messaging.startFlow import net.corda.core.serialization.CordaSerializable import net.corda.core.utilities.getOrThrow import net.corda.testing.core.ALICE_NAME +import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.driver import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.junit.Test @@ -28,7 +29,7 @@ class BlacklistKotlinClosureTest { @Test fun `closure sent via RPC`() { - driver(startNodesInProcess = true) { + driver(DriverParameters(startNodesInProcess = true)) { val rpc = startNode(providedName = ALICE_NAME).getOrThrow().rpc val packet = Packet { EVIL } assertThatExceptionOfType(KryoException::class.java) diff --git a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/FlowsExecutionModeRpcTest.kt b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/FlowsExecutionModeRpcTest.kt index f742b77b96..011d7d13aa 100644 --- a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/FlowsExecutionModeRpcTest.kt +++ b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/FlowsExecutionModeRpcTest.kt @@ -11,6 +11,7 @@ import net.corda.node.internal.StartedNode import net.corda.node.services.Permissions import net.corda.node.services.Permissions.Companion.invokeRpc import net.corda.testing.core.* +import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.driver import net.corda.testing.node.User @@ -29,7 +30,7 @@ class FlowsExecutionModeRpcTest { assumeFalse(System.getProperty("os.name").toLowerCase().startsWith("win")) val user = User("mark", "dadada", setOf(invokeRpc("setFlowsDrainingModeEnabled"), invokeRpc("isFlowsDrainingModeEnabled"))) - driver(isDebug = true, startNodesInProcess = true) { + driver(DriverParameters(isDebug = true, startNodesInProcess = true)) { val nodeName = { val nodeHandle = startNode(rpcUsers = listOf(user)).getOrThrow() val nodeName = nodeHandle.nodeInfo.chooseIdentity().name diff --git a/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt b/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt index 164e6f08f8..62cac9978e 100644 --- a/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt +++ b/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt @@ -15,6 +15,7 @@ import net.corda.finance.flows.CashPaymentFlow import net.corda.node.services.Permissions.Companion.invokeRpc import net.corda.node.services.Permissions.Companion.startFlow import net.corda.testing.core.* +import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.driver import net.corda.testing.node.User import org.junit.Test @@ -24,8 +25,8 @@ class IntegrationTestingTutorial { @Test fun `alice bob cash exchange example`() { // START 1 - driver(startNodesInProcess = true, - extraCordappPackagesToScan = listOf("net.corda.finance.contracts.asset")) { + driver(DriverParameters(startNodesInProcess = true, + extraCordappPackagesToScan = listOf("net.corda.finance.contracts.asset"))) { val aliceUser = User("aliceUser", "testPassword1", permissions = setOf( startFlow(), startFlow(), diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt index 255caf2271..8cc5f3ab61 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt @@ -19,6 +19,7 @@ import net.corda.finance.flows.CashPaymentFlow import net.corda.node.services.Permissions.Companion.invokeRpc import net.corda.node.services.Permissions.Companion.startFlow import net.corda.testing.core.ALICE_NAME +import net.corda.testing.driver.DriverParameters import net.corda.testing.node.User import net.corda.testing.driver.driver import org.graphstream.graph.Edge @@ -49,7 +50,7 @@ fun main(args: Array) { startFlow(), invokeRpc(CordaRPCOps::nodeInfo) )) - driver(driverDirectory = baseDirectory, extraCordappPackagesToScan = listOf("net.corda.finance"), waitForAllNodesToFinish = true) { + driver(DriverParameters(driverDirectory = baseDirectory, extraCordappPackagesToScan = listOf("net.corda.finance"), waitForAllNodesToFinish = true)) { val node = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).get() // END 1 diff --git a/node/src/integration-test/kotlin/net/corda/node/BootTests.kt b/node/src/integration-test/kotlin/net/corda/node/BootTests.kt index 11016c4bef..516663d17c 100644 --- a/node/src/integration-test/kotlin/net/corda/node/BootTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/BootTests.kt @@ -12,6 +12,7 @@ import net.corda.node.services.Permissions.Companion.startFlow import net.corda.testing.core.ALICE_NAME import net.corda.testing.node.User import net.corda.testing.common.internal.ProjectStructure.projectRootDir +import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.driver import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy @@ -36,7 +37,7 @@ class BootTests { fun `double node start doesn't write into log file`() { val logConfigFile = projectRootDir / "config" / "dev" / "log4j2.xml" assertThat(logConfigFile).isRegularFile() - driver(isDebug = true, systemProperties = mapOf("log4j.configurationFile" to logConfigFile.toString())) { + driver(DriverParameters(isDebug = true, systemProperties = mapOf("log4j.configurationFile" to logConfigFile.toString()))) { val alice = startNode(providedName = ALICE_NAME).get() val logFolder = alice.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME val logFile = logFolder.toFile().listFiles { _, name -> name.endsWith(".log") }.single() diff --git a/node/src/integration-test/kotlin/net/corda/node/NodeKeystoreCheckTest.kt b/node/src/integration-test/kotlin/net/corda/node/NodeKeystoreCheckTest.kt index af572b8296..392f835a04 100644 --- a/node/src/integration-test/kotlin/net/corda/node/NodeKeystoreCheckTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/NodeKeystoreCheckTest.kt @@ -8,6 +8,7 @@ import net.corda.nodeapi.internal.config.SSLConfiguration import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.testing.core.ALICE_NAME +import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.driver import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.Test @@ -17,7 +18,7 @@ import javax.security.auth.x500.X500Principal class NodeKeystoreCheckTest { @Test fun `starting node in non-dev mode with no key store`() { - driver(startNodesInProcess = true, notarySpecs = emptyList()) { + driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) { assertThatThrownBy { startNode(customOverrides = mapOf("devMode" to false)).getOrThrow() }.hasMessageContaining("Identity certificate not found") @@ -26,7 +27,7 @@ class NodeKeystoreCheckTest { @Test fun `node should throw exception if cert path doesn't chain to the trust root`() { - driver(startNodesInProcess = true, notarySpecs = emptyList()) { + driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) { // Create keystores val keystorePassword = "password" val config = object : SSLConfiguration { diff --git a/node/src/integration-test/kotlin/net/corda/node/NodePerformanceTests.kt b/node/src/integration-test/kotlin/net/corda/node/NodePerformanceTests.kt index 51604c1ea2..215f907976 100644 --- a/node/src/integration-test/kotlin/net/corda/node/NodePerformanceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/NodePerformanceTests.kt @@ -15,6 +15,7 @@ import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashPaymentFlow import net.corda.node.services.Permissions.Companion.startFlow import net.corda.testing.core.DUMMY_NOTARY_NAME +import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.InProcess import net.corda.testing.node.User import net.corda.testing.driver.driver @@ -47,7 +48,7 @@ class NodePerformanceTests { @Test fun `empty flow per second`() { - driver(startNodesInProcess = true) { + driver(DriverParameters(startNodesInProcess = true)) { val a = startNode(rpcUsers = listOf(User("A", "A", setOf(startFlow())))).get() CordaRPCClient(a.rpcAddress).use("A", "A") { connection -> diff --git a/node/src/integration-test/kotlin/net/corda/node/NodeUnloadHandlerTests.kt b/node/src/integration-test/kotlin/net/corda/node/NodeUnloadHandlerTests.kt index ef2cb6f2d4..b28e954608 100644 --- a/node/src/integration-test/kotlin/net/corda/node/NodeUnloadHandlerTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/NodeUnloadHandlerTests.kt @@ -6,6 +6,7 @@ import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.utilities.contextLogger import net.corda.core.utilities.getOrThrow import net.corda.testing.core.DUMMY_BANK_A_NAME +import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.driver import org.junit.Assert import org.junit.Test @@ -20,7 +21,7 @@ class NodeUnloadHandlerTests { @Test fun `should be able to register run on stop lambda`() { - driver(startNodesInProcess = true, extraCordappPackagesToScan = listOf("net.corda.node"), isDebug = true) { + driver(DriverParameters(startNodesInProcess = true, extraCordappPackagesToScan = listOf("net.corda.node"), isDebug = true)) { startNode(providedName = DUMMY_BANK_A_NAME).getOrThrow() // just want to fall off the end of this for the mo... } diff --git a/node/src/integration-test/kotlin/net/corda/node/SSHServerTest.kt b/node/src/integration-test/kotlin/net/corda/node/SSHServerTest.kt index e905b7f4d9..634b761d1f 100644 --- a/node/src/integration-test/kotlin/net/corda/node/SSHServerTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/SSHServerTest.kt @@ -13,6 +13,7 @@ import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.unwrap import net.corda.node.services.Permissions.Companion.startFlow import net.corda.testing.core.ALICE_NAME +import net.corda.testing.driver.DriverParameters import net.corda.testing.node.User import net.corda.testing.driver.driver import org.assertj.core.api.Assertions.assertThat @@ -92,7 +93,7 @@ class SSHServerTest { fun `ssh respects permissions`() { val user = User("u", "p", setOf(startFlow())) // The driver will automatically pick up the annotated flows below - driver(isDebug = true) { + driver(DriverParameters(isDebug = true)) { val node = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user), customOverrides = mapOf("sshd" to mapOf("port" to 2222))) node.getOrThrow() @@ -122,7 +123,7 @@ class SSHServerTest { fun `ssh runs flows`() { val user = User("u", "p", setOf(startFlow())) // The driver will automatically pick up the annotated flows below - driver(isDebug = true) { + driver(DriverParameters(isDebug = true)) { val node = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user), customOverrides = mapOf("sshd" to mapOf("port" to 2222))) node.getOrThrow() diff --git a/node/src/integration-test/kotlin/net/corda/node/modes/draining/P2PFlowsDrainingModeTest.kt b/node/src/integration-test/kotlin/net/corda/node/modes/draining/P2PFlowsDrainingModeTest.kt index 536b4d5c4e..de79e99732 100644 --- a/node/src/integration-test/kotlin/net/corda/node/modes/draining/P2PFlowsDrainingModeTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/modes/draining/P2PFlowsDrainingModeTest.kt @@ -10,6 +10,7 @@ import net.corda.core.utilities.loggerFor import net.corda.core.utilities.unwrap import net.corda.node.services.Permissions import net.corda.testing.core.chooseIdentity +import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.PortAllocation import net.corda.testing.driver.driver import net.corda.testing.node.User @@ -46,7 +47,7 @@ class P2PFlowsDrainingModeTest { @Test fun `flows draining mode suspends consumption of initial session messages`() { - driver(isDebug = true, startNodesInProcess = false, portAllocation = portAllocation) { + driver(DriverParameters(isDebug = true, startNodesInProcess = false, portAllocation = portAllocation)) { val initiatedNode = startNode().getOrThrow() val initiating = startNode(rpcUsers = users).getOrThrow().rpc val counterParty = initiatedNode.nodeInfo.chooseIdentity() diff --git a/node/src/integration-test/kotlin/net/corda/node/modes/draining/RpcFlowsDrainingModeTest.kt b/node/src/integration-test/kotlin/net/corda/node/modes/draining/RpcFlowsDrainingModeTest.kt index fee40961c4..2984b5e5c2 100644 --- a/node/src/integration-test/kotlin/net/corda/node/modes/draining/RpcFlowsDrainingModeTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/modes/draining/RpcFlowsDrainingModeTest.kt @@ -7,6 +7,7 @@ import net.corda.core.messaging.startFlow import net.corda.core.utilities.getOrThrow import net.corda.node.services.Permissions import net.corda.nodeapi.exceptions.RejectedCommandException +import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.PortAllocation import net.corda.testing.driver.driver import net.corda.testing.node.User @@ -23,7 +24,7 @@ class RpcFlowsDrainingModeTest { @Test fun `flows draining mode rejects start flows commands through rpc`() { - driver(isDebug = true, startNodesInProcess = false, portAllocation = portAllocation) { + driver(DriverParameters(isDebug = true, startNodesInProcess = false, portAllocation = portAllocation)) { startNode(rpcUsers = users).getOrThrow().rpc.apply { diff --git a/node/src/integration-test/kotlin/net/corda/node/services/DistributedServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/DistributedServiceTests.kt index 639cce9aa9..a172e5459d 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/DistributedServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/DistributedServiceTests.kt @@ -15,6 +15,7 @@ import net.corda.finance.flows.CashPaymentFlow import net.corda.node.services.Permissions.Companion.invokeRpc import net.corda.node.services.Permissions.Companion.startFlow import net.corda.testing.core.* +import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.OutOfProcess import net.corda.testing.driver.driver @@ -40,7 +41,7 @@ class DistributedServiceTests { invokeRpc(CordaRPCOps::nodeInfo), invokeRpc(CordaRPCOps::stateMachinesFeed)) ) - driver( + driver(DriverParameters( extraCordappPackagesToScan = listOf("net.corda.finance.contracts"), notarySpecs = listOf( NotarySpec( @@ -48,7 +49,7 @@ class DistributedServiceTests { rpcUsers = listOf(testUser), cluster = DummyClusterSpec(clusterSize = 3, compositeServiceIdentity = compositeIdentity)) ) - ) { + )) { alice = startNode(providedName = ALICE_NAME, rpcUsers = listOf(testUser)).getOrThrow() raftNotaryIdentity = defaultNotaryIdentity notaryNodes = defaultNotaryHandle.nodeHandles.getOrThrow().map { it as OutOfProcess } diff --git a/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt index b7d3a046fa..20b07017ad 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt @@ -15,6 +15,7 @@ import net.corda.testing.core.chooseIdentity import net.corda.testing.contracts.DummyContract import net.corda.testing.driver.driver import net.corda.testing.core.dummyCommand +import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.InProcess import net.corda.testing.node.ClusterSpec import net.corda.testing.node.NotarySpec @@ -29,11 +30,11 @@ class RaftNotaryServiceTests { @Test fun `detect double spend`() { - driver( + driver(DriverParameters( startNodesInProcess = true, extraCordappPackagesToScan = listOf("net.corda.testing.contracts"), notarySpecs = listOf(NotarySpec(notaryName, cluster = ClusterSpec.Raft(clusterSize = 3))) - ) { + )) { val bankA = startNode(providedName = DUMMY_BANK_A_NAME).map { (it as InProcess) }.getOrThrow() val inputState = issueState(bankA, defaultNotaryIdentity) diff --git a/node/src/integration-test/kotlin/net/corda/node/services/rpc/RpcSslTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/rpc/RpcSslTest.kt index bbe96ed7a7..d6dd26440a 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/rpc/RpcSslTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/rpc/RpcSslTest.kt @@ -6,6 +6,7 @@ import net.corda.core.utilities.getOrThrow import net.corda.node.services.Permissions.Companion.all import net.corda.node.testsupport.withCertificates import net.corda.node.testsupport.withKeyStores +import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.PortAllocation import net.corda.testing.driver.driver import net.corda.testing.internal.useSslRpcOverrides @@ -31,7 +32,7 @@ class RpcSslTest { withKeyStores(server, client) { nodeSslOptions, clientSslOptions -> var successful = false - driver(isDebug = true, startNodesInProcess = true, portAllocation = PortAllocation.RandomFree) { + driver(DriverParameters(isDebug = true, startNodesInProcess = true, portAllocation = PortAllocation.RandomFree)) { startNode(rpcUsers = listOf(user), customOverrides = nodeSslOptions.useSslRpcOverrides()).getOrThrow().use { node -> CordaRPCClient(node.rpcAddress, sslConfiguration = clientSslOptions).start(user.username, user.password).use { connection -> connection.proxy.apply { @@ -50,7 +51,7 @@ class RpcSslTest { fun rpc_client_not_using_ssl() { val user = User("mark", "dadada", setOf(all())) var successful = false - driver(isDebug = true, startNodesInProcess = true, portAllocation = PortAllocation.RandomFree) { + driver(DriverParameters(isDebug = true, startNodesInProcess = true, portAllocation = PortAllocation.RandomFree)) { startNode(rpcUsers = listOf(user)).getOrThrow().use { node -> CordaRPCClient(node.rpcAddress).start(user.username, user.password).use { connection -> connection.proxy.apply { diff --git a/node/src/integration-test/kotlin/net/corda/node/services/statemachine/LargeTransactionsTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/statemachine/LargeTransactionsTest.kt index ec2fd513e6..cd5f572d50 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/statemachine/LargeTransactionsTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/statemachine/LargeTransactionsTest.kt @@ -12,6 +12,7 @@ import net.corda.core.utilities.getOrThrow import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyState import net.corda.testing.core.* +import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.PortAllocation import net.corda.testing.driver.driver import net.corda.testing.node.User @@ -71,7 +72,7 @@ class LargeTransactionsTest { val bigFile2 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024 * 3, 1) val bigFile3 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024 * 3, 2) val bigFile4 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024 * 3, 3) - driver(startNodesInProcess = true, extraCordappPackagesToScan = listOf("net.corda.testing.contracts"), portAllocation = PortAllocation.RandomFree) { + driver(DriverParameters(startNodesInProcess = true, extraCordappPackagesToScan = listOf("net.corda.testing.contracts"), portAllocation = PortAllocation.RandomFree)) { val rpcUser = User("admin", "admin", setOf("ALL")) val (alice, _) = listOf(ALICE_NAME, BOB_NAME).map { startNode(providedName = it, rpcUsers = listOf(rpcUser)) }.transpose().getOrThrow() CordaRPCClient(alice.rpcAddress).use(rpcUser.username, rpcUser.password) { diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt index be183c8e3b..fb25de28ff 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt @@ -18,6 +18,7 @@ import net.corda.node.services.messaging.send import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.chooseIdentity import net.corda.testing.driver.DriverDSL +import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.InProcess import net.corda.testing.driver.driver import net.corda.testing.node.ClusterSpec @@ -110,7 +111,7 @@ class P2PMessagingTest { private fun startDriverWithDistributedService(dsl: DriverDSL.(List) -> Unit) { - driver(startNodesInProcess = true, notarySpecs = listOf(NotarySpec(DISTRIBUTED_SERVICE_NAME, cluster = ClusterSpec.Raft(clusterSize = 2)))) { + driver(DriverParameters(startNodesInProcess = true, notarySpecs = listOf(NotarySpec(DISTRIBUTED_SERVICE_NAME, cluster = ClusterSpec.Raft(clusterSize = 2))))) { dsl(defaultNotaryHandle.nodeHandles.getOrThrow().map { (it as InProcess) }) } } diff --git a/node/src/integration-test/kotlin/net/corda/test/node/NodeStatePersistenceTests.kt b/node/src/integration-test/kotlin/net/corda/test/node/NodeStatePersistenceTests.kt index 7f97117656..28acf69ea0 100644 --- a/node/src/integration-test/kotlin/net/corda/test/node/NodeStatePersistenceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/test/node/NodeStatePersistenceTests.kt @@ -22,6 +22,7 @@ import net.corda.node.services.Permissions.Companion.invokeRpc import net.corda.node.services.Permissions.Companion.startFlow import net.corda.testing.node.User import net.corda.testing.core.chooseIdentity +import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.PortAllocation import net.corda.testing.driver.driver import org.junit.Assume.assumeFalse @@ -42,7 +43,7 @@ class ONodeStatePersistenceTests { val user = User("mark", "dadada", setOf(startFlow(), invokeRpc("vaultQuery"))) val message = Message("Hello world!") - val stateAndRef: StateAndRef? = driver(isDebug = true, startNodesInProcess = isQuasarAgentSpecified(), portAllocation = PortAllocation.RandomFree) { + val stateAndRef: StateAndRef? = driver(DriverParameters(isDebug = true, startNodesInProcess = isQuasarAgentSpecified(), portAllocation = PortAllocation.RandomFree)) { val nodeName = { val nodeHandle = startNode(rpcUsers = listOf(user)).getOrThrow() val nodeName = nodeHandle.nodeInfo.chooseIdentity().name @@ -76,7 +77,7 @@ class ONodeStatePersistenceTests { val user = User("mark", "dadada", setOf(startFlow(), invokeRpc("vaultQuery"))) val message = Message("Hello world!") - val stateAndRef: StateAndRef? = driver(isDebug = true, startNodesInProcess = isQuasarAgentSpecified(), portAllocation = PortAllocation.RandomFree) { + val stateAndRef: StateAndRef? = driver(DriverParameters(isDebug = true, startNodesInProcess = isQuasarAgentSpecified(), portAllocation = PortAllocation.RandomFree)) { val nodeName = { val nodeHandle = startNode(rpcUsers = listOf(user)).getOrThrow() val nodeName = nodeHandle.nodeInfo.chooseIdentity().name diff --git a/node/src/test/kotlin/net/corda/node/services/schema/NodeSchemaServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/schema/NodeSchemaServiceTest.kt index a7bd4a9b15..3c6e6d3b39 100644 --- a/node/src/test/kotlin/net/corda/node/services/schema/NodeSchemaServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/schema/NodeSchemaServiceTest.kt @@ -11,6 +11,7 @@ import net.corda.core.utilities.getOrThrow import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.schema.NodeSchemaService.NodeCoreV1 import net.corda.node.services.schema.NodeSchemaService.NodeNotaryV1 +import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.InProcess import net.corda.testing.driver.driver import net.corda.testing.internal.vault.DummyLinearStateSchemaV1 @@ -68,7 +69,7 @@ class NodeSchemaServiceTest { */ @Test fun `auto scanning of custom schemas for testing with Driver`() { - driver(startNodesInProcess = true) { + driver(DriverParameters(startNodesInProcess = true)) { val result = defaultNotaryNode.getOrThrow().rpc.startFlow(::MappedSchemasFlow) val mappedSchemas = result.returnValue.getOrThrow() assertTrue(mappedSchemas.contains(TestSchema.name)) @@ -78,7 +79,7 @@ class NodeSchemaServiceTest { @Test fun `custom schemas are loaded eagerly`() { val expected = setOf("PARENTS", "CHILDREN") - val tables = driver(startNodesInProcess = true) { + val tables = driver(DriverParameters(startNodesInProcess = true)) { (defaultNotaryNode.getOrThrow() as InProcess).database.transaction { session.createNativeQuery("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES").list() } @@ -91,7 +92,7 @@ class NodeSchemaServiceTest { fun `check node runs with minimal core schema set using driverDSL`() { // TODO: driver limitation: cannot restrict CorDapps that get automatically created by default, // can ONLY specify additional ones using `extraCordappPackagesToScan` constructor argument. - driver(startNodesInProcess = true, notarySpecs = emptyList()) { + driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) { val node = startNode().getOrThrow() val result = node.rpc.startFlow(::MappedSchemasFlow) val mappedSchemas = result.returnValue.getOrThrow() @@ -104,7 +105,7 @@ class NodeSchemaServiceTest { @Test fun `check node runs inclusive of notary node schema set using driverDSL`() { - driver(startNodesInProcess = true) { + driver(DriverParameters(startNodesInProcess = true)) { val notaryNode = defaultNotaryNode.getOrThrow().rpc.startFlow(::MappedSchemasFlow) val mappedSchemas = notaryNode.returnValue.getOrThrow() // check against NodeCore + NodeNotary Schemas diff --git a/samples/attachment-demo/src/integration-test/kotlin/net/corda/attachmentdemo/AttachmentDemoTest.kt b/samples/attachment-demo/src/integration-test/kotlin/net/corda/attachmentdemo/AttachmentDemoTest.kt index e480657b6d..dfaf92f775 100644 --- a/samples/attachment-demo/src/integration-test/kotlin/net/corda/attachmentdemo/AttachmentDemoTest.kt +++ b/samples/attachment-demo/src/integration-test/kotlin/net/corda/attachmentdemo/AttachmentDemoTest.kt @@ -7,6 +7,7 @@ import net.corda.node.services.Permissions.Companion.invokeRpc import net.corda.node.services.Permissions.Companion.startFlow import net.corda.testing.core.DUMMY_BANK_A_NAME import net.corda.testing.core.DUMMY_BANK_B_NAME +import net.corda.testing.driver.DriverParameters import net.corda.testing.node.User import net.corda.testing.driver.PortAllocation import net.corda.testing.driver.driver @@ -19,7 +20,7 @@ class AttachmentDemoTest { @Test fun `attachment demo using a 10MB zip file`() { val numOfExpectedBytes = 10_000_000 - driver(isDebug = true, portAllocation = PortAllocation.Incremental(20000)) { + driver(DriverParameters(isDebug = true, portAllocation = PortAllocation.Incremental(20000))) { val demoUser = listOf(User("demo", "demo", setOf( startFlow(), invokeRpc(CordaRPCOps::attachmentExists), diff --git a/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/Main.kt b/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/Main.kt index 6c3c11e9c1..cdc6313a25 100644 --- a/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/Main.kt +++ b/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/Main.kt @@ -3,6 +3,7 @@ package net.corda.attachmentdemo import net.corda.core.internal.div import net.corda.testing.core.DUMMY_BANK_A_NAME import net.corda.testing.core.DUMMY_BANK_B_NAME +import net.corda.testing.driver.DriverParameters import net.corda.testing.node.User import net.corda.testing.driver.driver @@ -12,7 +13,7 @@ import net.corda.testing.driver.driver */ fun main(args: Array) { val demoUser = listOf(User("demo", "demo", setOf("StartFlow.net.corda.flows.FinalityFlow"))) - driver(isDebug = true, driverDirectory = "build" / "attachment-demo-nodes", waitForAllNodesToFinish = true) { + driver(DriverParameters(isDebug = true, driverDirectory = "build" / "attachment-demo-nodes", waitForAllNodesToFinish = true)) { startNode(providedName = DUMMY_BANK_A_NAME, rpcUsers = demoUser) startNode(providedName = DUMMY_BANK_B_NAME, rpcUsers = demoUser) } diff --git a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt index c10a266934..48a3705b27 100644 --- a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt +++ b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt @@ -16,6 +16,7 @@ import net.corda.testing.core.BOC_NAME import net.corda.testing.core.expect import net.corda.testing.core.expectEvents import net.corda.testing.core.sequence +import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.driver import net.corda.testing.node.User import org.junit.Test @@ -28,7 +29,7 @@ class BankOfCordaRPCClientTest { invokeRpc(CordaRPCOps::wellKnownPartyFromX500Name), invokeRpc(CordaRPCOps::notaryIdentities) ) - driver(extraCordappPackagesToScan = listOf("net.corda.finance"), isDebug = true) { + driver(DriverParameters(extraCordappPackagesToScan = listOf("net.corda.finance"), isDebug = true)) { val bocManager = User("bocManager", "password1", permissions = setOf( startFlow()) + commonPermissions) val bigCorpCFO = User("bigCorpCFO", "password2", permissions = emptySet() + commonPermissions) diff --git a/samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/Main.kt b/samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/Main.kt index c6de65501f..99a0887b52 100644 --- a/samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/Main.kt +++ b/samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/Main.kt @@ -4,6 +4,7 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.utilities.getOrThrow import net.corda.testing.core.DUMMY_BANK_A_NAME import net.corda.testing.core.DUMMY_BANK_B_NAME +import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.driver /** @@ -11,7 +12,7 @@ import net.corda.testing.driver.driver * Do not use in a production environment. */ fun main(args: Array) { - driver(useTestClock = true, isDebug = true, waitForAllNodesToFinish = true) { + driver(DriverParameters(useTestClock = true, isDebug = true, waitForAllNodesToFinish = true)) { val (nodeA, nodeB) = listOf( startNode(providedName = DUMMY_BANK_A_NAME), startNode(providedName = DUMMY_BANK_B_NAME), diff --git a/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt b/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt index 02e258e1e8..70fdb57b7a 100644 --- a/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt +++ b/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt @@ -28,6 +28,7 @@ import net.corda.testing.core.DUMMY_BANK_A_NAME import net.corda.testing.core.DUMMY_BANK_B_NAME import net.corda.testing.core.DUMMY_NOTARY_NAME import net.corda.testing.core.chooseIdentity +import net.corda.testing.driver.DriverParameters import net.corda.testing.http.HttpApi import net.corda.testing.node.NotarySpec import net.corda.testing.node.User @@ -50,12 +51,12 @@ class IRSDemoTest { @Test fun `runs IRS demo`() { - springDriver( + springDriver(DriverParameters( useTestClock = true, notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, rpcUsers = rpcUsers)), isDebug = true, extraCordappPackagesToScan = listOf("net.corda.irs") - ) { + )) { val (nodeA, nodeB) = listOf( startNode(providedName = DUMMY_BANK_A_NAME, rpcUsers = rpcUsers), startNode(providedName = DUMMY_BANK_B_NAME, rpcUsers = rpcUsers), diff --git a/samples/irs-demo/src/integration-test/kotlin/net/corda/test/spring/SpringDriver.kt b/samples/irs-demo/src/integration-test/kotlin/net/corda/test/spring/SpringDriver.kt index f425688c59..f4f1f37734 100644 --- a/samples/irs-demo/src/integration-test/kotlin/net/corda/test/spring/SpringDriver.kt +++ b/samples/irs-demo/src/integration-test/kotlin/net/corda/test/spring/SpringDriver.kt @@ -20,33 +20,11 @@ import java.util.concurrent.TimeUnit fun springDriver( defaultParameters: DriverParameters = DriverParameters(), - isDebug: Boolean = defaultParameters.isDebug, - driverDirectory: Path = defaultParameters.driverDirectory, - portAllocation: PortAllocation = defaultParameters.portAllocation, - debugPortAllocation: PortAllocation = defaultParameters.debugPortAllocation, - systemProperties: Map = defaultParameters.systemProperties, - useTestClock: Boolean = defaultParameters.useTestClock, - initialiseSerialization: Boolean = defaultParameters.initialiseSerialization, - startNodesInProcess: Boolean = defaultParameters.startNodesInProcess, - notarySpecs: List = defaultParameters.notarySpecs, - extraCordappPackagesToScan: List = defaultParameters.extraCordappPackagesToScan, - maxTransactionSize: Int = defaultParameters.maxTransactionSize, dsl: SpringBootDriverDSL.() -> A ): A { return genericDriver( defaultParameters = defaultParameters, - isDebug = isDebug, - driverDirectory = driverDirectory, - portAllocation = portAllocation, - debugPortAllocation = debugPortAllocation, - systemProperties = systemProperties, - useTestClock = useTestClock, - initialiseSerialization = initialiseSerialization, - startNodesInProcess = startNodesInProcess, - extraCordappPackagesToScan = extraCordappPackagesToScan, - notarySpecs = notarySpecs, driverDslWrapper = { driverDSL: DriverDSLImpl -> SpringBootDriverDSL(driverDSL) }, - maxTransactionSize = maxTransactionSize, coerce = { it }, dsl = dsl ) } diff --git a/samples/irs-demo/src/system-test/kotlin/IRSDemoDockerTest.kt b/samples/irs-demo/src/system-test/kotlin/IRSDemoDockerTest.kt index 5f0769f669..034d4ff2d4 100644 --- a/samples/irs-demo/src/system-test/kotlin/IRSDemoDockerTest.kt +++ b/samples/irs-demo/src/system-test/kotlin/IRSDemoDockerTest.kt @@ -72,9 +72,15 @@ class IRSDemoDockerTest { //Wait for deals to appear in a rows table val dealsList = driverWait.until({ + makeScreenshot(driver, "second") it?.findElement(By.cssSelector("table#deal-list tbody tr")) }) assertNotNull(dealsList) } + + private fun makeScreenshot(driver: PhantomJSDriver, name: String) { + val screenshotAs = driver.getScreenshotAs(OutputType.FILE) + Files.copy(screenshotAs.toPath(), Paths.get("/Users", "maksymilianpawlak", "phantomjs", name + System.currentTimeMillis() + ".png"), StandardCopyOption.REPLACE_EXISTING) + } } diff --git a/samples/simm-valuation-demo/src/integration-test/kotlin/net/corda/vega/SimmValuationTest.kt b/samples/simm-valuation-demo/src/integration-test/kotlin/net/corda/vega/SimmValuationTest.kt index d0b63477ed..4516c0e540 100644 --- a/samples/simm-valuation-demo/src/integration-test/kotlin/net/corda/vega/SimmValuationTest.kt +++ b/samples/simm-valuation-demo/src/integration-test/kotlin/net/corda/vega/SimmValuationTest.kt @@ -5,6 +5,7 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.utilities.getOrThrow import net.corda.testing.core.DUMMY_BANK_A_NAME import net.corda.testing.core.DUMMY_BANK_B_NAME +import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.driver import net.corda.testing.http.HttpApi import net.corda.vega.api.PortfolioApi @@ -27,7 +28,7 @@ class SimmValuationTest { @Test fun `runs SIMM valuation demo`() { - driver(isDebug = true, extraCordappPackagesToScan = listOf("net.corda.vega.contracts", "net.corda.vega.plugin.customserializers")) { + driver(DriverParameters(isDebug = true, extraCordappPackagesToScan = listOf("net.corda.vega.contracts", "net.corda.vega.plugin.customserializers"))) { val nodeAFuture = startNode(providedName = nodeALegalName) val nodeBFuture = startNode(providedName = nodeBLegalName) val (nodeA, nodeB) = listOf(nodeAFuture, nodeBFuture).map { it.getOrThrow() } diff --git a/samples/simm-valuation-demo/src/test/kotlin/net/corda/vega/Main.kt b/samples/simm-valuation-demo/src/test/kotlin/net/corda/vega/Main.kt index bae73f344a..e20f24a4f0 100644 --- a/samples/simm-valuation-demo/src/test/kotlin/net/corda/vega/Main.kt +++ b/samples/simm-valuation-demo/src/test/kotlin/net/corda/vega/Main.kt @@ -4,6 +4,7 @@ import net.corda.core.utilities.getOrThrow import net.corda.testing.core.DUMMY_BANK_A_NAME import net.corda.testing.core.DUMMY_BANK_B_NAME import net.corda.testing.core.DUMMY_BANK_C_NAME +import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.driver /** @@ -12,7 +13,7 @@ import net.corda.testing.driver.driver * via the web api. */ fun main(args: Array) { - driver(isDebug = true, waitForAllNodesToFinish = true) { + driver(DriverParameters(isDebug = true, waitForAllNodesToFinish = true)) { val (nodeA, nodeB, nodeC) = listOf( startNode(providedName = DUMMY_BANK_A_NAME), startNode(providedName = DUMMY_BANK_B_NAME), diff --git a/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt b/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt index f76f400ef3..e23d801e76 100644 --- a/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt +++ b/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt @@ -12,6 +12,7 @@ import net.corda.testing.core.BOC_NAME import net.corda.testing.core.DUMMY_BANK_A_NAME import net.corda.testing.core.DUMMY_BANK_B_NAME import net.corda.testing.core.chooseIdentity +import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.InProcess import net.corda.testing.driver.driver import net.corda.testing.node.User @@ -32,7 +33,7 @@ class TraderDemoTest { startFlow(), startFlow(), all())) - driver(startNodesInProcess = true, extraCordappPackagesToScan = listOf("net.corda.finance")) { + driver(DriverParameters(startNodesInProcess = true, extraCordappPackagesToScan = listOf("net.corda.finance"))) { val (nodeA, nodeB, bankNode) = listOf( startNode(providedName = DUMMY_BANK_A_NAME, rpcUsers = listOf(demoUser)), startNode(providedName = DUMMY_BANK_B_NAME, rpcUsers = listOf(demoUser)), diff --git a/samples/trader-demo/src/test/kotlin/net/corda/traderdemo/Main.kt b/samples/trader-demo/src/test/kotlin/net/corda/traderdemo/Main.kt index 04322ba86e..61d711312d 100644 --- a/samples/trader-demo/src/test/kotlin/net/corda/traderdemo/Main.kt +++ b/samples/trader-demo/src/test/kotlin/net/corda/traderdemo/Main.kt @@ -7,6 +7,7 @@ import net.corda.node.services.Permissions.Companion.startFlow import net.corda.testing.core.BOC_NAME import net.corda.testing.core.DUMMY_BANK_A_NAME import net.corda.testing.core.DUMMY_BANK_B_NAME +import net.corda.testing.driver.DriverParameters import net.corda.testing.node.User import net.corda.testing.driver.driver import net.corda.traderdemo.flow.CommercialPaperIssueFlow @@ -22,7 +23,7 @@ fun main(args: Array) { startFlow(), all()) val demoUser = listOf(User("demo", "demo", permissions)) - driver(driverDirectory = "build" / "trader-demo-nodes", isDebug = true, waitForAllNodesToFinish = true) { + driver(DriverParameters(driverDirectory = "build" / "trader-demo-nodes", isDebug = true, waitForAllNodesToFinish = true)) { val user = User("user1", "test", permissions = setOf(startFlow(), startFlow(), startFlow())) diff --git a/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt index bf3bdf648e..3a08b300c9 100644 --- a/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt +++ b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt @@ -69,7 +69,7 @@ class DriverTests { @Test fun `random free port allocation`() { - val nodeHandle = driver(portAllocation = PortAllocation.RandomFree) { + val nodeHandle = driver(DriverParameters(portAllocation = PortAllocation.RandomFree)) { val nodeInfo = startNode(providedName = DUMMY_BANK_A_NAME) nodeMustBeUp(nodeInfo) } @@ -81,7 +81,7 @@ class DriverTests { // Make sure we're using the log4j2 config which writes to the log file val logConfigFile = projectRootDir / "config" / "dev" / "log4j2.xml" assertThat(logConfigFile).isRegularFile() - driver(isDebug = true, systemProperties = mapOf("log4j.configurationFile" to logConfigFile.toString())) { + driver(DriverParameters(isDebug = true, systemProperties = mapOf("log4j.configurationFile" to logConfigFile.toString()))) { val baseDirectory = startNode(providedName = DUMMY_BANK_A_NAME).getOrThrow().baseDirectory val logFile = (baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME).list { it.sorted().findFirst().get() } val debugLinesPresent = logFile.readLines { lines -> lines.anyMatch { line -> line.startsWith("[DEBUG]") } } @@ -91,7 +91,7 @@ class DriverTests { @Test fun `monitoring mode enables jolokia exporting of JMX metrics via HTTP JSON`() { - driver(jmxPolicy = JmxPolicy(true)) { + driver(DriverParameters(jmxPolicy = JmxPolicy(true))) { // start another node so we gain access to node JMX metrics startNode(providedName = DUMMY_REGULATOR_NAME).getOrThrow() val webAddress = NetworkHostAndPort("localhost", 7006) diff --git a/testing/node-driver/src/integration-test/kotlin/net/corda/testing/node/FlowStackSnapshotTest.kt b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/node/FlowStackSnapshotTest.kt index 423b14c014..f92952f609 100644 --- a/testing/node-driver/src/integration-test/kotlin/net/corda/testing/node/FlowStackSnapshotTest.kt +++ b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/node/FlowStackSnapshotTest.kt @@ -10,6 +10,7 @@ import net.corda.core.internal.read import net.corda.core.messaging.startFlow import net.corda.core.serialization.CordaSerializable import net.corda.node.services.Permissions.Companion.startFlow +import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.driver import org.junit.Ignore import org.junit.Test @@ -227,7 +228,7 @@ fun assertFrame(expectedMethod: String, expectedEmpty: Boolean, frame: StackSnap class FlowStackSnapshotTest { @Test fun `flowStackSnapshot contains full frames when methods with side effects are called`() { - driver(startNodesInProcess = true) { + driver(DriverParameters(startNodesInProcess = true)) { val a = startNode(rpcUsers = listOf(User(Constants.USER, Constants.PASSWORD, setOf(startFlow())))).get() CordaRPCClient(a.rpcAddress).use(Constants.USER, Constants.PASSWORD) { connection -> val stackSnapshotFrames = connection.proxy.startFlow(::SideEffectFlow).returnValue.get() @@ -242,7 +243,7 @@ class FlowStackSnapshotTest { @Test fun `flowStackSnapshot contains empty frames when methods with no side effects are called`() { - driver(startNodesInProcess = true) { + driver(DriverParameters(startNodesInProcess = true)) { val a = startNode(rpcUsers = listOf(User(Constants.USER, Constants.PASSWORD, setOf(startFlow())))).get() CordaRPCClient(a.rpcAddress).use(Constants.USER, Constants.PASSWORD) { connection -> val stackSnapshotFrames = connection.proxy.startFlow(::NoSideEffectFlow).returnValue.get() @@ -257,7 +258,7 @@ class FlowStackSnapshotTest { @Test fun `persistFlowStackSnapshot persists empty frames to a file when methods with no side effects are called`() { - driver(startNodesInProcess = true) { + driver(DriverParameters(startNodesInProcess = true)) { val a = startNode(rpcUsers = listOf(User(Constants.USER, Constants.PASSWORD, setOf(startFlow())))).get() CordaRPCClient(a.rpcAddress).use(Constants.USER, Constants.PASSWORD) { connection -> val flowId = connection.proxy.startFlow(::PersistingNoSideEffectFlow).returnValue.get() @@ -273,7 +274,7 @@ class FlowStackSnapshotTest { @Test fun `persistFlowStackSnapshot persists multiple snapshots in different files`() { - driver(startNodesInProcess = true) { + driver(DriverParameters(startNodesInProcess = true)) { val a = startNode(rpcUsers = listOf(User(Constants.USER, Constants.PASSWORD, setOf(startFlow())))).get() CordaRPCClient(a.rpcAddress).use(Constants.USER, Constants.PASSWORD) { connection -> @@ -304,7 +305,7 @@ class FlowStackSnapshotTest { @Test fun `persistFlowStackSnapshot stack traces are aligned with stack objects`() { - driver(startNodesInProcess = true) { + driver(DriverParameters(startNodesInProcess = true)) { val a = startNode(rpcUsers = listOf(User(Constants.USER, Constants.PASSWORD, setOf(startFlow())))).get() CordaRPCClient(a.rpcAddress).use(Constants.USER, Constants.PASSWORD) { connection -> diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt index 6a74e2d673..883d61b88f 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt @@ -11,9 +11,7 @@ import net.corda.core.messaging.CordaRPCOps import net.corda.core.node.NodeInfo import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.internal.Node -import net.corda.node.internal.StartedNode import net.corda.node.services.api.StartedNodeServices -import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.VerifierType import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.testing.core.DUMMY_NOTARY_NAME @@ -152,57 +150,30 @@ data class JmxPolicy(val startJmxHttpServer: Boolean = false, */ fun driver( defaultParameters: DriverParameters = DriverParameters(), - isDebug: Boolean = defaultParameters.isDebug, - driverDirectory: Path = defaultParameters.driverDirectory, - portAllocation: PortAllocation = defaultParameters.portAllocation, - debugPortAllocation: PortAllocation = defaultParameters.debugPortAllocation, - systemProperties: Map = defaultParameters.systemProperties, - useTestClock: Boolean = defaultParameters.useTestClock, - initialiseSerialization: Boolean = defaultParameters.initialiseSerialization, - startNodesInProcess: Boolean = defaultParameters.startNodesInProcess, - waitForAllNodesToFinish: Boolean = defaultParameters.waitForAllNodesToFinish, - notarySpecs: List = defaultParameters.notarySpecs, - extraCordappPackagesToScan: List = defaultParameters.extraCordappPackagesToScan, - jmxPolicy: JmxPolicy = defaultParameters.jmxPolicy, - maxTransactionSize: Int = defaultParameters.maxTransactionSize, dsl: DriverDSL.() -> A ): A { return genericDriver( driverDsl = DriverDSLImpl( - portAllocation = portAllocation, - debugPortAllocation = debugPortAllocation, - systemProperties = systemProperties, - driverDirectory = driverDirectory.toAbsolutePath(), - useTestClock = useTestClock, - isDebug = isDebug, - startNodesInProcess = startNodesInProcess, - waitForNodesToFinish = waitForAllNodesToFinish, - notarySpecs = notarySpecs, - extraCordappPackagesToScan = extraCordappPackagesToScan, - jmxPolicy = jmxPolicy, + portAllocation = defaultParameters.portAllocation, + debugPortAllocation = defaultParameters.debugPortAllocation, + systemProperties = defaultParameters.systemProperties, + driverDirectory = defaultParameters.driverDirectory.toAbsolutePath(), + useTestClock = defaultParameters.useTestClock, + isDebug = defaultParameters.isDebug, + startNodesInProcess = defaultParameters.startNodesInProcess, + waitForAllNodesToFinish = defaultParameters.waitForAllNodesToFinish, + notarySpecs = defaultParameters.notarySpecs, + extraCordappPackagesToScan = defaultParameters.extraCordappPackagesToScan, + jmxPolicy = defaultParameters.jmxPolicy, compatibilityZone = null, - maxTransactionSize = maxTransactionSize + maxTransactionSize = defaultParameters.maxTransactionSize ), coerce = { it }, dsl = dsl, - initialiseSerialization = initialiseSerialization + initialiseSerialization = defaultParameters.initialiseSerialization ) } -/** - * Helper function for starting a [driver] with custom parameters from Java. - * - * @param parameters The default parameters for the driver. - * @param dsl The dsl itself. - * @return The value returned in the [dsl] closure. - */ -fun driver( - parameters: DriverParameters, - dsl: DriverDSL.() -> A -): A { - return driver(defaultParameters = parameters, dsl = dsl) -} - /** Helper builder for configuring a [driver] from Java. */ @Suppress("unused") data class DriverParameters( diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt index ec1477f7fa..4e0567e007 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt @@ -86,7 +86,7 @@ class DriverDSLImpl( val useTestClock: Boolean, val isDebug: Boolean, val startNodesInProcess: Boolean, - val waitForNodesToFinish: Boolean, + val waitForAllNodesToFinish: Boolean, extraCordappPackagesToScan: List, val jmxPolicy: JmxPolicy, val notarySpecs: List, @@ -138,7 +138,7 @@ class DriverDSLImpl( } override fun shutdown() { - if (waitForNodesToFinish) { + if (waitForAllNodesToFinish) { state.locked { processes.forEach { it.waitFor() } } @@ -665,7 +665,7 @@ class DriverDSLImpl( val debugPort = if (isDebug) debugPortAllocation.nextPort() else null val monitorPort = if (jmxPolicy.startJmxHttpServer) jmxPolicy.jmxHttpServerPortAllocation?.nextPort() else null val process = startOutOfProcessNode(config, quasarJarPath, debugPort, jolokiaJarPath, monitorPort, systemProperties, cordappPackages, maximumHeapSize) - if (waitForNodesToFinish) { + if (waitForAllNodesToFinish) { state.locked { processes += process } @@ -947,38 +947,25 @@ fun genericDriver( */ fun genericDriver( defaultParameters: DriverParameters = DriverParameters(), - isDebug: Boolean = defaultParameters.isDebug, - driverDirectory: Path = defaultParameters.driverDirectory, - portAllocation: PortAllocation = defaultParameters.portAllocation, - debugPortAllocation: PortAllocation = defaultParameters.debugPortAllocation, - systemProperties: Map = defaultParameters.systemProperties, - useTestClock: Boolean = defaultParameters.useTestClock, - initialiseSerialization: Boolean = defaultParameters.initialiseSerialization, - waitForNodesToFinish: Boolean = defaultParameters.waitForAllNodesToFinish, - startNodesInProcess: Boolean = defaultParameters.startNodesInProcess, - notarySpecs: List, - extraCordappPackagesToScan: List = defaultParameters.extraCordappPackagesToScan, - jmxPolicy: JmxPolicy = JmxPolicy(), - maxTransactionSize: Int, driverDslWrapper: (DriverDSLImpl) -> D, coerce: (D) -> DI, dsl: DI.() -> A ): A { - val serializationEnv = setGlobalSerialization(initialiseSerialization) + val serializationEnv = setGlobalSerialization(defaultParameters.initialiseSerialization) val driverDsl = driverDslWrapper( DriverDSLImpl( - portAllocation = portAllocation, - debugPortAllocation = debugPortAllocation, - systemProperties = systemProperties, - driverDirectory = driverDirectory.toAbsolutePath(), - useTestClock = useTestClock, - isDebug = isDebug, - startNodesInProcess = startNodesInProcess, - waitForNodesToFinish = waitForNodesToFinish, - extraCordappPackagesToScan = extraCordappPackagesToScan, - jmxPolicy = jmxPolicy, - notarySpecs = notarySpecs, + portAllocation = defaultParameters.portAllocation, + debugPortAllocation = defaultParameters.debugPortAllocation, + systemProperties = defaultParameters.systemProperties, + driverDirectory = defaultParameters.driverDirectory.toAbsolutePath(), + useTestClock = defaultParameters.useTestClock, + isDebug = defaultParameters.isDebug, + startNodesInProcess = defaultParameters.startNodesInProcess, + waitForAllNodesToFinish = defaultParameters.waitForAllNodesToFinish, + extraCordappPackagesToScan = defaultParameters.extraCordappPackagesToScan, + jmxPolicy = defaultParameters.jmxPolicy, + notarySpecs = defaultParameters.notarySpecs, compatibilityZone = null, - maxTransactionSize = maxTransactionSize + maxTransactionSize = defaultParameters.maxTransactionSize ) ) val shutdownHook = addShutdownHook(driverDsl::shutdown) @@ -1033,7 +1020,7 @@ fun internalDriver( useTestClock = useTestClock, isDebug = isDebug, startNodesInProcess = startNodesInProcess, - waitForNodesToFinish = waitForAllNodesToFinish, + waitForAllNodesToFinish = waitForAllNodesToFinish, notarySpecs = notarySpecs, extraCordappPackagesToScan = extraCordappPackagesToScan, jmxPolicy = jmxPolicy, diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt index 585f170fd3..e2770f4b80 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt @@ -119,7 +119,7 @@ fun rpcDriver( useTestClock = useTestClock, isDebug = isDebug, startNodesInProcess = startNodesInProcess, - waitForNodesToFinish = waitForNodesToFinish, + waitForAllNodesToFinish = waitForNodesToFinish, extraCordappPackagesToScan = extraCordappPackagesToScan, notarySpecs = notarySpecs, jmxPolicy = jmxPolicy, diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt index 62d9fc9dca..0eb1f05423 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt @@ -24,11 +24,8 @@ import net.corda.finance.flows.CashIssueAndPaymentFlow.IssueAndPaymentRequest import net.corda.node.services.Permissions.Companion.startFlow import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME +import net.corda.testing.driver.* import net.corda.testing.node.User -import net.corda.testing.driver.JmxPolicy -import net.corda.testing.driver.NodeHandle -import net.corda.testing.driver.PortAllocation -import net.corda.testing.driver.driver import java.time.Instant import java.util.* @@ -66,7 +63,7 @@ class ExplorerSimulation(private val options: OptionSet) { private fun startDemoNodes() { val portAllocation = PortAllocation.Incremental(20000) - driver(portAllocation = portAllocation, extraCordappPackagesToScan = listOf("net.corda.finance"), waitForAllNodesToFinish = true, jmxPolicy = JmxPolicy(true)) { + driver(DriverParameters(portAllocation = portAllocation, extraCordappPackagesToScan = listOf("net.corda.finance"), waitForAllNodesToFinish = true, jmxPolicy = JmxPolicy(true))) { // TODO : Supported flow should be exposed somehow from the node instead of set of ServiceInfo. val alice = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)) val bob = startNode(providedName = BOB_NAME, rpcUsers = listOf(user)) diff --git a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt index 70de05b28e..50c1b8d7e1 100644 --- a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt +++ b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt @@ -76,7 +76,7 @@ fun verifierDriver( useTestClock = useTestClock, isDebug = isDebug, startNodesInProcess = startNodesInProcess, - waitForNodesToFinish = waitForNodesToFinish, + waitForAllNodesToFinish = waitForNodesToFinish, extraCordappPackagesToScan = extraCordappPackagesToScan, notarySpecs = notarySpecs, jmxPolicy = jmxPolicy, From a702d792a72bcfdb458a7968e6e0e4e22c7038b5 Mon Sep 17 00:00:00 2001 From: Andrius Dagys Date: Thu, 1 Feb 2018 17:19:14 +0000 Subject: [PATCH 15/50] Make NotaryFlow.Client more modular and easier to customise --- .../kotlin/net/corda/core/flows/NotaryFlow.kt | 45 +++++++++++++++---- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt b/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt index 395bc6635e..8e38471384 100644 --- a/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt @@ -47,8 +47,18 @@ class NotaryFlow { @Suspendable @Throws(NotaryException::class) override fun call(): List { + val notaryParty = checkTransaction() progressTracker.currentStep = REQUESTING + val response = notarise(notaryParty) + progressTracker.currentStep = VALIDATING + return validateResponse(response, notaryParty) + } + /** + * Checks that the transaction specifies a valid notary, and verifies that it contains all required signatures + * apart from the notary's. + */ + protected fun checkTransaction(): Party { val notaryParty = stx.notary ?: throw IllegalStateException("Transaction does not specify a Notary") check(serviceHub.networkMapCache.isNotary(notaryParty)) { "$notaryParty is not a notary on the network" } check(stx.inputs.all { stateRef -> serviceHub.loadState(stateRef).notary == notaryParty }) { @@ -60,19 +70,18 @@ class NotaryFlow { } catch (ex: SignatureException) { throw NotaryException(NotaryError.TransactionInvalid(ex)) } + return notaryParty + } - val response = try { + @Throws(NotaryException::class) + @Suspendable + protected fun notarise(notaryParty: Party): UntrustworthyData> { + return try { val session = initiateFlow(notaryParty) if (serviceHub.networkMapCache.isValidatingNotary(notaryParty)) { - subFlow(SendTransactionWithRetry(session, stx)) - session.receive>() + sendAndReceiveValidating(session) } else { - val tx: Any = if (stx.isNotaryChangeTransaction()) { - stx.notaryChangeTx - } else { - stx.buildFilteredTransaction(Predicate { it is StateRef || it is TimeWindow || it == notaryParty }) - } - session.sendAndReceiveWithRetry(tx) + sendAndReceiveNonValidating(notaryParty, session) } } catch (e: NotaryException) { if (e.error is NotaryError.Conflict) { @@ -80,7 +89,25 @@ class NotaryFlow { } throw e } + } + @Suspendable + protected open fun sendAndReceiveValidating(session: FlowSession): UntrustworthyData> { + subFlow(SendTransactionWithRetry(session, stx)) + return session.receive() + } + + @Suspendable + protected open fun sendAndReceiveNonValidating(notaryParty: Party, session: FlowSession): UntrustworthyData> { + val tx: Any = if (stx.isNotaryChangeTransaction()) { + stx.notaryChangeTx // Notary change transactions do not support filtering + } else { + stx.buildFilteredTransaction(Predicate { it is StateRef || it is TimeWindow || it == notaryParty }) + } + return session.sendAndReceiveWithRetry(tx) + } + + protected fun validateResponse(response: UntrustworthyData>, notaryParty: Party): List { return response.unwrap { signatures -> signatures.forEach { validateSignature(it, stx.id, notaryParty) } signatures From 7924a5a83419ac8562e16dff616653c9a22ccf71 Mon Sep 17 00:00:00 2001 From: Andras Slemmer Date: Tue, 6 Feb 2018 19:02:06 +0000 Subject: [PATCH 16/50] Add RPC deduplication to client and server --- .idea/compiler.xml | 3 +- .../net/corda/client/rpc/RPCStabilityTests.kt | 48 +++- .../corda/client/rpc/internal/RPCClient.kt | 11 +- .../rpc/internal/RPCClientProxyHandler.kt | 111 ++++----- .../corda/client/rpc/RPCPerformanceTests.kt | 19 +- .../main/kotlin/net/corda/nodeapi/RPCApi.kt | 58 +++-- .../nodeapi/internal/DeduplicationChecker.kt | 29 +++ .../node/services/messaging/RPCServer.kt | 226 ++++++++++-------- 8 files changed, 306 insertions(+), 199 deletions(-) create mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/internal/DeduplicationChecker.kt diff --git a/.idea/compiler.xml b/.idea/compiler.xml index d8d9f1498a..dba1dad5e2 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -72,6 +72,7 @@ + @@ -159,4 +160,4 @@ - + \ No newline at end of file diff --git a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt index 45b5bd3d0b..de74d563cb 100644 --- a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt +++ b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt @@ -19,6 +19,7 @@ import org.apache.activemq.artemis.api.core.SimpleString import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue +import org.junit.Ignore import org.junit.Rule import org.junit.Test import rx.Observable @@ -127,7 +128,7 @@ class RPCStabilityTests { rpcDriver { fun startAndCloseServer(broker: RpcBrokerHandle) { startRpcServerWithBrokerRunning( - configuration = RPCServerConfiguration.default.copy(consumerPoolSize = 1, producerPoolBound = 1), + configuration = RPCServerConfiguration.default, ops = DummyOps, brokerHandle = broker ).rpcServer.close() @@ -148,7 +149,7 @@ class RPCStabilityTests { @Test fun `rpc client close doesnt leak broker resources`() { rpcDriver { - val server = startRpcServer(configuration = RPCServerConfiguration.default.copy(consumerPoolSize = 1, producerPoolBound = 1), ops = DummyOps).get() + val server = startRpcServer(configuration = RPCServerConfiguration.default, ops = DummyOps).get() RPCClient(server.broker.hostAndPort!!).start(RPCOps::class.java, rpcTestUser.username, rpcTestUser.password).close() val initial = server.broker.getStats() repeat(100) { @@ -337,11 +338,12 @@ class RPCStabilityTests { val request = RPCApi.ClientToServer.RpcRequest( clientAddress = SimpleString(myQueue), methodName = SlowConsumerRPCOps::streamAtInterval.name, - serialisedArguments = listOf(10.millis, 123456).serialize(context = SerializationDefaults.RPC_SERVER_CONTEXT).bytes, + serialisedArguments = listOf(10.millis, 123456).serialize(context = SerializationDefaults.RPC_SERVER_CONTEXT), replyId = Trace.InvocationId.newInstance(), sessionId = Trace.SessionId.newInstance() ) request.writeToClientMessage(message) + message.putLongProperty(RPCApi.DEDUPLICATION_SEQUENCE_NUMBER_FIELD_NAME, 0) producer.send(message) session.commit() @@ -350,6 +352,46 @@ class RPCStabilityTests { } } + interface StreamOps : RPCOps { + fun stream(streamInterval: Duration): Observable + } + class StreamOpsImpl : StreamOps { + override val protocolVersion = 0 + override fun stream(streamInterval: Duration): Observable { + return Observable.interval(streamInterval.toNanos(), TimeUnit.NANOSECONDS) + } + } + @Ignore("This is flaky as sometimes artemis delivers out of order messages after the kick") + @Test + fun `deduplication on the client side`() { + rpcDriver { + val server = startRpcServer(ops = StreamOpsImpl()).getOrThrow() + val proxy = startRpcClient( + server.broker.hostAndPort!!, + configuration = RPCClientConfiguration.default.copy( + connectionRetryInterval = 1.days // switch off failover + ) + ).getOrThrow() + // Find the internal address of the client + val clientAddress = server.broker.serverControl.addressNames.find { it.startsWith(RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX) } + val events = ArrayList() + // Start streaming an incrementing value 2000 times per second from the server. + val subscription = proxy.stream(streamInterval = Duration.ofNanos(500_000)).subscribe { + events.add(it) + } + // These sleeps are *fine*, the invariant should hold regardless of any delays + Thread.sleep(50) + // Kick the client. This seems to trigger redelivery of (presumably non-acked) messages. + server.broker.serverControl.closeConsumerConnectionsForAddress(clientAddress) + Thread.sleep(50) + subscription.unsubscribe() + for (i in 0 until events.size) { + require(events[i] == i.toLong()) { + "Events not incremental, possible duplicate, ${events[i]} != ${i.toLong()}\nExpected: ${(0..i).toList()}\nGot : $events\n" + } + } + } + } } fun RPCDriverDSL.pollUntilClientNumber(server: RpcServerHandle, expected: Int) { diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt index 756f07216e..a2520317d5 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt @@ -42,8 +42,6 @@ data class RPCClientConfiguration( val reapInterval: Duration, /** The number of threads to use for observations (for executing [Observable.onNext]) */ val observationExecutorPoolSize: Int, - /** The maximum number of producers to create to handle outgoing messages */ - val producerPoolBound: Int, /** * Determines the concurrency level of the Observable Cache. This is exposed because it implicitly determines * the limit on the number of leaked observables reaped because of garbage collection per reaping. @@ -56,9 +54,12 @@ data class RPCClientConfiguration( val connectionRetryIntervalMultiplier: Double, /** Maximum retry interval */ val connectionMaxRetryInterval: Duration, + /** Maximum reconnect attempts on failover */ val maxReconnectAttempts: Int, /** Maximum file size */ - val maxFileSize: Int + val maxFileSize: Int, + /** The cache expiry of a deduplication watermark per client. */ + val deduplicationCacheExpiry: Duration ) { companion object { val unlimitedReconnectAttempts = -1 @@ -68,14 +69,14 @@ data class RPCClientConfiguration( trackRpcCallSites = false, reapInterval = 1.seconds, observationExecutorPoolSize = 4, - producerPoolBound = 1, cacheConcurrencyLevel = 8, connectionRetryInterval = 5.seconds, connectionRetryIntervalMultiplier = 1.5, connectionMaxRetryInterval = 3.minutes, maxReconnectAttempts = unlimitedReconnectAttempts, /** 10 MiB maximum allowed file size for attachments, including message headers. TODO: acquire this value from Network Map when supported. */ - maxFileSize = 10485760 + maxFileSize = 10485760, + deduplicationCacheExpiry = 1.days ) } } diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt index 813569dc89..2f65f21e40 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt @@ -15,7 +15,6 @@ import net.corda.client.rpc.RPCSinceVersion import net.corda.core.context.Actor import net.corda.core.context.Trace import net.corda.core.context.Trace.InvocationId -import net.corda.core.internal.LazyPool import net.corda.core.internal.LazyStickyPool import net.corda.core.internal.LifeCycle import net.corda.core.internal.ThreadBox @@ -26,14 +25,12 @@ import net.corda.core.utilities.Try import net.corda.core.utilities.contextLogger import net.corda.core.utilities.debug import net.corda.core.utilities.getOrThrow -import net.corda.nodeapi.ArtemisConsumer -import net.corda.nodeapi.ArtemisProducer import net.corda.nodeapi.RPCApi +import net.corda.nodeapi.internal.DeduplicationChecker import org.apache.activemq.artemis.api.core.RoutingType import org.apache.activemq.artemis.api.core.SimpleString +import org.apache.activemq.artemis.api.core.client.* import org.apache.activemq.artemis.api.core.client.ActiveMQClient.DEFAULT_ACK_BATCH_SIZE -import org.apache.activemq.artemis.api.core.client.ClientMessage -import org.apache.activemq.artemis.api.core.client.ServerLocator import rx.Notification import rx.Observable import rx.subjects.UnicastSubject @@ -43,6 +40,7 @@ import java.time.Instant import java.util.* import java.util.concurrent.* import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.atomic.AtomicLong import kotlin.reflect.jvm.javaMethod /** @@ -111,6 +109,8 @@ class RPCClientProxyHandler( // Used for reaping private var reaperExecutor: ScheduledExecutorService? = null + // Used for sending + private var sendExecutor: ExecutorService? = null // A sticky pool for running Observable.onNext()s. We need the stickiness to preserve the observation ordering. private val observationExecutorThreadFactory = ThreadFactoryBuilder().setNameFormat("rpc-client-observation-pool-%d").setDaemon(true).build() @@ -161,22 +161,14 @@ class RPCClientProxyHandler( build() } - // We cannot pool consumers as we need to preserve the original muxed message order. - // TODO We may need to pool these somehow anyway, otherwise if the server sends many big messages in parallel a - // single consumer may be starved for flow control credits. Recheck this once Artemis's large message streaming is - // integrated properly. - private var sessionAndConsumer: ArtemisConsumer? = null - // Pool producers to reduce contention on the client side. - private val sessionAndProducerPool = LazyPool(bound = rpcConfiguration.producerPoolBound) { - // Note how we create new sessions *and* session factories per producer. - // We cannot simply pool producers on one session because sessions are single threaded. - // We cannot simply pool sessions on one session factory because flow control credits are tied to factories, so - // sessions tend to starve each other when used concurrently. - val sessionFactory = serverLocator.createSessionFactory() - val session = sessionFactory.createSession(rpcUsername, rpcPassword, false, true, true, false, DEFAULT_ACK_BATCH_SIZE) - session.start() - ArtemisProducer(sessionFactory, session, session.createProducer(RPCApi.RPC_SERVER_QUEUE_NAME)) - } + private var sessionFactory: ClientSessionFactory? = null + private var producerSession: ClientSession? = null + private var consumerSession: ClientSession? = null + private var rpcProducer: ClientProducer? = null + private var rpcConsumer: ClientConsumer? = null + + private val deduplicationChecker = DeduplicationChecker(rpcConfiguration.deduplicationCacheExpiry) + private val deduplicationSequenceNumber = AtomicLong(0) /** * Start the client. This creates the per-client queue, starts the consumer session and the reaper. @@ -187,22 +179,25 @@ class RPCClientProxyHandler( 1, ThreadFactoryBuilder().setNameFormat("rpc-client-reaper-%d").setDaemon(true).build() ) + sendExecutor = Executors.newSingleThreadExecutor( + ThreadFactoryBuilder().setNameFormat("rpc-client-sender-%d").build() + ) reaperScheduledFuture = reaperExecutor!!.scheduleAtFixedRate( this::reapObservablesAndNotify, rpcConfiguration.reapInterval.toMillis(), rpcConfiguration.reapInterval.toMillis(), TimeUnit.MILLISECONDS ) - sessionAndProducerPool.run { - it.session.createTemporaryQueue(clientAddress, RoutingType.ANYCAST, clientAddress) - } - val sessionFactory = serverLocator.createSessionFactory() - val session = sessionFactory.createSession(rpcUsername, rpcPassword, false, true, true, false, DEFAULT_ACK_BATCH_SIZE) - val consumer = session.createConsumer(clientAddress) - consumer.setMessageHandler(this@RPCClientProxyHandler::artemisMessageHandler) - sessionAndConsumer = ArtemisConsumer(sessionFactory, session, consumer) + sessionFactory = serverLocator.createSessionFactory() + producerSession = sessionFactory!!.createSession(rpcUsername, rpcPassword, false, true, true, false, DEFAULT_ACK_BATCH_SIZE) + rpcProducer = producerSession!!.createProducer(RPCApi.RPC_SERVER_QUEUE_NAME) + consumerSession = sessionFactory!!.createSession(rpcUsername, rpcPassword, false, true, true, false, DEFAULT_ACK_BATCH_SIZE) + consumerSession!!.createTemporaryQueue(clientAddress, RoutingType.ANYCAST, clientAddress) + rpcConsumer = consumerSession!!.createConsumer(clientAddress) + rpcConsumer!!.setMessageHandler(this::artemisMessageHandler) lifeCycle.transition(State.UNSTARTED, State.SERVER_VERSION_NOT_SET) - session.start() + consumerSession!!.start() + producerSession!!.start() } // This is the general function that transforms a client side RPC to internal Artemis messages. @@ -212,7 +207,7 @@ class RPCClientProxyHandler( if (method == toStringMethod) { return "Client RPC proxy for $rpcOpsClass" } - if (sessionAndConsumer!!.session.isClosed) { + if (consumerSession!!.isClosed) { throw RPCException("RPC Proxy is closed") } @@ -220,23 +215,20 @@ class RPCClientProxyHandler( callSiteMap?.set(replyId, Throwable("")) try { val serialisedArguments = (arguments?.toList() ?: emptyList()).serialize(context = serializationContextWithObservableContext) - val request = RPCApi.ClientToServer.RpcRequest(clientAddress, method.name, serialisedArguments.bytes, replyId, sessionId, externalTrace, impersonatedActor) + val request = RPCApi.ClientToServer.RpcRequest( + clientAddress, + method.name, + serialisedArguments, + replyId, + sessionId, + externalTrace, + impersonatedActor + ) val replyFuture = SettableFuture.create() - sessionAndProducerPool.run { - val message = it.session.createMessage(false) - request.writeToClientMessage(message) - - log.debug { - val argumentsString = arguments?.joinToString() ?: "" - "-> RPC(${replyId.value}) -> ${method.name}($argumentsString): ${method.returnType}" - } - - require(rpcReplyMap.put(replyId, replyFuture) == null) { - "Generated several RPC requests with same ID $replyId" - } - it.producer.send(message) - it.session.commit() + require(rpcReplyMap.put(replyId, replyFuture) == null) { + "Generated several RPC requests with same ID $replyId" } + sendMessage(request) return replyFuture.getOrThrow() } catch (e: RuntimeException) { // Already an unchecked exception, so just rethrow it @@ -249,9 +241,24 @@ class RPCClientProxyHandler( } } + private fun sendMessage(message: RPCApi.ClientToServer) { + val artemisMessage = producerSession!!.createMessage(false) + message.writeToClientMessage(artemisMessage) + sendExecutor!!.submit { + artemisMessage.putLongProperty(RPCApi.DEDUPLICATION_SEQUENCE_NUMBER_FIELD_NAME, deduplicationSequenceNumber.getAndIncrement()) + log.debug { "-> RPC -> $message" } + rpcProducer!!.send(artemisMessage) + } + } + // The handler for Artemis messages. private fun artemisMessageHandler(message: ClientMessage) { val serverToClient = RPCApi.ServerToClient.fromClientMessage(serializationContextWithObservableContext, message) + val deduplicationSequenceNumber = message.getLongProperty(RPCApi.DEDUPLICATION_SEQUENCE_NUMBER_FIELD_NAME) + if (deduplicationChecker.checkDuplicateMessageId(serverToClient.deduplicationIdentity, deduplicationSequenceNumber)) { + log.info("Message duplication detected, discarding message") + return + } log.debug { "Got message from RPC server $serverToClient" } when (serverToClient) { is RPCApi.ServerToClient.RpcReply -> { @@ -325,14 +332,12 @@ class RPCClientProxyHandler( * @param notify whether to notify observables or not. */ private fun close(notify: Boolean = true) { - sessionAndConsumer?.sessionFactory?.close() + sessionFactory?.close() reaperScheduledFuture?.cancel(false) observableContext.observableMap.invalidateAll() reapObservables(notify) reaperExecutor?.shutdownNow() - sessionAndProducerPool.close().forEach { - it.sessionFactory.close() - } + sendExecutor?.shutdownNow() // Note the ordering is important, we shut down the consumer *before* the observation executor, otherwise we may // leak borrowed executors. val observationExecutors = observationExecutorPool.close() @@ -385,11 +390,7 @@ class RPCClientProxyHandler( } if (observableIds != null) { log.debug { "Reaping ${observableIds.size} observables" } - sessionAndProducerPool.run { - val message = it.session.createMessage(false) - RPCApi.ClientToServer.ObservablesClosed(observableIds).writeToClientMessage(message) - it.producer.send(message) - } + sendMessage(RPCApi.ClientToServer.ObservablesClosed(observableIds)) } } } diff --git a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPerformanceTests.kt b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPerformanceTests.kt index b945487cb9..689674d22f 100644 --- a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPerformanceTests.kt +++ b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPerformanceTests.kt @@ -3,6 +3,7 @@ package net.corda.client.rpc import com.google.common.base.Stopwatch import net.corda.client.rpc.internal.RPCClientConfiguration import net.corda.core.messaging.RPCOps +import net.corda.core.utilities.days import net.corda.core.utilities.minutes import net.corda.core.utilities.seconds import net.corda.node.services.messaging.RPCServerConfiguration @@ -87,13 +88,10 @@ class RPCPerformanceTests : AbstractRPCTest() { val proxy = testProxy( RPCClientConfiguration.default.copy( cacheConcurrencyLevel = 16, - observationExecutorPoolSize = 2, - producerPoolBound = 2 + observationExecutorPoolSize = 2 ), RPCServerConfiguration.default.copy( - rpcThreadPoolSize = 8, - consumerPoolSize = 2, - producerPoolBound = 8 + rpcThreadPoolSize = 8 ) ) @@ -130,13 +128,10 @@ class RPCPerformanceTests : AbstractRPCTest() { val proxy = testProxy( RPCClientConfiguration.default.copy( reapInterval = 1.seconds, - cacheConcurrencyLevel = 16, - producerPoolBound = 8 + cacheConcurrencyLevel = 16 ), RPCServerConfiguration.default.copy( - rpcThreadPoolSize = 8, - consumerPoolSize = 1, - producerPoolBound = 8 + rpcThreadPoolSize = 8 ) ) startPublishingFixedRateInjector( @@ -165,9 +160,7 @@ class RPCPerformanceTests : AbstractRPCTest() { rpcDriver { val proxy = testProxy( RPCClientConfiguration.default, - RPCServerConfiguration.default.copy( - consumerPoolSize = 1 - ) + RPCServerConfiguration.default ) val numberOfMessages = 1000 val bigSize = 10_000_000 diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/RPCApi.kt b/node-api/src/main/kotlin/net/corda/nodeapi/RPCApi.kt index 96bd8c0560..338bac1bfd 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/RPCApi.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/RPCApi.kt @@ -10,6 +10,7 @@ import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.core.utilities.Id +import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.Try import org.apache.activemq.artemis.api.core.ActiveMQBuffer import org.apache.activemq.artemis.api.core.SimpleString @@ -72,6 +73,9 @@ object RPCApi { const val RPC_CLIENT_BINDING_ADDITIONS = "rpc.clientqueueadditions" const val RPC_TARGET_LEGAL_IDENTITY = "rpc-target-legal-identity" + const val DEDUPLICATION_SEQUENCE_NUMBER_FIELD_NAME = "deduplication-sequence-number" + + val RPC_CLIENT_BINDING_REMOVAL_FILTER_EXPRESSION = "${ManagementHelper.HDR_NOTIFICATION_TYPE} = '${CoreNotificationType.BINDING_REMOVED.name}' AND " + "${ManagementHelper.HDR_ROUTING_NAME} LIKE '$RPC_CLIENT_QUEUE_NAME_PREFIX.%'" @@ -94,6 +98,8 @@ object RPCApi { OBSERVABLES_CLOSED } + abstract fun writeToClientMessage(message: ClientMessage) + /** * Request to a server to trigger the specified method with the provided arguments. * @@ -105,13 +111,13 @@ object RPCApi { data class RpcRequest( val clientAddress: SimpleString, val methodName: String, - val serialisedArguments: ByteArray, + val serialisedArguments: OpaqueBytes, val replyId: InvocationId, val sessionId: SessionId, val externalTrace: Trace? = null, val impersonatedActor: Actor? = null ) : ClientToServer() { - fun writeToClientMessage(message: ClientMessage) { + override fun writeToClientMessage(message: ClientMessage) { MessageUtil.setJMSReplyTo(message, clientAddress) message.putIntProperty(TAG_FIELD_NAME, Tag.RPC_REQUEST.ordinal) @@ -122,12 +128,12 @@ object RPCApi { impersonatedActor?.mapToImpersonated(message) message.putStringProperty(METHOD_NAME_FIELD_NAME, methodName) - message.bodyBuffer.writeBytes(serialisedArguments) + message.bodyBuffer.writeBytes(serialisedArguments.bytes) } } data class ObservablesClosed(val ids: List) : ClientToServer() { - fun writeToClientMessage(message: ClientMessage) { + override fun writeToClientMessage(message: ClientMessage) { message.putIntProperty(TAG_FIELD_NAME, Tag.OBSERVABLES_CLOSED.ordinal) val buffer = message.bodyBuffer buffer.writeInt(ids.size) @@ -144,7 +150,7 @@ object RPCApi { RPCApi.ClientToServer.Tag.RPC_REQUEST -> RpcRequest( clientAddress = MessageUtil.getJMSReplyTo(message), methodName = message.getStringProperty(METHOD_NAME_FIELD_NAME), - serialisedArguments = message.getBodyAsByteArray(), + serialisedArguments = OpaqueBytes(message.getBodyAsByteArray()), replyId = message.replyId(), sessionId = message.sessionId(), externalTrace = message.externalTrace(), @@ -175,13 +181,21 @@ object RPCApi { abstract fun writeToClientMessage(context: SerializationContext, message: ClientMessage) - /** Reply in response to an [ClientToServer.RpcRequest]. */ + abstract val deduplicationIdentity: String + + /** + * Reply in response to an [ClientToServer.RpcRequest]. + * @property deduplicationSequenceNumber a sequence number strictly incrementing with each message. Use this for + * duplicate detection on the client. + */ data class RpcReply( val id: InvocationId, - val result: Try + val result: Try, + override val deduplicationIdentity: String ) : ServerToClient() { override fun writeToClientMessage(context: SerializationContext, message: ClientMessage) { message.putIntProperty(TAG_FIELD_NAME, Tag.RPC_REPLY.ordinal) + message.putStringProperty(DEDUPLICATION_IDENTITY_FIELD_NAME, deduplicationIdentity) id.mapTo(message, RPC_ID_FIELD_NAME, RPC_ID_TIMESTAMP_FIELD_NAME) message.bodyBuffer.writeBytes(result.safeSerialize(context) { Try.Failure(it) }.bytes) } @@ -189,10 +203,12 @@ object RPCApi { data class Observation( val id: InvocationId, - val content: Notification<*> + val content: Notification<*>, + override val deduplicationIdentity: String ) : ServerToClient() { override fun writeToClientMessage(context: SerializationContext, message: ClientMessage) { message.putIntProperty(TAG_FIELD_NAME, Tag.OBSERVATION.ordinal) + message.putStringProperty(DEDUPLICATION_IDENTITY_FIELD_NAME, deduplicationIdentity) id.mapTo(message, OBSERVABLE_ID_FIELD_NAME, OBSERVABLE_ID_TIMESTAMP_FIELD_NAME) message.bodyBuffer.writeBytes(content.safeSerialize(context) { Notification.createOnError(it) }.bytes) } @@ -207,17 +223,26 @@ object RPCApi { fun fromClientMessage(context: SerializationContext, message: ClientMessage): ServerToClient { val tag = Tag.values()[message.getIntProperty(TAG_FIELD_NAME)] + val deduplicationIdentity = message.getStringProperty(DEDUPLICATION_IDENTITY_FIELD_NAME) return when (tag) { RPCApi.ServerToClient.Tag.RPC_REPLY -> { val id = message.invocationId(RPC_ID_FIELD_NAME, RPC_ID_TIMESTAMP_FIELD_NAME) ?: throw IllegalStateException("Cannot parse invocation id from client message.") val poolWithIdContext = context.withProperty(RpcRequestOrObservableIdKey, id) - RpcReply(id, message.getBodyAsByteArray().deserialize(context = poolWithIdContext)) + RpcReply( + id = id, + deduplicationIdentity = deduplicationIdentity, + result = message.getBodyAsByteArray().deserialize(context = poolWithIdContext) + ) } RPCApi.ServerToClient.Tag.OBSERVATION -> { val observableId = message.invocationId(OBSERVABLE_ID_FIELD_NAME, OBSERVABLE_ID_TIMESTAMP_FIELD_NAME) ?: throw IllegalStateException("Cannot parse invocation id from client message.") val poolWithIdContext = context.withProperty(RpcRequestOrObservableIdKey, observableId) val payload = message.getBodyAsByteArray().deserialize>(context = poolWithIdContext) - Observation(observableId, payload) + Observation( + id = observableId, + deduplicationIdentity = deduplicationIdentity, + content = payload + ) } } } @@ -225,18 +250,6 @@ object RPCApi { } } -data class ArtemisProducer( - val sessionFactory: ClientSessionFactory, - val session: ClientSession, - val producer: ClientProducer -) - -data class ArtemisConsumer( - val sessionFactory: ClientSessionFactory, - val session: ClientSession, - val consumer: ClientConsumer -) - private val TAG_FIELD_NAME = "tag" private val RPC_ID_FIELD_NAME = "rpc-id" private val RPC_ID_TIMESTAMP_FIELD_NAME = "rpc-id-timestamp" @@ -249,6 +262,7 @@ private val RPC_EXTERNAL_SESSION_ID_TIMESTAMP_FIELD_NAME = "rpc-external-session private val RPC_IMPERSONATED_ACTOR_ID = "rpc-impersonated-actor-id" private val RPC_IMPERSONATED_ACTOR_STORE_ID = "rpc-impersonated-actor-store-id" private val RPC_IMPERSONATED_ACTOR_OWNING_LEGAL_IDENTITY = "rpc-impersonated-actor-owningLegalIdentity" +private val DEDUPLICATION_IDENTITY_FIELD_NAME = "deduplication-identity" private val OBSERVABLE_ID_FIELD_NAME = "observable-id" private val OBSERVABLE_ID_TIMESTAMP_FIELD_NAME = "observable-id-timestamp" private val METHOD_NAME_FIELD_NAME = "method-name" diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/DeduplicationChecker.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/DeduplicationChecker.kt new file mode 100644 index 0000000000..2fc69bbd1e --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/DeduplicationChecker.kt @@ -0,0 +1,29 @@ +package net.corda.nodeapi.internal + +import com.google.common.cache.CacheBuilder +import com.google.common.cache.CacheLoader +import java.time.Duration +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicLong + +/** + * A class allowing the deduplication of a strictly incrementing sequence number. + */ +class DeduplicationChecker(cacheExpiry: Duration) { + // dedupe identity -> watermark cache + private val watermarkCache = CacheBuilder.newBuilder() + .expireAfterAccess(cacheExpiry.toNanos(), TimeUnit.NANOSECONDS) + .build(WatermarkCacheLoader) + + private object WatermarkCacheLoader : CacheLoader() { + override fun load(key: Any) = AtomicLong(-1) + } + + /** + * @param identity the identity that generates the sequence numbers. + * @param sequenceNumber the sequence number to check. + */ + fun checkDuplicateMessageId(identity: Any, sequenceNumber: Long): Boolean { + return watermarkCache[identity].getAndUpdate { maxOf(sequenceNumber, it) } >= sequenceNumber + } +} diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt b/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt index 0e06674aa0..eca9ce1601 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt @@ -18,9 +18,7 @@ import net.corda.core.context.InvocationContext import net.corda.core.context.Trace import net.corda.core.context.Trace.InvocationId import net.corda.core.identity.CordaX500Name -import net.corda.core.internal.LazyStickyPool import net.corda.core.internal.LifeCycle -import net.corda.core.internal.join import net.corda.core.messaging.RPCOps import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationDefaults.RPC_SERVER_CONTEXT @@ -29,14 +27,14 @@ import net.corda.core.utilities.* import net.corda.node.internal.security.AuthorizingSubject import net.corda.node.internal.security.RPCSecurityManager import net.corda.node.services.logging.pushToLoggingContext -import net.corda.nodeapi.* +import net.corda.nodeapi.RPCApi +import net.corda.nodeapi.externalTrace +import net.corda.nodeapi.impersonatedActor +import net.corda.nodeapi.internal.DeduplicationChecker import org.apache.activemq.artemis.api.core.Message import org.apache.activemq.artemis.api.core.SimpleString +import org.apache.activemq.artemis.api.core.client.* import org.apache.activemq.artemis.api.core.client.ActiveMQClient.DEFAULT_ACK_BATCH_SIZE -import org.apache.activemq.artemis.api.core.client.ClientConsumer -import org.apache.activemq.artemis.api.core.client.ClientMessage -import org.apache.activemq.artemis.api.core.client.ClientSession -import org.apache.activemq.artemis.api.core.client.ServerLocator import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl import org.apache.activemq.artemis.api.core.management.CoreNotificationType import org.apache.activemq.artemis.api.core.management.ManagementHelper @@ -49,24 +47,26 @@ import rx.Subscription import java.lang.reflect.InvocationTargetException import java.lang.reflect.Method import java.time.Duration +import java.util.* import java.util.concurrent.* +import kotlin.concurrent.thread data class RPCServerConfiguration( /** The number of threads to use for handling RPC requests */ val rpcThreadPoolSize: Int, - /** The number of consumers to handle incoming messages */ - val consumerPoolSize: Int, - /** The maximum number of producers to create to handle outgoing messages */ - val producerPoolBound: Int, /** The interval of subscription reaping */ - val reapInterval: Duration + val reapInterval: Duration, + /** The cache expiry of a deduplication watermark per client. */ + val deduplicationCacheExpiry: Duration, + /** The size of the send queue */ + val sendJobQueueSize: Int ) { companion object { val default = RPCServerConfiguration( rpcThreadPoolSize = 4, - consumerPoolSize = 2, - producerPoolBound = 4, - reapInterval = 1.seconds + reapInterval = 1.seconds, + deduplicationCacheExpiry = 1.days, + sendJobQueueSize = 256 ) } } @@ -115,22 +115,24 @@ class RPCServer( /** The scheduled reaper handle. */ private var reaperScheduledFuture: ScheduledFuture<*>? = null - private var observationSendExecutor: ExecutorService? = null + private var senderThread: Thread? = null private var rpcExecutor: ScheduledExecutorService? = null private var reaperExecutor: ScheduledExecutorService? = null - private val sessionAndConsumers = ArrayList(rpcConfiguration.consumerPoolSize) - private val sessionAndProducerPool = LazyStickyPool(rpcConfiguration.producerPoolBound) { - val sessionFactory = serverLocator.createSessionFactory() - val session = sessionFactory.createSession(rpcServerUsername, rpcServerPassword, false, true, true, false, DEFAULT_ACK_BATCH_SIZE) - session.start() - ArtemisProducer(sessionFactory, session, session.createProducer()) - } + private var sessionFactory: ClientSessionFactory? = null + private var producerSession: ClientSession? = null + private var consumerSession: ClientSession? = null + private var rpcProducer: ClientProducer? = null + private var rpcConsumer: ClientConsumer? = null private var clientBindingRemovalConsumer: ClientConsumer? = null private var clientBindingAdditionConsumer: ClientConsumer? = null private var serverControl: ActiveMQServerControl? = null private val responseMessageBuffer = ConcurrentHashMap() + private val sendJobQueue = ArrayBlockingQueue(rpcConfiguration.sendJobQueueSize) + + private val deduplicationChecker = DeduplicationChecker(rpcConfiguration.deduplicationCacheExpiry) + private var deduplicationIdentity: String? = null init { val groupedMethods = ops.javaClass.declaredMethods.groupBy { it.name } @@ -154,16 +156,12 @@ class RPCServer( try { lifeCycle.requireState(State.UNSTARTED) log.info("Starting RPC server with configuration $rpcConfiguration") - observationSendExecutor = Executors.newFixedThreadPool( - 1, - ThreadFactoryBuilder().setNameFormat("rpc-observation-sender-%d").build() - ) + senderThread = startSenderThread() rpcExecutor = Executors.newScheduledThreadPool( rpcConfiguration.rpcThreadPoolSize, ThreadFactoryBuilder().setNameFormat("rpc-server-handler-pool-%d").build() ) - reaperExecutor = Executors.newScheduledThreadPool( - 1, + reaperExecutor = Executors.newSingleThreadScheduledExecutor( ThreadFactoryBuilder().setNameFormat("rpc-server-reaper-%d").build() ) reaperScheduledFuture = reaperExecutor!!.scheduleAtFixedRate( @@ -172,55 +170,77 @@ class RPCServer( rpcConfiguration.reapInterval.toMillis(), TimeUnit.MILLISECONDS ) - val sessions = createConsumerSessions() - createNotificationConsumers() + + sessionFactory = serverLocator.createSessionFactory() + producerSession = sessionFactory!!.createSession(rpcServerUsername, rpcServerPassword, false, true, true, false, DEFAULT_ACK_BATCH_SIZE) + createRpcProducer(producerSession!!) + consumerSession = sessionFactory!!.createSession(rpcServerUsername, rpcServerPassword, false, true, true, false, DEFAULT_ACK_BATCH_SIZE) + createRpcConsumer(consumerSession!!) + createNotificationConsumers(consumerSession!!) serverControl = activeMqServerControl + deduplicationIdentity = UUID.randomUUID().toString() lifeCycle.transition(State.UNSTARTED, State.STARTED) // We delay the consumer session start because Artemis starts delivering messages immediately, so we need to be // fully initialised. - sessions.forEach { - it.start() - } + producerSession!!.start() + consumerSession!!.start() } catch (exception: Throwable) { close() throw exception } } - private fun createConsumerSessions(): ArrayList { - val sessions = ArrayList() - for (i in 1..rpcConfiguration.consumerPoolSize) { - val sessionFactory = serverLocator.createSessionFactory() - val session = sessionFactory.createSession(rpcServerUsername, rpcServerPassword, false, true, true, false, DEFAULT_ACK_BATCH_SIZE) - val consumer = session.createConsumer(RPCApi.RPC_SERVER_QUEUE_NAME) - consumer.setMessageHandler(this@RPCServer::clientArtemisMessageHandler) - sessionAndConsumers.add(ArtemisConsumer(sessionFactory, session, consumer)) - sessions.add(session) - } - return sessions + private fun createRpcProducer(producerSession: ClientSession) { + rpcProducer = producerSession.createProducer() } - private fun createNotificationConsumers() { - clientBindingRemovalConsumer = sessionAndConsumers[0].session.createConsumer(RPCApi.RPC_CLIENT_BINDING_REMOVALS) + private fun createRpcConsumer(consumerSession: ClientSession) { + rpcConsumer = consumerSession.createConsumer(RPCApi.RPC_SERVER_QUEUE_NAME) + rpcConsumer!!.setMessageHandler(this::clientArtemisMessageHandler) + } + + private fun createNotificationConsumers(consumerSession: ClientSession) { + clientBindingRemovalConsumer = consumerSession.createConsumer(RPCApi.RPC_CLIENT_BINDING_REMOVALS) clientBindingRemovalConsumer!!.setMessageHandler(this::bindingRemovalArtemisMessageHandler) - clientBindingAdditionConsumer = sessionAndConsumers[0].session.createConsumer(RPCApi.RPC_CLIENT_BINDING_ADDITIONS) + clientBindingAdditionConsumer = consumerSession.createConsumer(RPCApi.RPC_CLIENT_BINDING_ADDITIONS) clientBindingAdditionConsumer!!.setMessageHandler(this::bindingAdditionArtemisMessageHandler) } + private fun startSenderThread(): Thread { + return thread(name = "rpc-server-sender", isDaemon = true) { + var deduplicationSequenceNumber = 0L + while (true) { + val job = sendJobQueue.poll() + when (job) { + is RpcSendJob.Send -> handleSendJob(deduplicationSequenceNumber++, job) + RpcSendJob.Stop -> return@thread + } + } + } + } + + private fun handleSendJob(sequenceNumber: Long, job: RpcSendJob.Send) { + try { + job.artemisMessage.putLongProperty(RPCApi.DEDUPLICATION_SEQUENCE_NUMBER_FIELD_NAME, sequenceNumber) + rpcProducer!!.send(job.clientAddress, job.artemisMessage) + log.debug { "<- RPC <- ${job.originalMessage}" } + } catch (throwable: Throwable) { + log.error("Failed to send message, kicking client. Message was ${job.originalMessage}", throwable) + serverControl!!.closeConsumerConnectionsForAddress(job.clientAddress.toString()) + invalidateClient(job.clientAddress) + } + } + fun close() { - observationSendExecutor?.join() + sendJobQueue.put(RpcSendJob.Stop) + senderThread?.join() reaperScheduledFuture?.cancel(false) rpcExecutor?.shutdownNow() reaperExecutor?.shutdownNow() securityManager.close() - sessionAndConsumers.forEach { - it.sessionFactory.close() - } + sessionFactory?.close() observableMap.invalidateAll() reapSubscriptions() - sessionAndProducerPool.close().forEach { - it.sessionFactory.close() - } lifeCycle.justTransition(State.FINISHED) } @@ -273,6 +293,14 @@ class RPCServer( log.debug { "-> RPC -> $clientToServer" } when (clientToServer) { is RPCApi.ClientToServer.RpcRequest -> { + val deduplicationSequenceNumber = artemisMessage.getLongProperty(RPCApi.DEDUPLICATION_SEQUENCE_NUMBER_FIELD_NAME) + if (deduplicationChecker.checkDuplicateMessageId( + identity = clientToServer.clientAddress, + sequenceNumber = deduplicationSequenceNumber + )) { + log.info("Message duplication detected, discarding message") + return + } val arguments = Try.on { clientToServer.serialisedArguments.deserialize>(context = RPC_SERVER_CONTEXT) } @@ -316,15 +344,16 @@ class RPCServer( } private fun sendReply(replyId: InvocationId, clientAddress: SimpleString, result: Try) { - val reply = RPCApi.ServerToClient.RpcReply(replyId, result) + val reply = RPCApi.ServerToClient.RpcReply( + id = replyId, + result = result, + deduplicationIdentity = deduplicationIdentity!! + ) val observableContext = ObservableContext( - replyId, observableMap, clientAddressToObservables, - clientAddress, - serverControl!!, - sessionAndProducerPool, - observationSendExecutor!! + deduplicationIdentity!!, + clientAddress ) val buffered = bufferIfQueueNotBound(clientAddress, reply, observableContext) @@ -370,6 +399,34 @@ class RPCServer( val targetLegalIdentity = message.getStringProperty(RPCApi.RPC_TARGET_LEGAL_IDENTITY)?.let(CordaX500Name.Companion::parse) ?: nodeLegalName return Pair(Actor(Id(validatedUser), securityManager.id, targetLegalIdentity), securityManager.buildSubject(validatedUser)) } + + // We construct an observable context on each RPC request. If subsequently a nested Observable is + // encountered this same context is propagated by the instrumented KryoPool. This way all + // observations rooted in a single RPC will be muxed correctly. Note that the context construction + // itself is quite cheap. + inner class ObservableContext( + val observableMap: ObservableSubscriptionMap, + val clientAddressToObservables: SetMultimap, + val deduplicationIdentity: String, + val clientAddress: SimpleString + ) { + private val serializationContextWithObservableContext = RpcServerObservableSerializer.createContext(this) + + fun sendMessage(serverToClient: RPCApi.ServerToClient) { + val artemisMessage = producerSession!!.createMessage(false) + serverToClient.writeToClientMessage(serializationContextWithObservableContext, artemisMessage) + sendJobQueue.put(RpcSendJob.Send(clientAddress, artemisMessage, serverToClient)) + } + } + + private sealed class RpcSendJob { + data class Send( + val clientAddress: SimpleString, + val artemisMessage: ClientMessage, + val originalMessage: RPCApi.ServerToClient + ) : RpcSendJob() + object Stop : RpcSendJob() + } } // TODO replace this by creating a new CordaRPCImpl for each request, passing the context, after we fix Shell and WebServer @@ -417,45 +474,11 @@ class ObservableSubscription( typealias ObservableSubscriptionMap = Cache -// We construct an observable context on each RPC request. If subsequently a nested Observable is -// encountered this same context is propagated by the instrumented KryoPool. This way all -// observations rooted in a single RPC will be muxed correctly. Note that the context construction -// itself is quite cheap. -class ObservableContext( - val invocationId: InvocationId, - val observableMap: ObservableSubscriptionMap, - val clientAddressToObservables: SetMultimap, - val clientAddress: SimpleString, - val serverControl: ActiveMQServerControl, - val sessionAndProducerPool: LazyStickyPool, - val observationSendExecutor: ExecutorService -) { - private companion object { - private val log = contextLogger() - } - - private val serializationContextWithObservableContext = RpcServerObservableSerializer.createContext(this) - - fun sendMessage(serverToClient: RPCApi.ServerToClient) { - try { - sessionAndProducerPool.run(invocationId) { - val artemisMessage = it.session.createMessage(false) - serverToClient.writeToClientMessage(serializationContextWithObservableContext, artemisMessage) - it.producer.send(clientAddress, artemisMessage) - log.debug("<- RPC <- $serverToClient") - } - } catch (throwable: Throwable) { - log.error("Failed to send message, kicking client. Message was $serverToClient", throwable) - serverControl.closeConsumerConnectionsForAddress(clientAddress.toString()) - } - } -} - object RpcServerObservableSerializer : Serializer>() { private object RpcObservableContextKey private val log = LoggerFactory.getLogger(javaClass) - fun createContext(observableContext: ObservableContext): SerializationContext { + fun createContext(observableContext: RPCServer.ObservableContext): SerializationContext { return RPC_SERVER_CONTEXT.withProperty(RpcServerObservableSerializer.RpcObservableContextKey, observableContext) } @@ -465,7 +488,7 @@ object RpcServerObservableSerializer : Serializer>() { override fun write(kryo: Kryo, output: Output, observable: Observable<*>) { val observableId = InvocationId.newInstance() - val observableContext = kryo.context[RpcObservableContextKey] as ObservableContext + val observableContext = kryo.context[RpcObservableContextKey] as RPCServer.ObservableContext output.writeInvocationId(observableId) val observableWithSubscription = ObservableSubscription( // We capture [observableContext] in the subscriber. Note that all synchronisation/kryo borrowing @@ -474,9 +497,12 @@ object RpcServerObservableSerializer : Serializer>() { object : Subscriber>() { override fun onNext(observation: Notification<*>) { if (!isUnsubscribed) { - observableContext.observationSendExecutor.submit { - observableContext.sendMessage(RPCApi.ServerToClient.Observation(observableId, observation)) - } + val message = RPCApi.ServerToClient.Observation( + id = observableId, + content = observation, + deduplicationIdentity = observableContext.deduplicationIdentity + ) + observableContext.sendMessage(message) } } From 117f4a721ea90ec8c4fed51a11720a92447529d4 Mon Sep 17 00:00:00 2001 From: Anthony Keenan Date: Tue, 13 Feb 2018 21:39:31 +0000 Subject: [PATCH 17/50] CORDA-939 Remove sslConfiguration from public constructor of CordaRPCClient (#2522) * Remove sslConfiguration from public constructor of CordaRPCClient * Address review comments * Update api-current.txt * sslConfiguration doesn't need to be a property --- .ci/api-current.txt | 18 +++++++++++++++++- .../net/corda/client/rpc/CordaRPCClient.kt | 15 ++++++++++++++- .../client/rpc/internal/CordaRPCClientUtils.kt | 13 +++++++++++++ .../net/corda/client/rpc/internal/RPCClient.kt | 2 ++ .../net/corda/node/services/rpc/RpcSslTest.kt | 3 ++- .../testing/node/internal/DriverDSLImpl.kt | 3 ++- 6 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 client/rpc/src/main/kotlin/net/corda/client/rpc/internal/CordaRPCClientUtils.kt diff --git a/.ci/api-current.txt b/.ci/api-current.txt index 8ae7805a7a..b5cf7b32e1 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -3918,8 +3918,20 @@ public class net.corda.testing.node.MockNetwork extends java.lang.Object public (List, net.corda.testing.node.MockNetworkParameters) public (List, net.corda.testing.node.MockNetworkParameters, boolean, boolean, net.corda.testing.node.InMemoryMessagingNetwork$ServicePeerAllocationStrategy, boolean, List, int) @org.jetbrains.annotations.NotNull public final java.nio.file.Path baseDirectory(int) + @org.jetbrains.annotations.NotNull public final net.corda.testing.node.StartedMockNode createNode() + @org.jetbrains.annotations.NotNull public final net.corda.testing.node.StartedMockNode createNode(net.corda.core.identity.CordaX500Name) + @org.jetbrains.annotations.NotNull public final net.corda.testing.node.StartedMockNode createNode(net.corda.core.identity.CordaX500Name, Integer) + @org.jetbrains.annotations.NotNull public final net.corda.testing.node.StartedMockNode createNode(net.corda.core.identity.CordaX500Name, Integer, java.math.BigInteger) + @org.jetbrains.annotations.NotNull public final net.corda.testing.node.StartedMockNode createNode(net.corda.core.identity.CordaX500Name, Integer, java.math.BigInteger, kotlin.jvm.functions.Function1) + @org.jetbrains.annotations.NotNull public final net.corda.testing.node.StartedMockNode createNode(net.corda.core.identity.CordaX500Name, Integer, java.math.BigInteger, kotlin.jvm.functions.Function1, net.corda.node.VersionInfo) @org.jetbrains.annotations.NotNull public final net.corda.testing.node.StartedMockNode createNode(net.corda.testing.node.MockNodeParameters) @org.jetbrains.annotations.NotNull public final net.corda.testing.node.StartedMockNode createPartyNode(net.corda.core.identity.CordaX500Name) + @org.jetbrains.annotations.NotNull public final net.corda.testing.node.UnstartedMockNode createUnstartedNode() + @org.jetbrains.annotations.NotNull public final net.corda.testing.node.UnstartedMockNode createUnstartedNode(net.corda.core.identity.CordaX500Name) + @org.jetbrains.annotations.NotNull public final net.corda.testing.node.UnstartedMockNode createUnstartedNode(net.corda.core.identity.CordaX500Name, Integer) + @org.jetbrains.annotations.NotNull public final net.corda.testing.node.UnstartedMockNode createUnstartedNode(net.corda.core.identity.CordaX500Name, Integer, java.math.BigInteger) + @org.jetbrains.annotations.NotNull public final net.corda.testing.node.UnstartedMockNode createUnstartedNode(net.corda.core.identity.CordaX500Name, Integer, java.math.BigInteger, kotlin.jvm.functions.Function1) + @org.jetbrains.annotations.NotNull public final net.corda.testing.node.UnstartedMockNode createUnstartedNode(net.corda.core.identity.CordaX500Name, Integer, java.math.BigInteger, kotlin.jvm.functions.Function1, net.corda.node.VersionInfo) @org.jetbrains.annotations.NotNull public final net.corda.testing.node.UnstartedMockNode createUnstartedNode(net.corda.testing.node.MockNodeParameters) @org.jetbrains.annotations.NotNull public final List getCordappPackages() @org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party getDefaultNotaryIdentity() @@ -4000,6 +4012,8 @@ public final class net.corda.testing.node.MockNodeParameters extends java.lang.O public String toString() ## public class net.corda.testing.node.MockServices extends java.lang.Object implements net.corda.core.node.StateLoader, net.corda.core.node.ServiceHub + public () + public (List) public (List, net.corda.core.identity.CordaX500Name) public (List, net.corda.core.identity.CordaX500Name, net.corda.core.node.services.IdentityService) public (net.corda.core.identity.CordaX500Name) @@ -4166,10 +4180,12 @@ public final class net.corda.testing.node.User extends java.lang.Object public final class net.corda.client.rpc.CordaRPCClient extends java.lang.Object public (net.corda.core.utilities.NetworkHostAndPort) public (net.corda.core.utilities.NetworkHostAndPort, net.corda.client.rpc.CordaRPCClientConfiguration) - public (net.corda.core.utilities.NetworkHostAndPort, net.corda.client.rpc.CordaRPCClientConfiguration, net.corda.nodeapi.internal.config.SSLConfiguration) @org.jetbrains.annotations.NotNull public final net.corda.client.rpc.CordaRPCConnection start(String, String) @org.jetbrains.annotations.NotNull public final net.corda.client.rpc.CordaRPCConnection start(String, String, net.corda.core.context.Trace, net.corda.core.context.Actor) public final Object use(String, String, kotlin.jvm.functions.Function1) + public static final net.corda.client.rpc.CordaRPCClient$Companion Companion +## +public static final class net.corda.client.rpc.CordaRPCClient$Companion extends java.lang.Object ## public final class net.corda.client.rpc.CordaRPCClientConfiguration extends java.lang.Object public (java.time.Duration) diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt index 0c72192854..ef1eb579f2 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt @@ -70,11 +70,24 @@ data class CordaRPCClientConfiguration(val connectionMaxRetryInterval: Duration) * @param configuration An optional configuration used to tweak client behaviour. * @param sslConfiguration An optional [SSLConfiguration] used to enable secure communication with the server. */ -class CordaRPCClient @JvmOverloads constructor( +class CordaRPCClient private constructor( hostAndPort: NetworkHostAndPort, configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT, sslConfiguration: SSLConfiguration? = null ) { + @JvmOverloads + constructor(hostAndPort: NetworkHostAndPort, configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT) : this(hostAndPort, configuration, null) + + companion object { + internal fun createWithSsl( + hostAndPort: NetworkHostAndPort, + configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT, + sslConfiguration: SSLConfiguration? = null + ): CordaRPCClient { + return CordaRPCClient(hostAndPort, configuration, sslConfiguration) + } + } + init { try { effectiveSerializationEnv diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/CordaRPCClientUtils.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/CordaRPCClientUtils.kt new file mode 100644 index 0000000000..d5787f5dec --- /dev/null +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/CordaRPCClientUtils.kt @@ -0,0 +1,13 @@ +package net.corda.client.rpc.internal + +import net.corda.client.rpc.CordaRPCClient +import net.corda.client.rpc.CordaRPCClientConfiguration +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.nodeapi.internal.config.SSLConfiguration + +/** Utility which exposes the internal Corda RPC constructor to other internal Corda components */ +fun createCordaRPCClientWithSsl( + hostAndPort: NetworkHostAndPort, + configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.DEFAULT, + sslConfiguration: SSLConfiguration? = null +) = CordaRPCClient.createWithSsl(hostAndPort, configuration, sslConfiguration) \ No newline at end of file diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt index 756f07216e..ea4f446fa0 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt @@ -1,5 +1,7 @@ package net.corda.client.rpc.internal +import net.corda.client.rpc.CordaRPCClient +import net.corda.client.rpc.CordaRPCClientConfiguration import net.corda.client.rpc.RPCConnection import net.corda.client.rpc.RPCException import net.corda.core.context.Actor diff --git a/node/src/integration-test/kotlin/net/corda/node/services/rpc/RpcSslTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/rpc/RpcSslTest.kt index d6dd26440a..8ccab804c9 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/rpc/RpcSslTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/rpc/RpcSslTest.kt @@ -1,6 +1,7 @@ package net.corda.node.services.rpc import net.corda.client.rpc.CordaRPCClient +import net.corda.client.rpc.internal.createCordaRPCClientWithSsl import net.corda.core.identity.CordaX500Name import net.corda.core.utilities.getOrThrow import net.corda.node.services.Permissions.Companion.all @@ -34,7 +35,7 @@ class RpcSslTest { var successful = false driver(DriverParameters(isDebug = true, startNodesInProcess = true, portAllocation = PortAllocation.RandomFree)) { startNode(rpcUsers = listOf(user), customOverrides = nodeSslOptions.useSslRpcOverrides()).getOrThrow().use { node -> - CordaRPCClient(node.rpcAddress, sslConfiguration = clientSslOptions).start(user.username, user.password).use { connection -> + createCordaRPCClientWithSsl(node.rpcAddress, sslConfiguration = clientSslOptions).start(user.username, user.password).use { connection -> connection.proxy.apply { nodeInfo() successful = true diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt index 4e0567e007..eb3ae63268 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt @@ -6,6 +6,7 @@ import com.typesafe.config.Config import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigRenderOptions import net.corda.client.rpc.CordaRPCClient +import net.corda.client.rpc.internal.createCordaRPCClientWithSsl import net.corda.cordform.CordformContext import net.corda.cordform.CordformNode import net.corda.core.concurrent.CordaFuture @@ -150,7 +151,7 @@ class DriverDSLImpl( private fun establishRpc(config: NodeConfig, processDeathFuture: CordaFuture): CordaFuture { val rpcAddress = config.corda.rpcOptions.address!! val client = if (config.corda.rpcOptions.useSsl) { - CordaRPCClient(rpcAddress, sslConfiguration = config.corda.rpcOptions.sslConfig) + createCordaRPCClientWithSsl(rpcAddress, sslConfiguration = config.corda.rpcOptions.sslConfig) } else { CordaRPCClient(rpcAddress) } From 174ed3c64b8a5dc290e1771122f4b68d7dd69e88 Mon Sep 17 00:00:00 2001 From: Clinton Date: Wed, 14 Feb 2018 14:49:59 +0000 Subject: [PATCH 18/50] CORDA-556: Added Cordapp Config and a sample (#2469) * Added per-cordapp configuration * Added new API for Cordformation cordapp declarations to support per-cordapp configuration * Added a cordapp configuration sample --- .ci/api-current.txt | 2 +- .idea/compiler.xml | 2 + constants.properties | 4 +- .../net/corda/core/cordapp/ConfigException.kt | 6 + .../net/corda/core/cordapp/CordappConfig.kt | 70 +++++++ .../net/corda/core/cordapp/CordappContext.kt | 10 +- .../net/corda/core/internal/InternalUtils.kt | 7 + .../kotlin/net/corda/core/node/ServiceHub.kt | 6 + docs/source/changelog.rst | 13 +- docs/source/cordapp-build-systems.rst | 17 ++ gradle-plugins/cordform-common/build.gradle | 7 +- .../corda/cordform/CordformDefinition.java | 14 +- .../net/corda/cordform/CordappDependency.kt | 10 + gradle-plugins/cordformation/build.gradle | 21 ++- .../main/kotlin/net/corda/plugins/Baseform.kt | 28 +-- .../main/kotlin/net/corda/plugins/Cordapp.kt | 26 +++ .../main/kotlin/net/corda/plugins/Cordform.kt | 21 ++- .../kotlin/net/corda/plugins/Cordformation.kt | 19 +- .../src/main/kotlin/net/corda/plugins/Node.kt | 176 ++++++++++++++---- .../kotlin/net/corda/plugins/NodeRunner.kt | 40 ++-- .../kotlin/net/corda/plugins/CordformTest.kt | 77 ++++++++ .../DeploySingleNodeWithCordapp.gradle | 33 ++++ .../DeploySingleNodeWithCordappConfig.gradle | 35 ++++ ...odeWithLocallyBuildCordappAndConfig.gradle | 39 ++++ ...tachmentsClassLoaderStaticContractTests.kt | 3 +- .../internal/AttachmentsClassLoaderTests.kt | 3 +- node/capsule/build.gradle | 5 - .../node/CordappConfigFileProviderTests.kt | 60 ++++++ .../node/services/AttachmentLoadingTests.kt | 3 +- .../net/corda/node/internal/AbstractNode.kt | 3 +- .../cordapp/CordappConfigFileProvider.kt | 36 ++++ .../internal/cordapp/CordappConfigProvider.kt | 7 + .../internal/cordapp/CordappProviderImpl.kt | 19 +- .../internal/cordapp/TypesafeCordappConfig.kt | 79 ++++++++ .../cordapp/CordappProviderImplTests.kt | 40 +++- .../cordapp/TypesafeCordappConfigTests.kt | 47 +++++ .../net/corda/bank/BankOfCordaCordformTest.kt | 4 +- .../net/corda/bank/BankOfCordaCordform.kt | 4 +- samples/cordapp-configuration/README.md | 23 +++ samples/cordapp-configuration/build.gradle | 54 ++++++ samples/cordapp-configuration/src/config.conf | 5 + .../corda/configsample/ConfigSampleFlow.kt | 10 + .../net/corda/notarydemo/BFTNotaryCordform.kt | 2 +- .../main/kotlin/net/corda/notarydemo/Clean.kt | 4 +- .../corda/notarydemo/CustomNotaryCordform.kt | 2 +- .../corda/notarydemo/RaftNotaryCordform.kt | 2 +- .../corda/notarydemo/SingleNotaryCordform.kt | 2 +- samples/simm-valuation-demo/build.gradle | 8 +- settings.gradle | 1 + .../internal/demorun/CordformNodeRunner.kt | 77 ++++++++ .../node/internal/demorun/DemoRunner.kt | 57 ------ .../testing/node/MockCordappConfigProvider.kt | 17 ++ .../testing/services/MockCordappProvider.kt | 9 +- 53 files changed, 1063 insertions(+), 206 deletions(-) create mode 100644 core/src/main/kotlin/net/corda/core/cordapp/ConfigException.kt create mode 100644 core/src/main/kotlin/net/corda/core/cordapp/CordappConfig.kt create mode 100644 gradle-plugins/cordform-common/src/main/kotlin/net/corda/cordform/CordappDependency.kt create mode 100644 gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordapp.kt create mode 100644 gradle-plugins/cordformation/src/test/kotlin/net/corda/plugins/CordformTest.kt create mode 100644 gradle-plugins/cordformation/src/test/resources/net/corda/plugins/DeploySingleNodeWithCordapp.gradle create mode 100644 gradle-plugins/cordformation/src/test/resources/net/corda/plugins/DeploySingleNodeWithCordappConfig.gradle create mode 100644 gradle-plugins/cordformation/src/test/resources/net/corda/plugins/DeploySingleNodeWithLocallyBuildCordappAndConfig.gradle create mode 100644 node/src/integration-test/kotlin/net/corda/node/CordappConfigFileProviderTests.kt create mode 100644 node/src/main/kotlin/net/corda/node/internal/cordapp/CordappConfigFileProvider.kt create mode 100644 node/src/main/kotlin/net/corda/node/internal/cordapp/CordappConfigProvider.kt create mode 100644 node/src/main/kotlin/net/corda/node/internal/cordapp/TypesafeCordappConfig.kt create mode 100644 node/src/test/kotlin/net/corda/node/internal/cordapp/TypesafeCordappConfigTests.kt create mode 100644 samples/cordapp-configuration/README.md create mode 100644 samples/cordapp-configuration/build.gradle create mode 100644 samples/cordapp-configuration/src/config.conf create mode 100644 samples/cordapp-configuration/src/main/kotlin/net/corda/configsample/ConfigSampleFlow.kt create mode 100644 testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/demorun/CordformNodeRunner.kt delete mode 100644 testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/demorun/DemoRunner.kt create mode 100644 testing/test-utils/src/main/kotlin/net/corda/testing/node/MockCordappConfigProvider.kt diff --git a/.ci/api-current.txt b/.ci/api-current.txt index b5cf7b32e1..ddf0e06ee7 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -635,7 +635,7 @@ public static final class net.corda.core.contracts.UniqueIdentifier$Companion ex @org.jetbrains.annotations.NotNull public abstract List getServices() ## public final class net.corda.core.cordapp.CordappContext extends java.lang.Object - public (net.corda.core.cordapp.Cordapp, net.corda.core.crypto.SecureHash, ClassLoader) + public (net.corda.core.cordapp.Cordapp, net.corda.core.crypto.SecureHash, ClassLoader, net.corda.core.cordapp.CordappConfig) @org.jetbrains.annotations.Nullable public final net.corda.core.crypto.SecureHash getAttachmentId() @org.jetbrains.annotations.NotNull public final ClassLoader getClassLoader() @org.jetbrains.annotations.NotNull public final net.corda.core.cordapp.Cordapp getCordapp() diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 9e259d9155..64293946a1 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -29,6 +29,8 @@ + + diff --git a/constants.properties b/constants.properties index 930a39acd2..1c0e8aabc6 100644 --- a/constants.properties +++ b/constants.properties @@ -1,4 +1,4 @@ -gradlePluginsVersion=3.0.5 +gradlePluginsVersion=3.0.6 kotlinVersion=1.2.20 platformVersion=2 guavaVersion=21.0 @@ -6,4 +6,4 @@ bouncycastleVersion=1.57 typesafeConfigVersion=1.3.1 jsr305Version=3.0.2 artifactoryPluginVersion=4.4.18 -snakeYamlVersion=1.19 \ No newline at end of file +snakeYamlVersion=1.19 diff --git a/core/src/main/kotlin/net/corda/core/cordapp/ConfigException.kt b/core/src/main/kotlin/net/corda/core/cordapp/ConfigException.kt new file mode 100644 index 0000000000..affbce62dd --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/cordapp/ConfigException.kt @@ -0,0 +1,6 @@ +package net.corda.core.cordapp + +/** + * Thrown if an exception occurs in accessing or parsing cordapp configuration + */ +class CordappConfigException(msg: String, e: Throwable) : Exception(msg, e) \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/cordapp/CordappConfig.kt b/core/src/main/kotlin/net/corda/core/cordapp/CordappConfig.kt new file mode 100644 index 0000000000..664e69fe80 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/cordapp/CordappConfig.kt @@ -0,0 +1,70 @@ +package net.corda.core.cordapp + +import net.corda.core.DoNotImplement + +/** + * Provides access to cordapp configuration independent of the configuration provider. + */ +@DoNotImplement +interface CordappConfig { + /** + * Check if a config exists at path + */ + fun exists(path: String): Boolean + + /** + * Get the value of the configuration at "path". + * + * @throws CordappConfigException If the configuration fails to load, parse, or find a value. + */ + fun get(path: String): Any + + /** + * Get the int value of the configuration at "path". + * + * @throws CordappConfigException If the configuration fails to load, parse, or find a value. + */ + fun getInt(path: String): Int + + /** + * Get the long value of the configuration at "path". + * + * @throws CordappConfigException If the configuration fails to load, parse, or find a value. + */ + fun getLong(path: String): Long + + /** + * Get the float value of the configuration at "path". + * + * @throws CordappConfigException If the configuration fails to load, parse, or find a value. + */ + fun getFloat(path: String): Float + + /** + * Get the double value of the configuration at "path". + * + * @throws CordappConfigException If the configuration fails to load, parse, or find a value. + */ + fun getDouble(path: String): Double + + /** + * Get the number value of the configuration at "path". + * + * @throws CordappConfigException If the configuration fails to load, parse, or find a value. + */ + fun getNumber(path: String): Number + + /** + * Get the string value of the configuration at "path". + * + * @throws CordappConfigException If the configuration fails to load, parse, or find a value. + */ + fun getString(path: String): String + + /** + * Get the boolean value of the configuration at "path". + * + * @throws CordappConfigException If the configuration fails to load, parse, or find a value. + */ + fun getBoolean(path: String): Boolean +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/cordapp/CordappContext.kt b/core/src/main/kotlin/net/corda/core/cordapp/CordappContext.kt index 3c7be4a3e2..b91acec452 100644 --- a/core/src/main/kotlin/net/corda/core/cordapp/CordappContext.kt +++ b/core/src/main/kotlin/net/corda/core/cordapp/CordappContext.kt @@ -2,8 +2,6 @@ package net.corda.core.cordapp import net.corda.core.crypto.SecureHash -// TODO: Add per app config - /** * An app context provides information about where an app was loaded from, access to its classloader, * and (in the included [Cordapp] object) lists of annotated classes discovered via scanning the JAR. @@ -15,5 +13,11 @@ import net.corda.core.crypto.SecureHash * @property attachmentId For CorDapps containing [Contract] or [UpgradedContract] implementations this will be populated * with the attachment containing those class files * @property classLoader the classloader used to load this cordapp's classes + * @property config Configuration for this CorDapp */ -class CordappContext(val cordapp: Cordapp, val attachmentId: SecureHash?, val classLoader: ClassLoader) +class CordappContext internal constructor( + val cordapp: Cordapp, + val attachmentId: SecureHash?, + val classLoader: ClassLoader, + val config: CordappConfig +) diff --git a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt index 18dcced593..f47e62e6d8 100644 --- a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt @@ -2,6 +2,9 @@ package net.corda.core.internal +import net.corda.core.cordapp.Cordapp +import net.corda.core.cordapp.CordappConfig +import net.corda.core.cordapp.CordappContext import net.corda.core.cordapp.CordappProvider import net.corda.core.crypto.* import net.corda.core.identity.CordaX500Name @@ -375,3 +378,7 @@ inline fun SerializedBytes.sign(keyPair: KeyPair): SignedData { } fun ByteBuffer.copyBytes() = ByteArray(remaining()).also { get(it) } + +fun createCordappContext(cordapp: Cordapp, attachmentId: SecureHash?, classLoader: ClassLoader, config: CordappConfig): CordappContext { + return CordappContext(cordapp, attachmentId, classLoader, config) +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt index 1832ac1cb5..2c31eb96f1 100644 --- a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt +++ b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt @@ -2,6 +2,7 @@ package net.corda.core.node import net.corda.core.DoNotImplement import net.corda.core.contracts.* +import net.corda.core.cordapp.CordappContext import net.corda.core.cordapp.CordappProvider import net.corda.core.crypto.Crypto import net.corda.core.crypto.SignableData @@ -369,4 +370,9 @@ interface ServiceHub : ServicesForResolution { * node starts. */ fun registerUnloadHandler(runOnStop: () -> Unit) + + /** + * See [CordappProvider.getAppContext] + */ + fun getAppContext(): CordappContext = cordappProvider.getAppContext() } diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 8bac1b49d6..816bba7ec9 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -7,6 +7,10 @@ from the previous milestone release. UNRELEASED ---------- +* Per CorDapp configuration is now exposed. ``CordappContext`` now exposes a ``CordappConfig`` object that is populated +at CorDapp context creation time from a file source during runtime. + + * Introduced Flow Draining mode, in which a node continues executing existing flows, but does not start new. This is to support graceful node shutdown/restarts. In particular, when this mode is on, new flows through RPC will be rejected, scheduled flows will be ignored, and initial session messages will not be consumed. This will ensure that the number of checkpoints will strictly diminish with time, allowing for a clean shutdown. @@ -188,18 +192,9 @@ UNRELEASED * Marked ``stateMachine`` on ``FlowLogic`` as ``CordaInternal`` to make clear that is it not part of the public api and is only for internal use -* Provided experimental support for specifying your own webserver to be used instead of the default development - webserver in ``Cordform`` using the ``webserverJar`` argument - * Created new ``StartedMockNode`` and ``UnstartedMockNode`` classes which are wrappers around our MockNode implementation that expose relevant methods for testing without exposing internals, create these using a ``MockNetwork``. -* The test utils in ``Expect.kt``, ``SerializationTestHelpers.kt``, ``TestConstants.kt`` and ``TestUtils.kt`` have moved - from the ``net.corda.testing`` package to the ``net.corda.testing.core`` package, and ``FlowStackSnapshot.kt`` has moved to the - ``net.corda.testing.services`` package. Moving items out of the ``net.corda.testing.*`` package will help make it clearer which - parts of the api are stable. The bash script ``tools\scripts\update-test-packages.sh`` can be used to smooth the upgrade - process for existing projects. - .. _changelog_v1: Release 1.0 diff --git a/docs/source/cordapp-build-systems.rst b/docs/source/cordapp-build-systems.rst index 3900680d81..2de5cca3c7 100644 --- a/docs/source/cordapp-build-systems.rst +++ b/docs/source/cordapp-build-systems.rst @@ -159,3 +159,20 @@ Installing the CorDapp JAR At runtime, nodes will load any CorDapps present in their ``cordapps`` folder. Therefore in order to install a CorDapp on a node, the CorDapp JAR must be added to the ``/cordapps/`` folder, where ``node_dir`` is the folder in which the node's JAR and configuration files are stored. + +CorDapp configuration files +--------------------------- + +CorDapp configuration files should be placed in ``/cordapps/config``. The name of the file should match the +name of the JAR of the CorDapp (eg; if your CorDapp is called ``hello-0.1.jar`` the config should be ``config/hello-0.1.conf``). + +Config files are currently only available in the `Typesafe/Lightbend `_ config format. +These files are loaded when a CorDapp context is created and so can change during runtime. + +CorDapp configuration can be accessed from ``CordappContext::config`` whenever a ``CordappContext`` is available. + +There is an example project that demonstrates in ``samples` called ``cordapp-configuration`` and API documentation in +`_. + + + diff --git a/gradle-plugins/cordform-common/build.gradle b/gradle-plugins/cordform-common/build.gradle index be2fa0cf16..aca55ab2b4 100644 --- a/gradle-plugins/cordform-common/build.gradle +++ b/gradle-plugins/cordform-common/build.gradle @@ -1,4 +1,5 @@ apply plugin: 'java' +apply plugin: 'kotlin' apply plugin: 'maven-publish' apply plugin: 'net.corda.plugins.publish-utils' apply plugin: 'com.jfrog.artifactory' @@ -7,11 +8,9 @@ repositories { mavenCentral() } -// This tracks the gradle plugins version and not Corda -version gradle_plugins_version -group 'net.corda.plugins' - dependencies { + compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" + // JSR 305: Nullability annotations compile "com.google.code.findbugs:jsr305:$jsr305_version" diff --git a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformDefinition.java b/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformDefinition.java index fc62b1bbee..de4601d005 100644 --- a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformDefinition.java +++ b/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformDefinition.java @@ -10,7 +10,12 @@ import java.util.function.Consumer; public abstract class CordformDefinition { private Path nodesDirectory = Paths.get("build", "nodes"); private final List> nodeConfigurers = new ArrayList<>(); - private final List cordappPackages = new ArrayList<>(); + /** + * A list of Cordapp maven coordinates and project name + * + * If maven coordinates are set project name is ignored + */ + private final List cordappDeps = new ArrayList<>(); public Path getNodesDirectory() { return nodesDirectory; @@ -28,8 +33,11 @@ public abstract class CordformDefinition { nodeConfigurers.add(configurer); } - public List getCordappPackages() { - return cordappPackages; + /** + * Cordapp maven coordinates or project names (ie; net.corda:finance:0.1 or ":finance") to scan for when resolving cordapp JARs + */ + public List getCordappDependencies() { + return cordappDeps; } /** diff --git a/gradle-plugins/cordform-common/src/main/kotlin/net/corda/cordform/CordappDependency.kt b/gradle-plugins/cordform-common/src/main/kotlin/net/corda/cordform/CordappDependency.kt new file mode 100644 index 0000000000..f677e2278c --- /dev/null +++ b/gradle-plugins/cordform-common/src/main/kotlin/net/corda/cordform/CordappDependency.kt @@ -0,0 +1,10 @@ +package net.corda.cordform + +data class CordappDependency( + val mavenCoordinates: String? = null, + val projectName: String? = null +) { + init { + require((mavenCoordinates != null) != (projectName != null), { "Only one of maven coordinates or project name must be set" }) + } +} \ No newline at end of file diff --git a/gradle-plugins/cordformation/build.gradle b/gradle-plugins/cordformation/build.gradle index 5ac1e33473..f68adb96ad 100644 --- a/gradle-plugins/cordformation/build.gradle +++ b/gradle-plugins/cordformation/build.gradle @@ -9,6 +9,7 @@ buildscript { } apply plugin: 'kotlin' +apply plugin: 'java-gradle-plugin' apply plugin: 'net.corda.plugins.publish-utils' apply plugin: 'com.jfrog.artifactory' @@ -33,18 +34,22 @@ sourceSets { } dependencies { - compile gradleApi() + gradleApi() + compile project(":cordapp") + compile project(':cordform-common') compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" + compile "commons-io:commons-io:2.6" noderunner "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" - compile project(':cordform-common') + testCompile "junit:junit:4.12" // TODO: Unify with core + testCompile "org.assertj:assertj-core:3.8.0" // Docker-compose file generation compile "org.yaml:snakeyaml:$snake_yaml_version" } -task createNodeRunner(type: Jar, dependsOn: [classes]) { +task createNodeRunner(type: Jar) { manifest { attributes('Main-Class': 'net.corda.plugins.NodeRunnerKt') } @@ -53,12 +58,12 @@ task createNodeRunner(type: Jar, dependsOn: [classes]) { from sourceSets.runnodes.output } -jar { +publish { + name project.name +} + +processResources { from(createNodeRunner) { rename { 'net/corda/plugins/runnodes.jar' } } } - -publish { - name project.name -} diff --git a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Baseform.kt b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Baseform.kt index ea528aaf08..010ee69e5c 100644 --- a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Baseform.kt +++ b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Baseform.kt @@ -2,11 +2,9 @@ package net.corda.plugins import groovy.lang.Closure import net.corda.cordform.CordformDefinition -import org.apache.tools.ant.filters.FixCrLfFilter import org.gradle.api.DefaultTask import org.gradle.api.plugins.JavaPluginConvention import org.gradle.api.tasks.SourceSet.MAIN_SOURCE_SET_NAME -import org.gradle.api.tasks.TaskAction import java.io.File import java.lang.reflect.InvocationTargetException import java.net.URLClassLoader @@ -111,20 +109,27 @@ open class Baseform : DefaultTask() { } } - protected fun initializeConfiguration() { + internal fun initializeConfiguration() { if (definitionClass != null) { val cd = loadCordformDefinition() // If the user has specified their own directory (even if it's the same default path) then let them know // it's not used and should just rely on the one in CordformDefinition require(directory === defaultDirectory) { + project.logger.info("User has used '$directory', default directory is '${defaultDirectory}'") "'directory' cannot be used when 'definitionClass' is specified. Use CordformDefinition.nodesDirectory instead." } directory = cd.nodesDirectory - val cordapps = cd.getMatchingCordapps() + val cordapps = cd.cordappDependencies cd.nodeConfigurers.forEach { val node = node { } it.accept(node) - node.additionalCordapps.addAll(cordapps) + cordapps.forEach { + if (it.mavenCoordinates != null) { + node.cordapp(project.project(it.mavenCoordinates!!)) + } else { + node.cordapp(it.projectName!!) + } + } node.rootDir(directory) } cd.setup { nodeName -> project.projectDir.toPath().resolve(getNodeByName(nodeName)!!.nodeDir.toPath()) } @@ -134,7 +139,6 @@ open class Baseform : DefaultTask() { } } } - protected fun bootstrapNetwork() { val networkBootstrapperClass = loadNetworkBootstrapperClass() val networkBootstrapper = networkBootstrapperClass.newInstance() @@ -148,18 +152,6 @@ open class Baseform : DefaultTask() { } } - private fun CordformDefinition.getMatchingCordapps(): List { - val cordappJars = project.configuration("cordapp").files - return cordappPackages.map { `package` -> - val cordappsWithPackage = cordappJars.filter { it.containsPackage(`package`) } - when (cordappsWithPackage.size) { - 0 -> throw IllegalArgumentException("There are no cordapp dependencies containing the package $`package`") - 1 -> cordappsWithPackage[0] - else -> throw IllegalArgumentException("More than one cordapp dependency contains the package $`package`: $cordappsWithPackage") - } - } - } - private fun File.containsPackage(`package`: String): Boolean { JarInputStream(inputStream()).use { while (true) { diff --git a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordapp.kt b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordapp.kt new file mode 100644 index 0000000000..a4cba09969 --- /dev/null +++ b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordapp.kt @@ -0,0 +1,26 @@ +package net.corda.plugins + +import org.gradle.api.Project +import java.io.File + +open class Cordapp private constructor(val coordinates: String?, val project: Project?) { + constructor(coordinates: String) : this(coordinates, null) + constructor(cordappProject: Project) : this(null, cordappProject) + + // The configuration text that will be written + internal var config: String? = null + + /** + * Set the configuration text that will be written to the cordapp's configuration file + */ + fun config(config: String) { + this.config = config + } + + /** + * Reads config from the file and later writes it to the cordapp's configuration file + */ + fun config(configFile: File) { + this.config = configFile.readText() + } +} \ No newline at end of file diff --git a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordform.kt b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordform.kt index f207dc1818..74f9a02664 100644 --- a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordform.kt +++ b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordform.kt @@ -1,9 +1,6 @@ package net.corda.plugins import org.apache.tools.ant.filters.FixCrLfFilter -import org.gradle.api.DefaultTask -import org.gradle.api.plugins.JavaPluginConvention -import org.gradle.api.tasks.SourceSet.MAIN_SOURCE_SET_NAME import org.gradle.api.tasks.TaskAction import java.nio.file.Path import java.nio.file.Paths @@ -15,18 +12,25 @@ import java.nio.file.Paths */ @Suppress("unused") open class Cordform : Baseform() { - private companion object { + internal companion object { val nodeJarName = "corda.jar" - private val defaultDirectory: Path = Paths.get("build", "nodes") } + /** + * Returns a node by name. + * + * @param name The name of the node as specified in the node configuration DSL. + * @return A node instance. + */ + private fun getNodeByName(name: String): Node? = nodes.firstOrNull { it.name == name } + /** * Installs the run script into the nodes directory. */ private fun installRunScript() { project.copy { it.apply { - from(Cordformation.getPluginFile(project, "net/corda/plugins/runnodes.jar")) + from(Cordformation.getPluginFile(project, "runnodes.jar")) fileMode = Cordformation.executableFileMode into("$directory/") } @@ -34,7 +38,7 @@ open class Cordform : Baseform() { project.copy { it.apply { - from(Cordformation.getPluginFile(project, "net/corda/plugins/runnodes")) + from(Cordformation.getPluginFile(project, "runnodes")) // Replaces end of line with lf to avoid issues with the bash interpreter and Windows style line endings. filter(mapOf("eol" to FixCrLfFilter.CrLf.newInstance("lf")), FixCrLfFilter::class.java) fileMode = Cordformation.executableFileMode @@ -44,7 +48,7 @@ open class Cordform : Baseform() { project.copy { it.apply { - from(Cordformation.getPluginFile(project, "net/corda/plugins/runnodes.bat")) + from(Cordformation.getPluginFile(project, "runnodes.bat")) into("$directory/") } } @@ -63,4 +67,5 @@ open class Cordform : Baseform() { bootstrapNetwork() nodes.forEach(Node::build) } + } diff --git a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordformation.kt b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordformation.kt index 43444ca270..644116e35b 100644 --- a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordformation.kt +++ b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordformation.kt @@ -3,6 +3,7 @@ package net.corda.plugins import org.gradle.api.Plugin import org.gradle.api.Project import java.io.File +import java.io.InputStream /** * The Cordformation plugin deploys nodes to a directory in a state ready to be used by a developer for experimentation, @@ -13,19 +14,19 @@ class Cordformation : Plugin { const val CORDFORMATION_TYPE = "cordformationInternal" /** - * Gets a resource file from this plugin's JAR file. + * Gets a resource file from this plugin's JAR file by creating an intermediate tmp dir * - * @param project The project environment this plugin executes in. * @param filePathInJar The file in the JAR, relative to root, you wish to access. * @return A file handle to the file in the JAR. */ fun getPluginFile(project: Project, filePathInJar: String): File { - val archive = project.rootProject.buildscript.configurations - .single { it.name == "classpath" } - .first { it.name.contains("cordformation") } - return project.rootProject.resources.text - .fromArchiveEntry(archive, filePathInJar) - .asFile() + val tmpDir = File(project.buildDir, "tmp") + val outputFile = File(tmpDir, filePathInJar) + tmpDir.mkdir() + outputFile.outputStream().use { + Cordformation::class.java.getResourceAsStream(filePathInJar).copyTo(it) + } + return outputFile } /** @@ -38,7 +39,7 @@ class Cordformation : Plugin { fun verifyAndGetRuntimeJar(project: Project, jarName: String): File { val releaseVersion = project.rootProject.ext("corda_release_version") val maybeJar = project.configuration("runtime").filter { - "$jarName-$releaseVersion.jar" in it.toString() || "$jarName-enterprise-$releaseVersion.jar" in it.toString() + "$jarName-$releaseVersion.jar" in it.toString() || "$jarName-r3-$releaseVersion.jar" in it.toString() } if (maybeJar.isEmpty) { throw IllegalStateException("No $jarName JAR found. Have you deployed the Corda project to Maven? Looked for \"$jarName-$releaseVersion.jar\"") diff --git a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Node.kt b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Node.kt index 10c408cc71..0086854162 100644 --- a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Node.kt +++ b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Node.kt @@ -1,22 +1,28 @@ package net.corda.plugins import com.typesafe.config.ConfigFactory +import com.typesafe.config.ConfigObject import com.typesafe.config.ConfigRenderOptions import com.typesafe.config.ConfigValueFactory -import com.typesafe.config.ConfigObject import groovy.lang.Closure import net.corda.cordform.CordformNode import net.corda.cordform.RpcSettings +import org.apache.commons.io.FilenameUtils +import org.gradle.api.GradleException import org.gradle.api.Project +import org.gradle.api.artifacts.ProjectDependency import java.io.File import java.nio.charset.StandardCharsets import java.nio.file.Files import java.nio.file.Path +import javax.inject.Inject /** * Represents a node that will be installed. */ -class Node(private val project: Project) : CordformNode() { +open class Node @Inject constructor(private val project: Project) : CordformNode() { + private data class ResolvedCordapp(val jarFile: File, val config: String?) + companion object { @JvmStatic val webJarName = "corda-webserver.jar" @@ -30,8 +36,17 @@ class Node(private val project: Project) : CordformNode() { * @note Your app will be installed by default and does not need to be included here. * @note Type is any due to gradle's use of "GStrings" - each value will have "toString" called on it */ - var cordapps = mutableListOf() - internal var additionalCordapps = mutableListOf() + var cordapps: MutableList + get() = internalCordapps as MutableList + @Deprecated("Use cordapp instead - setter will be removed by Corda V4.0") + set(value) { + value.forEach { + cordapp(it.toString()) + } + } + + private val internalCordapps = mutableListOf() + private val builtCordapp = Cordapp(project) internal lateinit var nodeDir: File private set internal lateinit var rootDir: File @@ -76,8 +91,83 @@ class Node(private val project: Project) : CordformNode() { * * @param sshdPort The port for SSH server to listen on */ - fun sshdPort(sshdPort: Int?) { - config = config.withValue("sshd.port", ConfigValueFactory.fromAnyRef(sshdPort)) + fun sshdPort(sshdPort: Int) { + config = config.withValue("sshdAddress", + ConfigValueFactory.fromAnyRef("$DEFAULT_HOST:$sshdPort")) + } + + /** + * Install a cordapp to this node + * + * @param coordinates The coordinates of the [Cordapp] + * @param configureClosure A groovy closure to configure a [Cordapp] object + * @return The created and inserted [Cordapp] + */ + fun cordapp(coordinates: String, configureClosure: Closure): Cordapp { + val cordapp = project.configure(Cordapp(coordinates), configureClosure) as Cordapp + internalCordapps += cordapp + return cordapp + } + + /** + * Install a cordapp to this node + * + * @param cordappProject A project that produces a cordapp JAR + * @param configureClosure A groovy closure to configure a [Cordapp] object + * @return The created and inserted [Cordapp] + */ + fun cordapp(cordappProject: Project, configureClosure: Closure): Cordapp { + val cordapp = project.configure(Cordapp(cordappProject), configureClosure) as Cordapp + internalCordapps += cordapp + return cordapp + } + + /** + * Install a cordapp to this node + * + * @param cordappProject A project that produces a cordapp JAR + * @return The created and inserted [Cordapp] + */ + fun cordapp(cordappProject: Project): Cordapp { + return Cordapp(cordappProject).apply { + internalCordapps += this + } + } + + /** + * Install a cordapp to this node + * + * @param coordinates The coordinates of the [Cordapp] + * @return The created and inserted [Cordapp] + */ + fun cordapp(coordinates: String): Cordapp { + return Cordapp(coordinates).apply { + internalCordapps += this + } + } + + /** + * Install a cordapp to this node + * + * @param configureFunc A lambda to configure a [Cordapp] object + * @return The created and inserted [Cordapp] + */ + fun cordapp(coordinates: String, configureFunc: Cordapp.() -> Unit): Cordapp { + return Cordapp(coordinates).apply { + configureFunc() + internalCordapps += this + } + } + + /** + * Configures the default cordapp automatically added to this node from this project + * + * @param configureClosure A groovy closure to configure a [Cordapp] object + * @return The created and inserted [Cordapp] + */ + fun projectCordapp(configureClosure: Closure): Cordapp { + project.configure(builtCordapp, configureClosure) as Cordapp + return builtCordapp } /** @@ -96,8 +186,8 @@ class Node(private val project: Project) : CordformNode() { installWebserverJar() } installAgentJar() - installBuiltCordapp() installCordapps() + installConfig() } internal fun buildDocker() { @@ -109,7 +199,6 @@ class Node(private val project: Project) : CordformNode() { } } installAgentJar() - installBuiltCordapp() installCordapps() } @@ -160,19 +249,6 @@ class Node(private val project: Project) : CordformNode() { } } - /** - * Installs this project's cordapp to this directory. - */ - private fun installBuiltCordapp() { - val cordappsDir = File(nodeDir, "cordapps") - project.copy { - it.apply { - from(project.tasks.getByName("jar")) - into(cordappsDir) - } - } - } - /** * Installs the jolokia monitoring agent JAR to the node/drivers directory */ @@ -197,6 +273,14 @@ class Node(private val project: Project) : CordformNode() { } } + private fun installCordappConfigs(cordapps: Collection) { + val cordappsDir = project.file(File(nodeDir, "cordapps")) + cordappsDir.mkdirs() + cordapps.filter { it.config != null } + .map { Pair("${FilenameUtils.removeExtension(it.jarFile.name)}.conf", it.config!!) } + .forEach { project.file(File(cordappsDir, it.first)).writeText(it.second) } + } + private fun createTempConfigFile(configObject: ConfigObject): File { val options = ConfigRenderOptions .defaults() @@ -217,7 +301,7 @@ class Node(private val project: Project) : CordformNode() { /** * Installs the configuration file to the root directory and detokenises it. */ - internal fun installConfig() { + fun installConfig() { configureProperties() val tmpConfFile = createTempConfigFile(config.root()) appendOptionalConfig(tmpConfFile) @@ -269,31 +353,57 @@ class Node(private val project: Project) : CordformNode() { } } + /** - * Installs other cordapps to this node's cordapps directory. + * Installs the jolokia monitoring agent JAR to the node/drivers directory */ - internal fun installCordapps() { - additionalCordapps.addAll(getCordappList()) + private fun installCordapps() { + val cordapps = getCordappList() val cordappsDir = File(nodeDir, "cordapps") project.copy { it.apply { - from(additionalCordapps) - into(cordappsDir) + from(cordapps.map { it.jarFile }) + into(project.file(cordappsDir)) } } + + installCordappConfigs(cordapps) } + /** * Gets a list of cordapps based on what dependent cordapps were specified. * * @return List of this node's cordapps. */ - private fun getCordappList(): Collection { - // Cordapps can sometimes contain a GString instance which fails the equality test with the Java string - @Suppress("RemoveRedundantCallsOfConversionMethods") - val cordapps: List = cordapps.map { it.toString() } - return project.configuration("cordapp").files { - cordapps.contains(it.group + ":" + it.name + ":" + it.version) + private fun getCordappList(): Collection = + internalCordapps.map { cordapp -> resolveCordapp(cordapp) } + resolveBuiltCordapp() + + private fun resolveCordapp(cordapp: Cordapp): ResolvedCordapp { + val cordappConfiguration = project.configuration("cordapp") + val cordappName = if (cordapp.project != null) cordapp.project.name else cordapp.coordinates + val cordappFile = cordappConfiguration.files { + when { + (it is ProjectDependency) && (cordapp.project != null) -> it.dependencyProject == cordapp.project + cordapp.coordinates != null -> { + // Cordapps can sometimes contain a GString instance which fails the equality test with the Java string + @Suppress("RemoveRedundantCallsOfConversionMethods") + val coordinates = cordapp.coordinates.toString() + coordinates == (it.group + ":" + it.name + ":" + it.version) + } + else -> false + } + } + + return when { + cordappFile.size == 0 -> throw GradleException("Cordapp $cordappName not found in cordapps configuration.") + cordappFile.size > 1 -> throw GradleException("Multiple files found for $cordappName") + else -> ResolvedCordapp(cordappFile.single(), cordapp.config) } } + + private fun resolveBuiltCordapp(): ResolvedCordapp { + val projectCordappFile = project.tasks.getByName("jar").outputs.files.singleFile + return ResolvedCordapp(projectCordappFile, builtCordapp.config) + } } diff --git a/gradle-plugins/cordformation/src/noderunner/kotlin/net/corda/plugins/NodeRunner.kt b/gradle-plugins/cordformation/src/noderunner/kotlin/net/corda/plugins/NodeRunner.kt index bb308a8191..cffa06300f 100644 --- a/gradle-plugins/cordformation/src/noderunner/kotlin/net/corda/plugins/NodeRunner.kt +++ b/gradle-plugins/cordformation/src/noderunner/kotlin/net/corda/plugins/NodeRunner.kt @@ -109,35 +109,43 @@ private abstract class JavaCommand( private class HeadlessJavaCommand(jarName: String, dir: File, debugPort: Int?, monitoringPort: Int?, args: List, jvmArgs: List) : JavaCommand(jarName, dir, debugPort, monitoringPort, dir.name, { add("--no-local-shell") }, args, jvmArgs) { - override fun processBuilder() = ProcessBuilder(command).redirectError(File("error.$nodeName.log")).inheritIO() + override fun processBuilder(): ProcessBuilder { + println("Running command: ${command.joinToString(" ")}") + return ProcessBuilder(command).redirectError(File("error.$nodeName.log")).inheritIO() + } + override fun getJavaPath() = File(File(System.getProperty("java.home"), "bin"), "java").path } private class TerminalWindowJavaCommand(jarName: String, dir: File, debugPort: Int?, monitoringPort: Int?, args: List, jvmArgs: List) : JavaCommand(jarName, dir, debugPort, monitoringPort, "${dir.name}-$jarName", {}, args, jvmArgs) { - override fun processBuilder() = ProcessBuilder(when (os) { - OS.MACOS -> { - listOf("osascript", "-e", """tell app "Terminal" + override fun processBuilder(): ProcessBuilder { + val params = when (os) { + OS.MACOS -> { + listOf("osascript", "-e", """tell app "Terminal" activate delay 0.5 tell app "System Events" to tell process "Terminal" to keystroke "t" using command down delay 0.5 do script "bash -c 'cd \"$dir\" ; \"${command.joinToString("""\" \"""")}\" && exit'" in selected tab of the front window end tell""") - } - OS.WINDOWS -> { - listOf("cmd", "/C", "start ${command.joinToString(" ") { windowsSpaceEscape(it) }}") - } - OS.LINUX -> { - // Start shell to keep window open unless java terminated normally or due to SIGTERM: - val command = "${unixCommand()}; [ $? -eq 0 -o $? -eq 143 ] || sh" - if (isTmux()) { - listOf("tmux", "new-window", "-n", nodeName, command) - } else { - listOf("xterm", "-T", nodeName, "-e", command) + } + OS.WINDOWS -> { + listOf("cmd", "/C", "start ${command.joinToString(" ") { windowsSpaceEscape(it) }}") + } + OS.LINUX -> { + // Start shell to keep window open unless java terminated normally or due to SIGTERM: + val command = "${unixCommand()}; [ $? -eq 0 -o $? -eq 143 ] || sh" + if (isTmux()) { + listOf("tmux", "new-window", "-n", nodeName, command) + } else { + listOf("xterm", "-T", nodeName, "-e", command) + } } } - }) + println("Running command: ${params.joinToString(" ")}") + return ProcessBuilder(params) + } private fun unixCommand() = command.map(::quotedFormOf).joinToString(" ") override fun getJavaPath(): String = File(File(System.getProperty("java.home"), "bin"), "java").path diff --git a/gradle-plugins/cordformation/src/test/kotlin/net/corda/plugins/CordformTest.kt b/gradle-plugins/cordformation/src/test/kotlin/net/corda/plugins/CordformTest.kt new file mode 100644 index 0000000000..3f934191be --- /dev/null +++ b/gradle-plugins/cordformation/src/test/kotlin/net/corda/plugins/CordformTest.kt @@ -0,0 +1,77 @@ +package net.corda.plugins + +import org.apache.commons.io.FileUtils +import org.apache.commons.io.IOUtils +import org.assertj.core.api.Assertions.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.io.File +import java.io.IOException +import java.nio.charset.StandardCharsets +import java.nio.file.Files +import org.gradle.testkit.runner.GradleRunner +import org.gradle.testkit.runner.TaskOutcome + +class CordformTest { + @Rule + @JvmField + val testProjectDir = TemporaryFolder() + private var buildFile: File? = null + + private companion object { + val cordaFinanceJarName = "corda-finance-3.0-SNAPSHOT" + val localCordappJarName = "locally-built-cordapp" + val notaryNodeName = "Notary Service" + } + + @Before + fun setup() { + buildFile = testProjectDir.newFile("build.gradle") + } + + @Test + fun `a node with cordapp dependency`() { + val runner = getStandardGradleRunnerFor("DeploySingleNodeWithCordapp.gradle") + + val result = runner.build() + + assertThat(result.task(":deployNodes")!!.outcome).isEqualTo(TaskOutcome.SUCCESS) + assertThat(getNodeCordappJar(notaryNodeName, cordaFinanceJarName)).exists() + } + + @Test + fun `deploy a node with cordapp config`() { + val runner = getStandardGradleRunnerFor("DeploySingleNodeWithCordappConfig.gradle") + + val result = runner.build() + + assertThat(result.task(":deployNodes")!!.outcome).isEqualTo(TaskOutcome.SUCCESS) + assertThat(getNodeCordappJar(notaryNodeName, cordaFinanceJarName)).exists() + assertThat(getNodeCordappConfig(notaryNodeName, cordaFinanceJarName)).exists() + } + + @Test + fun `deploy the locally built cordapp with cordapp config`() { + val runner = getStandardGradleRunnerFor("DeploySingleNodeWithLocallyBuildCordappAndConfig.gradle") + + val result = runner.build() + + assertThat(result.task(":deployNodes")!!.outcome).isEqualTo(TaskOutcome.SUCCESS) + assertThat(getNodeCordappJar(notaryNodeName, localCordappJarName)).exists() + assertThat(getNodeCordappConfig(notaryNodeName, localCordappJarName)).exists() + } + + private fun getStandardGradleRunnerFor(buildFileResourceName: String): GradleRunner { + createBuildFile(buildFileResourceName) + return GradleRunner.create() + .withProjectDir(testProjectDir.root) + .withArguments("deployNodes", "-s") + .withPluginClasspath() + } + + private fun createBuildFile(buildFileResourceName: String) = IOUtils.copy(javaClass.getResourceAsStream(buildFileResourceName), buildFile!!.outputStream()) + private fun getNodeCordappJar(nodeName: String, cordappJarName: String) = File(testProjectDir.root, "build/nodes/$nodeName/cordapps/$cordappJarName.jar") + private fun getNodeCordappConfig(nodeName: String, cordappJarName: String) = File(testProjectDir.root, "build/nodes/$nodeName/cordapps/$cordappJarName.conf") +} \ No newline at end of file diff --git a/gradle-plugins/cordformation/src/test/resources/net/corda/plugins/DeploySingleNodeWithCordapp.gradle b/gradle-plugins/cordformation/src/test/resources/net/corda/plugins/DeploySingleNodeWithCordapp.gradle new file mode 100644 index 0000000000..10eca84d66 --- /dev/null +++ b/gradle-plugins/cordformation/src/test/resources/net/corda/plugins/DeploySingleNodeWithCordapp.gradle @@ -0,0 +1,33 @@ +buildscript { + ext { + corda_group = 'net.corda' + corda_release_version = '3.0-SNAPSHOT' // TODO: Set to 3.0.0 when Corda 3 is released + jolokia_version = '1.3.7' + } +} + +plugins { + id 'java' + id 'net.corda.plugins.cordformation' +} + +repositories { + mavenCentral() + maven { url 'https://ci-artifactory.corda.r3cev.com/artifactory/corda-dev' } +} + +dependencies { + runtime "$corda_group:corda:$corda_release_version" + runtime "$corda_group:corda-node-api:$corda_release_version" + cordapp "$corda_group:corda-finance:$corda_release_version" +} + +task deployNodes(type: net.corda.plugins.Cordform) { + node { + name 'O=Notary Service,L=Zurich,C=CH' + notary = [validating : true] + p2pPort 10002 + rpcPort 10003 + cordapps = ["$corda_group:corda-finance:$corda_release_version"] + } +} \ No newline at end of file diff --git a/gradle-plugins/cordformation/src/test/resources/net/corda/plugins/DeploySingleNodeWithCordappConfig.gradle b/gradle-plugins/cordformation/src/test/resources/net/corda/plugins/DeploySingleNodeWithCordappConfig.gradle new file mode 100644 index 0000000000..4bb6642752 --- /dev/null +++ b/gradle-plugins/cordformation/src/test/resources/net/corda/plugins/DeploySingleNodeWithCordappConfig.gradle @@ -0,0 +1,35 @@ +buildscript { + ext { + corda_group = 'net.corda' + corda_release_version = '3.0-SNAPSHOT' // TODO: Set to 3.0.0 when Corda 3 is released + jolokia_version = '1.3.7' + } +} + +plugins { + id 'java' + id 'net.corda.plugins.cordformation' +} + +repositories { + mavenCentral() + maven { url 'https://ci-artifactory.corda.r3cev.com/artifactory/corda-dev' } +} + +dependencies { + runtime "$corda_group:corda:$corda_release_version" + runtime "$corda_group:corda-node-api:$corda_release_version" + cordapp "$corda_group:corda-finance:$corda_release_version" +} + +task deployNodes(type: net.corda.plugins.Cordform) { + node { + name 'O=Notary Service,L=Zurich,C=CH' + notary = [validating : true] + p2pPort 10002 + rpcPort 10003 + cordapp "$corda_group:corda-finance:$corda_release_version", { + config "a=b" + } + } +} \ No newline at end of file diff --git a/gradle-plugins/cordformation/src/test/resources/net/corda/plugins/DeploySingleNodeWithLocallyBuildCordappAndConfig.gradle b/gradle-plugins/cordformation/src/test/resources/net/corda/plugins/DeploySingleNodeWithLocallyBuildCordappAndConfig.gradle new file mode 100644 index 0000000000..6e0447764f --- /dev/null +++ b/gradle-plugins/cordformation/src/test/resources/net/corda/plugins/DeploySingleNodeWithLocallyBuildCordappAndConfig.gradle @@ -0,0 +1,39 @@ +buildscript { + ext { + corda_group = 'net.corda' + corda_release_version = '3.0-SNAPSHOT' // TODO: Set to 3.0.0 when Corda 3 is released + jolokia_version = '1.3.7' + } +} + +plugins { + id 'java' + id 'net.corda.plugins.cordformation' +} + +repositories { + mavenCentral() + maven { url 'https://ci-artifactory.corda.r3cev.com/artifactory/corda-dev' } +} + +dependencies { + runtime "$corda_group:corda:$corda_release_version" + runtime "$corda_group:corda-node-api:$corda_release_version" + cordapp "$corda_group:corda-finance:$corda_release_version" +} + +jar { + baseName 'locally-built-cordapp' +} + +task deployNodes(type: net.corda.plugins.Cordform, dependsOn: [jar]) { + node { + name 'O=Notary Service,L=Zurich,C=CH' + notary = [validating : true] + p2pPort 10002 + rpcPort 10003 + cordapp { + config "a=b" + } + } +} \ No newline at end of file diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderStaticContractTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderStaticContractTests.kt index 519c1632fc..12ec7230b8 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderStaticContractTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderStaticContractTests.kt @@ -17,6 +17,7 @@ import net.corda.testing.core.DUMMY_NOTARY_NAME import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.core.TestIdentity import net.corda.testing.internal.rigorousMock +import net.corda.testing.node.MockCordappConfigProvider import net.corda.testing.services.MockAttachmentStorage import org.junit.Assert.* import org.junit.Rule @@ -58,7 +59,7 @@ class AttachmentsClassLoaderStaticContractTests { } private val serviceHub = rigorousMock().also { - doReturn(CordappProviderImpl(CordappLoader.createWithTestPackages(listOf("net.corda.nodeapi.internal")), MockAttachmentStorage())).whenever(it).cordappProvider + doReturn(CordappProviderImpl(CordappLoader.createWithTestPackages(listOf("net.corda.nodeapi.internal")), MockCordappConfigProvider(), MockAttachmentStorage())).whenever(it).cordappProvider } @Test diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderTests.kt index af8e806874..77406d26a1 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderTests.kt @@ -23,6 +23,7 @@ import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.core.TestIdentity import net.corda.testing.internal.kryoSpecific import net.corda.testing.internal.rigorousMock +import net.corda.testing.node.MockCordappConfigProvider import net.corda.testing.services.MockAttachmentStorage import org.apache.commons.io.IOUtils import org.junit.Assert.* @@ -57,7 +58,7 @@ class AttachmentsClassLoaderTests { @JvmField val testSerialization = SerializationEnvironmentRule() private val attachments = MockAttachmentStorage() - private val cordappProvider = CordappProviderImpl(CordappLoader.createDevMode(listOf(ISOLATED_CONTRACTS_JAR_PATH)), attachments) + private val cordappProvider = CordappProviderImpl(CordappLoader.createDevMode(listOf(ISOLATED_CONTRACTS_JAR_PATH)), MockCordappConfigProvider(), attachments) private val cordapp get() = cordappProvider.cordapps.first() private val attachmentId get() = cordappProvider.getCordappAttachmentId(cordapp)!! private val appContext get() = cordappProvider.getAppContext(cordapp) diff --git a/node/capsule/build.gradle b/node/capsule/build.gradle index 3572e594fb..ca320e6b5d 100644 --- a/node/capsule/build.gradle +++ b/node/capsule/build.gradle @@ -59,11 +59,6 @@ task buildCordaJAR(type: FatCapsule, dependsOn: project(':node').compileJava) { // If you change these flags, please also update Driver.kt jvmArgs = ['-Xmx200m', '-XX:+UseG1GC'] } - - // Make the resulting JAR file directly executable on UNIX by prepending a shell script to it. - // This lets you run the file like so: ./corda.jar - // Other than being slightly less typing, this has one big advantage: Ctrl-C works properly in the terminal. - reallyExecutable { trampolining() } } artifacts { diff --git a/node/src/integration-test/kotlin/net/corda/node/CordappConfigFileProviderTests.kt b/node/src/integration-test/kotlin/net/corda/node/CordappConfigFileProviderTests.kt new file mode 100644 index 0000000000..ee2fd4674f --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/node/CordappConfigFileProviderTests.kt @@ -0,0 +1,60 @@ +package net.corda.node + +import com.typesafe.config.Config +import com.typesafe.config.ConfigException +import com.typesafe.config.ConfigFactory +import com.typesafe.config.ConfigRenderOptions +import net.corda.node.internal.cordapp.CordappConfigFileProvider +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import java.io.File +import java.nio.file.Files +import java.nio.file.Path + +class CordappConfigFileProviderTests { + private companion object { + val cordappConfDir = File("build/tmp/cordapps/config") + val cordappName = "test" + val cordappConfFile = File(cordappConfDir, cordappName + ".conf").toPath() + + val validConfig = ConfigFactory.parseString("key=value") + val alternateValidConfig = ConfigFactory.parseString("key=alternateValue") + val invalidConfig = "Invalid" + } + + val provider = CordappConfigFileProvider(cordappConfDir) + + @Test + fun `test that config can be loaded`() { + writeConfig(validConfig, cordappConfFile) + assertThat(provider.getConfigByName(cordappName)).isEqualTo(validConfig) + } + + @Test + fun `config is idempotent if the underlying file is not changed`() { + writeConfig(validConfig, cordappConfFile) + assertThat(provider.getConfigByName(cordappName)).isEqualTo(validConfig) + assertThat(provider.getConfigByName(cordappName)).isEqualTo(validConfig) + } + + @Test + fun `config is not idempotent if the underlying file is changed`() { + writeConfig(validConfig, cordappConfFile) + assertThat(provider.getConfigByName(cordappName)).isEqualTo(validConfig) + + writeConfig(alternateValidConfig, cordappConfFile) + assertThat(provider.getConfigByName(cordappName)).isEqualTo(alternateValidConfig) + } + + @Test(expected = ConfigException.Parse::class) + fun `an invalid config throws an exception`() { + Files.write(cordappConfFile, invalidConfig.toByteArray()) + + provider.getConfigByName(cordappName) + } + + /** + * Writes the config to the path provided - will (and must) overwrite any existing config + */ + private fun writeConfig(config: Config, to: Path) = Files.write(cordappConfFile, config.root().render(ConfigRenderOptions.concise()).toByteArray()) +} \ No newline at end of file diff --git a/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt index 5fa206af81..65dd56c3f1 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt @@ -29,6 +29,7 @@ import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.driver import net.corda.testing.internal.rigorousMock import net.corda.testing.internal.withoutTestSerialization +import net.corda.testing.node.MockCordappConfigProvider import net.corda.testing.services.MockAttachmentStorage import org.junit.Assert.assertEquals import org.junit.Rule @@ -42,7 +43,7 @@ class AttachmentLoadingTests { @JvmField val testSerialization = SerializationEnvironmentRule() private val attachments = MockAttachmentStorage() - private val provider = CordappProviderImpl(CordappLoader.createDevMode(listOf(isolatedJAR)), attachments) + private val provider = CordappProviderImpl(CordappLoader.createDevMode(listOf(isolatedJAR)), MockCordappConfigProvider(), attachments) private val cordapp get() = provider.cordapps.first() private val attachmentId get() = provider.getCordappAttachmentId(cordapp)!! private val appContext get() = provider.getAppContext(cordapp) diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index a88bd73b5d..c00e1fc175 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -32,6 +32,7 @@ import net.corda.core.utilities.debug import net.corda.core.utilities.getOrThrow import net.corda.node.VersionInfo import net.corda.node.internal.classloading.requireAnnotation +import net.corda.node.internal.cordapp.CordappConfigFileProvider import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.internal.cordapp.CordappProviderImpl import net.corda.node.internal.cordapp.CordappProviderInternal @@ -539,7 +540,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, checkpointStorage = DBCheckpointStorage() val metrics = MetricRegistry() attachments = NodeAttachmentService(metrics, configuration.attachmentContentCacheSizeBytes, configuration.attachmentCacheBound) - val cordappProvider = CordappProviderImpl(cordappLoader, attachments) + val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(), attachments) val keyManagementService = makeKeyManagementService(identityService, keyPairs) _services = ServiceHubInternalImpl( identityService, diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappConfigFileProvider.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappConfigFileProvider.kt new file mode 100644 index 0000000000..ce30f5f0f6 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappConfigFileProvider.kt @@ -0,0 +1,36 @@ +package net.corda.node.internal.cordapp + +import com.typesafe.config.Config +import com.typesafe.config.ConfigFactory +import net.corda.core.internal.cordapp.CordappConfigProvider +import net.corda.core.utilities.loggerFor +import sun.plugin.dom.exception.InvalidStateException +import java.io.File + +class CordappConfigFileProvider(val configDir: File = DEFAULT_CORDAPP_CONFIG_DIR) : CordappConfigProvider { + companion object { + val DEFAULT_CORDAPP_CONFIG_DIR = File("cordapps/config") + val CONFIG_EXT = ".conf" + val logger = loggerFor() + } + + init { + configDir.mkdirs() + } + + override fun getConfigByName(name: String): Config { + val configFile = File(configDir, name + CONFIG_EXT) + return if (configFile.exists()) { + if (configFile.isDirectory) { + throw InvalidStateException("ile at ${configFile.absolutePath} is a directory, expected a config file") + } else { + logger.info("Found config for cordapp $name in ${configFile.absolutePath}") + ConfigFactory.parseFile(configFile) + } + } else { + logger.info("No config found for cordapp $name in ${configFile.absolutePath}") + ConfigFactory.empty() + } + } + +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappConfigProvider.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappConfigProvider.kt new file mode 100644 index 0000000000..f632481d1c --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappConfigProvider.kt @@ -0,0 +1,7 @@ +package net.corda.core.internal.cordapp + +import com.typesafe.config.Config + +interface CordappConfigProvider { + fun getConfigByName(name: String): Config +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderImpl.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderImpl.kt index cfed6d0cd2..a25d872124 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappProviderImpl.kt @@ -5,21 +5,27 @@ import net.corda.core.contracts.ContractClassName import net.corda.core.cordapp.Cordapp import net.corda.core.cordapp.CordappContext import net.corda.core.crypto.SecureHash +import net.corda.core.internal.cordapp.CordappConfigProvider +import net.corda.core.internal.createCordappContext import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.AttachmentStorage import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.utilities.loggerFor import java.net.URL +import java.util.concurrent.ConcurrentHashMap /** * Cordapp provider and store. For querying CorDapps for their attachment and vice versa. */ -open class CordappProviderImpl(private val cordappLoader: CordappLoader, attachmentStorage: AttachmentStorage) : SingletonSerializeAsToken(), CordappProviderInternal { +open class CordappProviderImpl(private val cordappLoader: CordappLoader, private val cordappConfigProvider: CordappConfigProvider, attachmentStorage: AttachmentStorage) : SingletonSerializeAsToken(), CordappProviderInternal { companion object { private val log = loggerFor() } + private val contextCache = ConcurrentHashMap() + + override fun getAppContext(): CordappContext { // TODO: Use better supported APIs in Java 9 Exception().stackTrace.forEach { stackFrame -> @@ -51,7 +57,7 @@ open class CordappProviderImpl(private val cordappLoader: CordappLoader, attachm private fun loadContractsIntoAttachmentStore(attachmentStorage: AttachmentStorage): Map { val cordappsWithAttachments = cordapps.filter { !it.contractClassNames.isEmpty() }.map { it.jarPath } - val attachmentIds = cordappsWithAttachments.map { it.openStream().use { attachmentStorage.importOrGetAttachment(it) }} + val attachmentIds = cordappsWithAttachments.map { it.openStream().use { attachmentStorage.importOrGetAttachment(it) } } return attachmentIds.zip(cordappsWithAttachments).toMap() } @@ -62,7 +68,14 @@ open class CordappProviderImpl(private val cordappLoader: CordappLoader, attachm * @return A cordapp context for the given CorDapp */ fun getAppContext(cordapp: Cordapp): CordappContext { - return CordappContext(cordapp, getCordappAttachmentId(cordapp), cordappLoader.appClassLoader) + return contextCache.computeIfAbsent(cordapp, { + createCordappContext( + cordapp, + getCordappAttachmentId(cordapp), + cordappLoader.appClassLoader, + TypesafeCordappConfig(cordappConfigProvider.getConfigByName(cordapp.name)) + ) + }) } /** diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/TypesafeCordappConfig.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/TypesafeCordappConfig.kt new file mode 100644 index 0000000000..73f5633350 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/TypesafeCordappConfig.kt @@ -0,0 +1,79 @@ +package net.corda.node.internal.cordapp + +import com.typesafe.config.Config +import com.typesafe.config.ConfigException +import net.corda.core.cordapp.CordappConfig +import net.corda.core.cordapp.CordappConfigException + +/** + * Provides configuration from a typesafe config source + */ +class TypesafeCordappConfig(private val cordappConfig: Config) : CordappConfig { + override fun exists(path: String): Boolean { + return cordappConfig.hasPath(path) + } + + override fun get(path: String): Any { + try { + return cordappConfig.getAnyRef(path) + } catch (e: ConfigException) { + throw CordappConfigException("Cordapp configuration is incorrect due to exception", e) + } + } + + override fun getInt(path: String): Int { + try { + return cordappConfig.getInt(path) + } catch (e: ConfigException) { + throw CordappConfigException("Cordapp configuration is incorrect due to exception", e) + } + } + + override fun getLong(path: String): Long { + try { + return cordappConfig.getLong(path) + } catch (e: ConfigException) { + throw CordappConfigException("Cordapp configuration is incorrect due to exception", e) + } + } + + override fun getFloat(path: String): Float { + try { + return cordappConfig.getDouble(path).toFloat() + } catch (e: ConfigException) { + throw CordappConfigException("Cordapp configuration is incorrect due to exception", e) + } + } + + override fun getDouble(path: String): Double { + try { + return cordappConfig.getDouble(path) + } catch (e: ConfigException) { + throw CordappConfigException("Cordapp configuration is incorrect due to exception", e) + } + } + + override fun getNumber(path: String): Number { + try { + return cordappConfig.getNumber(path) + } catch (e: ConfigException) { + throw CordappConfigException("Cordapp configuration is incorrect due to exception", e) + } + } + + override fun getString(path: String): String { + try { + return cordappConfig.getString(path) + } catch (e: ConfigException) { + throw CordappConfigException("Cordapp configuration is incorrect due to exception", e) + } + } + + override fun getBoolean(path: String): Boolean { + try { + return cordappConfig.getBoolean(path) + } catch (e: ConfigException) { + throw CordappConfigException("Cordapp configuration is incorrect due to exception", e) + } + } +} \ No newline at end of file diff --git a/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappProviderImplTests.kt b/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappProviderImplTests.kt index 8526b36bcd..934c906e5d 100644 --- a/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappProviderImplTests.kt +++ b/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappProviderImplTests.kt @@ -1,15 +1,27 @@ package net.corda.node.internal.cordapp +import com.typesafe.config.Config +import com.typesafe.config.ConfigFactory +import net.corda.core.internal.cordapp.CordappConfigProvider import net.corda.core.node.services.AttachmentStorage +import net.corda.testing.node.MockCordappConfigProvider import net.corda.testing.services.MockAttachmentStorage +import org.assertj.core.api.Assertions.assertThat import org.junit.Assert import org.junit.Before import org.junit.Test class CordappProviderImplTests { - companion object { - private val isolatedJAR = this::class.java.getResource("isolated.jar")!! - private val emptyJAR = this::class.java.getResource("empty.jar")!! + private companion object { + val isolatedJAR = this::class.java.getResource("isolated.jar")!! + // TODO: Cordapp name should differ from the JAR name + val isolatedCordappName = "isolated" + val emptyJAR = this::class.java.getResource("empty.jar")!! + val validConfig = ConfigFactory.parseString("key=value") + + val stubConfigProvider = object : CordappConfigProvider { + override fun getConfigByName(name: String): Config = ConfigFactory.empty() + } } private lateinit var attachmentStore: AttachmentStorage @@ -22,7 +34,7 @@ class CordappProviderImplTests { @Test fun `isolated jar is loaded into the attachment store`() { val loader = CordappLoader.createDevMode(listOf(isolatedJAR)) - val provider = CordappProviderImpl(loader, attachmentStore) + val provider = CordappProviderImpl(loader, stubConfigProvider, attachmentStore) val maybeAttachmentId = provider.getCordappAttachmentId(provider.cordapps.first()) Assert.assertNotNull(maybeAttachmentId) @@ -32,14 +44,14 @@ class CordappProviderImplTests { @Test fun `empty jar is not loaded into the attachment store`() { val loader = CordappLoader.createDevMode(listOf(emptyJAR)) - val provider = CordappProviderImpl(loader, attachmentStore) + val provider = CordappProviderImpl(loader, stubConfigProvider, attachmentStore) Assert.assertNull(provider.getCordappAttachmentId(provider.cordapps.first())) } @Test fun `test that we find a cordapp class that is loaded into the store`() { val loader = CordappLoader.createDevMode(listOf(isolatedJAR)) - val provider = CordappProviderImpl(loader, attachmentStore) + val provider = CordappProviderImpl(loader, stubConfigProvider, attachmentStore) val className = "net.corda.finance.contracts.isolated.AnotherDummyContract" val expected = provider.cordapps.first() @@ -50,9 +62,9 @@ class CordappProviderImplTests { } @Test - fun `test that we find an attachment for a cordapp contrat class`() { + fun `test that we find an attachment for a cordapp contract class`() { val loader = CordappLoader.createDevMode(listOf(isolatedJAR)) - val provider = CordappProviderImpl(loader, attachmentStore) + val provider = CordappProviderImpl(loader, stubConfigProvider, attachmentStore) val className = "net.corda.finance.contracts.isolated.AnotherDummyContract" val expected = provider.getAppContext(provider.cordapps.first()).attachmentId val actual = provider.getContractAttachmentID(className) @@ -60,4 +72,16 @@ class CordappProviderImplTests { Assert.assertNotNull(actual) Assert.assertEquals(actual!!, expected) } + + @Test + fun `test cordapp configuration`() { + val configProvider = MockCordappConfigProvider() + configProvider.cordappConfigs.put(isolatedCordappName, validConfig) + val loader = CordappLoader.createDevMode(listOf(isolatedJAR)) + val provider = CordappProviderImpl(loader, configProvider, attachmentStore) + + val expected = provider.getAppContext(provider.cordapps.first()).config + + assertThat(expected.getString("key")).isEqualTo("value") + } } diff --git a/node/src/test/kotlin/net/corda/node/internal/cordapp/TypesafeCordappConfigTests.kt b/node/src/test/kotlin/net/corda/node/internal/cordapp/TypesafeCordappConfigTests.kt new file mode 100644 index 0000000000..6a966ea4b0 --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/internal/cordapp/TypesafeCordappConfigTests.kt @@ -0,0 +1,47 @@ +package net.corda.node.internal.cordapp + +import com.typesafe.config.ConfigFactory +import net.corda.core.cordapp.CordappConfigException +import org.junit.Test +import org.assertj.core.api.Assertions.assertThat + +class TypesafeCordappConfigTests { + @Test + fun `test that all value types can be retrieved`() { + val config = ConfigFactory.parseString("string=string\nint=1\nfloat=1.0\ndouble=1.0\nnumber=2\ndouble=1.01\nbool=false") + val cordappConf = TypesafeCordappConfig(config) + + assertThat(cordappConf.get("string")).isEqualTo("string") + assertThat(cordappConf.getString("string")).isEqualTo("string") + assertThat(cordappConf.getInt("int")).isEqualTo(1) + assertThat(cordappConf.getFloat("float")).isEqualTo(1.0F) + assertThat(cordappConf.getDouble("double")).isEqualTo(1.01) + assertThat(cordappConf.getNumber("number")).isEqualTo(2) + assertThat(cordappConf.getBoolean("bool")).isEqualTo(false) + } + + @Test + fun `test a nested path`() { + val config = ConfigFactory.parseString("outer: { inner: string }") + val cordappConf = TypesafeCordappConfig(config) + + assertThat(cordappConf.getString("outer.inner")).isEqualTo("string") + } + + @Test + fun `test exists determines existence and lack of existence correctly`() { + val config = ConfigFactory.parseString("exists=exists") + val cordappConf = TypesafeCordappConfig(config) + + assertThat(cordappConf.exists("exists")).isTrue() + assertThat(cordappConf.exists("notexists")).isFalse() + } + + @Test(expected = CordappConfigException::class) + fun `test that an exception is thrown when trying to access a non-extant field`() { + val config = ConfigFactory.empty() + val cordappConf = TypesafeCordappConfig(config) + + cordappConf.get("anything") + } +} \ No newline at end of file diff --git a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaCordformTest.kt b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaCordformTest.kt index 34432277a1..e5235553b6 100644 --- a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaCordformTest.kt +++ b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaCordformTest.kt @@ -2,13 +2,13 @@ package net.corda.bank import net.corda.finance.DOLLARS import net.corda.finance.POUNDS -import net.corda.testing.node.internal.demorun.deployNodesThen +import net.corda.testing.node.internal.demorun.nodeRunner import org.junit.Test class BankOfCordaCordformTest { @Test fun `run demo`() { - BankOfCordaCordform().deployNodesThen { + BankOfCordaCordform().nodeRunner().scanPackages(listOf("net.corda.finance")).deployAndRunNodesThen { IssueCash.requestWebIssue(30000.POUNDS) IssueCash.requestRpcIssue(20000.DOLLARS) } diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaCordform.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaCordform.kt index 260d37365f..e750504a8f 100644 --- a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaCordform.kt +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaCordform.kt @@ -25,8 +25,8 @@ private const val BOC_RPC_ADMIN_PORT = 10015 private const val BOC_WEB_PORT = 10007 class BankOfCordaCordform : CordformDefinition() { + // TODO: Readd finance dependency - will fail without it init { - cordappPackages += "net.corda.finance" node { name(NOTARY_NAME) notary(NotaryConfig(validating = true)) @@ -65,7 +65,7 @@ class BankOfCordaCordform : CordformDefinition() { object DeployNodes { @JvmStatic fun main(args: Array) { - BankOfCordaCordform().deployNodes() + BankOfCordaCordform().nodeRunner().scanPackages(listOf("net.corda.finance")).deployAndRunNodes() } } diff --git a/samples/cordapp-configuration/README.md b/samples/cordapp-configuration/README.md new file mode 100644 index 0000000000..651e8debc9 --- /dev/null +++ b/samples/cordapp-configuration/README.md @@ -0,0 +1,23 @@ +# Cordapp Configuration Sample + +This sample shows a simple example of how to use per-cordapp configuration. It includes; + +* A configuration file +* Gradle build file to show how to install your Cordapp configuration +* A flow that consumes the Cordapp configuration + +## Usage + +To run the sample you must first build it from the project root with; + + ./gradlew deployNodes + +This will deploy the node with the configuration installed. +The relevant section is the ``deployNodes`` task. + +## Running + +* Windows: `build\nodes\runnodes` +* Mac/Linux: `./build/nodes/runnodes` + +Once the nodes have started up and show a prompt you can now run your flow. \ No newline at end of file diff --git a/samples/cordapp-configuration/build.gradle b/samples/cordapp-configuration/build.gradle new file mode 100644 index 0000000000..37edc09147 --- /dev/null +++ b/samples/cordapp-configuration/build.gradle @@ -0,0 +1,54 @@ +apply plugin: 'kotlin' +apply plugin: 'java' +apply plugin: 'net.corda.plugins.cordapp' +apply plugin: 'net.corda.plugins.cordformation' + +dependencies { + cordaCompile project(":core") + cordaCompile project(":node-api") + cordaCompile project(path: ":node:capsule", configuration: 'runtimeArtifacts') + cordaCompile project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts') +} + +task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { + ext.rpcUsers = [['username': "default", 'password': "default", 'permissions': [ 'ALL' ]]] + + directory "./build/nodes" + node { + name "O=Notary Service,L=Zurich,C=CH" + notary = [validating : true] + p2pPort 10002 + rpcSettings { + port 10003 + adminPort 10004 + } + } + node { + name "O=Bank A,L=London,C=GB" + p2pPort 10005 + cordapps = [] + rpcUsers = ext.rpcUsers + // This configures the default cordapp for this node + projectCordapp { + config "someStringValue=test" + } + rpcSettings { + port 10007 + adminPort 10008 + } + } + node { + name "O=Bank B,L=New York,C=US" + p2pPort 10009 + cordapps = [] + rpcUsers = ext.rpcUsers + // This configures the default cordapp for this node + projectCordapp { + config project.file("src/config.conf") + } + rpcSettings { + port 10011 + adminPort 10012 + } + } +} \ No newline at end of file diff --git a/samples/cordapp-configuration/src/config.conf b/samples/cordapp-configuration/src/config.conf new file mode 100644 index 0000000000..5e2d9fdcd6 --- /dev/null +++ b/samples/cordapp-configuration/src/config.conf @@ -0,0 +1,5 @@ +someStringValue=hello world +someIntValue=1 +nested: { + value: a string +} \ No newline at end of file diff --git a/samples/cordapp-configuration/src/main/kotlin/net/corda/configsample/ConfigSampleFlow.kt b/samples/cordapp-configuration/src/main/kotlin/net/corda/configsample/ConfigSampleFlow.kt new file mode 100644 index 0000000000..251830f538 --- /dev/null +++ b/samples/cordapp-configuration/src/main/kotlin/net/corda/configsample/ConfigSampleFlow.kt @@ -0,0 +1,10 @@ +package net.corda.configsample + +import net.corda.core.flows.FlowLogic + +class ConfigSampleFlow : FlowLogic() { + override fun call(): String { + val config = serviceHub.getAppContext().config + return config.getString("someStringValue") + } +} \ No newline at end of file diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt index 132636f05a..9f5c8802e4 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt @@ -14,7 +14,7 @@ import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import java.nio.file.Paths -fun main(args: Array) = BFTNotaryCordform().deployNodes() +fun main(args: Array) = BFTNotaryCordform().nodeRunner().deployAndRunNodes() private val clusterSize = 4 // Minimum size that tolerates a faulty replica. private val notaryNames = createNotaryNames(clusterSize) diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/Clean.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/Clean.kt index 91aa5ba967..2fbba77b26 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/Clean.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/Clean.kt @@ -1,9 +1,9 @@ package net.corda.notarydemo -import net.corda.testing.node.internal.demorun.clean +import net.corda.testing.node.internal.demorun.nodeRunner fun main(args: Array) { - listOf(SingleNotaryCordform(), RaftNotaryCordform(), BFTNotaryCordform()).forEach { + listOf(SingleNotaryCordform(), RaftNotaryCordform(), BFTNotaryCordform()).map { it.nodeRunner() }.forEach { it.clean() } } diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/CustomNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/CustomNotaryCordform.kt index e9a857b1cb..3aa6b38654 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/CustomNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/CustomNotaryCordform.kt @@ -9,7 +9,7 @@ import net.corda.testing.core.BOB_NAME import net.corda.testing.core.DUMMY_NOTARY_NAME import java.nio.file.Paths -fun main(args: Array) = CustomNotaryCordform().deployNodes() +fun main(args: Array) = CustomNotaryCordform().nodeRunner().deployAndRunNodes() class CustomNotaryCordform : CordformDefinition() { init { diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt index c4cbcd0d8e..2a2aec2582 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt @@ -13,7 +13,7 @@ import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import java.nio.file.Paths -fun main(args: Array) = RaftNotaryCordform().deployNodes() +fun main(args: Array) = RaftNotaryCordform().nodeRunner().deployAndRunNodes() internal fun createNotaryNames(clusterSize: Int) = (0 until clusterSize).map { CordaX500Name("Notary Service $it", "Zurich", "CH") } diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt index af449ce5cb..d5acdb135c 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt @@ -11,7 +11,7 @@ import net.corda.testing.node.User import net.corda.testing.node.internal.demorun.* import java.nio.file.Paths -fun main(args: Array) = SingleNotaryCordform().deployNodes() +fun main(args: Array) = SingleNotaryCordform().nodeRunner().deployAndRunNodes() val notaryDemoUser = User("demou", "demop", setOf(all())) diff --git a/samples/simm-valuation-demo/build.gradle b/samples/simm-valuation-demo/build.gradle index b560d1aa4f..9edd4f2853 100644 --- a/samples/simm-valuation-demo/build.gradle +++ b/samples/simm-valuation-demo/build.gradle @@ -68,7 +68,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { name "O=Notary Service,L=Zurich,C=CH" notary = [validating : true] p2pPort 10002 - cordapps = ["$project.group:finance:$corda_release_version"] + cordapp project(':finance') extraConfig = [ jvmArgs : [ "-Xmx1g"] ] @@ -81,7 +81,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { address("localhost:10016") adminAddress("localhost:10017") } - cordapps = ["$project.group:finance:$corda_release_version"] + cordapp project(':finance') rpcUsers = ext.rpcUsers extraConfig = [ jvmArgs : [ "-Xmx1g"] @@ -95,7 +95,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { address("localhost:10026") adminAddress("localhost:10027") } - cordapps = ["$project.group:finance:$corda_release_version"] + cordapp project(':finance') rpcUsers = ext.rpcUsers extraConfig = [ jvmArgs : [ "-Xmx1g"] @@ -109,7 +109,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { address("localhost:10036") adminAddress("localhost:10037") } - cordapps = ["$project.group:finance:$corda_release_version"] + cordapp project(':finance') rpcUsers = ext.rpcUsers extraConfig = [ jvmArgs : [ "-Xmx1g"] diff --git a/settings.gradle b/settings.gradle index 4b0a92c815..1191e87414 100644 --- a/settings.gradle +++ b/settings.gradle @@ -46,3 +46,4 @@ include 'samples:network-visualiser' include 'samples:simm-valuation-demo' include 'samples:notary-demo' include 'samples:bank-of-corda-demo' +include 'samples:cordapp-configuration' diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/demorun/CordformNodeRunner.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/demorun/CordformNodeRunner.kt new file mode 100644 index 0000000000..bbaf5724a5 --- /dev/null +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/demorun/CordformNodeRunner.kt @@ -0,0 +1,77 @@ +package net.corda.testing.node.internal.demorun + +import net.corda.cordform.CordformDefinition +import net.corda.cordform.CordformNode +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.getOrThrow +import net.corda.testing.driver.JmxPolicy +import net.corda.testing.driver.PortAllocation +import net.corda.testing.node.internal.internalDriver + +/** + * Creates a demo runner for this cordform definition + */ +fun CordformDefinition.nodeRunner() = CordformNodeRunner(this) + +/** + * A node runner creates and runs nodes for a given [[CordformDefinition]]. + */ +class CordformNodeRunner(val cordformDefinition: CordformDefinition) { + private var extraPackagesToScan = emptyList() + + /** + * Builder method to sets the extra cordapp scan packages + */ + fun scanPackages(packages: List): CordformNodeRunner { + extraPackagesToScan = packages + return this + } + + fun clean() { + System.err.println("Deleting: ${cordformDefinition.nodesDirectory}") + cordformDefinition.nodesDirectory.toFile().deleteRecursively() + } + + /** + * Deploy the nodes specified in the given [CordformDefinition]. This will block until all the nodes and webservers + * have terminated. + */ + fun deployAndRunNodes() { + runNodes(waitForAllNodesToFinish = true) { } + } + + /** + * Deploy the nodes specified in the given [CordformDefinition] and then execute the given [block] once all the nodes + * and webservers are up. After execution all these processes will be terminated. + */ + fun deployAndRunNodesThen(block: () -> Unit) { + runNodes(waitForAllNodesToFinish = false, block = block) + } + + private fun runNodes(waitForAllNodesToFinish: Boolean, block: () -> Unit) { + clean() + val nodes = cordformDefinition.nodeConfigurers.map { configurer -> CordformNode().also { configurer.accept(it) } } + val maxPort = nodes + .flatMap { listOf(it.p2pAddress, it.rpcAddress, it.webAddress) } + .mapNotNull { address -> address?.let { NetworkHostAndPort.parse(it).port } } + .max()!! + internalDriver( + isDebug = true, + jmxPolicy = JmxPolicy(true), + driverDirectory = cordformDefinition.nodesDirectory, + extraCordappPackagesToScan = extraPackagesToScan, + // Notaries are manually specified in Cordform so we don't want the driver automatically starting any + notarySpecs = emptyList(), + // Start from after the largest port used to prevent port clash + portAllocation = PortAllocation.Incremental(maxPort + 1), + waitForAllNodesToFinish = waitForAllNodesToFinish + ) { + cordformDefinition.setup(this) + startCordformNodes(nodes).getOrThrow() // Only proceed once everything is up and running + println("All nodes and webservers are ready...") + block() + } + } +} + + diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/demorun/DemoRunner.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/demorun/DemoRunner.kt deleted file mode 100644 index 185a851e99..0000000000 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/demorun/DemoRunner.kt +++ /dev/null @@ -1,57 +0,0 @@ -@file:JvmName("DemoRunner") - -package net.corda.testing.node.internal.demorun - -import net.corda.cordform.CordformDefinition -import net.corda.cordform.CordformNode -import net.corda.core.utilities.NetworkHostAndPort -import net.corda.core.utilities.getOrThrow -import net.corda.testing.driver.JmxPolicy -import net.corda.testing.driver.PortAllocation -import net.corda.testing.node.internal.internalDriver - -fun CordformDefinition.clean() { - System.err.println("Deleting: $nodesDirectory") - nodesDirectory.toFile().deleteRecursively() -} - -/** - * Deploy the nodes specified in the given [CordformDefinition]. This will block until all the nodes and webservers - * have terminated. - */ -fun CordformDefinition.deployNodes() { - runNodes(waitForAllNodesToFinish = true) { } -} - -/** - * Deploy the nodes specified in the given [CordformDefinition] and then execute the given [block] once all the nodes - * and webservers are up. After execution all these processes will be terminated. - */ -fun CordformDefinition.deployNodesThen(block: () -> Unit) { - runNodes(waitForAllNodesToFinish = false, block = block) -} - -private fun CordformDefinition.runNodes(waitForAllNodesToFinish: Boolean, block: () -> Unit) { - clean() - val nodes = nodeConfigurers.map { configurer -> CordformNode().also { configurer.accept(it) } } - val maxPort = nodes - .flatMap { listOf(it.p2pAddress, it.rpcAddress, it.webAddress) } - .mapNotNull { address -> address?.let { NetworkHostAndPort.parse(it).port } } - .max()!! - internalDriver( - isDebug = true, - jmxPolicy = JmxPolicy(true), - driverDirectory = nodesDirectory, - extraCordappPackagesToScan = cordappPackages, - // Notaries are manually specified in Cordform so we don't want the driver automatically starting any - notarySpecs = emptyList(), - // Start from after the largest port used to prevent port clash - portAllocation = PortAllocation.Incremental(maxPort + 1), - waitForAllNodesToFinish = waitForAllNodesToFinish - ) { - setup(this) - startCordformNodes(nodes).getOrThrow() // Only proceed once everything is up and running - println("All nodes and webservers are ready...") - block() - } -} diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/node/MockCordappConfigProvider.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/node/MockCordappConfigProvider.kt new file mode 100644 index 0000000000..d351154982 --- /dev/null +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/node/MockCordappConfigProvider.kt @@ -0,0 +1,17 @@ +package net.corda.testing.node + +import com.typesafe.config.Config +import com.typesafe.config.ConfigFactory +import net.corda.core.internal.cordapp.CordappConfigProvider + +class MockCordappConfigProvider : CordappConfigProvider { + val cordappConfigs = mutableMapOf () + + override fun getConfigByName(name: String): Config { + return if(cordappConfigs.containsKey(name)) { + cordappConfigs[name]!! + } else { + ConfigFactory.empty() + } + } +} \ No newline at end of file diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockCordappProvider.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockCordappProvider.kt index dba07b73be..82ca31d6f9 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockCordappProvider.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockCordappProvider.kt @@ -7,10 +7,17 @@ import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.AttachmentStorage import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.internal.cordapp.CordappProviderImpl +import net.corda.testing.node.MockCordappConfigProvider import java.nio.file.Paths import java.util.* -class MockCordappProvider(cordappLoader: CordappLoader, attachmentStorage: AttachmentStorage) : CordappProviderImpl(cordappLoader, attachmentStorage) { +class MockCordappProvider( + cordappLoader: CordappLoader, + attachmentStorage: AttachmentStorage, + val cordappConfigProvider: MockCordappConfigProvider = MockCordappConfigProvider() +) : CordappProviderImpl(cordappLoader, cordappConfigProvider, attachmentStorage) { + constructor(cordappLoader: CordappLoader, attachmentStorage: AttachmentStorage) : this(cordappLoader, attachmentStorage, MockCordappConfigProvider()) + val cordappRegistry = mutableListOf>() fun addMockCordapp(contractClassName: ContractClassName, attachments: MockAttachmentStorage) { From 81b16776f3d5109e977d86d75287ef0a7f773eb5 Mon Sep 17 00:00:00 2001 From: Andras Slemmer Date: Wed, 14 Feb 2018 15:16:59 +0000 Subject: [PATCH 19/50] Fix RPC observation vs reply ordering --- .../node/services/messaging/RPCServer.kt | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt b/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt index eca9ce1601..deac23fa84 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt @@ -221,11 +221,16 @@ class RPCServer( private fun handleSendJob(sequenceNumber: Long, job: RpcSendJob.Send) { try { - job.artemisMessage.putLongProperty(RPCApi.DEDUPLICATION_SEQUENCE_NUMBER_FIELD_NAME, sequenceNumber) - rpcProducer!!.send(job.clientAddress, job.artemisMessage) - log.debug { "<- RPC <- ${job.originalMessage}" } + val artemisMessage = producerSession!!.createMessage(false) + // We must do the serialisation here as any encountered Observables may already have events, which would + // trigger more sends. We must make sure that the root of the Observables (e.g. the RPC reply) is sent + // before any child observations. + job.message.writeToClientMessage(job.serializationContext, artemisMessage) + artemisMessage.putLongProperty(RPCApi.DEDUPLICATION_SEQUENCE_NUMBER_FIELD_NAME, sequenceNumber) + rpcProducer!!.send(job.clientAddress, artemisMessage) + log.debug { "<- RPC <- ${job.message}" } } catch (throwable: Throwable) { - log.error("Failed to send message, kicking client. Message was ${job.originalMessage}", throwable) + log.error("Failed to send message, kicking client. Message was ${job.message}", throwable) serverControl!!.closeConsumerConnectionsForAddress(job.clientAddress.toString()) invalidateClient(job.clientAddress) } @@ -413,17 +418,15 @@ class RPCServer( private val serializationContextWithObservableContext = RpcServerObservableSerializer.createContext(this) fun sendMessage(serverToClient: RPCApi.ServerToClient) { - val artemisMessage = producerSession!!.createMessage(false) - serverToClient.writeToClientMessage(serializationContextWithObservableContext, artemisMessage) - sendJobQueue.put(RpcSendJob.Send(clientAddress, artemisMessage, serverToClient)) + sendJobQueue.put(RpcSendJob.Send(clientAddress, serializationContextWithObservableContext, serverToClient)) } } private sealed class RpcSendJob { data class Send( val clientAddress: SimpleString, - val artemisMessage: ClientMessage, - val originalMessage: RPCApi.ServerToClient + val serializationContext: SerializationContext, + val message: RPCApi.ServerToClient ) : RpcSendJob() object Stop : RpcSendJob() } From 3e8d76334ea20be14f428d701f41f7ebd6ee1932 Mon Sep 17 00:00:00 2001 From: Anthony Keenan Date: Wed, 14 Feb 2018 16:42:56 +0000 Subject: [PATCH 20/50] CORDA-939 Modify Api Scanner to check api for internal exposures (#2510) * Update check api changes to look for internals * Update several more uses of internal * Make check-api-changes script filter out internal class usages * Make CordaClock part of API * Update api-current.txt * Remove exclusion of nodeapi.internal * Remove access to CordaPersistence from public api * Don't expose DB Connection from StartedMockNode and remove unnecessary transaction from CustomVaultQueryTest * Make internal tests that use need db access use InternalMockNetwork * Make test certificates internal * Address further review comments * Revert some accidental changes to api-current.txt * Address Shams' review comments * Update Api Scanner to filter out CordaInternal attribute * Update api-current.txt * Remove superfluous brackets * Add transaction to StartedMockNode * More leaky transaction fixes --- .ci/api-current.txt | 36 +----------- .ci/check-api-changes.sh | 20 ++++++- .../confidential/IdentitySyncFlowTests.kt | 5 +- .../confidential/SwapIdentitiesFlowTests.kt | 5 +- .../core/flows/CollectSignaturesFlowTests.kt | 15 ++--- .../core/identity/PartyAndCertificateTest.kt | 2 +- .../internal/ResolveTransactionsFlowTest.kt | 15 ++--- .../LedgerTransactionQueryTests.kt | 3 +- .../net/corda/docs/CustomVaultQueryTest.kt | 14 ++--- .../docs/FxTransactionBuildTutorialTest.kt | 9 +-- .../finance/flows/CashPaymentFlowTests.kt | 13 +++-- gradle-plugins/api-scanner/build.gradle | 2 + .../main/java/net/corda/plugins/ScanApi.java | 13 +++++ ...tachmentsClassLoaderStaticContractTests.kt | 2 +- .../internal/AttachmentsClassLoaderTests.kt | 2 +- .../node/services/AttachmentLoadingTests.kt | 2 +- .../node/services/BFTNotaryServiceTests.kt | 17 +++--- .../node/services/RaftNotaryServiceTests.kt | 3 +- .../registration/NodeRegistrationTest.kt | 2 +- .../corda/node/{internal => }/CordaClock.kt | 2 +- .../net/corda/node/internal/AbstractNode.kt | 1 + .../kotlin/net/corda/node/internal/Node.kt | 2 + .../services/events/NodeSchedulerService.kt | 4 +- .../net/corda/node/utilities/DemoClock.kt | 2 +- .../cordapp/CordappProviderImplTests.kt | 2 +- .../identity/InMemoryIdentityServiceTests.kt | 2 + .../PersistentIdentityServiceTests.kt | 2 + .../services/network/NetworkMapCacheTest.kt | 4 +- .../services/network/NetworkMapClientTest.kt | 2 +- .../services/network/NetworkMapUpdaterTest.kt | 1 + .../network/NetworkParametersReaderTest.kt | 2 +- .../services/schema/NodeSchemaServiceTest.kt | 3 +- .../corda/node/utilities/ClockUtilsTest.kt | 4 +- .../kotlin/net/corda/testing/driver/Driver.kt | 2 +- .../testing/driver/internal/DriverInternal.kt | 3 +- .../testing/node/InMemoryMessagingNetwork.kt | 16 +++++- .../net/corda/testing/node/MockNetwork.kt | 11 +++- .../net/corda/testing/node/MockServices.kt | 18 ++++-- .../net/corda/testing/node/TestClock.kt | 2 +- .../testing/node/internal/DriverDSLImpl.kt | 2 +- .../node/internal/InternalMockNetwork.kt | 4 +- .../testing/core/SerializationTestHelpers.kt | 43 +-------------- .../net/corda/testing/core/TestConstants.kt | 4 -- .../net/corda/testing/core/TestUtils.kt | 2 + .../kotlin/net/corda/testing/dsl/TestDSL.kt | 2 +- .../InternalSerializationTestHelpers.kt | 55 ++++++++++++++++++- .../testing/internal/InternalTestConstants.kt | 7 +++ .../MockCordappConfigProvider.kt | 2 +- .../MockCordappProvider.kt | 6 +- .../testing/internal/TestNodeInfoBuilder.kt | 2 - 50 files changed, 223 insertions(+), 171 deletions(-) rename node/src/main/kotlin/net/corda/node/{internal => }/CordaClock.kt (98%) create mode 100644 testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestConstants.kt rename testing/test-utils/src/main/kotlin/net/corda/testing/{node => internal}/MockCordappConfigProvider.kt (92%) rename testing/test-utils/src/main/kotlin/net/corda/testing/{services => internal}/MockCordappProvider.kt (94%) diff --git a/.ci/api-current.txt b/.ci/api-current.txt index ddf0e06ee7..b4c94505e9 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -1233,7 +1233,6 @@ public abstract class net.corda.core.flows.FlowLogic extends java.lang.Object @org.jetbrains.annotations.Nullable public net.corda.core.utilities.ProgressTracker getProgressTracker() @org.jetbrains.annotations.NotNull public final net.corda.core.flows.StateMachineRunId getRunId() @org.jetbrains.annotations.NotNull public final net.corda.core.node.ServiceHub getServiceHub() - @net.corda.core.CordaInternal @org.jetbrains.annotations.NotNull public final net.corda.core.internal.FlowStateMachine getStateMachine() @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public final net.corda.core.flows.FlowSession initiateFlow(net.corda.core.identity.Party) @co.paralleluniverse.fibers.Suspendable public final void persistFlowStackSnapshot() @kotlin.Deprecated @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public net.corda.core.utilities.UntrustworthyData receive(Class, net.corda.core.identity.Party) @@ -1242,7 +1241,6 @@ public abstract class net.corda.core.flows.FlowLogic extends java.lang.Object public final void recordAuditEvent(String, String, Map) @kotlin.Deprecated @co.paralleluniverse.fibers.Suspendable public void send(net.corda.core.identity.Party, Object) @kotlin.Deprecated @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public net.corda.core.utilities.UntrustworthyData sendAndReceive(Class, net.corda.core.identity.Party, Object) - @net.corda.core.CordaInternal public final void setStateMachine(net.corda.core.internal.FlowStateMachine) @co.paralleluniverse.fibers.Suspendable @kotlin.jvm.JvmStatic public static final void sleep(java.time.Duration) @co.paralleluniverse.fibers.Suspendable public Object subFlow(net.corda.core.flows.FlowLogic) @org.jetbrains.annotations.Nullable public final net.corda.core.messaging.DataFeed track() @@ -3700,7 +3698,6 @@ public final class net.corda.testing.driver.DriverParameters extends java.lang.O public String toString() ## @net.corda.core.DoNotImplement public interface net.corda.testing.driver.InProcess extends net.corda.testing.driver.NodeHandle - @org.jetbrains.annotations.NotNull public abstract net.corda.nodeapi.internal.persistence.CordaPersistence getDatabase() @org.jetbrains.annotations.NotNull public abstract net.corda.node.services.api.StartedNodeServices getServices() @org.jetbrains.annotations.NotNull public abstract rx.Observable registerInitiatedFlow(Class) ## @@ -3805,7 +3802,6 @@ public static final class net.corda.testing.node.ClusterSpec$Raft extends net.co public String toString() ## @javax.annotation.concurrent.ThreadSafe public final class net.corda.testing.node.InMemoryMessagingNetwork extends net.corda.core.serialization.SingletonSerializeAsToken - public (boolean, net.corda.testing.node.InMemoryMessagingNetwork$ServicePeerAllocationStrategy, org.apache.activemq.artemis.utils.ReusableLatch) @org.jetbrains.annotations.NotNull public synchronized final List getEndpoints() @org.jetbrains.annotations.NotNull public final rx.Observable getReceivedMessages() @org.jetbrains.annotations.NotNull public final rx.Observable getSentMessages() @@ -3888,10 +3884,6 @@ public static final class net.corda.testing.node.InMemoryMessagingNetwork$Servic @org.jetbrains.annotations.Nullable public abstract net.corda.testing.node.InMemoryMessagingNetwork$MessageTransfer pumpReceive(boolean) public abstract void stop() ## -public static final class net.corda.testing.node.InMemoryMessagingNetwork$pumpSend$$inlined$schedule$1 extends java.util.TimerTask - public (net.corda.testing.node.InMemoryMessagingNetwork, net.corda.testing.node.InMemoryMessagingNetwork$MessageTransfer, net.corda.core.internal.concurrent.OpenFuture) - public void run() -## public class net.corda.testing.node.MessagingServiceSpy extends java.lang.Object implements net.corda.node.services.messaging.MessagingService public (net.corda.node.services.messaging.MessagingService) @org.jetbrains.annotations.NotNull public net.corda.node.services.messaging.MessageHandlerRegistration addMessageHandler(String, kotlin.jvm.functions.Function2) @@ -4018,6 +4010,7 @@ public class net.corda.testing.node.MockServices extends java.lang.Object implem public (List, net.corda.core.identity.CordaX500Name, net.corda.core.node.services.IdentityService) public (net.corda.core.identity.CordaX500Name) public (net.corda.core.identity.CordaX500Name, net.corda.core.node.services.IdentityService) + public final void addMockCordapp(String) @org.jetbrains.annotations.NotNull public net.corda.core.transactions.SignedTransaction addSignature(net.corda.core.transactions.SignedTransaction) @org.jetbrains.annotations.NotNull public net.corda.core.transactions.SignedTransaction addSignature(net.corda.core.transactions.SignedTransaction, java.security.PublicKey) @org.jetbrains.annotations.NotNull public net.corda.core.serialization.SerializeAsToken cordaService(Class) @@ -4033,7 +4026,6 @@ public class net.corda.testing.node.MockServices extends java.lang.Object implem @org.jetbrains.annotations.NotNull public net.corda.core.node.services.IdentityService getIdentityService() @org.jetbrains.annotations.NotNull public net.corda.core.node.services.KeyManagementService getKeyManagementService() @org.jetbrains.annotations.NotNull public static final net.corda.node.VersionInfo getMOCK_VERSION_INFO() - @org.jetbrains.annotations.NotNull public final net.corda.testing.services.MockCordappProvider getMockCordappProvider() @org.jetbrains.annotations.NotNull public net.corda.core.node.NodeInfo getMyInfo() @org.jetbrains.annotations.NotNull public net.corda.core.node.services.NetworkMapCache getNetworkMapCache() @org.jetbrains.annotations.NotNull public net.corda.core.node.services.TransactionVerifierService getTransactionVerifierService() @@ -4139,7 +4131,6 @@ public final class net.corda.testing.node.NotarySpec extends java.lang.Object ## public final class net.corda.testing.node.StartedMockNode extends java.lang.Object @org.jetbrains.annotations.NotNull public final List findStateMachines(Class) - @org.jetbrains.annotations.NotNull public final net.corda.nodeapi.internal.persistence.CordaPersistence getDatabase() public final int getId() @org.jetbrains.annotations.NotNull public final net.corda.core.node.NodeInfo getInfo() @org.jetbrains.annotations.NotNull public final net.corda.node.services.messaging.MessagingService getNetwork() @@ -4152,7 +4143,7 @@ public final class net.corda.testing.node.StartedMockNode extends java.lang.Obje ## public static final class net.corda.testing.node.StartedMockNode$Companion extends java.lang.Object ## -@javax.annotation.concurrent.ThreadSafe public final class net.corda.testing.node.TestClock extends net.corda.node.internal.MutableClock +@javax.annotation.concurrent.ThreadSafe public final class net.corda.testing.node.TestClock extends net.corda.node.MutableClock public (java.time.Clock) public synchronized final void advanceBy(java.time.Duration) public synchronized final void setTo(java.time.Instant) @@ -4411,24 +4402,7 @@ public static final class net.corda.testing.core.SerializationEnvironmentRule$Co public static final class net.corda.testing.core.SerializationEnvironmentRule$apply$1 extends org.junit.runners.model.Statement public void evaluate() ## -public final class net.corda.testing.core.SerializationTestHelpersKt extends java.lang.Object - @org.jetbrains.annotations.NotNull public static final net.corda.testing.core.GlobalSerializationEnvironment setGlobalSerialization(boolean) -## -public static final class net.corda.testing.core.SerializationTestHelpersKt$createTestSerializationEnv$1 extends net.corda.core.serialization.internal.SerializationEnvironmentImpl - @org.jetbrains.annotations.NotNull public String toString() -## -public static final class net.corda.testing.core.SerializationTestHelpersKt$setGlobalSerialization$1 extends java.lang.Object implements net.corda.testing.core.GlobalSerializationEnvironment, net.corda.core.serialization.internal.SerializationEnvironment - @org.jetbrains.annotations.NotNull public net.corda.core.serialization.SerializationContext getCheckpointContext() - @org.jetbrains.annotations.NotNull public net.corda.core.serialization.SerializationContext getP2pContext() - @org.jetbrains.annotations.NotNull public net.corda.core.serialization.SerializationContext getRpcClientContext() - @org.jetbrains.annotations.NotNull public net.corda.core.serialization.SerializationContext getRpcServerContext() - @org.jetbrains.annotations.NotNull public net.corda.core.serialization.SerializationFactory getSerializationFactory() - @org.jetbrains.annotations.NotNull public net.corda.core.serialization.SerializationContext getStorageContext() - public void unset() -## public final class net.corda.testing.core.TestConstants extends java.lang.Object - @org.jetbrains.annotations.NotNull public static final net.corda.nodeapi.internal.crypto.CertificateAndKeyPair getDEV_INTERMEDIATE_CA() - @org.jetbrains.annotations.NotNull public static final net.corda.nodeapi.internal.crypto.CertificateAndKeyPair getDEV_ROOT_CA() @kotlin.jvm.JvmField @org.jetbrains.annotations.NotNull public static final net.corda.core.identity.CordaX500Name ALICE_NAME @kotlin.jvm.JvmField @org.jetbrains.annotations.NotNull public static final net.corda.core.identity.CordaX500Name BOB_NAME @kotlin.jvm.JvmField @org.jetbrains.annotations.NotNull public static final net.corda.core.identity.CordaX500Name BOC_NAME @@ -4681,9 +4655,3 @@ public static final class net.corda.testing.services.MockAttachmentStorage$Compa public static final class net.corda.testing.services.MockAttachmentStorage$openAttachment$1 extends net.corda.core.internal.AbstractAttachment @org.jetbrains.annotations.NotNull public net.corda.core.crypto.SecureHash getId() ## -public final class net.corda.testing.services.MockCordappProvider extends net.corda.node.internal.cordapp.CordappProviderImpl - public (net.corda.node.internal.cordapp.CordappLoader, net.corda.core.node.services.AttachmentStorage) - public final void addMockCordapp(String, net.corda.testing.services.MockAttachmentStorage) - @org.jetbrains.annotations.Nullable public net.corda.core.crypto.SecureHash getContractAttachmentID(String) - @org.jetbrains.annotations.NotNull public final List getCordappRegistry() -## diff --git a/.ci/check-api-changes.sh b/.ci/check-api-changes.sh index 987d0ced3c..5896f6ebbf 100755 --- a/.ci/check-api-changes.sh +++ b/.ci/check-api-changes.sh @@ -31,8 +31,7 @@ if [ $removalCount -gt 0 ]; then fi # Adding new abstract methods could also break the API. -# However, first exclude anything with the @DoNotImplement annotation. - +# However, first exclude classes marked with the @DoNotImplement annotation function forUserImpl() { awk '/DoNotImplement/,/^##/{ next }{ print }' $1 } @@ -45,13 +44,28 @@ $newAbstracts EOF ` +#Get a list of any methods that expose classes in .internal. namespaces, and any classes which extend/implement +#an internal class +newInternalExposures=$(echo "$userDiffContents" | grep "^+" | grep "\.internal\." ) + +internalCount=`grep -v "^$" < + private lateinit var bobNode: StartedNode + private lateinit var charlieNode: StartedNode private lateinit var alice: Party private lateinit var bob: Party private lateinit var charlie: Party @@ -40,7 +41,7 @@ class CollectSignaturesFlowTests { @Before fun setup() { - mockNet = MockNetwork(cordappPackages = listOf("net.corda.testing.contracts")) + mockNet = InternalMockNetwork(cordappPackages = listOf("net.corda.testing.contracts")) aliceNode = mockNet.createPartyNode(ALICE_NAME) bobNode = mockNet.createPartyNode(BOB_NAME) charlieNode = mockNet.createPartyNode(CHARLIE_NAME) diff --git a/core/src/test/kotlin/net/corda/core/identity/PartyAndCertificateTest.kt b/core/src/test/kotlin/net/corda/core/identity/PartyAndCertificateTest.kt index f89f6a7829..8831e5905e 100644 --- a/core/src/test/kotlin/net/corda/core/identity/PartyAndCertificateTest.kt +++ b/core/src/test/kotlin/net/corda/core/identity/PartyAndCertificateTest.kt @@ -7,9 +7,9 @@ import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.nodeapi.internal.crypto.X509KeyStore import net.corda.nodeapi.internal.crypto.X509Utilities -import net.corda.testing.core.DEV_ROOT_CA import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.core.getTestPartyAndCertificate +import net.corda.testing.internal.DEV_ROOT_CA import org.assertj.core.api.Assertions.assertThat import org.junit.Rule import org.junit.Test diff --git a/core/src/test/kotlin/net/corda/core/internal/ResolveTransactionsFlowTest.kt b/core/src/test/kotlin/net/corda/core/internal/ResolveTransactionsFlowTest.kt index fa6ab2b0ff..f6d9432f6a 100644 --- a/core/src/test/kotlin/net/corda/core/internal/ResolveTransactionsFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/internal/ResolveTransactionsFlowTest.kt @@ -8,10 +8,11 @@ import net.corda.core.identity.Party import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.sequence +import net.corda.node.internal.StartedNode import net.corda.testing.contracts.DummyContract -import net.corda.testing.node.MockNetwork import net.corda.testing.core.singleIdentity -import net.corda.testing.node.StartedMockNode +import net.corda.testing.node.internal.InternalMockNetwork +import net.corda.testing.node.internal.InternalMockNetwork.MockNode import net.corda.testing.node.startFlow import org.junit.After import org.junit.Before @@ -27,17 +28,17 @@ import kotlin.test.assertNull // DOCSTART 3 class ResolveTransactionsFlowTest { - private lateinit var mockNet: MockNetwork - private lateinit var notaryNode: StartedMockNode - private lateinit var megaCorpNode: StartedMockNode - private lateinit var miniCorpNode: StartedMockNode + private lateinit var mockNet: InternalMockNetwork + private lateinit var notaryNode: StartedNode + private lateinit var megaCorpNode: StartedNode + private lateinit var miniCorpNode: StartedNode private lateinit var megaCorp: Party private lateinit var miniCorp: Party private lateinit var notary: Party @Before fun setup() { - mockNet = MockNetwork(cordappPackages = listOf("net.corda.testing.contracts")) + mockNet = InternalMockNetwork(cordappPackages = listOf("net.corda.testing.contracts")) notaryNode = mockNet.defaultNotaryNode megaCorpNode = mockNet.createPartyNode(CordaX500Name("MegaCorp", "London", "GB")) miniCorpNode = mockNet.createPartyNode(CordaX500Name("MiniCorp", "London", "GB")) diff --git a/core/src/test/kotlin/net/corda/core/transactions/LedgerTransactionQueryTests.kt b/core/src/test/kotlin/net/corda/core/transactions/LedgerTransactionQueryTests.kt index fc87fd23ea..9482764d99 100644 --- a/core/src/test/kotlin/net/corda/core/transactions/LedgerTransactionQueryTests.kt +++ b/core/src/test/kotlin/net/corda/core/transactions/LedgerTransactionQueryTests.kt @@ -10,6 +10,7 @@ import net.corda.core.identity.Party import net.corda.node.services.api.IdentityServiceInternal import net.corda.testing.contracts.DummyContract import net.corda.testing.core.* +import net.corda.testing.internal.MockCordappProvider import net.corda.testing.internal.rigorousMock import net.corda.testing.node.MockServices import org.junit.Before @@ -37,7 +38,7 @@ class LedgerTransactionQueryTests { @Before fun setup() { - services.mockCordappProvider.addMockCordapp(DummyContract.PROGRAM_ID, services.attachments) + services.addMockCordapp(DummyContract.PROGRAM_ID) } interface Commands { diff --git a/docs/source/example-code/src/test/kotlin/net/corda/docs/CustomVaultQueryTest.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/CustomVaultQueryTest.kt index 2b194b1e94..6867695965 100644 --- a/docs/source/example-code/src/test/kotlin/net/corda/docs/CustomVaultQueryTest.kt +++ b/docs/source/example-code/src/test/kotlin/net/corda/docs/CustomVaultQueryTest.kt @@ -74,16 +74,14 @@ class CustomVaultQueryTest { private fun getBalances(): Pair>, Map>> { // Print out the balances - val balancesNodesA = - nodeA.database.transaction { - nodeA.services.getCashBalances() - } + val balancesNodesA = nodeA.transaction { + nodeA.services.getCashBalances() + } println("BalanceA\n" + balancesNodesA) - val balancesNodesB = - nodeB.database.transaction { - nodeB.services.getCashBalances() - } + val balancesNodesB = nodeB.transaction { + nodeB.services.getCashBalances() + } println("BalanceB\n" + balancesNodesB) return Pair(balancesNodesA, balancesNodesB) diff --git a/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt index 0805565e03..67a4c919f7 100644 --- a/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt +++ b/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt @@ -68,13 +68,14 @@ class FxTransactionBuildTutorialTest { doIt.getOrThrow() // Get the balances when the vault updates nodeAVaultUpdate.get() - val balancesA = nodeA.database.transaction { + val balancesA = nodeA.transaction { nodeA.services.getCashBalances() } nodeBVaultUpdate.get() - val balancesB = nodeB.database.transaction { + val balancesB = nodeB.transaction { nodeB.services.getCashBalances() } + println("BalanceA\n" + balancesA) println("BalanceB\n" + balancesB) // Verify the transfers occurred as expected @@ -86,10 +87,10 @@ class FxTransactionBuildTutorialTest { private fun printBalances() { // Print out the balances - nodeA.database.transaction { + nodeA.transaction { println("BalanceA\n" + nodeA.services.getCashBalances()) } - nodeB.database.transaction { + nodeB.transaction { println("BalanceB\n" + nodeB.services.getCashBalances()) } } diff --git a/finance/src/test/kotlin/net/corda/finance/flows/CashPaymentFlowTests.kt b/finance/src/test/kotlin/net/corda/finance/flows/CashPaymentFlowTests.kt index 2d3b901f9a..d9b74c3cec 100644 --- a/finance/src/test/kotlin/net/corda/finance/flows/CashPaymentFlowTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/flows/CashPaymentFlowTests.kt @@ -9,10 +9,11 @@ import net.corda.core.utilities.getOrThrow import net.corda.finance.DOLLARS import net.corda.finance.`issued by` import net.corda.finance.contracts.asset.Cash +import net.corda.node.internal.StartedNode import net.corda.testing.core.* import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin -import net.corda.testing.node.MockNetwork -import net.corda.testing.node.StartedMockNode +import net.corda.testing.node.internal.InternalMockNetwork +import net.corda.testing.node.internal.InternalMockNetwork.MockNode import net.corda.testing.node.startFlow import org.junit.After import org.junit.Before @@ -21,16 +22,16 @@ import kotlin.test.assertEquals import kotlin.test.assertFailsWith class CashPaymentFlowTests { - private lateinit var mockNet: MockNetwork + private lateinit var mockNet: InternalMockNetwork private val initialBalance = 2000.DOLLARS private val ref = OpaqueBytes.of(0x01) - private lateinit var bankOfCordaNode: StartedMockNode + private lateinit var bankOfCordaNode: StartedNode private lateinit var bankOfCorda: Party - private lateinit var aliceNode: StartedMockNode + private lateinit var aliceNode: StartedNode @Before fun start() { - mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin(), cordappPackages = listOf("net.corda.finance.contracts.asset")) + mockNet = InternalMockNetwork(servicePeerAllocationStrategy = RoundRobin(), cordappPackages = listOf("net.corda.finance.contracts.asset")) bankOfCordaNode = mockNet.createPartyNode(BOC_NAME) bankOfCorda = bankOfCordaNode.info.identityFromX500Name(BOC_NAME) aliceNode = mockNet.createPartyNode(ALICE_NAME) diff --git a/gradle-plugins/api-scanner/build.gradle b/gradle-plugins/api-scanner/build.gradle index c178472d58..13dca34301 100644 --- a/gradle-plugins/api-scanner/build.gradle +++ b/gradle-plugins/api-scanner/build.gradle @@ -5,6 +5,7 @@ apply plugin: 'com.jfrog.artifactory' description "Generates a summary of the artifact's public API" repositories { + mavenLocal() mavenCentral() } @@ -17,3 +18,4 @@ dependencies { publish { name project.name } + diff --git a/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ScanApi.java b/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ScanApi.java index eb2b7e5599..a9567035f6 100644 --- a/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ScanApi.java +++ b/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ScanApi.java @@ -202,6 +202,14 @@ public class ScanApi extends DefaultTask { // These classes belong to internal Corda packages. return; } + if (className.contains("$$inlined$")) { + /* + * These classes are internally generated by the Kotlin compiler + * and are not exposed as part of the public API + * TODO: Filter out using EnclosingMethod attribute in classfile + */ + return; + } ClassInfo classInfo = allInfo.get(className); if (classInfo.getClassLoaders() == null) { // Ignore classes that belong to one of our target ClassLoader's parents. @@ -275,6 +283,7 @@ public class ScanApi extends DefaultTask { for (MethodInfo method : methods) { if (isVisible(method.getAccessFlags()) // Only public and protected methods && isValid(method.getAccessFlags(), METHOD_MASK) // Excludes bridge and synthetic methods + && !hasCordaInternal(method.getAnnotationNames()) // Excludes methods annotated as @CordaInternal && !isKotlinInternalScope(method)) { writer.append(" ").println(filterAnnotationsFor(method)); } @@ -355,6 +364,10 @@ public class ScanApi extends DefaultTask { return method.getMethodName().indexOf('$') >= 0; } + private static boolean hasCordaInternal(Collection annotationNames) { + return annotationNames.contains("net.corda.core.CordaInternal"); + } + private static boolean isValid(int modifiers, int mask) { return (modifiers & mask) == modifiers; } diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderStaticContractTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderStaticContractTests.kt index 12ec7230b8..c264264bc6 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderStaticContractTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderStaticContractTests.kt @@ -17,7 +17,7 @@ import net.corda.testing.core.DUMMY_NOTARY_NAME import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.core.TestIdentity import net.corda.testing.internal.rigorousMock -import net.corda.testing.node.MockCordappConfigProvider +import net.corda.testing.internal.MockCordappConfigProvider import net.corda.testing.services.MockAttachmentStorage import org.junit.Assert.* import org.junit.Rule diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderTests.kt index 77406d26a1..5ee6355980 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/AttachmentsClassLoaderTests.kt @@ -23,7 +23,7 @@ import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.core.TestIdentity import net.corda.testing.internal.kryoSpecific import net.corda.testing.internal.rigorousMock -import net.corda.testing.node.MockCordappConfigProvider +import net.corda.testing.internal.MockCordappConfigProvider import net.corda.testing.services.MockAttachmentStorage import org.apache.commons.io.IOUtils import org.junit.Assert.* diff --git a/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt index 65dd56c3f1..01582516b5 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt @@ -29,7 +29,7 @@ import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.driver import net.corda.testing.internal.rigorousMock import net.corda.testing.internal.withoutTestSerialization -import net.corda.testing.node.MockCordappConfigProvider +import net.corda.testing.internal.MockCordappConfigProvider import net.corda.testing.services.MockAttachmentStorage import org.junit.Assert.assertEquals import org.junit.Rule diff --git a/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt index e972d71468..e4ca31dc7d 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt @@ -13,25 +13,26 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.internal.deleteIfExists import net.corda.core.internal.div +import net.corda.core.node.NotaryInfo import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.Try import net.corda.core.utilities.getOrThrow +import net.corda.node.internal.StartedNode import net.corda.node.services.config.BFTSMaRtConfiguration import net.corda.node.services.config.NotaryConfig import net.corda.node.services.transactions.minClusterSize import net.corda.node.services.transactions.minCorrectReplicas import net.corda.nodeapi.internal.DevIdentityGenerator import net.corda.nodeapi.internal.network.NetworkParametersCopier -import net.corda.core.node.NotaryInfo -import net.corda.testing.core.chooseIdentity import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.contracts.DummyContract +import net.corda.testing.core.chooseIdentity import net.corda.testing.core.dummyCommand -import net.corda.testing.node.MockNetwork -import net.corda.testing.node.StartedMockNode import net.corda.testing.node.MockNodeParameters +import net.corda.testing.node.internal.InternalMockNetwork +import net.corda.testing.node.internal.InternalMockNetwork.MockNode import net.corda.testing.node.startFlow import org.junit.After import org.junit.Before @@ -41,13 +42,13 @@ import kotlin.test.assertEquals import kotlin.test.assertTrue class BFTNotaryServiceTests { - private lateinit var mockNet: MockNetwork + private lateinit var mockNet: InternalMockNetwork private lateinit var notary: Party - private lateinit var node: StartedMockNode + private lateinit var node: StartedNode @Before fun before() { - mockNet = MockNetwork(emptyList()) + mockNet = InternalMockNetwork(emptyList()) } @After @@ -153,7 +154,7 @@ class BFTNotaryServiceTests { } } - private fun StartedMockNode.signInitialTransaction(notary: Party, block: TransactionBuilder.() -> Any?): SignedTransaction { + private fun StartedNode.signInitialTransaction(notary: Party, block: TransactionBuilder.() -> Any?): SignedTransaction { return services.signInitialTransaction( TransactionBuilder(notary).apply { addCommand(dummyCommand(services.myInfo.chooseIdentity().owningKey)) diff --git a/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt index 20b07017ad..09f8733f1b 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt @@ -17,6 +17,7 @@ import net.corda.testing.driver.driver import net.corda.testing.core.dummyCommand import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.InProcess +import net.corda.testing.driver.internal.InProcessImpl import net.corda.testing.node.ClusterSpec import net.corda.testing.node.NotarySpec import net.corda.testing.node.startFlow @@ -62,7 +63,7 @@ class RaftNotaryServiceTests { } private fun issueState(nodeHandle: InProcess, notary: Party): StateAndRef<*> { - return nodeHandle.database.transaction { + return (nodeHandle as InProcessImpl).database.transaction { val builder = DummyContract.generateInitial(Random().nextInt(), notary, nodeHandle.services.myInfo.chooseIdentity().ref(0)) val stx = nodeHandle.services.signInitialTransaction(builder) diff --git a/node/src/integration-test/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt b/node/src/integration-test/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt index 560987cdf7..c8ab1e3952 100644 --- a/node/src/integration-test/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt @@ -16,10 +16,10 @@ import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_CLIENT_CA import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_INTERMEDIATE_CA import net.corda.nodeapi.internal.crypto.X509Utilities.CORDA_ROOT_CA import net.corda.testing.common.internal.testNetworkParameters -import net.corda.testing.core.DEV_ROOT_CA import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.core.singleIdentity import net.corda.testing.driver.PortAllocation +import net.corda.testing.internal.DEV_ROOT_CA import net.corda.testing.node.NotarySpec import net.corda.testing.node.internal.CompatibilityZoneParams import net.corda.testing.node.internal.internalDriver diff --git a/node/src/main/kotlin/net/corda/node/internal/CordaClock.kt b/node/src/main/kotlin/net/corda/node/CordaClock.kt similarity index 98% rename from node/src/main/kotlin/net/corda/node/internal/CordaClock.kt rename to node/src/main/kotlin/net/corda/node/CordaClock.kt index c08961ee77..f838364bf6 100644 --- a/node/src/main/kotlin/net/corda/node/internal/CordaClock.kt +++ b/node/src/main/kotlin/net/corda/node/CordaClock.kt @@ -1,4 +1,4 @@ -package net.corda.node.internal +package net.corda.node import net.corda.core.serialization.SerializeAsToken import net.corda.core.serialization.SerializeAsTokenContext diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index c00e1fc175..92d58d863d 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -30,6 +30,7 @@ import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.debug import net.corda.core.utilities.getOrThrow +import net.corda.node.CordaClock import net.corda.node.VersionInfo import net.corda.node.internal.classloading.requireAnnotation import net.corda.node.internal.cordapp.CordappConfigFileProvider diff --git a/node/src/main/kotlin/net/corda/node/internal/Node.kt b/node/src/main/kotlin/net/corda/node/internal/Node.kt index 910c454e74..8762a1bf0e 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -16,6 +16,8 @@ import net.corda.core.serialization.internal.SerializationEnvironmentImpl import net.corda.core.serialization.internal.nodeSerializationEnv import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.contextLogger +import net.corda.node.CordaClock +import net.corda.node.SimpleClock import net.corda.node.VersionInfo import net.corda.node.internal.artemis.ArtemisBroker import net.corda.node.internal.artemis.BrokerAddresses diff --git a/node/src/main/kotlin/net/corda/node/services/events/NodeSchedulerService.kt b/node/src/main/kotlin/net/corda/node/services/events/NodeSchedulerService.kt index edc508c0e3..b79d7b3acd 100644 --- a/node/src/main/kotlin/net/corda/node/services/events/NodeSchedulerService.kt +++ b/node/src/main/kotlin/net/corda/node/services/events/NodeSchedulerService.kt @@ -21,8 +21,8 @@ import net.corda.core.schemas.PersistentStateRef import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.utilities.contextLogger import net.corda.core.utilities.trace -import net.corda.node.internal.CordaClock -import net.corda.node.internal.MutableClock +import net.corda.node.CordaClock +import net.corda.node.MutableClock import net.corda.node.services.api.FlowStarter import net.corda.node.services.api.NodePropertiesStore import net.corda.node.services.api.SchedulerService diff --git a/node/src/main/kotlin/net/corda/node/utilities/DemoClock.kt b/node/src/main/kotlin/net/corda/node/utilities/DemoClock.kt index 0d6ab6b48c..ac2e82e962 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/DemoClock.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/DemoClock.kt @@ -1,7 +1,7 @@ package net.corda.node.utilities import net.corda.core.internal.until -import net.corda.node.internal.MutableClock +import net.corda.node.MutableClock import java.time.Clock import java.time.LocalDate import javax.annotation.concurrent.ThreadSafe diff --git a/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappProviderImplTests.kt b/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappProviderImplTests.kt index 934c906e5d..9d12bffb05 100644 --- a/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappProviderImplTests.kt +++ b/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappProviderImplTests.kt @@ -4,7 +4,7 @@ import com.typesafe.config.Config import com.typesafe.config.ConfigFactory import net.corda.core.internal.cordapp.CordappConfigProvider import net.corda.core.node.services.AttachmentStorage -import net.corda.testing.node.MockCordappConfigProvider +import net.corda.testing.internal.MockCordappConfigProvider import net.corda.testing.services.MockAttachmentStorage import org.assertj.core.api.Assertions.assertThat import org.junit.Assert diff --git a/node/src/test/kotlin/net/corda/node/services/identity/InMemoryIdentityServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/identity/InMemoryIdentityServiceTests.kt index f1256db27d..ff11860082 100644 --- a/node/src/test/kotlin/net/corda/node/services/identity/InMemoryIdentityServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/identity/InMemoryIdentityServiceTests.kt @@ -11,6 +11,8 @@ import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.crypto.x509Certificates import net.corda.testing.core.* +import net.corda.testing.internal.DEV_INTERMEDIATE_CA +import net.corda.testing.internal.DEV_ROOT_CA import org.junit.Rule import org.junit.Test import kotlin.test.assertEquals diff --git a/node/src/test/kotlin/net/corda/node/services/identity/PersistentIdentityServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/identity/PersistentIdentityServiceTests.kt index dda6516173..6ac282ef88 100644 --- a/node/src/test/kotlin/net/corda/node/services/identity/PersistentIdentityServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/identity/PersistentIdentityServiceTests.kt @@ -15,6 +15,8 @@ import net.corda.nodeapi.internal.crypto.x509Certificates import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.testing.core.* +import net.corda.testing.internal.DEV_INTERMEDIATE_CA +import net.corda.testing.internal.DEV_ROOT_CA import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties import net.corda.testing.node.makeTestIdentityService import org.junit.After diff --git a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapCacheTest.kt b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapCacheTest.kt index e8809bb833..03bb1a1667 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapCacheTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapCacheTest.kt @@ -6,9 +6,9 @@ import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import net.corda.node.services.api.NetworkMapCacheInternal import net.corda.testing.core.getTestPartyAndCertificate -import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNodeParameters import net.corda.testing.core.singleIdentity +import net.corda.testing.node.internal.InternalMockNetwork import org.assertj.core.api.Assertions.assertThat import org.junit.After import org.junit.Test @@ -18,7 +18,7 @@ import kotlin.test.assertNotNull import kotlin.test.assertNull class NetworkMapCacheTest { - private val mockNet = MockNetwork(emptyList()) + private val mockNet = InternalMockNetwork(emptyList()) @After fun teardown() { diff --git a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapClientTest.kt b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapClientTest.kt index 1edd35db01..283ae2c71a 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapClientTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapClientTest.kt @@ -7,10 +7,10 @@ import net.corda.core.serialization.serialize import net.corda.core.utilities.seconds import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME -import net.corda.testing.core.DEV_ROOT_CA import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.driver.PortAllocation +import net.corda.testing.internal.DEV_ROOT_CA import net.corda.testing.internal.TestNodeInfoBuilder import net.corda.testing.internal.createNodeInfoAndSigned import net.corda.testing.internal.signWith diff --git a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt index 8609520c73..a00bf44a35 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt @@ -28,6 +28,7 @@ import net.corda.nodeapi.internal.network.ParametersUpdate import net.corda.nodeapi.internal.network.verifiedNetworkMapCert import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.* +import net.corda.testing.internal.DEV_ROOT_CA import net.corda.testing.internal.TestNodeInfoBuilder import net.corda.testing.internal.createNodeInfoAndSigned import org.assertj.core.api.Assertions.assertThat diff --git a/node/src/test/kotlin/net/corda/node/services/network/NetworkParametersReaderTest.kt b/node/src/test/kotlin/net/corda/node/services/network/NetworkParametersReaderTest.kt index b6b9e107cd..abd8d33fd1 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/NetworkParametersReaderTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/NetworkParametersReaderTest.kt @@ -12,9 +12,9 @@ import net.corda.nodeapi.internal.network.NETWORK_PARAMS_UPDATE_FILE_NAME import net.corda.nodeapi.internal.network.NetworkParametersCopier import net.corda.nodeapi.internal.network.verifiedNetworkMapCert import net.corda.testing.common.internal.testNetworkParameters -import net.corda.testing.core.DEV_ROOT_CA import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.driver.PortAllocation +import net.corda.testing.internal.DEV_ROOT_CA import net.corda.testing.node.internal.network.NetworkMapServer import org.junit.After import org.junit.Before diff --git a/node/src/test/kotlin/net/corda/node/services/schema/NodeSchemaServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/schema/NodeSchemaServiceTest.kt index 3c6e6d3b39..aa31cf7dd8 100644 --- a/node/src/test/kotlin/net/corda/node/services/schema/NodeSchemaServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/schema/NodeSchemaServiceTest.kt @@ -14,6 +14,7 @@ import net.corda.node.services.schema.NodeSchemaService.NodeNotaryV1 import net.corda.testing.driver.DriverParameters import net.corda.testing.driver.InProcess import net.corda.testing.driver.driver +import net.corda.testing.driver.internal.InProcessImpl import net.corda.testing.internal.vault.DummyLinearStateSchemaV1 import net.corda.testing.node.MockNetwork import org.hibernate.annotations.Cascade @@ -80,7 +81,7 @@ class NodeSchemaServiceTest { fun `custom schemas are loaded eagerly`() { val expected = setOf("PARENTS", "CHILDREN") val tables = driver(DriverParameters(startNodesInProcess = true)) { - (defaultNotaryNode.getOrThrow() as InProcess).database.transaction { + (defaultNotaryNode.getOrThrow() as InProcessImpl).database.transaction { session.createNativeQuery("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES").list() } } diff --git a/node/src/test/kotlin/net/corda/node/utilities/ClockUtilsTest.kt b/node/src/test/kotlin/net/corda/node/utilities/ClockUtilsTest.kt index d8dbfac65f..571dee482b 100644 --- a/node/src/test/kotlin/net/corda/node/utilities/ClockUtilsTest.kt +++ b/node/src/test/kotlin/net/corda/node/utilities/ClockUtilsTest.kt @@ -8,8 +8,8 @@ import com.google.common.util.concurrent.SettableFuture import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.hours import net.corda.core.utilities.minutes -import net.corda.node.internal.CordaClock -import net.corda.node.internal.SimpleClock +import net.corda.node.CordaClock +import net.corda.node.SimpleClock import net.corda.node.services.events.NodeSchedulerService import net.corda.testing.node.TestClock import org.junit.After diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt index 883d61b88f..bf06b7cb08 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt @@ -25,6 +25,7 @@ import java.net.InetSocketAddress import java.net.ServerSocket import java.nio.file.Path import java.nio.file.Paths +import java.sql.Connection import java.util.concurrent.atomic.AtomicInteger /** @@ -58,7 +59,6 @@ interface OutOfProcess : NodeHandle { @DoNotImplement interface InProcess : NodeHandle { - val database: CordaPersistence val services: StartedNodeServices /** * Register a flow that is initiated by another flow diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/internal/DriverInternal.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/internal/DriverInternal.kt index 3f658d6086..972677dd4d 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/internal/DriverInternal.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/internal/DriverInternal.kt @@ -15,6 +15,7 @@ import net.corda.testing.driver.OutOfProcess import net.corda.testing.node.User import rx.Observable import java.nio.file.Path +import java.sql.Connection interface NodeHandleInternal : NodeHandle { val configuration: NodeConfiguration @@ -57,7 +58,7 @@ data class InProcessImpl( private val onStopCallback: () -> Unit, private val node: StartedNode ) : InProcess, NodeHandleInternal { - override val database: CordaPersistence get() = node.database + val database: CordaPersistence = node.database override val services: StartedNodeServices get() = node.services override val rpcUsers: List = configuration.rpcUsers.map { User(it.username, it.password, it.permissions) } override fun stop() { diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt index 744ceb1dbd..e2aa643797 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt @@ -1,5 +1,6 @@ package net.corda.testing.node +import net.corda.core.CordaInternal import net.corda.core.DoNotImplement import net.corda.core.crypto.CompositeKey import net.corda.core.identity.CordaX500Name @@ -48,7 +49,7 @@ import kotlin.concurrent.thread * a service is addressed. */ @ThreadSafe -class InMemoryMessagingNetwork internal constructor( +class InMemoryMessagingNetwork private constructor( private val sendManuallyPumped: Boolean, private val servicePeerAllocationStrategy: ServicePeerAllocationStrategy = InMemoryMessagingNetwork.ServicePeerAllocationStrategy.Random(), private val messagesInFlight: ReusableLatch = ReusableLatch() @@ -56,6 +57,13 @@ class InMemoryMessagingNetwork internal constructor( companion object { private const val MESSAGES_LOG_NAME = "messages" private val log = LoggerFactory.getLogger(MESSAGES_LOG_NAME) + + internal fun create( + sendManuallyPumped: Boolean, + servicePeerAllocationStrategy: ServicePeerAllocationStrategy = InMemoryMessagingNetwork.ServicePeerAllocationStrategy.Random(), + messagesInFlight: ReusableLatch = ReusableLatch()): InMemoryMessagingNetwork { + return InMemoryMessagingNetwork(sendManuallyPumped, servicePeerAllocationStrategy, messagesInFlight) + } } private var counter = 0 // -1 means stopped. @@ -115,7 +123,8 @@ class InMemoryMessagingNetwork internal constructor( val peerHandle = PeerHandle(id, description) peersMapping[peerHandle.description] = peerHandle // Assume that the same name - the same entity in MockNetwork. notaryService?.let { if (it.owningKey !is CompositeKey) peersMapping[it.name] = peerHandle } - val serviceHandles = notaryService?.let { listOf(ServiceHandle(it.party)) } ?: emptyList() //TODO only notary can be distributed? + val serviceHandles = notaryService?.let { listOf(ServiceHandle(it.party)) } + ?: emptyList() //TODO only notary can be distributed? synchronized(this) { val node = InMemoryMessaging(manuallyPumped, peerHandle, executor, database) handleEndpointMap[peerHandle] = node @@ -304,7 +313,8 @@ class InMemoryMessagingNetwork internal constructor( override fun getAddressOfParty(partyInfo: PartyInfo): MessageRecipients { return when (partyInfo) { - is PartyInfo.SingleNode -> peersMapping[partyInfo.party.name] ?: throw IllegalArgumentException("No StartedMockNode for party ${partyInfo.party.name}") + is PartyInfo.SingleNode -> peersMapping[partyInfo.party.name] + ?: throw IllegalArgumentException("No StartedMockNode for party ${partyInfo.party.name}") is PartyInfo.DistributedNode -> ServiceHandle(partyInfo.party) } } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetwork.kt index 66a930ce3e..ad951d99c1 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetwork.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetwork.kt @@ -13,6 +13,8 @@ import net.corda.node.services.api.StartedNodeServices import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.messaging.MessagingService import net.corda.nodeapi.internal.persistence.CordaPersistence +import net.corda.nodeapi.internal.persistence.DatabaseTransaction +import net.corda.nodeapi.internal.persistence.TransactionIsolationLevel import net.corda.testing.core.DUMMY_NOTARY_NAME import net.corda.testing.node.internal.InternalMockNetwork import net.corda.testing.node.internal.setMessagingServiceSpy @@ -87,7 +89,6 @@ class StartedMockNode private constructor(private val node: StartedNode> findStateMachines(flowClass: Class): List>> = node.smm.findStateMachines(flowClass) + + fun transaction(statement: () -> T): T { + return node.database.transaction { + statement() + } + } } /** @@ -211,4 +218,4 @@ open class MockNetwork( /** Get the base directory for the given node id. **/ fun baseDirectory(nodeId: Int): Path = internalMockNetwork.baseDirectory(nodeId) -} \ No newline at end of file +} diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt index a86f4e380b..feeef6ab8b 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -1,6 +1,7 @@ package net.corda.testing.node import com.google.common.collect.MutableClassToInstanceMap +import net.corda.core.contracts.ContractClassName import net.corda.core.cordapp.CordappProvider import net.corda.core.crypto.* import net.corda.core.flows.FlowLogic @@ -31,11 +32,10 @@ import net.corda.node.services.vault.NodeVaultService import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.HibernateConfiguration -import net.corda.testing.common.internal.testNetworkParameters -import net.corda.testing.core.DEV_ROOT_CA import net.corda.testing.core.TestIdentity +import net.corda.testing.internal.DEV_ROOT_CA import net.corda.testing.services.MockAttachmentStorage -import net.corda.testing.services.MockCordappProvider +import net.corda.testing.internal.MockCordappProvider import org.bouncycastle.operator.ContentSigner import rx.Observable import rx.subjects.PublishSubject @@ -209,7 +209,7 @@ open class MockServices private constructor( return NodeInfo(emptyList(), listOf(initialIdentity.identity), 1, serial = 1L) } override val transactionVerifierService: TransactionVerifierService get() = InMemoryTransactionVerifierService(2) - val mockCordappProvider = MockCordappProvider(cordappLoader, attachments) + private val mockCordappProvider: MockCordappProvider = MockCordappProvider(cordappLoader, attachments) override val cordappProvider: CordappProvider get() = mockCordappProvider internal fun makeVaultService(hibernateConfig: HibernateConfiguration, schemaService: SchemaService): VaultServiceInternal { @@ -221,12 +221,17 @@ open class MockServices private constructor( val cordappServices: MutableClassToInstanceMap = MutableClassToInstanceMap.create() override fun cordaService(type: Class): T { require(type.isAnnotationPresent(CordaService::class.java)) { "${type.name} is not a Corda service" } - return cordappServices.getInstance(type) ?: throw IllegalArgumentException("Corda service ${type.name} does not exist") + return cordappServices.getInstance(type) + ?: throw IllegalArgumentException("Corda service ${type.name} does not exist") } override fun jdbcSession(): Connection = throw UnsupportedOperationException() override fun registerUnloadHandler(runOnStop: () -> Unit) = throw UnsupportedOperationException() + + fun addMockCordapp(contractClassName: ContractClassName) { + mockCordappProvider.addMockCordapp(contractClassName, attachments) + } } class MockKeyManagementService(val identityService: IdentityService, @@ -252,7 +257,8 @@ class MockKeyManagementService(val identityService: IdentityService, private fun getSigner(publicKey: PublicKey): ContentSigner = getSigner(getSigningKeyPair(publicKey)) private fun getSigningKeyPair(publicKey: PublicKey): KeyPair { - val pk = publicKey.keys.firstOrNull { keyStore.containsKey(it) } ?: throw IllegalArgumentException("Public key not found: ${publicKey.toStringShort()}") + val pk = publicKey.keys.firstOrNull { keyStore.containsKey(it) } + ?: throw IllegalArgumentException("Public key not found: ${publicKey.toStringShort()}") return KeyPair(pk, keyStore[pk]!!) } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/TestClock.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/TestClock.kt index 75d6563ae4..c99f1d128d 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/TestClock.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/TestClock.kt @@ -1,7 +1,7 @@ package net.corda.testing.node import net.corda.core.internal.until -import net.corda.node.internal.MutableClock +import net.corda.node.MutableClock import java.time.Clock import java.time.Duration import java.time.Instant diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt index eb3ae63268..f5978eded5 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt @@ -43,7 +43,7 @@ import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import net.corda.testing.core.DUMMY_BANK_A_NAME -import net.corda.testing.core.setGlobalSerialization +import net.corda.testing.internal.setGlobalSerialization import net.corda.testing.driver.* import net.corda.testing.driver.internal.InProcessImpl import net.corda.testing.driver.internal.NodeHandleInternal diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt index b3366e8dbc..05646d3f20 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt @@ -50,7 +50,6 @@ import net.corda.nodeapi.internal.network.NetworkParametersCopier import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.testing.common.internal.testNetworkParameters -import net.corda.testing.core.setGlobalSerialization import net.corda.testing.internal.rigorousMock import net.corda.testing.internal.testThreadFactory import net.corda.testing.node.InMemoryMessagingNetwork @@ -60,6 +59,7 @@ import net.corda.testing.node.MockNetworkParameters import net.corda.testing.node.MockNodeParameters import net.corda.testing.node.MockServices.Companion.MOCK_VERSION_INFO import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties +import net.corda.testing.internal.setGlobalSerialization import net.corda.testing.node.TestClock import org.apache.activemq.artemis.utils.ReusableLatch import org.apache.sshd.common.util.security.SecurityUtils @@ -120,7 +120,7 @@ open class InternalMockNetwork(private val cordappPackages: List, private set private val filesystem = Jimfs.newFileSystem(unix()) private val busyLatch = ReusableLatch() - val messagingNetwork = InMemoryMessagingNetwork(networkSendManuallyPumped, servicePeerAllocationStrategy, busyLatch) + val messagingNetwork = InMemoryMessagingNetwork.create(networkSendManuallyPumped, servicePeerAllocationStrategy, busyLatch) // A unique identifier for this network to segregate databases with the same nodeID but different networks. private val networkId = random63BitValue() private val networkParameters: NetworkParametersCopier diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/core/SerializationTestHelpers.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/core/SerializationTestHelpers.kt index 698003380d..2feefc269b 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/core/SerializationTestHelpers.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/core/SerializationTestHelpers.kt @@ -16,6 +16,8 @@ import net.corda.nodeapi.internal.serialization.* import net.corda.nodeapi.internal.serialization.amqp.AMQPClientSerializationScheme import net.corda.nodeapi.internal.serialization.amqp.AMQPServerSerializationScheme import net.corda.testing.common.internal.asContextEnv +import net.corda.testing.internal.createTestSerializationEnv +import net.corda.testing.internal.inVMExecutors import net.corda.testing.internal.rigorousMock import net.corda.testing.internal.testThreadFactory import org.apache.activemq.artemis.core.remoting.impl.invm.InVMConnector @@ -26,8 +28,6 @@ import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ExecutorService import java.util.concurrent.Executors -private val inVMExecutors = ConcurrentHashMap() - /** @param inheritable whether new threads inherit the environment, use sparingly. */ class SerializationEnvironmentRule(private val inheritable: Boolean = false) : TestRule { companion object { @@ -78,42 +78,3 @@ interface GlobalSerializationEnvironment : SerializationEnvironment { fun unset() } -/** - * Should only be used by Driver and MockNode. - * @param armed true to install, false to do nothing and return a dummy env. - */ -fun setGlobalSerialization(armed: Boolean): GlobalSerializationEnvironment { - return if (armed) { - object : GlobalSerializationEnvironment, SerializationEnvironment by createTestSerializationEnv("") { - override fun unset() { - _globalSerializationEnv.set(null) - inVMExecutors.remove(this) - } - }.also { - _globalSerializationEnv.set(it) - } - } else { - rigorousMock().also { - doNothing().whenever(it).unset() - } - } -} - -private fun createTestSerializationEnv(label: String): SerializationEnvironmentImpl { - val factory = SerializationFactoryImpl().apply { - registerScheme(KryoClientSerializationScheme()) - registerScheme(KryoServerSerializationScheme()) - registerScheme(AMQPClientSerializationScheme(emptyList())) - registerScheme(AMQPServerSerializationScheme(emptyList())) - } - return object : SerializationEnvironmentImpl( - factory, - AMQP_P2P_CONTEXT, - KRYO_RPC_SERVER_CONTEXT, - KRYO_RPC_CLIENT_CONTEXT, - AMQP_STORAGE_CONTEXT, - KRYO_CHECKPOINT_CONTEXT - ) { - override fun toString() = "testSerializationEnv($label)" - } -} diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/core/TestConstants.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/core/TestConstants.kt index 0a87461d8d..6cbecab924 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/core/TestConstants.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/core/TestConstants.kt @@ -30,10 +30,6 @@ val BOB_NAME = CordaX500Name("Bob Plc", "Rome", "IT") @JvmField val CHARLIE_NAME = CordaX500Name("Charlie Ltd", "Athens", "GR") -val DEV_INTERMEDIATE_CA: CertificateAndKeyPair by lazy { net.corda.nodeapi.internal.DEV_INTERMEDIATE_CA } - -val DEV_ROOT_CA: CertificateAndKeyPair by lazy { net.corda.nodeapi.internal.DEV_ROOT_CA } - fun dummyCommand(vararg signers: PublicKey = arrayOf(generateKeyPair().public)) = Command(DummyCommandData, signers.toList()) object DummyCommandData : TypeOnlyCommandData() diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/core/TestUtils.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/core/TestUtils.kt index 762ea036be..5229cde993 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/core/TestUtils.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/core/TestUtils.kt @@ -20,6 +20,8 @@ import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.X509CertificateFactory import net.corda.nodeapi.internal.crypto.X509Utilities +import net.corda.testing.internal.DEV_INTERMEDIATE_CA +import net.corda.testing.internal.DEV_ROOT_CA import java.math.BigInteger import java.security.KeyPair import java.security.PublicKey diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt index 8548ecc1fb..bf2745f8e3 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt @@ -14,7 +14,7 @@ import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction import net.corda.testing.services.MockAttachmentStorage -import net.corda.testing.services.MockCordappProvider +import net.corda.testing.internal.MockCordappProvider import net.corda.testing.core.dummyCommand import java.io.InputStream import java.security.PublicKey diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalSerializationTestHelpers.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalSerializationTestHelpers.kt index f11693d527..aaa9e3a15d 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalSerializationTestHelpers.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalSerializationTestHelpers.kt @@ -1,8 +1,19 @@ package net.corda.testing.internal -import net.corda.core.serialization.internal._contextSerializationEnv -import net.corda.core.serialization.internal._inheritableContextSerializationEnv +import com.nhaarman.mockito_kotlin.doNothing +import com.nhaarman.mockito_kotlin.whenever +import net.corda.client.rpc.internal.KryoClientSerializationScheme +import net.corda.core.serialization.internal.* +import net.corda.node.serialization.KryoServerSerializationScheme +import net.corda.nodeapi.internal.serialization.* +import net.corda.nodeapi.internal.serialization.amqp.AMQPClientSerializationScheme +import net.corda.nodeapi.internal.serialization.amqp.AMQPServerSerializationScheme +import net.corda.testing.core.GlobalSerializationEnvironment import net.corda.testing.core.SerializationEnvironmentRule +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ExecutorService + +val inVMExecutors = ConcurrentHashMap() /** * For example your test class uses [SerializationEnvironmentRule] but you want to turn it off for one method. @@ -17,3 +28,43 @@ fun withoutTestSerialization(callable: () -> T): T { // TODO: Delete this, s property.set(env) } } + +internal fun createTestSerializationEnv(label: String): SerializationEnvironmentImpl { + val factory = SerializationFactoryImpl().apply { + registerScheme(KryoClientSerializationScheme()) + registerScheme(KryoServerSerializationScheme()) + registerScheme(AMQPClientSerializationScheme(emptyList())) + registerScheme(AMQPServerSerializationScheme(emptyList())) + } + return object : SerializationEnvironmentImpl( + factory, + AMQP_P2P_CONTEXT, + KRYO_RPC_SERVER_CONTEXT, + KRYO_RPC_CLIENT_CONTEXT, + AMQP_STORAGE_CONTEXT, + KRYO_CHECKPOINT_CONTEXT + ) { + override fun toString() = "testSerializationEnv($label)" + } +} + +/** + * Should only be used by Driver and MockNode. + * @param armed true to install, false to do nothing and return a dummy env. + */ +fun setGlobalSerialization(armed: Boolean): GlobalSerializationEnvironment { + return if (armed) { + object : GlobalSerializationEnvironment, SerializationEnvironment by createTestSerializationEnv("") { + override fun unset() { + _globalSerializationEnv.set(null) + inVMExecutors.remove(this) + } + }.also { + _globalSerializationEnv.set(it) + } + } else { + rigorousMock().also { + doNothing().whenever(it).unset() + } + } +} \ No newline at end of file diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestConstants.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestConstants.kt new file mode 100644 index 0000000000..d9165951a8 --- /dev/null +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestConstants.kt @@ -0,0 +1,7 @@ +package net.corda.testing.internal + +import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair + +val DEV_INTERMEDIATE_CA: CertificateAndKeyPair by lazy { net.corda.nodeapi.internal.DEV_INTERMEDIATE_CA } + +val DEV_ROOT_CA: CertificateAndKeyPair by lazy { net.corda.nodeapi.internal.DEV_ROOT_CA } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/node/MockCordappConfigProvider.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappConfigProvider.kt similarity index 92% rename from testing/test-utils/src/main/kotlin/net/corda/testing/node/MockCordappConfigProvider.kt rename to testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappConfigProvider.kt index d351154982..6f2f99cfa8 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/node/MockCordappConfigProvider.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappConfigProvider.kt @@ -1,4 +1,4 @@ -package net.corda.testing.node +package net.corda.testing.internal import com.typesafe.config.Config import com.typesafe.config.ConfigFactory diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockCordappProvider.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt similarity index 94% rename from testing/test-utils/src/main/kotlin/net/corda/testing/services/MockCordappProvider.kt rename to testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt index 82ca31d6f9..4638d73fb3 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockCordappProvider.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt @@ -1,4 +1,4 @@ -package net.corda.testing.services +package net.corda.testing.internal import net.corda.core.contracts.ContractClassName import net.corda.core.cordapp.Cordapp @@ -7,7 +7,7 @@ import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.AttachmentStorage import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.internal.cordapp.CordappProviderImpl -import net.corda.testing.node.MockCordappConfigProvider +import net.corda.testing.services.MockAttachmentStorage import java.nio.file.Paths import java.util.* @@ -31,7 +31,7 @@ class MockCordappProvider( serializationWhitelists = emptyList(), serializationCustomSerializers = emptyList(), customSchemas = emptySet(), - jarPath = Paths.get(".").toUri().toURL()) + jarPath = Paths.get("").toUri().toURL()) if (cordappRegistry.none { it.first.contractClassNames.contains(contractClassName) }) { cordappRegistry.add(Pair(cordapp, findOrImportAttachment(contractClassName.toByteArray(), attachments))) } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestNodeInfoBuilder.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestNodeInfoBuilder.kt index c99ca190cf..8757adde2b 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestNodeInfoBuilder.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/TestNodeInfoBuilder.kt @@ -12,8 +12,6 @@ import net.corda.nodeapi.internal.createDevNodeCa import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.X509Utilities -import net.corda.testing.core.DEV_INTERMEDIATE_CA -import net.corda.testing.core.DEV_ROOT_CA import java.security.KeyPair import java.security.PrivateKey import java.security.cert.X509Certificate From eeb96535ee0abbee781c9eb9f189f562b5ab1687 Mon Sep 17 00:00:00 2001 From: Clinton Date: Wed, 14 Feb 2018 17:04:08 +0000 Subject: [PATCH 21/50] Re-added missing changelog entries --- docs/source/changelog.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 816bba7ec9..8ddbc09382 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -10,6 +10,9 @@ UNRELEASED * Per CorDapp configuration is now exposed. ``CordappContext`` now exposes a ``CordappConfig`` object that is populated at CorDapp context creation time from a file source during runtime. +* Provided experimental support for specifying your own webserver to be used instead of the default development webserver in ``Cordform`` using the ``webserverJar`` argument + +* The test utils in ``Expect.kt``, ``SerializationTestHelpers.kt``, ``TestConstants.kt`` and ``TestUtils.kt`` have moved from the ``net.corda.testing`` package to the ``net.corda.testing.core`` package, and ``FlowStackSnapshot.kt`` has moved to the ``net.corda.testing.services`` package. Moving items out of the ``net.corda.testing.*`` package will help make it clearer which parts of the api are stable. The bash script ``tools\scripts\update-test-packages.sh`` can be used to smooth the upgrade process for existing projects. * Introduced Flow Draining mode, in which a node continues executing existing flows, but does not start new. This is to support graceful node shutdown/restarts. In particular, when this mode is on, new flows through RPC will be rejected, scheduled flows will be ignored, and initial session messages will not be consumed. From 5de1ca7127a698a344a85aeb0af78469bf17ca5d Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Wed, 14 Feb 2018 17:22:08 +0000 Subject: [PATCH 22/50] CORDA-1032 - unnamed ctor param serialization issue (#2532) --- .../serialization/amqp/SerializationHelper.kt | 9 +++++-- .../amqp/JavaSerializationOutputTests.java | 24 +++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt index 4ce172cc78..c9f41fcf22 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt @@ -213,8 +213,13 @@ internal fun propertiesForSerializationFromConstructor( return mutableListOf().apply { kotlinConstructor.parameters.withIndex().forEach { param -> - val name = param.value.name ?: throw NotSerializableException( - "Constructor parameter of $clazz has no name.") + // If a parameter doesn't have a name *at all* then chances are it's a synthesised + // one. A good example of this is non static nested classes in Java where instances + // of the nested class require access to the outer class without breaking + // encapsulation. Thus a parameter is inserted into the constructor that passes a + // reference to the enclosing class. In this case we can't do anything with + // it so just ignore it as it'll be supplied at runtime anyway on invocation + val name = param.value.name ?: return@forEach val propertyReader = if (name in classProperties) { if (classProperties[name]!!.getter != null) { diff --git a/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java index 0441b2720f..c51ae5aff9 100644 --- a/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java +++ b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java @@ -1,5 +1,8 @@ package net.corda.nodeapi.internal.serialization.amqp; +import com.google.common.collect.ImmutableList; +import net.corda.core.contracts.ContractState; +import net.corda.core.identity.AbstractParty; import net.corda.nodeapi.internal.serialization.AllWhitelist; import net.corda.core.serialization.SerializedBytes; import org.apache.qpid.proton.codec.DecoderImpl; @@ -9,6 +12,7 @@ import org.junit.Test; import javax.annotation.Nonnull; import java.io.NotSerializableException; import java.nio.ByteBuffer; +import java.util.List; import java.util.Objects; import static org.junit.Assert.assertTrue; @@ -237,4 +241,24 @@ public class JavaSerializationOutputTests { BoxedFooNotNull obj = new BoxedFooNotNull("Hello World!", 123); serdes(obj); } + + protected class DummyState implements ContractState { + @Override + public List getParticipants() { + return ImmutableList.of(); + } + } + + @Test + public void dummyStateSerialize() throws NotSerializableException { + SerializerFactory factory1 = new SerializerFactory( + AllWhitelist.INSTANCE, + ClassLoader.getSystemClassLoader(), + new EvolutionSerializerGetter(), + new SerializerFingerPrinter()); + + SerializationOutput serializer = new SerializationOutput(factory1); + + serializer.serialize(new DummyState()); + } } From 6a4f783106663a4c1788e2c726c667896b68a6ba Mon Sep 17 00:00:00 2001 From: Andras Slemmer Date: Wed, 14 Feb 2018 17:30:32 +0000 Subject: [PATCH 23/50] Hack around database-with-observables issue --- .../internal/persistence/CordaPersistence.kt | 5 ++++- .../node/services/messaging/RPCServer.kt | 20 ++++++++++++------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt index 9a2b2c3b64..4884c21832 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt @@ -43,7 +43,10 @@ enum class TransactionIsolationLevel { } private val _contextDatabase = ThreadLocal() -val contextDatabase get() = _contextDatabase.get() ?: error("Was expecting to find CordaPersistence set on current thread: ${Strand.currentStrand()}") +var contextDatabase: CordaPersistence + get() = _contextDatabase.get() ?: error("Was expecting to find CordaPersistence set on current thread: ${Strand.currentStrand()}") + set(database) = _contextDatabase.set(database) +val contextDatabaseOrNull: CordaPersistence? get() = _contextDatabase.get() class CordaPersistence( val dataSource: DataSource, diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt b/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt index deac23fa84..58788db721 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt @@ -31,6 +31,9 @@ import net.corda.nodeapi.RPCApi import net.corda.nodeapi.externalTrace import net.corda.nodeapi.impersonatedActor import net.corda.nodeapi.internal.DeduplicationChecker +import net.corda.nodeapi.internal.persistence.CordaPersistence +import net.corda.nodeapi.internal.persistence.contextDatabase +import net.corda.nodeapi.internal.persistence.contextDatabaseOrNull import org.apache.activemq.artemis.api.core.Message import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.client.* @@ -57,16 +60,13 @@ data class RPCServerConfiguration( /** The interval of subscription reaping */ val reapInterval: Duration, /** The cache expiry of a deduplication watermark per client. */ - val deduplicationCacheExpiry: Duration, - /** The size of the send queue */ - val sendJobQueueSize: Int + val deduplicationCacheExpiry: Duration ) { companion object { val default = RPCServerConfiguration( rpcThreadPoolSize = 4, reapInterval = 1.seconds, - deduplicationCacheExpiry = 1.days, - sendJobQueueSize = 256 + deduplicationCacheExpiry = 1.days ) } } @@ -129,7 +129,7 @@ class RPCServer( private var serverControl: ActiveMQServerControl? = null private val responseMessageBuffer = ConcurrentHashMap() - private val sendJobQueue = ArrayBlockingQueue(rpcConfiguration.sendJobQueueSize) + private val sendJobQueue = LinkedBlockingQueue() private val deduplicationChecker = DeduplicationChecker(rpcConfiguration.deduplicationCacheExpiry) private var deduplicationIdentity: String? = null @@ -222,6 +222,9 @@ class RPCServer( private fun handleSendJob(sequenceNumber: Long, job: RpcSendJob.Send) { try { val artemisMessage = producerSession!!.createMessage(false) + if (job.database != null) { + contextDatabase = job.database + } // We must do the serialisation here as any encountered Observables may already have events, which would // trigger more sends. We must make sure that the root of the Observables (e.g. the RPC reply) is sent // before any child observations. @@ -418,12 +421,15 @@ class RPCServer( private val serializationContextWithObservableContext = RpcServerObservableSerializer.createContext(this) fun sendMessage(serverToClient: RPCApi.ServerToClient) { - sendJobQueue.put(RpcSendJob.Send(clientAddress, serializationContextWithObservableContext, serverToClient)) + sendJobQueue.put(RpcSendJob.Send(contextDatabaseOrNull, clientAddress, serializationContextWithObservableContext, serverToClient)) } } private sealed class RpcSendJob { data class Send( + // TODO HACK this is because during serialisation we subscribe to observables that may use + // DatabaseTransactionWrappingSubscriber which tries to access the current database, + val database: CordaPersistence?, val clientAddress: SimpleString, val serializationContext: SerializationContext, val message: RPCApi.ServerToClient From 311475a81c1028fe460b505a67f8eae88768ddb2 Mon Sep 17 00:00:00 2001 From: cburlinchon Date: Wed, 14 Feb 2018 17:32:00 +0000 Subject: [PATCH 24/50] Switch to using our own quasar fork with thread leak fix (#2443) * Switch to using our own quasar fork with thread leak fix * Update quasar.jar in lib * Review changes * Bump to 3.0.7 --- build.gradle | 4 +++- confidential-identities/build.gradle | 2 +- constants.properties | 2 +- core/build.gradle | 2 +- experimental/kryo-hook/build.gradle | 2 +- .../net/corda/plugins/QuasarPlugin.groovy | 2 +- lib/quasar.jar | Bin 1272691 -> 1272883 bytes .../statemachine/StateMachineManagerImpl.kt | 1 + 8 files changed, 9 insertions(+), 6 deletions(-) diff --git a/build.gradle b/build.gradle index b20d78d4d3..f022e0e095 100644 --- a/build.gradle +++ b/build.gradle @@ -13,7 +13,9 @@ buildscript { // // TODO: Sort this alphabetically. ext.kotlin_version = constants.getProperty("kotlinVersion") - ext.quasar_version = '0.7.9' + // use our fork of quasar + ext.quasar_group = 'com.github.corda.quasar' + ext.quasar_version = '7629695563deae6cc95adcfbebcbc8322fd0241a' // gradle-capsule-plugin:1.0.2 contains capsule:1.0.1 // TODO: Upgrade gradle-capsule-plugin to a version with capsule:1.0.3 diff --git a/confidential-identities/build.gradle b/confidential-identities/build.gradle index c6a29c0d2e..309dce7946 100644 --- a/confidential-identities/build.gradle +++ b/confidential-identities/build.gradle @@ -14,7 +14,7 @@ dependencies { compile project(':core') // Quasar, for suspendable fibres. - compileOnly "co.paralleluniverse:quasar-core:$quasar_version:jdk8" + compileOnly "$quasar_group:quasar-core:$quasar_version:jdk8" testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" testCompile "junit:junit:$junit_version" diff --git a/constants.properties b/constants.properties index 1c0e8aabc6..f246bd4e90 100644 --- a/constants.properties +++ b/constants.properties @@ -1,4 +1,4 @@ -gradlePluginsVersion=3.0.6 +gradlePluginsVersion=3.0.7 kotlinVersion=1.2.20 platformVersion=2 guavaVersion=21.0 diff --git a/core/build.gradle b/core/build.gradle index 5965a1cbc8..f3e24ed0ca 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -75,7 +75,7 @@ dependencies { testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" // Quasar, for suspendable fibres. - compileOnly "co.paralleluniverse:quasar-core:$quasar_version:jdk8" + compileOnly "$quasar_group:quasar-core:$quasar_version:jdk8" // Thread safety annotations compile "com.google.code.findbugs:jsr305:$jsr305_version" diff --git a/experimental/kryo-hook/build.gradle b/experimental/kryo-hook/build.gradle index cf52f3c9bb..4ba1fe7495 100644 --- a/experimental/kryo-hook/build.gradle +++ b/experimental/kryo-hook/build.gradle @@ -34,7 +34,7 @@ dependencies { compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" compile "javassist:javassist:$javaassist_version" compile "com.esotericsoftware:kryo:4.0.0" - compile "co.paralleluniverse:quasar-core:$quasar_version:jdk8" + compile "$quasar_group:quasar-core:$quasar_version:jdk8" } jar { diff --git a/gradle-plugins/quasar-utils/src/main/groovy/net/corda/plugins/QuasarPlugin.groovy b/gradle-plugins/quasar-utils/src/main/groovy/net/corda/plugins/QuasarPlugin.groovy index 6a4ffaf25a..fc309c2826 100644 --- a/gradle-plugins/quasar-utils/src/main/groovy/net/corda/plugins/QuasarPlugin.groovy +++ b/gradle-plugins/quasar-utils/src/main/groovy/net/corda/plugins/QuasarPlugin.groovy @@ -13,7 +13,7 @@ class QuasarPlugin implements Plugin { project.configurations.create("quasar") // To add a local .jar dependency: // project.dependencies.add("quasar", project.files("${project.rootProject.projectDir}/lib/quasar.jar")) - project.dependencies.add("quasar", "co.paralleluniverse:quasar-core:${project.rootProject.ext.quasar_version}:jdk8@jar") + project.dependencies.add("quasar", "${project.rootProject.ext.quasar_group}:quasar-core:${project.rootProject.ext.quasar_version}:jdk8@jar") project.dependencies.add("runtime", project.configurations.getByName("quasar")) project.tasks.withType(Test) { diff --git a/lib/quasar.jar b/lib/quasar.jar index c9f0010e79ccfcd54dc5740c3b10de9ffba7adad..074ecc3b7ca804cf895c93cbd158db58aa280ecb 100644 GIT binary patch delta 66747 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 diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManagerImpl.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManagerImpl.kt index b59a40b86f..1ac69e1a59 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManagerImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManagerImpl.kt @@ -211,6 +211,7 @@ class StateMachineManagerImpl( liveFibers.await() checkpointCheckerThread?.let { MoreExecutors.shutdownAndAwaitTermination(it, 5, SECONDS) } check(!unrestorableCheckpoints) { "Unrestorable checkpoints where created, please check the logs for details." } + scheduler.shutdown() } /** From 1cd028ebbea0e7db298fdc5bc67a37c8947834ba Mon Sep 17 00:00:00 2001 From: Clinton Date: Wed, 14 Feb 2018 17:39:56 +0000 Subject: [PATCH 25/50] Moved gradle plugins to another repo (#2520) --- gradle-plugins/README.rst | 29 -- gradle-plugins/api-scanner/README.md | 79 ---- gradle-plugins/api-scanner/build.gradle | 21 - .../java/net/corda/plugins/ApiScanner.java | 70 --- .../java/net/corda/plugins/GenerateApi.java | 61 --- .../main/java/net/corda/plugins/ScanApi.java | 402 ----------------- .../net/corda/plugins/ScannerExtension.java | 37 -- .../net.corda.plugins.api-scanner.properties | 1 - gradle-plugins/build.gradle | 84 ---- gradle-plugins/cordapp/README.md | 10 - gradle-plugins/cordapp/build.gradle | 19 - .../kotlin/net/corda/plugins/CordappPlugin.kt | 75 ---- .../main/kotlin/net/corda/plugins/Utils.kt | 35 -- .../net.corda.plugins.cordapp.properties | 1 - gradle-plugins/cordform-common/README.md | 4 - gradle-plugins/cordform-common/build.gradle | 23 - .../net/corda/cordform/CordformContext.java | 7 - .../corda/cordform/CordformDefinition.java | 48 -- .../java/net/corda/cordform/CordformNode.java | 190 -------- .../net/corda/cordform/NodeDefinition.java | 9 - .../java/net/corda/cordform/RpcSettings.java | 80 ---- .../java/net/corda/cordform/SslOptions.java | 56 --- gradle-plugins/cordformation/README.rst | 1 - gradle-plugins/cordformation/build.gradle | 69 --- .../main/kotlin/net/corda/plugins/Baseform.kt | 166 ------- .../main/kotlin/net/corda/plugins/Cordform.kt | 71 --- .../kotlin/net/corda/plugins/Cordformation.kt | 63 --- .../kotlin/net/corda/plugins/Dockerform.kt | 66 --- .../src/main/kotlin/net/corda/plugins/Node.kt | 409 ------------------ ...net.corda.plugins.cordformation.properties | 1 - .../resources/net/corda/plugins/Dockerfile | 44 -- .../resources/net/corda/plugins/run-corda.sh | 10 - .../main/resources/net/corda/plugins/runnodes | 13 - .../resources/net/corda/plugins/runnodes.bat | 8 - .../kotlin/net/corda/plugins/NodeRunner.kt | 159 ------- gradle-plugins/publish-utils/README.rst | 92 ---- gradle-plugins/publish-utils/build.gradle | 109 ----- .../plugins/ProjectPublishExtension.groovy | 39 -- .../net/corda/plugins/PublishTasks.groovy | 167 ------- .../bintray/BintrayConfigExtension.groovy | 70 --- .../corda/plugins/bintray/Developer.groovy | 16 - .../net/corda/plugins/bintray/License.groovy | 16 - ...net.corda.plugins.publish-utils.properties | 1 - gradle-plugins/quasar-utils/README.rst | 4 - gradle-plugins/quasar-utils/build.gradle | 19 - .../net/corda/plugins/QuasarPlugin.groovy | 28 -- .../net.corda.plugins.quasar-utils.properties | 1 - gradle-plugins/settings.gradle | 7 - 48 files changed, 2990 deletions(-) delete mode 100644 gradle-plugins/README.rst delete mode 100644 gradle-plugins/api-scanner/README.md delete mode 100644 gradle-plugins/api-scanner/build.gradle delete mode 100644 gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ApiScanner.java delete mode 100644 gradle-plugins/api-scanner/src/main/java/net/corda/plugins/GenerateApi.java delete mode 100644 gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ScanApi.java delete mode 100644 gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ScannerExtension.java delete mode 100644 gradle-plugins/api-scanner/src/main/resources/META-INF/gradle-plugins/net.corda.plugins.api-scanner.properties delete mode 100644 gradle-plugins/build.gradle delete mode 100644 gradle-plugins/cordapp/README.md delete mode 100644 gradle-plugins/cordapp/build.gradle delete mode 100644 gradle-plugins/cordapp/src/main/kotlin/net/corda/plugins/CordappPlugin.kt delete mode 100644 gradle-plugins/cordapp/src/main/kotlin/net/corda/plugins/Utils.kt delete mode 100644 gradle-plugins/cordapp/src/main/resources/META-INF/gradle-plugins/net.corda.plugins.cordapp.properties delete mode 100644 gradle-plugins/cordform-common/README.md delete mode 100644 gradle-plugins/cordform-common/build.gradle delete mode 100644 gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformContext.java delete mode 100644 gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformDefinition.java delete mode 100644 gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformNode.java delete mode 100644 gradle-plugins/cordform-common/src/main/java/net/corda/cordform/NodeDefinition.java delete mode 100644 gradle-plugins/cordform-common/src/main/java/net/corda/cordform/RpcSettings.java delete mode 100644 gradle-plugins/cordform-common/src/main/java/net/corda/cordform/SslOptions.java delete mode 100644 gradle-plugins/cordformation/README.rst delete mode 100644 gradle-plugins/cordformation/build.gradle delete mode 100644 gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Baseform.kt delete mode 100644 gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordform.kt delete mode 100644 gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordformation.kt delete mode 100644 gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Dockerform.kt delete mode 100644 gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Node.kt delete mode 100644 gradle-plugins/cordformation/src/main/resources/META-INF/gradle-plugins/net.corda.plugins.cordformation.properties delete mode 100644 gradle-plugins/cordformation/src/main/resources/net/corda/plugins/Dockerfile delete mode 100644 gradle-plugins/cordformation/src/main/resources/net/corda/plugins/run-corda.sh delete mode 100644 gradle-plugins/cordformation/src/main/resources/net/corda/plugins/runnodes delete mode 100644 gradle-plugins/cordformation/src/main/resources/net/corda/plugins/runnodes.bat delete mode 100644 gradle-plugins/cordformation/src/noderunner/kotlin/net/corda/plugins/NodeRunner.kt delete mode 100644 gradle-plugins/publish-utils/README.rst delete mode 100644 gradle-plugins/publish-utils/build.gradle delete mode 100644 gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/ProjectPublishExtension.groovy delete mode 100644 gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/PublishTasks.groovy delete mode 100644 gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/bintray/BintrayConfigExtension.groovy delete mode 100644 gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/bintray/Developer.groovy delete mode 100644 gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/bintray/License.groovy delete mode 100644 gradle-plugins/publish-utils/src/main/resources/META-INF/gradle-plugins/net.corda.plugins.publish-utils.properties delete mode 100644 gradle-plugins/quasar-utils/README.rst delete mode 100644 gradle-plugins/quasar-utils/build.gradle delete mode 100644 gradle-plugins/quasar-utils/src/main/groovy/net/corda/plugins/QuasarPlugin.groovy delete mode 100644 gradle-plugins/quasar-utils/src/main/resources/META-INF/gradle-plugins/net.corda.plugins.quasar-utils.properties delete mode 100644 gradle-plugins/settings.gradle diff --git a/gradle-plugins/README.rst b/gradle-plugins/README.rst deleted file mode 100644 index 0ede7c216f..0000000000 --- a/gradle-plugins/README.rst +++ /dev/null @@ -1,29 +0,0 @@ -Gradle Plugins for Cordapps -=========================== - -The projects at this level of the project are gradle plugins for cordapps and are published to Maven Local with -the rest of the Corda libraries. - -.. note:: - - Some of the plugins here are duplicated with the ones in buildSrc. While the duplication is unwanted any - currently known solution (such as publishing from buildSrc or setting up a separate project/repo) would - introduce a two step build which is less convenient. - -Version number --------------- - -To modify the version number edit constants.properties in root dir - -Installing ----------- - -If you need to bootstrap the corda repository you can install these plugins with - -.. code-block:: text - - cd publish-utils - ../../gradlew -u install - cd ../ - ../gradlew install - diff --git a/gradle-plugins/api-scanner/README.md b/gradle-plugins/api-scanner/README.md deleted file mode 100644 index aba34357d4..0000000000 --- a/gradle-plugins/api-scanner/README.md +++ /dev/null @@ -1,79 +0,0 @@ -# API Scanner - -Generates a text summary of Corda's public API that we can check for API-breaking changes. - -```bash -$ gradlew generateApi -``` - -See [here](../../docs/source/corda-api.rst) for Corda's public API strategy. We will need to -apply this plugin to other modules in future Corda releases as those modules' APIs stabilise. - -Basically, this plugin will document a module's `public` and `protected` classes/methods/fields, -excluding those from our `*.internal.*` packages, any synthetic methods, bridge methods, or methods -identified as having Kotlin's `internal` scope. (Kotlin doesn't seem to have implemented `internal` -scope for classes or fields yet as these are currently `public` inside the `.class` file.) - -## Usage -Include this line in the `build.gradle` file of every Corda module that exports public API: - -```gradle -apply plugin: 'net.corda.plugins.api-scanner' -``` - -This will create a Gradle task called `scanApi` which will analyse that module's Jar artifacts. More precisely, -it will analyse all of the Jar artifacts that have not been assigned a Maven classifier, on the basis -that these should be the module's main artifacts. - -The `scanApi` task supports the following configuration options: -```gradle -scanApi { - // Make the classpath-scanning phase more verbose. - verbose = {true|false} - - // Enable / disable the task within this module. - enabled = {true|false} - - // Names of classes that should be excluded from the output. - excludeClasses = [ - ... - ] -} -``` - -All of the `ScanApi` tasks write their output files to their own `$buildDir/api` directory, where they -are collated into a single output file by the `GenerateApi` task. The `GenerateApi` task is declared -in the root project's `build.gradle` file: - -```gradle -task generateApi(type: net.corda.plugins.GenerateApi){ - baseName = "api-corda" -} -``` - -The final API file is written to `$buildDir/api/$baseName-$project.version.txt` - -### Sample Output -``` -public interface net.corda.core.contracts.Attachment extends net.corda.core.contracts.NamedByHash - public abstract void extractFile(String, java.io.OutputStream) - @org.jetbrains.annotations.NotNull public abstract List getSigners() - @org.jetbrains.annotations.NotNull public abstract java.io.InputStream open() - @org.jetbrains.annotations.NotNull public abstract jar.JarInputStream openAsJAR() -## -public interface net.corda.core.contracts.AttachmentConstraint - public abstract boolean isSatisfiedBy(net.corda.core.contracts.Attachment) -## -public final class net.corda.core.contracts.AttachmentResolutionException extends net.corda.core.flows.FlowException - public (net.corda.core.crypto.SecureHash) - @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getHash() -## -``` - -#### Notes -The `GenerateApi` task will collate the output of every `ScanApi` task found either in the same project, -or in any of that project's subprojects. So it is _theoretically_ possible also to collate the API output -from subtrees of modules simply by defining a new `GenerateApi` task at the root of that subtree. - -## Plugin Installation -See [here](../README.rst) for full installation instructions. diff --git a/gradle-plugins/api-scanner/build.gradle b/gradle-plugins/api-scanner/build.gradle deleted file mode 100644 index 13dca34301..0000000000 --- a/gradle-plugins/api-scanner/build.gradle +++ /dev/null @@ -1,21 +0,0 @@ -apply plugin: 'java' -apply plugin: 'net.corda.plugins.publish-utils' -apply plugin: 'com.jfrog.artifactory' - -description "Generates a summary of the artifact's public API" - -repositories { - mavenLocal() - mavenCentral() -} - -dependencies { - compile gradleApi() - compile "io.github.lukehutch:fast-classpath-scanner:2.7.0" - testCompile "junit:junit:4.12" -} - -publish { - name project.name -} - diff --git a/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ApiScanner.java b/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ApiScanner.java deleted file mode 100644 index 75f238891c..0000000000 --- a/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ApiScanner.java +++ /dev/null @@ -1,70 +0,0 @@ -package net.corda.plugins; - -import org.gradle.api.Plugin; -import org.gradle.api.Project; -import org.gradle.api.artifacts.ConfigurationContainer; -import org.gradle.api.file.FileCollection; -import org.gradle.api.tasks.TaskCollection; -import org.gradle.jvm.tasks.Jar; - -public class ApiScanner implements Plugin { - - /** - * Identify the Gradle Jar tasks creating jars - * without Maven classifiers, and generate API - * documentation for them. - * @param p Current project. - */ - @Override - public void apply(Project p) { - p.getLogger().info("Applying API scanner to {}", p.getName()); - - ScannerExtension extension = p.getExtensions().create("scanApi", ScannerExtension.class); - - p.afterEvaluate(project -> { - TaskCollection jarTasks = project.getTasks() - .withType(Jar.class) - .matching(jarTask -> jarTask.getClassifier().isEmpty() && jarTask.isEnabled()); - if (jarTasks.isEmpty()) { - return; - } - - project.getLogger().info("Adding scanApi task to {}", project.getName()); - project.getTasks().create("scanApi", ScanApi.class, scanTask -> { - scanTask.setClasspath(compilationClasspath(project.getConfigurations())); - // Automatically creates a dependency on jar tasks. - scanTask.setSources(project.files(jarTasks)); - scanTask.setExcludeClasses(extension.getExcludeClasses()); - scanTask.setVerbose(extension.isVerbose()); - scanTask.setEnabled(extension.isEnabled()); - - // Declare this ScanApi task to be a dependency of any - // GenerateApi tasks belonging to any of our ancestors. - project.getRootProject().getTasks() - .withType(GenerateApi.class) - .matching(generateTask -> isAncestorOf(generateTask.getProject(), project)) - .forEach(generateTask -> generateTask.dependsOn(scanTask)); - }); - }); - } - - /* - * Recurse through a child project's parents until we reach the root, - * and return true iff we find our target project along the way. - */ - private static boolean isAncestorOf(Project target, Project child) { - Project p = child; - while (p != null) { - if (p == target) { - return true; - } - p = p.getParent(); - } - return false; - } - - private static FileCollection compilationClasspath(ConfigurationContainer configurations) { - return configurations.getByName("compile") - .plus(configurations.getByName("compileOnly")); - } -} diff --git a/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/GenerateApi.java b/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/GenerateApi.java deleted file mode 100644 index 9c87224075..0000000000 --- a/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/GenerateApi.java +++ /dev/null @@ -1,61 +0,0 @@ -package net.corda.plugins; - -import org.gradle.api.DefaultTask; -import org.gradle.api.file.FileCollection; -import org.gradle.api.tasks.InputFiles; -import org.gradle.api.tasks.OutputFile; -import org.gradle.api.tasks.TaskAction; - -import java.io.*; -import java.nio.file.Files; - -import static java.util.Comparator.comparing; -import static java.util.stream.Collectors.toList; - -@SuppressWarnings("unused") -public class GenerateApi extends DefaultTask { - - private final File outputDir; - private String baseName; - - public GenerateApi() { - outputDir = new File(getProject().getBuildDir(), "api"); - baseName = "api-" + getProject().getName(); - } - - public void setBaseName(String baseName) { - this.baseName = baseName; - } - - @InputFiles - public FileCollection getSources() { - return getProject().files(getProject().getAllprojects().stream() - .flatMap(project -> project.getTasks() - .withType(ScanApi.class) - .matching(ScanApi::isEnabled) - .stream()) - .flatMap(scanTask -> scanTask.getTargets().getFiles().stream()) - .sorted(comparing(File::getName)) - .collect(toList()) - ); - } - - @OutputFile - public File getTarget() { - return new File(outputDir, String.format("%s-%s.txt", baseName, getProject().getVersion())); - } - - @TaskAction - public void generate() { - FileCollection apiFiles = getSources(); - if (!apiFiles.isEmpty()) { - try (OutputStream output = new BufferedOutputStream(new FileOutputStream(getTarget()))) { - for (File apiFile : apiFiles) { - Files.copy(apiFile.toPath(), output); - } - } catch (IOException e) { - getLogger().error("Failed to generate API file", e); - } - } - } -} diff --git a/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ScanApi.java b/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ScanApi.java deleted file mode 100644 index a9567035f6..0000000000 --- a/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ScanApi.java +++ /dev/null @@ -1,402 +0,0 @@ -package net.corda.plugins; - -import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner; -import io.github.lukehutch.fastclasspathscanner.scanner.ClassInfo; -import io.github.lukehutch.fastclasspathscanner.scanner.FieldInfo; -import io.github.lukehutch.fastclasspathscanner.scanner.MethodInfo; -import io.github.lukehutch.fastclasspathscanner.scanner.ScanResult; -import org.gradle.api.DefaultTask; -import org.gradle.api.file.ConfigurableFileCollection; -import org.gradle.api.file.FileCollection; -import org.gradle.api.tasks.CompileClasspath; -import org.gradle.api.tasks.Input; -import org.gradle.api.tasks.InputFiles; -import org.gradle.api.tasks.OutputFiles; -import org.gradle.api.tasks.TaskAction; - -import java.io.*; -import java.lang.annotation.Annotation; -import java.lang.annotation.Inherited; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLClassLoader; -import java.util.*; -import java.util.stream.StreamSupport; - -import static java.util.Collections.*; -import static java.util.stream.Collectors.*; - -@SuppressWarnings("unused") -public class ScanApi extends DefaultTask { - private static final int CLASS_MASK = Modifier.classModifiers(); - private static final int INTERFACE_MASK = Modifier.interfaceModifiers() & ~Modifier.ABSTRACT; - private static final int METHOD_MASK = Modifier.methodModifiers(); - private static final int FIELD_MASK = Modifier.fieldModifiers(); - private static final int VISIBILITY_MASK = Modifier.PUBLIC | Modifier.PROTECTED; - - private static final Set ANNOTATION_BLACKLIST; - static { - Set blacklist = new LinkedHashSet<>(); - blacklist.add("kotlin.jvm.JvmOverloads"); - ANNOTATION_BLACKLIST = unmodifiableSet(blacklist); - } - - /** - * This information has been lifted from: - * @link Metadata.kt - */ - private static final String KOTLIN_METADATA = "kotlin.Metadata"; - private static final String KOTLIN_CLASSTYPE_METHOD = "k"; - private static final int KOTLIN_SYNTHETIC = 3; - - private final ConfigurableFileCollection sources; - private final ConfigurableFileCollection classpath; - private final Set excludeClasses; - private final File outputDir; - private boolean verbose; - - public ScanApi() { - sources = getProject().files(); - classpath = getProject().files(); - excludeClasses = new LinkedHashSet<>(); - outputDir = new File(getProject().getBuildDir(), "api"); - } - - @InputFiles - public FileCollection getSources() { - return sources; - } - - void setSources(FileCollection sources) { - this.sources.setFrom(sources); - } - - @CompileClasspath - @InputFiles - public FileCollection getClasspath() { - return classpath; - } - - void setClasspath(FileCollection classpath) { - this.classpath.setFrom(classpath); - } - - @Input - public Collection getExcludeClasses() { - return unmodifiableSet(excludeClasses); - } - - void setExcludeClasses(Collection excludeClasses) { - this.excludeClasses.clear(); - this.excludeClasses.addAll(excludeClasses); - } - - @OutputFiles - public FileCollection getTargets() { - return getProject().files( - StreamSupport.stream(sources.spliterator(), false) - .map(this::toTarget) - .collect(toList()) - ); - } - - public boolean isVerbose() { - return verbose; - } - - void setVerbose(boolean verbose) { - this.verbose = verbose; - } - - private File toTarget(File source) { - return new File(outputDir, source.getName().replaceAll(".jar$", ".txt")); - } - - @TaskAction - public void scan() { - try (Scanner scanner = new Scanner(classpath)) { - for (File source : sources) { - scanner.scan(source); - } - } catch (IOException e) { - getLogger().error("Failed to write API file", e); - } - } - - class Scanner implements Closeable { - private final URLClassLoader classpathLoader; - private final Class metadataClass; - private final Method classTypeMethod; - - @SuppressWarnings("unchecked") - Scanner(URLClassLoader classpathLoader) { - this.classpathLoader = classpathLoader; - - Class kClass; - Method kMethod; - try { - kClass = (Class) Class.forName(KOTLIN_METADATA, true, classpathLoader); - kMethod = kClass.getDeclaredMethod(KOTLIN_CLASSTYPE_METHOD); - } catch (ClassNotFoundException | NoSuchMethodException e) { - kClass = null; - kMethod = null; - } - - metadataClass = kClass; - classTypeMethod = kMethod; - } - - Scanner(FileCollection classpath) throws MalformedURLException { - this(new URLClassLoader(toURLs(classpath))); - } - - @Override - public void close() throws IOException { - classpathLoader.close(); - } - - void scan(File source) { - File target = toTarget(source); - try ( - URLClassLoader appLoader = new URLClassLoader(new URL[]{ toURL(source) }, classpathLoader); - PrintWriter writer = new PrintWriter(target, "UTF-8") - ) { - scan(writer, appLoader); - } catch (IOException e) { - getLogger().error("API scan has failed", e); - } - } - - void scan(PrintWriter writer, ClassLoader appLoader) { - ScanResult result = new FastClasspathScanner(getScanSpecification()) - .overrideClassLoaders(appLoader) - .ignoreParentClassLoaders() - .ignoreMethodVisibility() - .ignoreFieldVisibility() - .enableMethodInfo() - .enableFieldInfo() - .verbose(verbose) - .scan(); - writeApis(writer, result); - } - - private String[] getScanSpecification() { - String[] spec = new String[2 + excludeClasses.size()]; - spec[0] = "!"; // Don't blacklist system classes from the output. - spec[1] = "-dir:"; // Ignore classes on the filesystem. - - int i = 2; - for (String excludeClass : excludeClasses) { - spec[i++] = '-' + excludeClass; - } - return spec; - } - - private void writeApis(PrintWriter writer, ScanResult result) { - Map allInfo = result.getClassNameToClassInfo(); - result.getNamesOfAllClasses().forEach(className -> { - if (className.contains(".internal.")) { - // These classes belong to internal Corda packages. - return; - } - if (className.contains("$$inlined$")) { - /* - * These classes are internally generated by the Kotlin compiler - * and are not exposed as part of the public API - * TODO: Filter out using EnclosingMethod attribute in classfile - */ - return; - } - ClassInfo classInfo = allInfo.get(className); - if (classInfo.getClassLoaders() == null) { - // Ignore classes that belong to one of our target ClassLoader's parents. - return; - } - - Class javaClass = result.classNameToClassRef(className); - if (!isVisible(javaClass.getModifiers())) { - // Excludes private and package-protected classes - return; - } - - int kotlinClassType = getKotlinClassType(javaClass); - if (kotlinClassType == KOTLIN_SYNTHETIC) { - // Exclude classes synthesised by the Kotlin compiler. - return; - } - - writeClass(writer, classInfo, javaClass.getModifiers()); - writeMethods(writer, classInfo.getMethodAndConstructorInfo()); - writeFields(writer, classInfo.getFieldInfo()); - writer.println("##"); - }); - } - - private void writeClass(PrintWriter writer, ClassInfo classInfo, int modifiers) { - if (classInfo.isAnnotation()) { - /* - * Annotation declaration. - */ - writer.append(Modifier.toString(modifiers & INTERFACE_MASK)); - writer.append(" @interface ").print(classInfo); - } else if (classInfo.isStandardClass()) { - /* - * Class declaration. - */ - List annotationNames = toNames(readClassAnnotationsFor(classInfo)); - if (!annotationNames.isEmpty()) { - writer.append(asAnnotations(annotationNames)); - } - writer.append(Modifier.toString(modifiers & CLASS_MASK)); - writer.append(" class ").print(classInfo); - Set superclasses = classInfo.getDirectSuperclasses(); - if (!superclasses.isEmpty()) { - writer.append(" extends ").print(stringOf(superclasses)); - } - Set interfaces = classInfo.getDirectlyImplementedInterfaces(); - if (!interfaces.isEmpty()) { - writer.append(" implements ").print(stringOf(interfaces)); - } - } else { - /* - * Interface declaration. - */ - List annotationNames = toNames(readInterfaceAnnotationsFor(classInfo)); - if (!annotationNames.isEmpty()) { - writer.append(asAnnotations(annotationNames)); - } - writer.append(Modifier.toString(modifiers & INTERFACE_MASK)); - writer.append(" interface ").print(classInfo); - Set superinterfaces = classInfo.getDirectSuperinterfaces(); - if (!superinterfaces.isEmpty()) { - writer.append(" extends ").print(stringOf(superinterfaces)); - } - } - writer.println(); - } - - private void writeMethods(PrintWriter writer, List methods) { - sort(methods); - for (MethodInfo method : methods) { - if (isVisible(method.getAccessFlags()) // Only public and protected methods - && isValid(method.getAccessFlags(), METHOD_MASK) // Excludes bridge and synthetic methods - && !hasCordaInternal(method.getAnnotationNames()) // Excludes methods annotated as @CordaInternal - && !isKotlinInternalScope(method)) { - writer.append(" ").println(filterAnnotationsFor(method)); - } - } - } - - private void writeFields(PrintWriter output, List fields) { - sort(fields); - for (FieldInfo field : fields) { - if (isVisible(field.getAccessFlags()) && isValid(field.getAccessFlags(), FIELD_MASK)) { - output.append(" ").println(field); - } - } - } - - private int getKotlinClassType(Class javaClass) { - if (metadataClass != null) { - Annotation metadata = javaClass.getAnnotation(metadataClass); - if (metadata != null) { - try { - return (int) classTypeMethod.invoke(metadata); - } catch (IllegalAccessException | InvocationTargetException e) { - getLogger().error("Failed to read Kotlin annotation", e); - } - } - } - return 0; - } - - private List toNames(Collection classes) { - return classes.stream() - .map(ClassInfo::toString) - .filter(ScanApi::isApplicationClass) - .collect(toList()); - } - - private Set readClassAnnotationsFor(ClassInfo classInfo) { - Set annotations = new HashSet<>(classInfo.getAnnotations()); - annotations.addAll(selectInheritedAnnotations(classInfo.getSuperclasses())); - annotations.addAll(selectInheritedAnnotations(classInfo.getImplementedInterfaces())); - return annotations; - } - - private Set readInterfaceAnnotationsFor(ClassInfo classInfo) { - Set annotations = new HashSet<>(classInfo.getAnnotations()); - annotations.addAll(selectInheritedAnnotations(classInfo.getSuperinterfaces())); - return annotations; - } - - /** - * Returns those annotations which have themselves been annotated as "Inherited". - */ - private List selectInheritedAnnotations(Collection classes) { - return classes.stream() - .flatMap(cls -> cls.getAnnotations().stream()) - .filter(ann -> ann.hasMetaAnnotation(Inherited.class.getName())) - .collect(toList()); - } - - private MethodInfo filterAnnotationsFor(MethodInfo method) { - return new MethodInfo( - method.getClassName(), - method.getMethodName(), - method.getAccessFlags(), - method.getTypeDescriptor(), - method.getAnnotationNames().stream() - .filter(ScanApi::isVisibleAnnotation) - .collect(toList()) - ); - } - } - - private static boolean isVisibleAnnotation(String annotationName) { - return !ANNOTATION_BLACKLIST.contains(annotationName); - } - - private static boolean isKotlinInternalScope(MethodInfo method) { - return method.getMethodName().indexOf('$') >= 0; - } - - private static boolean hasCordaInternal(Collection annotationNames) { - return annotationNames.contains("net.corda.core.CordaInternal"); - } - - private static boolean isValid(int modifiers, int mask) { - return (modifiers & mask) == modifiers; - } - - private static boolean isVisible(int accessFlags) { - return (accessFlags & VISIBILITY_MASK) != 0; - } - - private static String stringOf(Collection items) { - return items.stream().map(ClassInfo::toString).collect(joining(", ")); - } - - private static String asAnnotations(Collection items) { - return items.stream().collect(joining(" @", "@", " ")); - } - - private static boolean isApplicationClass(String typeName) { - return !typeName.startsWith("java.") && !typeName.startsWith("kotlin."); - } - - private static URL toURL(File file) throws MalformedURLException { - return file.toURI().toURL(); - } - - private static URL[] toURLs(Iterable files) throws MalformedURLException { - List urls = new LinkedList<>(); - for (File file : files) { - urls.add(toURL(file)); - } - return urls.toArray(new URL[urls.size()]); - } -} diff --git a/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ScannerExtension.java b/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ScannerExtension.java deleted file mode 100644 index 4e77437cfc..0000000000 --- a/gradle-plugins/api-scanner/src/main/java/net/corda/plugins/ScannerExtension.java +++ /dev/null @@ -1,37 +0,0 @@ -package net.corda.plugins; - -import java.util.List; - -import static java.util.Collections.emptyList; - -@SuppressWarnings("unused") -public class ScannerExtension { - - private boolean verbose; - private boolean enabled = true; - private List excludeClasses = emptyList(); - - public boolean isVerbose() { - return verbose; - } - - public void setVerbose(boolean verbose) { - this.verbose = verbose; - } - - public boolean isEnabled() { - return enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public List getExcludeClasses() { - return excludeClasses; - } - - public void setExcludeClasses(List excludeClasses) { - this.excludeClasses = excludeClasses; - } -} diff --git a/gradle-plugins/api-scanner/src/main/resources/META-INF/gradle-plugins/net.corda.plugins.api-scanner.properties b/gradle-plugins/api-scanner/src/main/resources/META-INF/gradle-plugins/net.corda.plugins.api-scanner.properties deleted file mode 100644 index fc9e2277a5..0000000000 --- a/gradle-plugins/api-scanner/src/main/resources/META-INF/gradle-plugins/net.corda.plugins.api-scanner.properties +++ /dev/null @@ -1 +0,0 @@ -implementation-class=net.corda.plugins.ApiScanner diff --git a/gradle-plugins/build.gradle b/gradle-plugins/build.gradle deleted file mode 100644 index 32763f85f6..0000000000 --- a/gradle-plugins/build.gradle +++ /dev/null @@ -1,84 +0,0 @@ -// This script exists just to allow bootstrapping the gradle plugins if maven central or jcenter are unavailable -// or if you are developing these plugins. See the readme for more information. - -buildscript { - // For sharing constants between builds - Properties constants = new Properties() - file("$projectDir/../constants.properties").withInputStream { constants.load(it) } - - // If you bump this version you must re-bootstrap the codebase. See the README for more information. - ext { - gradle_plugins_version = constants.getProperty("gradlePluginsVersion") - bouncycastle_version = constants.getProperty("bouncycastleVersion") - typesafe_config_version = constants.getProperty("typesafeConfigVersion") - jsr305_version = constants.getProperty("jsr305Version") - kotlin_version = constants.getProperty("kotlinVersion") - artifactory_plugin_version = constants.getProperty('artifactoryPluginVersion') - snake_yaml_version = constants.getProperty('snakeYamlVersion') - } - - repositories { - mavenLocal() - jcenter() - } - - dependencies { - classpath "net.corda.plugins:publish-utils:$gradle_plugins_version" - classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7.3' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath "org.jfrog.buildinfo:build-info-extractor-gradle:$artifactory_plugin_version" - } -} - -apply plugin: 'net.corda.plugins.publish-utils' -apply plugin: 'com.jfrog.artifactory' - -allprojects { - version gradle_plugins_version - group 'net.corda.plugins' -} - -bintrayConfig { - user = System.getenv('CORDA_BINTRAY_USER') - key = System.getenv('CORDA_BINTRAY_KEY') - repo = 'corda' - org = 'r3' - licenses = ['Apache-2.0'] - vcsUrl = 'https://github.com/corda/corda' - projectUrl = 'https://github.com/corda/corda' - gpgSign = true - gpgPassphrase = System.getenv('CORDA_BINTRAY_GPG_PASSPHRASE') - publications = ['cordformation', 'quasar-utils', 'cordform-common', 'api-scanner', 'cordapp'] - license { - name = 'Apache-2.0' - url = 'https://www.apache.org/licenses/LICENSE-2.0' - distribution = 'repo' - } - developer { - id = 'R3' - name = 'R3' - email = 'dev@corda.net' - } -} - -artifactory { - publish { - contextUrl = 'https://ci-artifactory.corda.r3cev.com/artifactory' - repository { - repoKey = 'corda-dev' - username = 'teamcity' - password = System.getenv('CORDA_ARTIFACTORY_PASSWORD') - } - - defaults { - // Publish utils does not have a publish block because it would be circular for it to apply it's own - // extensions to itself - if(project.name == 'publish-utils') { - publications('publishUtils') - // Root project applies the plugin (for this block) but does not need to be published - } else if(project != rootProject) { - publications(project.extensions.publish.name()) - } - } - } -} \ No newline at end of file diff --git a/gradle-plugins/cordapp/README.md b/gradle-plugins/cordapp/README.md deleted file mode 100644 index 6b0cebe690..0000000000 --- a/gradle-plugins/cordapp/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# Cordapp Gradle Plugin - -## Purpose - -To transform any project this plugin is applied to into a cordapp project that generates a cordapp JAR. - -## Effects - -Will modify the default JAR task to create a CorDapp format JAR instead [see here](https://docs.corda.net/cordapp-build-systems.html) -for more information. \ No newline at end of file diff --git a/gradle-plugins/cordapp/build.gradle b/gradle-plugins/cordapp/build.gradle deleted file mode 100644 index 3d1ecb6b53..0000000000 --- a/gradle-plugins/cordapp/build.gradle +++ /dev/null @@ -1,19 +0,0 @@ -apply plugin: 'kotlin' -apply plugin: 'net.corda.plugins.publish-utils' -apply plugin: 'com.jfrog.artifactory' - -description 'Turns a project into a cordapp project that produces cordapp fat JARs' - -repositories { - mavenCentral() - jcenter() -} - -dependencies { - compile gradleApi() - compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" -} - -publish { - name project.name -} \ No newline at end of file diff --git a/gradle-plugins/cordapp/src/main/kotlin/net/corda/plugins/CordappPlugin.kt b/gradle-plugins/cordapp/src/main/kotlin/net/corda/plugins/CordappPlugin.kt deleted file mode 100644 index 624194fd32..0000000000 --- a/gradle-plugins/cordapp/src/main/kotlin/net/corda/plugins/CordappPlugin.kt +++ /dev/null @@ -1,75 +0,0 @@ -package net.corda.plugins - -import org.gradle.api.* -import org.gradle.api.artifacts.* -import org.gradle.jvm.tasks.Jar -import java.io.File - -/** - * The Cordapp plugin will turn a project into a cordapp project which builds cordapp JARs with the correct format - * and with the information needed to run on Corda. - */ -class CordappPlugin : Plugin { - override fun apply(project: Project) { - project.logger.info("Configuring ${project.name} as a cordapp") - - Utils.createCompileConfiguration("cordapp", project) - Utils.createCompileConfiguration("cordaCompile", project) - - val configuration: Configuration = project.configurations.create("cordaRuntime") - configuration.isTransitive = false - project.configurations.single { it.name == "runtime" }.extendsFrom(configuration) - - configureCordappJar(project) - } - - /** - * Configures this project's JAR as a Cordapp JAR - */ - private fun configureCordappJar(project: Project) { - // Note: project.afterEvaluate did not have full dependency resolution completed, hence a task is used instead - val task = project.task("configureCordappFatJar") - val jarTask = project.tasks.getByName("jar") as Jar - task.doLast { - jarTask.from(getDirectNonCordaDependencies(project).map { project.zipTree(it)}).apply { - exclude("META-INF/*.SF") - exclude("META-INF/*.DSA") - exclude("META-INF/*.RSA") - } - } - jarTask.dependsOn(task) - } - - private fun getDirectNonCordaDependencies(project: Project): Set { - project.logger.info("Finding direct non-corda dependencies for inclusion in CorDapp JAR") - val excludes = listOf( - mapOf("group" to "org.jetbrains.kotlin", "name" to "kotlin-stdlib"), - mapOf("group" to "org.jetbrains.kotlin", "name" to "kotlin-stdlib-jre8"), - mapOf("group" to "org.jetbrains.kotlin", "name" to "kotlin-reflect"), - mapOf("group" to "co.paralleluniverse", "name" to "quasar-core") - ) - - val runtimeConfiguration = project.configuration("runtime") - // The direct dependencies of this project - val excludeDeps = project.configuration("cordapp").allDependencies + - project.configuration("cordaCompile").allDependencies + - project.configuration("cordaRuntime").allDependencies - val directDeps = runtimeConfiguration.allDependencies - excludeDeps - // We want to filter out anything Corda related or provided by Corda, like kotlin-stdlib and quasar - val filteredDeps = directDeps.filter { dep -> - excludes.none { exclude -> (exclude["group"] == dep.group) && (exclude["name"] == dep.name) } - } - filteredDeps.forEach { - // net.corda or com.r3.corda.enterprise may be a core dependency which shouldn't be included in this cordapp so give a warning - val group = it.group?.toString() ?: "" - if (group.startsWith("net.corda.") || group.startsWith("com.r3.corda.enterprise.")) { - project.logger.warn("You appear to have included a Corda platform component ($it) using a 'compile' or 'runtime' dependency." + - "This can cause node stability problems. Please use 'corda' instead." + - "See http://docs.corda.net/cordapp-build-systems.html") - } else { - project.logger.info("Including dependency in CorDapp JAR: $it") - } - } - return filteredDeps.map { runtimeConfiguration.files(it) }.flatten().toSet() - } -} diff --git a/gradle-plugins/cordapp/src/main/kotlin/net/corda/plugins/Utils.kt b/gradle-plugins/cordapp/src/main/kotlin/net/corda/plugins/Utils.kt deleted file mode 100644 index 7572cd9876..0000000000 --- a/gradle-plugins/cordapp/src/main/kotlin/net/corda/plugins/Utils.kt +++ /dev/null @@ -1,35 +0,0 @@ -package net.corda.plugins - -import org.gradle.api.Project -import org.gradle.api.Task -import org.gradle.api.artifacts.Configuration -import org.gradle.api.plugins.ExtraPropertiesExtension - -/** - * Mimics the "project.ext" functionality in groovy which provides a direct - * accessor to the "ext" extention (See: ExtraPropertiesExtension) - */ -@Suppress("UNCHECKED_CAST") -fun Project.ext(name: String): T = (extensions.findByName("ext") as ExtraPropertiesExtension).get(name) as T -fun Project.configuration(name: String): Configuration = configurations.single { it.name == name } - -class Utils { - companion object { - @JvmStatic - fun createCompileConfiguration(name: String, project: Project) { - if(!project.configurations.any { it.name == name }) { - val configuration = project.configurations.create(name) - configuration.isTransitive = false - project.configurations.single { it.name == "compile" }.extendsFrom(configuration) - } - } - fun createRuntimeConfiguration(name: String, project: Project) { - if(!project.configurations.any { it.name == name }) { - val configuration = project.configurations.create(name) - configuration.isTransitive = false - project.configurations.single { it.name == "runtime" }.extendsFrom(configuration) - } - } - } - -} \ No newline at end of file diff --git a/gradle-plugins/cordapp/src/main/resources/META-INF/gradle-plugins/net.corda.plugins.cordapp.properties b/gradle-plugins/cordapp/src/main/resources/META-INF/gradle-plugins/net.corda.plugins.cordapp.properties deleted file mode 100644 index 90871e27c8..0000000000 --- a/gradle-plugins/cordapp/src/main/resources/META-INF/gradle-plugins/net.corda.plugins.cordapp.properties +++ /dev/null @@ -1 +0,0 @@ -implementation-class=net.corda.plugins.CordappPlugin diff --git a/gradle-plugins/cordform-common/README.md b/gradle-plugins/cordform-common/README.md deleted file mode 100644 index 8b83c20e93..0000000000 --- a/gradle-plugins/cordform-common/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# Cordform Common - -This project contains common node types that both the Corda gradle plugin suite and Corda project -require in order to build Corda nodes. \ No newline at end of file diff --git a/gradle-plugins/cordform-common/build.gradle b/gradle-plugins/cordform-common/build.gradle deleted file mode 100644 index aca55ab2b4..0000000000 --- a/gradle-plugins/cordform-common/build.gradle +++ /dev/null @@ -1,23 +0,0 @@ -apply plugin: 'java' -apply plugin: 'kotlin' -apply plugin: 'maven-publish' -apply plugin: 'net.corda.plugins.publish-utils' -apply plugin: 'com.jfrog.artifactory' - -repositories { - mavenCentral() -} - -dependencies { - compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" - - // JSR 305: Nullability annotations - compile "com.google.code.findbugs:jsr305:$jsr305_version" - - // TypeSafe Config: for simple and human friendly config files. - compile "com.typesafe:config:$typesafe_config_version" -} - -publish { - name project.name -} \ No newline at end of file diff --git a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformContext.java b/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformContext.java deleted file mode 100644 index 7687f68a11..0000000000 --- a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformContext.java +++ /dev/null @@ -1,7 +0,0 @@ -package net.corda.cordform; - -import java.nio.file.Path; - -public interface CordformContext { - Path baseDirectory(String nodeName); -} diff --git a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformDefinition.java b/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformDefinition.java deleted file mode 100644 index de4601d005..0000000000 --- a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformDefinition.java +++ /dev/null @@ -1,48 +0,0 @@ -package net.corda.cordform; - -import javax.annotation.Nonnull; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.List; -import java.util.function.Consumer; - -public abstract class CordformDefinition { - private Path nodesDirectory = Paths.get("build", "nodes"); - private final List> nodeConfigurers = new ArrayList<>(); - /** - * A list of Cordapp maven coordinates and project name - * - * If maven coordinates are set project name is ignored - */ - private final List cordappDeps = new ArrayList<>(); - - public Path getNodesDirectory() { - return nodesDirectory; - } - - public void setNodesDirectory(Path nodesDirectory) { - this.nodesDirectory = nodesDirectory; - } - - public List> getNodeConfigurers() { - return nodeConfigurers; - } - - public void addNode(Consumer configurer) { - nodeConfigurers.add(configurer); - } - - /** - * Cordapp maven coordinates or project names (ie; net.corda:finance:0.1 or ":finance") to scan for when resolving cordapp JARs - */ - public List getCordappDependencies() { - return cordappDeps; - } - - /** - * Make arbitrary changes to the node directories before they are started. - * @param context Lookup of node directory by node name. - */ - public abstract void setup(@Nonnull CordformContext context); -} diff --git a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformNode.java b/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformNode.java deleted file mode 100644 index c3dc01a5da..0000000000 --- a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformNode.java +++ /dev/null @@ -1,190 +0,0 @@ -package net.corda.cordform; - -import com.typesafe.config.Config; -import com.typesafe.config.ConfigFactory; -import com.typesafe.config.ConfigValueFactory; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.util.List; -import java.util.Map; - -import static java.util.Collections.emptyList; - -public class CordformNode implements NodeDefinition { - /** - * Path relative to the running node where the serialized NodeInfos are stored. - */ - public static final String NODE_INFO_DIRECTORY = "additional-node-infos"; - - protected static final String DEFAULT_HOST = "localhost"; - - /** - * Name of the node. Node will be placed in directory based on this name - all lowercase with whitespaces removed. - * Actual node name inside node.conf will be as set here. - */ - private String name; - - public String getName() { - return name; - } - - /** - * p2p Port. - */ - private int p2pPort = 10002; - - public int getP2pPort() { return p2pPort; } - - /** - * RPC Port. - */ - private int rpcPort = 10003; - - public int getRpcPort() { return rpcPort; } - - /** - * Set the RPC users for this node. This configuration block allows arbitrary configuration. - * The recommended current structure is: - * [[['username': "username_here", 'password': "password_here", 'permissions': ["permissions_here"]]] - * The above is a list to a map of keys to values using Groovy map and list shorthands. - * - * Incorrect configurations will not cause a DSL error. - */ - public List> rpcUsers = emptyList(); - - /** - * Apply the notary configuration if this node is a notary. The map is the config structure of - * net.corda.node.services.config.NotaryConfig - */ - public Map notary = null; - - public Map extraConfig = null; - - protected Config config = ConfigFactory.empty(); - - public Config getConfig() { - return config; - } - - /** - * Set the name of the node. - * - * @param name The node name. - */ - public void name(String name) { - this.name = name; - setValue("myLegalName", name); - } - - /** - * Get the artemis address for this node. - * - * @return This node's P2P address. - */ - @Nonnull - public String getP2pAddress() { - return config.getString("p2pAddress"); - } - - /** - * Set the Artemis P2P port for this node on localhost. - * - * @param p2pPort The Artemis messaging queue port. - */ - public void p2pPort(int p2pPort) { - p2pAddress(DEFAULT_HOST + ':' + p2pPort); - this.p2pPort = p2pPort; - } - - /** - * Set the Artemis P2P address for this node. - * - * @param p2pAddress The Artemis messaging queue host and port. - */ - public void p2pAddress(String p2pAddress) { - setValue("p2pAddress", p2pAddress); - } - - /** - * Returns the RPC address for this node, or null if one hasn't been specified. - */ - @Nullable - public String getRpcAddress() { - if (config.hasPath("rpcSettings.address")) { - return config.getConfig("rpcSettings").getString("address"); - } - return getOptionalString("rpcAddress"); - } - - /** - * Set the Artemis RPC port for this node on localhost. - * - * @param rpcPort The Artemis RPC queue port. - * @deprecated Use {@link CordformNode#rpcSettings(RpcSettings)} instead. - */ - @Deprecated - public void rpcPort(int rpcPort) { - rpcAddress(DEFAULT_HOST + ':' + rpcPort); - this.rpcPort = rpcPort; - } - - /** - * Set the Artemis RPC address for this node. - * - * @param rpcAddress The Artemis RPC queue host and port. - * @deprecated Use {@link CordformNode#rpcSettings(RpcSettings)} instead. - */ - @Deprecated - public void rpcAddress(String rpcAddress) { - setValue("rpcAddress", rpcAddress); - } - - /** - * Returns the address of the web server that will connect to the node, or null if one hasn't been specified. - */ - @Nullable - public String getWebAddress() { - return getOptionalString("webAddress"); - } - - /** - * Configure a webserver to connect to the node via RPC. This port will specify the port it will listen on. The node - * must have an RPC address configured. - */ - public void webPort(int webPort) { - webAddress(DEFAULT_HOST + ':' + webPort); - } - - /** - * Configure a webserver to connect to the node via RPC. This address will specify the port it will listen on. The node - * must have an RPC address configured. - */ - public void webAddress(String webAddress) { - setValue("webAddress", webAddress); - } - - /** - * Specifies RPC settings for the node. - */ - public void rpcSettings(RpcSettings settings) { - config = settings.addTo("rpcSettings", config); - } - - /** - * Set the path to a file with optional properties, which are appended to the generated node.conf file. - * - * @param configFile The file path. - */ - public void configFile(String configFile) { - setValue("configFile", configFile); - } - - private String getOptionalString(String path) { - return config.hasPath(path) ? config.getString(path) : null; - } - - private void setValue(String path, Object value) { - config = config.withValue(path, ConfigValueFactory.fromAnyRef(value)); - } -} diff --git a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/NodeDefinition.java b/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/NodeDefinition.java deleted file mode 100644 index 0b86b98627..0000000000 --- a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/NodeDefinition.java +++ /dev/null @@ -1,9 +0,0 @@ -package net.corda.cordform; - -import com.typesafe.config.Config; - -public interface NodeDefinition { - String getName(); - - Config getConfig(); -} diff --git a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/RpcSettings.java b/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/RpcSettings.java deleted file mode 100644 index 1869733271..0000000000 --- a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/RpcSettings.java +++ /dev/null @@ -1,80 +0,0 @@ -package net.corda.cordform; - -import com.typesafe.config.Config; -import com.typesafe.config.ConfigFactory; -import com.typesafe.config.ConfigValueFactory; - -public final class RpcSettings { - - private Config config = ConfigFactory.empty(); - - private int port = 10003; - private int adminPort = 10005; - - public int getPort() { - return port; - } - - public int getAdminPort() { - return adminPort; - } - - /** - * RPC address for the node. - */ - public final void address(final String value) { - setValue("address", value); - } - - /** - * RPC Port for the node - */ - public final void port(final int value) { - this.port = value; - setValue("address", "localhost:"+port); - } - - /** - * RPC admin address for the node (necessary if [useSsl] is false or unset). - */ - public final void adminAddress(final String value) { - setValue("adminAddress", value); - } - - public final void adminPort(final int value) { - this.adminPort = value; - setValue("adminAddress", "localhost:"+adminPort); - } - - /** - * Specifies whether the node RPC layer will require SSL from clients. - */ - public final void useSsl(final Boolean value) { - setValue("useSsl", value); - } - - /** - * Specifies whether the RPC broker is separate from the node. - */ - public final void standAloneBroker(final Boolean value) { - setValue("standAloneBroker", value); - } - - /** - * Specifies SSL certificates options for the RPC layer. - */ - public final void ssl(final SslOptions options) { - config = options.addTo("ssl", config); - } - - public final Config addTo(final String key, final Config config) { - if (this.config.isEmpty()) { - return config; - } - return config.withValue(key, this.config.root()); - } - - private void setValue(String path, Object value) { - config = config.withValue(path, ConfigValueFactory.fromAnyRef(value)); - } -} diff --git a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/SslOptions.java b/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/SslOptions.java deleted file mode 100644 index 1444d4ed8c..0000000000 --- a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/SslOptions.java +++ /dev/null @@ -1,56 +0,0 @@ -package net.corda.cordform; - -import com.typesafe.config.Config; -import com.typesafe.config.ConfigFactory; -import com.typesafe.config.ConfigValueFactory; - -public final class SslOptions { - - private Config config = ConfigFactory.empty(); - - /** - * Password for the keystore. - */ - public final void keyStorePassword(final String value) { - setValue("keyStorePassword", value); - } - - /** - * Password for the truststore. - */ - public final void trustStorePassword(final String value) { - setValue("trustStorePassword", value); - } - - /** - * Directory under which key stores are to be placed. - */ - public final void certificatesDirectory(final String value) { - setValue("certificatesDirectory", value); - } - - /** - * Absolute path to SSL keystore. Default: "[certificatesDirectory]/sslkeystore.jks" - */ - public final void sslKeystore(final String value) { - setValue("sslKeystore", value); - } - - /** - * Absolute path to SSL truststore. Default: "[certificatesDirectory]/truststore.jks" - */ - public final void trustStoreFile(final String value) { - setValue("trustStoreFile", value); - } - - public final Config addTo(final String key, final Config config) { - if (this.config.isEmpty()) { - return config; - } - return config.withValue(key, this.config.root()); - } - - private void setValue(String path, Object value) { - config = config.withValue(path, ConfigValueFactory.fromAnyRef(value)); - } -} diff --git a/gradle-plugins/cordformation/README.rst b/gradle-plugins/cordformation/README.rst deleted file mode 100644 index f619737a91..0000000000 --- a/gradle-plugins/cordformation/README.rst +++ /dev/null @@ -1 +0,0 @@ -Please refer to the documentation in /doc/build/html/running-a-node.html#cordformation. \ No newline at end of file diff --git a/gradle-plugins/cordformation/build.gradle b/gradle-plugins/cordformation/build.gradle deleted file mode 100644 index f68adb96ad..0000000000 --- a/gradle-plugins/cordformation/build.gradle +++ /dev/null @@ -1,69 +0,0 @@ -buildscript { - repositories { - mavenCentral() - } - - dependencies { - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - -apply plugin: 'kotlin' -apply plugin: 'java-gradle-plugin' -apply plugin: 'net.corda.plugins.publish-utils' -apply plugin: 'com.jfrog.artifactory' - -description 'A small gradle plugin for adding some basic Quasar tasks and configurations to reduce build.gradle bloat.' - -repositories { - mavenCentral() -} - -configurations { - noderunner - compile.extendsFrom noderunner -} - -sourceSets { - runnodes { - kotlin { - srcDir file('src/noderunner/kotlin') - compileClasspath += configurations.noderunner - } - } -} - -dependencies { - gradleApi() - - compile project(":cordapp") - compile project(':cordform-common') - compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" - compile "commons-io:commons-io:2.6" - - noderunner "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" - - testCompile "junit:junit:4.12" // TODO: Unify with core - testCompile "org.assertj:assertj-core:3.8.0" - // Docker-compose file generation - compile "org.yaml:snakeyaml:$snake_yaml_version" -} - -task createNodeRunner(type: Jar) { - manifest { - attributes('Main-Class': 'net.corda.plugins.NodeRunnerKt') - } - classifier = 'fatjar' - from { configurations.noderunner.collect { it.isDirectory() ? it : zipTree(it) } } - from sourceSets.runnodes.output -} - -publish { - name project.name -} - -processResources { - from(createNodeRunner) { - rename { 'net/corda/plugins/runnodes.jar' } - } -} diff --git a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Baseform.kt b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Baseform.kt deleted file mode 100644 index 010ee69e5c..0000000000 --- a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Baseform.kt +++ /dev/null @@ -1,166 +0,0 @@ -package net.corda.plugins - -import groovy.lang.Closure -import net.corda.cordform.CordformDefinition -import org.gradle.api.DefaultTask -import org.gradle.api.plugins.JavaPluginConvention -import org.gradle.api.tasks.SourceSet.MAIN_SOURCE_SET_NAME -import java.io.File -import java.lang.reflect.InvocationTargetException -import java.net.URLClassLoader -import java.nio.file.Path -import java.nio.file.Paths -import java.util.jar.JarInputStream - -/** - * Creates nodes based on the configuration of this task in the gradle configuration DSL. - * - * See documentation for examples. - */ -@Suppress("unused") -open class Baseform : DefaultTask() { - private companion object { - val nodeJarName = "corda.jar" - private val defaultDirectory: Path = Paths.get("build", "nodes") - } - - /** - * Optionally the name of a CordformDefinition subclass to which all configuration will be delegated. - */ - @Suppress("MemberVisibilityCanPrivate") - var definitionClass: String? = null - var directory = defaultDirectory - protected val nodes = mutableListOf() - - /** - * Set the directory to install nodes into. - * - * @param directory The directory the nodes will be installed into. - */ - fun directory(directory: String) { - this.directory = Paths.get(directory) - } - - /** - * Add a node configuration. - * - * @param configureClosure A node configuration that will be deployed. - */ - @Suppress("MemberVisibilityCanPrivate") - fun node(configureClosure: Closure) { - nodes += project.configure(Node(project), configureClosure) as Node - } - - /** - * Add a node configuration - * - * @param configureFunc A node configuration that will be deployed - */ - @Suppress("MemberVisibilityCanPrivate") - fun node(configureFunc: Node.() -> Any?): Node { - val node = Node(project).apply { configureFunc() } - nodes += node - return node - } - - /** - * Returns a node by name. - * - * @param name The name of the node as specified in the node configuration DSL. - * @return A node instance. - */ - private fun getNodeByName(name: String): Node? = nodes.firstOrNull { it.name == name } - - /** - * The definitionClass needn't be compiled until just before our build method, so we load it manually via sourceSets.main.runtimeClasspath. - */ - private fun loadCordformDefinition(): CordformDefinition { - val plugin = project.convention.getPlugin(JavaPluginConvention::class.java) - val classpath = plugin.sourceSets.getByName(MAIN_SOURCE_SET_NAME).runtimeClasspath - val urls = classpath.files.map { it.toURI().toURL() }.toTypedArray() - return URLClassLoader(urls, CordformDefinition::class.java.classLoader) - .loadClass(definitionClass) - .asSubclass(CordformDefinition::class.java) - .newInstance() - } - - /** - * The NetworkBootstrapper needn't be compiled until just before our build method, so we load it manually via sourceSets.main.runtimeClasspath. - */ - private fun loadNetworkBootstrapperClass(): Class<*> { - val plugin = project.convention.getPlugin(JavaPluginConvention::class.java) - val classpath = plugin.sourceSets.getByName(MAIN_SOURCE_SET_NAME).runtimeClasspath - val urls = classpath.files.map { it.toURI().toURL() }.toTypedArray() - return URLClassLoader(urls, javaClass.classLoader).loadClass("net.corda.nodeapi.internal.network.NetworkBootstrapper") - } - - /** - * Installs the corda fat JAR to the root directory, for the network bootstrapper to use. - */ - protected fun installCordaJar() { - val cordaJar = Cordformation.verifyAndGetRuntimeJar(project, "corda") - project.copy { - it.apply { - from(cordaJar) - into(directory) - rename(cordaJar.name, nodeJarName) - fileMode = Cordformation.executableFileMode - } - } - } - - internal fun initializeConfiguration() { - if (definitionClass != null) { - val cd = loadCordformDefinition() - // If the user has specified their own directory (even if it's the same default path) then let them know - // it's not used and should just rely on the one in CordformDefinition - require(directory === defaultDirectory) { - project.logger.info("User has used '$directory', default directory is '${defaultDirectory}'") - "'directory' cannot be used when 'definitionClass' is specified. Use CordformDefinition.nodesDirectory instead." - } - directory = cd.nodesDirectory - val cordapps = cd.cordappDependencies - cd.nodeConfigurers.forEach { - val node = node { } - it.accept(node) - cordapps.forEach { - if (it.mavenCoordinates != null) { - node.cordapp(project.project(it.mavenCoordinates!!)) - } else { - node.cordapp(it.projectName!!) - } - } - node.rootDir(directory) - } - cd.setup { nodeName -> project.projectDir.toPath().resolve(getNodeByName(nodeName)!!.nodeDir.toPath()) } - } else { - nodes.forEach { - it.rootDir(directory) - } - } - } - protected fun bootstrapNetwork() { - val networkBootstrapperClass = loadNetworkBootstrapperClass() - val networkBootstrapper = networkBootstrapperClass.newInstance() - val bootstrapMethod = networkBootstrapperClass.getMethod("bootstrap", Path::class.java).apply { isAccessible = true } - // Call NetworkBootstrapper.bootstrap - try { - val rootDir = project.projectDir.toPath().resolve(directory).toAbsolutePath().normalize() - bootstrapMethod.invoke(networkBootstrapper, rootDir) - } catch (e: InvocationTargetException) { - throw e.cause!! - } - } - - private fun File.containsPackage(`package`: String): Boolean { - JarInputStream(inputStream()).use { - while (true) { - val name = it.nextJarEntry?.name ?: break - if (name.endsWith(".class") && name.replace('/', '.').startsWith(`package`)) { - return true - } - } - return false - } - } -} diff --git a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordform.kt b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordform.kt deleted file mode 100644 index 74f9a02664..0000000000 --- a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordform.kt +++ /dev/null @@ -1,71 +0,0 @@ -package net.corda.plugins - -import org.apache.tools.ant.filters.FixCrLfFilter -import org.gradle.api.tasks.TaskAction -import java.nio.file.Path -import java.nio.file.Paths - -/** - * Creates nodes based on the configuration of this task in the gradle configuration DSL. - * - * See documentation for examples. - */ -@Suppress("unused") -open class Cordform : Baseform() { - internal companion object { - val nodeJarName = "corda.jar" - } - - /** - * Returns a node by name. - * - * @param name The name of the node as specified in the node configuration DSL. - * @return A node instance. - */ - private fun getNodeByName(name: String): Node? = nodes.firstOrNull { it.name == name } - - /** - * Installs the run script into the nodes directory. - */ - private fun installRunScript() { - project.copy { - it.apply { - from(Cordformation.getPluginFile(project, "runnodes.jar")) - fileMode = Cordformation.executableFileMode - into("$directory/") - } - } - - project.copy { - it.apply { - from(Cordformation.getPluginFile(project, "runnodes")) - // Replaces end of line with lf to avoid issues with the bash interpreter and Windows style line endings. - filter(mapOf("eol" to FixCrLfFilter.CrLf.newInstance("lf")), FixCrLfFilter::class.java) - fileMode = Cordformation.executableFileMode - into("$directory/") - } - } - - project.copy { - it.apply { - from(Cordformation.getPluginFile(project, "runnodes.bat")) - into("$directory/") - } - } - } - - /** - * This task action will create and install the nodes based on the node configurations added. - */ - @TaskAction - fun build() { - project.logger.info("Running Cordform task") - initializeConfiguration() - nodes.forEach(Node::installConfig) - installCordaJar() - installRunScript() - bootstrapNetwork() - nodes.forEach(Node::build) - } - -} diff --git a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordformation.kt b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordformation.kt deleted file mode 100644 index 644116e35b..0000000000 --- a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordformation.kt +++ /dev/null @@ -1,63 +0,0 @@ -package net.corda.plugins - -import org.gradle.api.Plugin -import org.gradle.api.Project -import java.io.File -import java.io.InputStream - -/** - * The Cordformation plugin deploys nodes to a directory in a state ready to be used by a developer for experimentation, - * testing, and debugging. It will prepopulate several fields in the configuration and create a simple node runner. - */ -class Cordformation : Plugin { - internal companion object { - const val CORDFORMATION_TYPE = "cordformationInternal" - - /** - * Gets a resource file from this plugin's JAR file by creating an intermediate tmp dir - * - * @param filePathInJar The file in the JAR, relative to root, you wish to access. - * @return A file handle to the file in the JAR. - */ - fun getPluginFile(project: Project, filePathInJar: String): File { - val tmpDir = File(project.buildDir, "tmp") - val outputFile = File(tmpDir, filePathInJar) - tmpDir.mkdir() - outputFile.outputStream().use { - Cordformation::class.java.getResourceAsStream(filePathInJar).copyTo(it) - } - return outputFile - } - - /** - * Gets a current built corda jar file - * - * @param project The project environment this plugin executes in. - * @param jarName The name of the JAR you wish to access. - * @return A file handle to the file in the JAR. - */ - fun verifyAndGetRuntimeJar(project: Project, jarName: String): File { - val releaseVersion = project.rootProject.ext("corda_release_version") - val maybeJar = project.configuration("runtime").filter { - "$jarName-$releaseVersion.jar" in it.toString() || "$jarName-r3-$releaseVersion.jar" in it.toString() - } - if (maybeJar.isEmpty) { - throw IllegalStateException("No $jarName JAR found. Have you deployed the Corda project to Maven? Looked for \"$jarName-$releaseVersion.jar\"") - } else { - val jar = maybeJar.singleFile - require(jar.isFile) - return jar - } - } - - val executableFileMode = "0755".toInt(8) - } - - override fun apply(project: Project) { - Utils.createCompileConfiguration("cordapp", project) - Utils.createRuntimeConfiguration(CORDFORMATION_TYPE, project) - // TODO: improve how we re-use existing declared external variables from root gradle.build - val jolokiaVersion = try { project.rootProject.ext("jolokia_version") } catch (e: Exception) { "1.3.7" } - project.dependencies.add(CORDFORMATION_TYPE, "org.jolokia:jolokia-jvm:$jolokiaVersion:agent") - } -} diff --git a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Dockerform.kt b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Dockerform.kt deleted file mode 100644 index 4e86e5caac..0000000000 --- a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Dockerform.kt +++ /dev/null @@ -1,66 +0,0 @@ -package net.corda.plugins - -import org.apache.tools.ant.filters.FixCrLfFilter -import org.gradle.api.DefaultTask -import org.gradle.api.plugins.JavaPluginConvention -import org.gradle.api.tasks.SourceSet.MAIN_SOURCE_SET_NAME -import org.gradle.api.tasks.TaskAction -import org.yaml.snakeyaml.DumperOptions -import java.nio.file.Path -import java.nio.file.Paths -import org.yaml.snakeyaml.Yaml -import java.nio.charset.StandardCharsets -import java.nio.file.Files - -/** - * Creates docker-compose file and image definitions based on the configuration of this task in the gradle configuration DSL. - * - * See documentation for examples. - */ -@Suppress("unused") -open class Dockerform : Baseform() { - private companion object { - val nodeJarName = "corda.jar" - private val defaultDirectory: Path = Paths.get("build", "docker") - - private val dockerComposeFileVersion = "3" - - private val yamlOptions = DumperOptions().apply { - indent = 2 - defaultFlowStyle = DumperOptions.FlowStyle.BLOCK - } - private val yaml = Yaml(yamlOptions) - } - - private val directoryPath = project.projectDir.toPath().resolve(directory) - - val dockerComposePath = directoryPath.resolve("docker-compose.yml") - - /** - * This task action will create and install the nodes based on the node configurations added. - */ - @TaskAction - fun build() { - project.logger.info("Running Cordform task") - initializeConfiguration() - nodes.forEach(Node::installDockerConfig) - installCordaJar() - bootstrapNetwork() - nodes.forEach(Node::buildDocker) - - - // Transform nodes path the absolute ones - val services = nodes.map { it.containerName to mapOf( - "build" to directoryPath.resolve(it.nodeDir.name).toAbsolutePath().toString(), - "ports" to listOf(it.rpcPort)) }.toMap() - - - val dockerComposeObject = mapOf( - "version" to dockerComposeFileVersion, - "services" to services) - - val dockerComposeContent = yaml.dump(dockerComposeObject) - - Files.write(dockerComposePath, dockerComposeContent.toByteArray(StandardCharsets.UTF_8)) - } -} diff --git a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Node.kt b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Node.kt deleted file mode 100644 index 0086854162..0000000000 --- a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Node.kt +++ /dev/null @@ -1,409 +0,0 @@ -package net.corda.plugins - -import com.typesafe.config.ConfigFactory -import com.typesafe.config.ConfigObject -import com.typesafe.config.ConfigRenderOptions -import com.typesafe.config.ConfigValueFactory -import groovy.lang.Closure -import net.corda.cordform.CordformNode -import net.corda.cordform.RpcSettings -import org.apache.commons.io.FilenameUtils -import org.gradle.api.GradleException -import org.gradle.api.Project -import org.gradle.api.artifacts.ProjectDependency -import java.io.File -import java.nio.charset.StandardCharsets -import java.nio.file.Files -import java.nio.file.Path -import javax.inject.Inject - -/** - * Represents a node that will be installed. - */ -open class Node @Inject constructor(private val project: Project) : CordformNode() { - private data class ResolvedCordapp(val jarFile: File, val config: String?) - - companion object { - @JvmStatic - val webJarName = "corda-webserver.jar" - private val configFileProperty = "configFile" - } - - /** - * Set the list of CorDapps to install to the plugins directory. Each cordapp is a fully qualified Maven - * dependency name, eg: com.example:product-name:0.1 - * - * @note Your app will be installed by default and does not need to be included here. - * @note Type is any due to gradle's use of "GStrings" - each value will have "toString" called on it - */ - var cordapps: MutableList - get() = internalCordapps as MutableList - @Deprecated("Use cordapp instead - setter will be removed by Corda V4.0") - set(value) { - value.forEach { - cordapp(it.toString()) - } - } - - private val internalCordapps = mutableListOf() - private val builtCordapp = Cordapp(project) - internal lateinit var nodeDir: File - private set - internal lateinit var rootDir: File - private set - internal lateinit var containerName: String - private set - internal var rpcSettings: RpcSettings = RpcSettings() - private set - internal var webserverJar: String? = null - private set - - /** - * Sets whether this node will use HTTPS communication. - * - * @param isHttps True if this node uses HTTPS communication. - */ - fun https(isHttps: Boolean) { - config = config.withValue("useHTTPS", ConfigValueFactory.fromAnyRef(isHttps)) - } - - /** - * Sets the H2 port for this node - */ - fun h2Port(h2Port: Int) { - config = config.withValue("h2port", ConfigValueFactory.fromAnyRef(h2Port)) - } - - fun useTestClock(useTestClock: Boolean) { - config = config.withValue("useTestClock", ConfigValueFactory.fromAnyRef(useTestClock)) - } - - /** - * Specifies RPC settings for the node. - */ - fun rpcSettings(configureClosure: Closure) { - rpcSettings = project.configure(RpcSettings(), configureClosure) as RpcSettings - config = rpcSettings.addTo("rpcSettings", config) - } - - /** - * Enables SSH access on given port - * - * @param sshdPort The port for SSH server to listen on - */ - fun sshdPort(sshdPort: Int) { - config = config.withValue("sshdAddress", - ConfigValueFactory.fromAnyRef("$DEFAULT_HOST:$sshdPort")) - } - - /** - * Install a cordapp to this node - * - * @param coordinates The coordinates of the [Cordapp] - * @param configureClosure A groovy closure to configure a [Cordapp] object - * @return The created and inserted [Cordapp] - */ - fun cordapp(coordinates: String, configureClosure: Closure): Cordapp { - val cordapp = project.configure(Cordapp(coordinates), configureClosure) as Cordapp - internalCordapps += cordapp - return cordapp - } - - /** - * Install a cordapp to this node - * - * @param cordappProject A project that produces a cordapp JAR - * @param configureClosure A groovy closure to configure a [Cordapp] object - * @return The created and inserted [Cordapp] - */ - fun cordapp(cordappProject: Project, configureClosure: Closure): Cordapp { - val cordapp = project.configure(Cordapp(cordappProject), configureClosure) as Cordapp - internalCordapps += cordapp - return cordapp - } - - /** - * Install a cordapp to this node - * - * @param cordappProject A project that produces a cordapp JAR - * @return The created and inserted [Cordapp] - */ - fun cordapp(cordappProject: Project): Cordapp { - return Cordapp(cordappProject).apply { - internalCordapps += this - } - } - - /** - * Install a cordapp to this node - * - * @param coordinates The coordinates of the [Cordapp] - * @return The created and inserted [Cordapp] - */ - fun cordapp(coordinates: String): Cordapp { - return Cordapp(coordinates).apply { - internalCordapps += this - } - } - - /** - * Install a cordapp to this node - * - * @param configureFunc A lambda to configure a [Cordapp] object - * @return The created and inserted [Cordapp] - */ - fun cordapp(coordinates: String, configureFunc: Cordapp.() -> Unit): Cordapp { - return Cordapp(coordinates).apply { - configureFunc() - internalCordapps += this - } - } - - /** - * Configures the default cordapp automatically added to this node from this project - * - * @param configureClosure A groovy closure to configure a [Cordapp] object - * @return The created and inserted [Cordapp] - */ - fun projectCordapp(configureClosure: Closure): Cordapp { - project.configure(builtCordapp, configureClosure) as Cordapp - return builtCordapp - } - - /** - * The webserver JAR to be used by this node. - * - * If not provided, the default development webserver is used. - * - * @param webserverJar The file path of the webserver JAR to use. - */ - fun webserverJar(webserverJar: String) { - this.webserverJar = webserverJar - } - - internal fun build() { - if (config.hasPath("webAddress")) { - installWebserverJar() - } - installAgentJar() - installCordapps() - installConfig() - } - - internal fun buildDocker() { - project.copy { - it.apply { - from(Cordformation.getPluginFile(project, "net/corda/plugins/Dockerfile")) - from(Cordformation.getPluginFile(project, "net/corda/plugins/run-corda.sh")) - into("$nodeDir/") - } - } - installAgentJar() - installCordapps() - } - - internal fun rootDir(rootDir: Path) { - if (name == null) { - project.logger.error("Node has a null name - cannot create node") - throw IllegalStateException("Node has a null name - cannot create node") - } - // Parsing O= part directly because importing BouncyCastle provider in Cordformation causes problems - // with loading our custom X509EdDSAEngine. - val organizationName = name.trim().split(",").firstOrNull { it.startsWith("O=") }?.substringAfter("=") - val dirName = organizationName ?: name - containerName = dirName.replace("\\s+".toRegex(), "-").toLowerCase() - this.rootDir = rootDir.toFile() - nodeDir = File(this.rootDir, dirName.replace("\\s", "")) - Files.createDirectories(nodeDir.toPath()) - } - - private fun configureProperties() { - config = config.withValue("rpcUsers", ConfigValueFactory.fromIterable(rpcUsers)) - if (notary != null) { - config = config.withValue("notary", ConfigValueFactory.fromMap(notary)) - } - if (extraConfig != null) { - config = config.withFallback(ConfigFactory.parseMap(extraConfig)) - } - } - - /** - * Installs the corda webserver JAR to the node directory - */ - private fun installWebserverJar() { - // If no webserver JAR is provided, the default development webserver is used. - val webJar = if (webserverJar == null) { - project.logger.info("Using default development webserver.") - Cordformation.verifyAndGetRuntimeJar(project, "corda-webserver") - } else { - project.logger.info("Using custom webserver: $webserverJar.") - File(webserverJar) - } - - project.copy { - it.apply { - from(webJar) - into(nodeDir) - rename(webJar.name, webJarName) - } - } - } - - /** - * Installs the jolokia monitoring agent JAR to the node/drivers directory - */ - private fun installAgentJar() { - // TODO: improve how we re-use existing declared external variables from root gradle.build - val jolokiaVersion = try { project.rootProject.ext("jolokia_version") } catch (e: Exception) { "1.3.7" } - val agentJar = project.configuration("runtime").files { - (it.group == "org.jolokia") && - (it.name == "jolokia-jvm") && - (it.version == jolokiaVersion) - // TODO: revisit when classifier attribute is added. eg && (it.classifier = "agent") - }.first() // should always be the jolokia agent fat jar: eg. jolokia-jvm-1.3.7-agent.jar - project.logger.info("Jolokia agent jar: $agentJar") - if (agentJar.isFile) { - val driversDir = File(nodeDir, "drivers") - project.copy { - it.apply { - from(agentJar) - into(driversDir) - } - } - } - } - - private fun installCordappConfigs(cordapps: Collection) { - val cordappsDir = project.file(File(nodeDir, "cordapps")) - cordappsDir.mkdirs() - cordapps.filter { it.config != null } - .map { Pair("${FilenameUtils.removeExtension(it.jarFile.name)}.conf", it.config!!) } - .forEach { project.file(File(cordappsDir, it.first)).writeText(it.second) } - } - - private fun createTempConfigFile(configObject: ConfigObject): File { - val options = ConfigRenderOptions - .defaults() - .setOriginComments(false) - .setComments(false) - .setFormatted(true) - .setJson(false) - val configFileText = configObject.render(options).split("\n").toList() - // Need to write a temporary file first to use the project.copy, which resolves directories correctly. - val tmpDir = File(project.buildDir, "tmp") - Files.createDirectories(tmpDir.toPath()) - var fileName = "${nodeDir.name}.conf" - val tmpConfFile = File(tmpDir, fileName) - Files.write(tmpConfFile.toPath(), configFileText, StandardCharsets.UTF_8) - return tmpConfFile - } - - /** - * Installs the configuration file to the root directory and detokenises it. - */ - fun installConfig() { - configureProperties() - val tmpConfFile = createTempConfigFile(config.root()) - appendOptionalConfig(tmpConfFile) - project.copy { - it.apply { - from(tmpConfFile) - into(rootDir) - } - } - } - - /** - * Installs the Dockerized configuration file to the root directory and detokenises it. - */ - internal fun installDockerConfig() { - configureProperties() - val dockerConf = config - .withValue("p2pAddress", ConfigValueFactory.fromAnyRef("$containerName:$p2pPort")) - .withValue("rpcSettings.address", ConfigValueFactory.fromAnyRef("$containerName:${rpcSettings.port}")) - .withValue("rpcSettings.adminAddress", ConfigValueFactory.fromAnyRef("$containerName:${rpcSettings.adminPort}")) - .withValue("detectPublicIp", ConfigValueFactory.fromAnyRef(false)) - val tmpConfFile = createTempConfigFile(dockerConf.root()) - appendOptionalConfig(tmpConfFile) - project.copy { - it.apply { - from(tmpConfFile) - into(rootDir) - } - } - } - - /** - * Appends installed config file with properties from an optional file. - */ - private fun appendOptionalConfig(confFile: File) { - val optionalConfig: File? = when { - project.findProperty(configFileProperty) != null -> //provided by -PconfigFile command line property when running Gradle task - File(project.findProperty(configFileProperty) as String) - config.hasPath(configFileProperty) -> File(config.getString(configFileProperty)) - else -> null - } - - if (optionalConfig != null) { - if (!optionalConfig.exists()) { - project.logger.error("$configFileProperty '$optionalConfig' not found") - } else { - confFile.appendBytes(optionalConfig.readBytes()) - } - } - } - - - /** - * Installs the jolokia monitoring agent JAR to the node/drivers directory - */ - private fun installCordapps() { - val cordapps = getCordappList() - val cordappsDir = File(nodeDir, "cordapps") - project.copy { - it.apply { - from(cordapps.map { it.jarFile }) - into(project.file(cordappsDir)) - } - } - - installCordappConfigs(cordapps) - } - - - /** - * Gets a list of cordapps based on what dependent cordapps were specified. - * - * @return List of this node's cordapps. - */ - private fun getCordappList(): Collection = - internalCordapps.map { cordapp -> resolveCordapp(cordapp) } + resolveBuiltCordapp() - - private fun resolveCordapp(cordapp: Cordapp): ResolvedCordapp { - val cordappConfiguration = project.configuration("cordapp") - val cordappName = if (cordapp.project != null) cordapp.project.name else cordapp.coordinates - val cordappFile = cordappConfiguration.files { - when { - (it is ProjectDependency) && (cordapp.project != null) -> it.dependencyProject == cordapp.project - cordapp.coordinates != null -> { - // Cordapps can sometimes contain a GString instance which fails the equality test with the Java string - @Suppress("RemoveRedundantCallsOfConversionMethods") - val coordinates = cordapp.coordinates.toString() - coordinates == (it.group + ":" + it.name + ":" + it.version) - } - else -> false - } - } - - return when { - cordappFile.size == 0 -> throw GradleException("Cordapp $cordappName not found in cordapps configuration.") - cordappFile.size > 1 -> throw GradleException("Multiple files found for $cordappName") - else -> ResolvedCordapp(cordappFile.single(), cordapp.config) - } - } - - private fun resolveBuiltCordapp(): ResolvedCordapp { - val projectCordappFile = project.tasks.getByName("jar").outputs.files.singleFile - return ResolvedCordapp(projectCordappFile, builtCordapp.config) - } -} diff --git a/gradle-plugins/cordformation/src/main/resources/META-INF/gradle-plugins/net.corda.plugins.cordformation.properties b/gradle-plugins/cordformation/src/main/resources/META-INF/gradle-plugins/net.corda.plugins.cordformation.properties deleted file mode 100644 index 4475d5c692..0000000000 --- a/gradle-plugins/cordformation/src/main/resources/META-INF/gradle-plugins/net.corda.plugins.cordformation.properties +++ /dev/null @@ -1 +0,0 @@ -implementation-class=net.corda.plugins.Cordformation diff --git a/gradle-plugins/cordformation/src/main/resources/net/corda/plugins/Dockerfile b/gradle-plugins/cordformation/src/main/resources/net/corda/plugins/Dockerfile deleted file mode 100644 index 66ab277852..0000000000 --- a/gradle-plugins/cordformation/src/main/resources/net/corda/plugins/Dockerfile +++ /dev/null @@ -1,44 +0,0 @@ -# Base image from (http://phusion.github.io/baseimage-docker) -FROM openjdk:8u151-jre-alpine - -ENV CORDA_VERSION=${BUILDTIME_CORDA_VERSION} -ENV JAVA_OPTIONS=${BUILDTIME_JAVA_OPTIONS} - -# Set image labels -LABEL net.corda.version = ${CORDA_VERSION} \ - maintainer = "" \ - vendor = "R3" - -RUN apk upgrade --update && \ - apk add --update --no-cache bash iputils && \ - rm -rf /var/cache/apk/* && \ - # Add user to run the app && \ - addgroup corda && \ - adduser -G corda -D -s /bin/bash corda && \ - # Create /opt/corda directory && \ - mkdir -p /opt/corda/plugins && \ - mkdir -p /opt/corda/logs - -# Copy corda files -ADD --chown=corda:corda corda.jar /opt/corda/corda.jar -ADD --chown=corda:corda node.conf /opt/corda/node.conf -ADD --chown=corda:corda network-parameters /opt/corda/ -ADD --chown=corda:corda cordapps/ /opt/corda/cordapps -ADD --chown=corda:corda additional-node-infos/ /opt/corda/additional-node-infos -ADD --chown=corda:corda certificates/ /opt/corda/certificates -ADD --chown=corda:corda drivers/ /opt/corda/drivers -ADD --chown=corda:corda persistence* /opt/corda/ - -COPY run-corda.sh /run-corda.sh - -RUN chmod +x /run-corda.sh && \ - sync && \ - chown -R corda:corda /opt/corda - -# Working directory for Corda -WORKDIR /opt/corda -ENV HOME=/opt/corda -USER corda - -# Start it -CMD ["/run-corda.sh"] \ No newline at end of file diff --git a/gradle-plugins/cordformation/src/main/resources/net/corda/plugins/run-corda.sh b/gradle-plugins/cordformation/src/main/resources/net/corda/plugins/run-corda.sh deleted file mode 100644 index c10604f859..0000000000 --- a/gradle-plugins/cordformation/src/main/resources/net/corda/plugins/run-corda.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/sh - -# If variable not present use default values -: ${CORDA_HOME:=/opt/corda} -: ${JAVA_OPTIONS:=-Xmx512m} - -export CORDA_HOME JAVA_OPTIONS - -cd ${CORDA_HOME} -java $JAVA_OPTIONS -jar ${CORDA_HOME}/corda.jar 2>&1 \ No newline at end of file diff --git a/gradle-plugins/cordformation/src/main/resources/net/corda/plugins/runnodes b/gradle-plugins/cordformation/src/main/resources/net/corda/plugins/runnodes deleted file mode 100644 index 9e3ba4c5be..0000000000 --- a/gradle-plugins/cordformation/src/main/resources/net/corda/plugins/runnodes +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -# Allow the script to be run from outside the nodes directory. -basedir=$( dirname "$0" ) -cd "$basedir" - -if which osascript >/dev/null; then - /usr/libexec/java_home -v 1.8 --exec java -jar runnodes.jar "$@" -else - "${JAVA_HOME:+$JAVA_HOME/bin/}java" -jar runnodes.jar "$@" -fi diff --git a/gradle-plugins/cordformation/src/main/resources/net/corda/plugins/runnodes.bat b/gradle-plugins/cordformation/src/main/resources/net/corda/plugins/runnodes.bat deleted file mode 100644 index a6acf1f737..0000000000 --- a/gradle-plugins/cordformation/src/main/resources/net/corda/plugins/runnodes.bat +++ /dev/null @@ -1,8 +0,0 @@ -@echo off - -REM Change to the directory of this script (%~dp0) -Pushd %~dp0 - -java -jar runnodes.jar %* - -Popd \ No newline at end of file diff --git a/gradle-plugins/cordformation/src/noderunner/kotlin/net/corda/plugins/NodeRunner.kt b/gradle-plugins/cordformation/src/noderunner/kotlin/net/corda/plugins/NodeRunner.kt deleted file mode 100644 index cffa06300f..0000000000 --- a/gradle-plugins/cordformation/src/noderunner/kotlin/net/corda/plugins/NodeRunner.kt +++ /dev/null @@ -1,159 +0,0 @@ -package net.corda.plugins - -import java.awt.GraphicsEnvironment -import java.io.File -import java.nio.file.Files -import java.util.* - -private val HEADLESS_FLAG = "--headless" -private val CAPSULE_DEBUG_FLAG = "--capsule-debug" - -private val os by lazy { - val osName = System.getProperty("os.name", "generic").toLowerCase(Locale.ENGLISH) - if ("mac" in osName || "darwin" in osName) OS.MACOS - else if ("win" in osName) OS.WINDOWS - else OS.LINUX -} - -private enum class OS { MACOS, WINDOWS, LINUX } - -private object debugPortAlloc { - private var basePort = 5005 - internal fun next() = basePort++ -} - -private object monitoringPortAlloc { - private var basePort = 7005 - internal fun next() = basePort++ -} - -fun main(args: Array) { - val startedProcesses = mutableListOf() - val headless = GraphicsEnvironment.isHeadless() || args.contains(HEADLESS_FLAG) - val capsuleDebugMode = args.contains(CAPSULE_DEBUG_FLAG) - val workingDir = File(System.getProperty("user.dir")) - val javaArgs = args.filter { it != HEADLESS_FLAG && it != CAPSULE_DEBUG_FLAG } - val jvmArgs = if (capsuleDebugMode) listOf("-Dcapsule.log=verbose") else emptyList() - println("Starting nodes in $workingDir") - workingDir.listFiles { file -> file.isDirectory }.forEach { dir -> - listOf(NodeJarType, WebJarType).forEach { jarType -> - jarType.acceptDirAndStartProcess(dir, headless, javaArgs, jvmArgs)?.let { startedProcesses += it } - } - } - println("Started ${startedProcesses.size} processes") - println("Finished starting nodes") -} - -private abstract class JarType(private val jarName: String) { - internal abstract fun acceptNodeConf(nodeConf: File): Boolean - internal fun acceptDirAndStartProcess(dir: File, headless: Boolean, javaArgs: List, jvmArgs: List): Process? { - if (!File(dir, jarName).exists()) { - return null - } - if (!File(dir, "node.conf").let { it.exists() && acceptNodeConf(it) }) { - return null - } - val debugPort = debugPortAlloc.next() - val monitoringPort = monitoringPortAlloc.next() - println("Starting $jarName in $dir on debug port $debugPort") - val process = (if (headless) ::HeadlessJavaCommand else ::TerminalWindowJavaCommand)(jarName, dir, debugPort, monitoringPort, javaArgs, jvmArgs).start() - if (os == OS.MACOS) Thread.sleep(1000) - return process - } -} - -private object NodeJarType : JarType("corda.jar") { - override fun acceptNodeConf(nodeConf: File) = true -} - -private object WebJarType : JarType("corda-webserver.jar") { - // TODO: Add a webserver.conf, or use TypeSafe config instead of this hack - override fun acceptNodeConf(nodeConf: File) = Files.lines(nodeConf.toPath()).anyMatch { "webAddress" in it } -} - -private abstract class JavaCommand( - jarName: String, - internal val dir: File, - debugPort: Int?, - monitoringPort: Int?, - internal val nodeName: String, - init: MutableList.() -> Unit, args: List, - jvmArgs: List -) { - private val jolokiaJar by lazy { - File("$dir/drivers").listFiles { _, filename -> - filename.matches("jolokia-jvm-.*-agent\\.jar$".toRegex()) - }.first().name - } - - internal val command: List = mutableListOf().apply { - add(getJavaPath()) - addAll(jvmArgs) - add("-Dname=$nodeName") - val jvmArgs: MutableList = mutableListOf() - null != debugPort && jvmArgs.add("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$debugPort") - null != monitoringPort && jvmArgs.add("-javaagent:drivers/$jolokiaJar=port=$monitoringPort") - if (jvmArgs.isNotEmpty()) { - add("-Dcapsule.jvm.args=${jvmArgs.joinToString(separator = " ")}") - } - add("-jar") - add(jarName) - init() - addAll(args) - } - - internal abstract fun processBuilder(): ProcessBuilder - internal fun start() = processBuilder().directory(dir).start() - internal abstract fun getJavaPath(): String -} - -private class HeadlessJavaCommand(jarName: String, dir: File, debugPort: Int?, monitoringPort: Int?, args: List, jvmArgs: List) - : JavaCommand(jarName, dir, debugPort, monitoringPort, dir.name, { add("--no-local-shell") }, args, jvmArgs) { - override fun processBuilder(): ProcessBuilder { - println("Running command: ${command.joinToString(" ")}") - return ProcessBuilder(command).redirectError(File("error.$nodeName.log")).inheritIO() - } - - override fun getJavaPath() = File(File(System.getProperty("java.home"), "bin"), "java").path -} - -private class TerminalWindowJavaCommand(jarName: String, dir: File, debugPort: Int?, monitoringPort: Int?, args: List, jvmArgs: List) - : JavaCommand(jarName, dir, debugPort, monitoringPort, "${dir.name}-$jarName", {}, args, jvmArgs) { - override fun processBuilder(): ProcessBuilder { - val params = when (os) { - OS.MACOS -> { - listOf("osascript", "-e", """tell app "Terminal" - activate - delay 0.5 - tell app "System Events" to tell process "Terminal" to keystroke "t" using command down - delay 0.5 - do script "bash -c 'cd \"$dir\" ; \"${command.joinToString("""\" \"""")}\" && exit'" in selected tab of the front window -end tell""") - } - OS.WINDOWS -> { - listOf("cmd", "/C", "start ${command.joinToString(" ") { windowsSpaceEscape(it) }}") - } - OS.LINUX -> { - // Start shell to keep window open unless java terminated normally or due to SIGTERM: - val command = "${unixCommand()}; [ $? -eq 0 -o $? -eq 143 ] || sh" - if (isTmux()) { - listOf("tmux", "new-window", "-n", nodeName, command) - } else { - listOf("xterm", "-T", nodeName, "-e", command) - } - } - } - println("Running command: ${params.joinToString(" ")}") - return ProcessBuilder(params) - } - - private fun unixCommand() = command.map(::quotedFormOf).joinToString(" ") - override fun getJavaPath(): String = File(File(System.getProperty("java.home"), "bin"), "java").path - - // Replace below is to fix an issue with spaces in paths on Windows. - // Quoting the entire path does not work, only the space or directory within the path. - private fun windowsSpaceEscape(s:String) = s.replace(" ", "\" \"") -} - -private fun quotedFormOf(text: String) = "'${text.replace("'", "'\\''")}'" // Suitable for UNIX shells. -private fun isTmux() = System.getenv("TMUX")?.isNotEmpty() ?: false diff --git a/gradle-plugins/publish-utils/README.rst b/gradle-plugins/publish-utils/README.rst deleted file mode 100644 index d1657ee5fe..0000000000 --- a/gradle-plugins/publish-utils/README.rst +++ /dev/null @@ -1,92 +0,0 @@ -Publish Utils -============= - -Publishing utilities adds a couple of tasks to any project it is applied to that hide some boilerplate that would -otherwise be placed in the Cordapp template's build.gradle. - -There are two tasks exposed: `sourceJar` and `javadocJar` and both return a `FileCollection`. - -It is used within the `publishing` block of a build.gradle as such; - -.. code-block:: text - - // This will publish the sources, javadoc, and Java components to Maven. - // See the `maven-publish` plugin for more info: https://docs.gradle.org/current/userguide/publishing_maven.html - publishing { - publications { - jarAndSources(MavenPublication) { - from components.java - // The two lines below are the tasks added by this plugin. - artifact sourceJar - artifact javadocJar - } - } - } - -Bintray Publishing ------------------- - -For large multibuild projects it can be inconvenient to store the entire configuration for bintray and maven central -per project (with a bintray and publishing block with extended POM information). Publish utils can bring the number of -configuration blocks down to one in the ideal scenario. - -To use this plugin you must first apply it to both the root project and any project that will be published with - -.. code-block:: text - - apply plugin: 'net.corda.plugins.publish-utils' - -Next you must setup the general bintray configuration you wish to use project wide, for example: - -.. code-block:: text - - bintrayConfig { - user = - key = - repo = 'example repo' - org = 'example organisation' - licenses = ['a license'] - vcsUrl = 'https://example.com' - projectUrl = 'https://example.com' - gpgSign = true // Whether to GPG sign - gpgPassphrase = // Only required if gpgSign is true and your key is passworded - publications = ['example'] // a list of publications (see below) - license { - name = 'example' - url = 'https://example.com' - distribution = 'repo' - } - developer { - id = 'a developer id' - name = 'a developer name' - email = 'example@example.com' - } - } - -.. note:: You can currently only have one license and developer in the maven POM sections - -**Publications** - -This plugin assumes, by default, that publications match the name of the project. This means, by default, you can -just list the names of the projects you wish to publish (e.g. to publish `test:myapp` you need `publications = ['myapp']`. -If a project requires a different name you can configure it *per project* with the project configuration block. - -The project configuration block has the following structure: - -.. code-block:: text - - publish { - disableDefaultJar = false // set to true to disable the default JAR being created (e.g. when creating a fat JAR) - name 'non-default-project-name' // Always put this last because it causes configuration to happen - } - -**Artifacts** - -To add additional artifacts to the project you can use the default gradle `artifacts` block with the `publish` -configuration. For example: - - artifacts { - publish buildFatJar { - // You can configure this as a regular maven publication - } - } diff --git a/gradle-plugins/publish-utils/build.gradle b/gradle-plugins/publish-utils/build.gradle deleted file mode 100644 index da9c659498..0000000000 --- a/gradle-plugins/publish-utils/build.gradle +++ /dev/null @@ -1,109 +0,0 @@ -apply plugin: 'groovy' -apply plugin: 'maven-publish' -apply plugin: 'com.jfrog.bintray' -apply plugin: 'com.jfrog.artifactory' - -// Used for bootstrapping project -buildscript { - Properties constants = new Properties() - file("../../constants.properties").withInputStream { constants.load(it) } - - ext { - gradle_plugins_version = constants.getProperty("gradlePluginsVersion") - artifactory_plugin_version = constants.getProperty('artifactoryPluginVersion') - } - - repositories { - jcenter() - } - - dependencies { - classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.4' - classpath "org.jfrog.buildinfo:build-info-extractor-gradle:$artifactory_plugin_version" - } -} - -version "$gradle_plugins_version" - -dependencies { - compile gradleApi() - compile localGroovy() -} - -repositories { - mavenCentral() -} - -task("sourceJar", type: Jar, dependsOn: classes) { - classifier = 'sources' - from sourceSets.main.allSource -} - -task("javadocJar", type: Jar, dependsOn: javadoc) { - classifier = 'javadoc' - from javadoc.destinationDir -} - -bintray { - user = System.getenv('CORDA_BINTRAY_USER') - key = System.getenv('CORDA_BINTRAY_KEY') - publications = ['publishUtils'] - dryRun = false - pkg { - repo = 'corda' - name = 'publish-utils' - userOrg = 'r3' - licenses = ['Apache-2.0'] - - version { - gpg { - sign = true - passphrase = System.getenv('CORDA_BINTRAY_GPG_PASSPHRASE') - } - } - } -} - -publishing { - publications { - publishUtils(MavenPublication) { - from components.java - groupId 'net.corda.plugins' - artifactId 'publish-utils' - - artifact sourceJar - artifact javadocJar - - pom.withXml { - asNode().children().last() + { - resolveStrategy = Closure.DELEGATE_FIRST - name 'publish-utils' - description 'A small gradle plugin that adds a couple of convenience functions for publishing to Maven' - url 'https://github.com/corda/corda' - scm { - url 'https://github.com/corda/corda' - } - - licenses { - license { - name 'Apache-2.0' - url 'https://www.apache.org/licenses/LICENSE-2.0' - distribution 'repo' - } - } - - developers { - developer { - id 'R3' - name 'R3' - email 'dev@corda.net' - } - } - } - } - } - } -} - -// Aliasing the publishToMavenLocal for simplicity. -task(install, dependsOn: 'publishToMavenLocal') diff --git a/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/ProjectPublishExtension.groovy b/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/ProjectPublishExtension.groovy deleted file mode 100644 index edb543fa51..0000000000 --- a/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/ProjectPublishExtension.groovy +++ /dev/null @@ -1,39 +0,0 @@ -package net.corda.plugins - -class ProjectPublishExtension { - private PublishTasks task - - void setPublishTask(PublishTasks task) { - this.task = task - } - - /** - * Use a different name from the current project name for publishing. - * Set this after all other settings that need to be configured - */ - void name(String name) { - task.setPublishName(name) - } - - /** - * Get the publishing name for this project. - */ - String name() { - return task.getPublishName() - } - - /** - * True when we do not want to publish default Java components - */ - Boolean disableDefaultJar = false - - /** - * True if publishing a WAR instead of a JAR. Forces disableDefaultJAR to "true" when true - */ - Boolean publishWar = false - - /** - * True if publishing sources to remote repositories - */ - Boolean publishSources = true -} \ No newline at end of file diff --git a/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/PublishTasks.groovy b/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/PublishTasks.groovy deleted file mode 100644 index 772ac99d23..0000000000 --- a/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/PublishTasks.groovy +++ /dev/null @@ -1,167 +0,0 @@ -package net.corda.plugins - -import org.gradle.api.* -import org.gradle.api.tasks.bundling.Jar -import org.gradle.api.tasks.javadoc.Javadoc -import org.gradle.api.Project -import org.gradle.api.publish.maven.MavenPublication -import org.gradle.api.publish.maven.MavenPom -import net.corda.plugins.bintray.* - -/** - * A utility plugin that when applied will automatically create source and javadoc publishing tasks - * To apply this plugin you must also add 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.4' to your - * buildscript's classpath dependencies. - * - * To use this plugin you can add a new configuration block (extension) to your root build.gradle. See the fields - * in BintrayConfigExtension. - */ -class PublishTasks implements Plugin { - Project project - String publishName - ProjectPublishExtension publishConfig - - void apply(Project project) { - this.project = project - this.publishName = project.name - - createTasks() - createExtensions() - createConfigurations() - } - - /** - * This call must come at the end of any publish block because it configures the publishing and any - * values set after this call in the DSL will not be configured properly (and will use the default value) - */ - void setPublishName(String publishName) { - project.logger.info("Changing publishing name from ${project.name} to ${publishName}") - this.publishName = publishName - checkAndConfigurePublishing() - } - - void checkAndConfigurePublishing() { - project.logger.info("Checking whether to publish $publishName") - def bintrayConfig = project.rootProject.extensions.findByType(BintrayConfigExtension.class) - if((bintrayConfig != null) && (bintrayConfig.publications) && (bintrayConfig.publications.findAll { it == publishName }.size() > 0)) { - configurePublishing(bintrayConfig) - } - } - - void configurePublishing(BintrayConfigExtension bintrayConfig) { - project.logger.info("Configuring bintray for ${publishName}") - configureMavenPublish(bintrayConfig) - configureBintray(bintrayConfig) - } - - void configureMavenPublish(BintrayConfigExtension bintrayConfig) { - project.logger.info("Configuring maven publish for $publishName") - project.apply([plugin: 'maven-publish']) - project.publishing.publications.create(publishName, MavenPublication) { - groupId project.group - artifactId publishName - - if (publishConfig.publishSources) { - project.logger.info("Publishing sources for $publishName") - artifact project.tasks.sourceJar - } - artifact project.tasks.javadocJar - - project.configurations.publish.artifacts.each { - project.logger.debug("Adding artifact: $it") - delegate.artifact it - } - - if (!publishConfig.disableDefaultJar && !publishConfig.publishWar) { - from project.components.java - } else if (publishConfig.publishWar) { - from project.components.web - } - - extendPomForMavenCentral(pom, bintrayConfig) - } - project.task("install", dependsOn: "publishToMavenLocal") - } - - // Maven central requires all of the below fields for this to be a valid POM - void extendPomForMavenCentral(MavenPom pom, BintrayConfigExtension config) { - pom.withXml { - asNode().children().last() + { - resolveStrategy = Closure.DELEGATE_FIRST - name publishName - description project.description - url config.projectUrl - scm { - url config.vcsUrl - } - - licenses { - license { - name config.license.name - url config.license.url - distribution config.license.url - } - } - - developers { - developer { - id config.developer.id - name config.developer.name - email config.developer.email - } - } - } - } - } - - void configureBintray(BintrayConfigExtension bintrayConfig) { - project.apply([plugin: 'com.jfrog.bintray']) - project.bintray { - user = bintrayConfig.user - key = bintrayConfig.key - publications = [ publishName ] - dryRun = bintrayConfig.dryRun ?: false - pkg { - repo = bintrayConfig.repo - name = publishName - userOrg = bintrayConfig.org - licenses = bintrayConfig.licenses - - version { - gpg { - sign = bintrayConfig.gpgSign ?: false - passphrase = bintrayConfig.gpgPassphrase - } - } - } - } - } - - void createTasks() { - if(project.hasProperty('classes')) { - project.task("sourceJar", type: Jar, dependsOn: project.classes) { - classifier = 'sources' - from project.sourceSets.main.allSource - } - } - - if(project.hasProperty('javadoc')) { - project.task("javadocJar", type: Jar, dependsOn: project.javadoc) { - classifier = 'javadoc' - from project.javadoc.destinationDir - } - } - } - - void createExtensions() { - if(project == project.rootProject) { - project.extensions.create("bintrayConfig", BintrayConfigExtension) - } - publishConfig = project.extensions.create("publish", ProjectPublishExtension) - publishConfig.setPublishTask(this) - } - - void createConfigurations() { - project.configurations.create("publish") - } -} diff --git a/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/bintray/BintrayConfigExtension.groovy b/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/bintray/BintrayConfigExtension.groovy deleted file mode 100644 index 1a1c4e49e5..0000000000 --- a/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/bintray/BintrayConfigExtension.groovy +++ /dev/null @@ -1,70 +0,0 @@ -package net.corda.plugins.bintray - -import org.gradle.util.ConfigureUtil - -class BintrayConfigExtension { - /** - * Bintray username - */ - String user - /** - * Bintray access key - */ - String key - /** - * Bintray repository - */ - String repo - /** - * Bintray organisation - */ - String org - /** - * Licenses for packages uploaded by this configuration - */ - String[] licenses - /** - * Whether to sign packages uploaded by this configuration - */ - Boolean gpgSign - /** - * The passphrase for the key used to sign releases. - */ - String gpgPassphrase - /** - * VCS URL - */ - String vcsUrl - /** - * Project URL - */ - String projectUrl - /** - * The publications that will be uploaded as a part of this configuration. These must match both the name on - * bintray and the gradle module name. ie; it must be "some-package" as a gradle sub-module (root project not - * supported, this extension is to improve multi-build bintray uploads). The publication must also be called - * "some-package". Only one publication can be uploaded per module (a bintray plugin restriction(. - * If any of these conditions are not met your package will not be uploaded. - */ - String[] publications - /** - * Whether to test the publication without uploading to bintray. - */ - Boolean dryRun - /** - * The license this project will use (currently limited to one) - */ - License license = new License() - /** - * The developer of this project (currently limited to one) - */ - Developer developer = new Developer() - - void license(Closure closure) { - ConfigureUtil.configure(closure, license) - } - - void developer(Closure closure) { - ConfigureUtil.configure(closure, developer) - } -} \ No newline at end of file diff --git a/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/bintray/Developer.groovy b/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/bintray/Developer.groovy deleted file mode 100644 index 1d66f68c7d..0000000000 --- a/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/bintray/Developer.groovy +++ /dev/null @@ -1,16 +0,0 @@ -package net.corda.plugins.bintray - -class Developer { - /** - * A unique identifier the developer (eg; organisation ID) - */ - String id - /** - * The full name of the developer - */ - String name - /** - * An email address for contacting the developer - */ - String email -} \ No newline at end of file diff --git a/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/bintray/License.groovy b/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/bintray/License.groovy deleted file mode 100644 index 1d06867bcf..0000000000 --- a/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/bintray/License.groovy +++ /dev/null @@ -1,16 +0,0 @@ -package net.corda.plugins.bintray - -class License { - /** - * The name of license (eg; Apache 2.0) - */ - String name - /** - * URL to the full license file - */ - String url - /** - * The distribution level this license corresponds to (eg: repo) - */ - String distribution -} \ No newline at end of file diff --git a/gradle-plugins/publish-utils/src/main/resources/META-INF/gradle-plugins/net.corda.plugins.publish-utils.properties b/gradle-plugins/publish-utils/src/main/resources/META-INF/gradle-plugins/net.corda.plugins.publish-utils.properties deleted file mode 100644 index b680f7d301..0000000000 --- a/gradle-plugins/publish-utils/src/main/resources/META-INF/gradle-plugins/net.corda.plugins.publish-utils.properties +++ /dev/null @@ -1 +0,0 @@ -implementation-class=net.corda.plugins.PublishTasks diff --git a/gradle-plugins/quasar-utils/README.rst b/gradle-plugins/quasar-utils/README.rst deleted file mode 100644 index 481d8ec66b..0000000000 --- a/gradle-plugins/quasar-utils/README.rst +++ /dev/null @@ -1,4 +0,0 @@ -Quasar Utils -============ - -Quasar utilities adds several tasks and configuration that provide a default Quasar setup and removes some boilerplate. \ No newline at end of file diff --git a/gradle-plugins/quasar-utils/build.gradle b/gradle-plugins/quasar-utils/build.gradle deleted file mode 100644 index 8f9eca30f2..0000000000 --- a/gradle-plugins/quasar-utils/build.gradle +++ /dev/null @@ -1,19 +0,0 @@ -apply plugin: 'groovy' -apply plugin: 'maven-publish' -apply plugin: 'net.corda.plugins.publish-utils' -apply plugin: 'com.jfrog.artifactory' - -description 'A small gradle plugin for adding some basic Quasar tasks and configurations to reduce build.gradle bloat.' - -repositories { - mavenCentral() -} - -dependencies { - compile gradleApi() - compile localGroovy() -} - -publish { - name project.name -} diff --git a/gradle-plugins/quasar-utils/src/main/groovy/net/corda/plugins/QuasarPlugin.groovy b/gradle-plugins/quasar-utils/src/main/groovy/net/corda/plugins/QuasarPlugin.groovy deleted file mode 100644 index fc309c2826..0000000000 --- a/gradle-plugins/quasar-utils/src/main/groovy/net/corda/plugins/QuasarPlugin.groovy +++ /dev/null @@ -1,28 +0,0 @@ -package net.corda.plugins - -import org.gradle.api.Project -import org.gradle.api.Plugin -import org.gradle.api.tasks.testing.Test -import org.gradle.api.tasks.JavaExec - -/** - * QuasarPlugin creates a "quasar" configuration and adds quasar as a dependency. - */ -class QuasarPlugin implements Plugin { - void apply(Project project) { - project.configurations.create("quasar") -// To add a local .jar dependency: -// project.dependencies.add("quasar", project.files("${project.rootProject.projectDir}/lib/quasar.jar")) - project.dependencies.add("quasar", "${project.rootProject.ext.quasar_group}:quasar-core:${project.rootProject.ext.quasar_version}:jdk8@jar") - project.dependencies.add("runtime", project.configurations.getByName("quasar")) - - project.tasks.withType(Test) { - jvmArgs "-javaagent:${project.configurations.quasar.singleFile}" - jvmArgs "-Dco.paralleluniverse.fibers.verifyInstrumentation" - } - project.tasks.withType(JavaExec) { - jvmArgs "-javaagent:${project.configurations.quasar.singleFile}" - jvmArgs "-Dco.paralleluniverse.fibers.verifyInstrumentation" - } - } -} diff --git a/gradle-plugins/quasar-utils/src/main/resources/META-INF/gradle-plugins/net.corda.plugins.quasar-utils.properties b/gradle-plugins/quasar-utils/src/main/resources/META-INF/gradle-plugins/net.corda.plugins.quasar-utils.properties deleted file mode 100644 index fb7042bb42..0000000000 --- a/gradle-plugins/quasar-utils/src/main/resources/META-INF/gradle-plugins/net.corda.plugins.quasar-utils.properties +++ /dev/null @@ -1 +0,0 @@ -implementation-class=net.corda.plugins.QuasarPlugin diff --git a/gradle-plugins/settings.gradle b/gradle-plugins/settings.gradle deleted file mode 100644 index 995cd8c899..0000000000 --- a/gradle-plugins/settings.gradle +++ /dev/null @@ -1,7 +0,0 @@ -rootProject.name = 'corda-gradle-plugins' -include 'publish-utils' -include 'quasar-utils' -include 'cordformation' -include 'cordform-common' -include 'api-scanner' -include 'cordapp' \ No newline at end of file From d25ab7d8970b168c33f2997a434132fcef96ebbd Mon Sep 17 00:00:00 2001 From: Clinton Alexander Date: Wed, 14 Feb 2018 17:58:14 +0000 Subject: [PATCH 26/50] Removed remaining gradle plugins --- .../net/corda/cordform/CordappDependency.kt | 10 --- .../main/kotlin/net/corda/plugins/Cordapp.kt | 26 ------- .../kotlin/net/corda/plugins/CordformTest.kt | 77 ------------------- .../DeploySingleNodeWithCordapp.gradle | 33 -------- .../DeploySingleNodeWithCordappConfig.gradle | 35 --------- ...odeWithLocallyBuildCordappAndConfig.gradle | 39 ---------- 6 files changed, 220 deletions(-) delete mode 100644 gradle-plugins/cordform-common/src/main/kotlin/net/corda/cordform/CordappDependency.kt delete mode 100644 gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordapp.kt delete mode 100644 gradle-plugins/cordformation/src/test/kotlin/net/corda/plugins/CordformTest.kt delete mode 100644 gradle-plugins/cordformation/src/test/resources/net/corda/plugins/DeploySingleNodeWithCordapp.gradle delete mode 100644 gradle-plugins/cordformation/src/test/resources/net/corda/plugins/DeploySingleNodeWithCordappConfig.gradle delete mode 100644 gradle-plugins/cordformation/src/test/resources/net/corda/plugins/DeploySingleNodeWithLocallyBuildCordappAndConfig.gradle diff --git a/gradle-plugins/cordform-common/src/main/kotlin/net/corda/cordform/CordappDependency.kt b/gradle-plugins/cordform-common/src/main/kotlin/net/corda/cordform/CordappDependency.kt deleted file mode 100644 index f677e2278c..0000000000 --- a/gradle-plugins/cordform-common/src/main/kotlin/net/corda/cordform/CordappDependency.kt +++ /dev/null @@ -1,10 +0,0 @@ -package net.corda.cordform - -data class CordappDependency( - val mavenCoordinates: String? = null, - val projectName: String? = null -) { - init { - require((mavenCoordinates != null) != (projectName != null), { "Only one of maven coordinates or project name must be set" }) - } -} \ No newline at end of file diff --git a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordapp.kt b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordapp.kt deleted file mode 100644 index a4cba09969..0000000000 --- a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordapp.kt +++ /dev/null @@ -1,26 +0,0 @@ -package net.corda.plugins - -import org.gradle.api.Project -import java.io.File - -open class Cordapp private constructor(val coordinates: String?, val project: Project?) { - constructor(coordinates: String) : this(coordinates, null) - constructor(cordappProject: Project) : this(null, cordappProject) - - // The configuration text that will be written - internal var config: String? = null - - /** - * Set the configuration text that will be written to the cordapp's configuration file - */ - fun config(config: String) { - this.config = config - } - - /** - * Reads config from the file and later writes it to the cordapp's configuration file - */ - fun config(configFile: File) { - this.config = configFile.readText() - } -} \ No newline at end of file diff --git a/gradle-plugins/cordformation/src/test/kotlin/net/corda/plugins/CordformTest.kt b/gradle-plugins/cordformation/src/test/kotlin/net/corda/plugins/CordformTest.kt deleted file mode 100644 index 3f934191be..0000000000 --- a/gradle-plugins/cordformation/src/test/kotlin/net/corda/plugins/CordformTest.kt +++ /dev/null @@ -1,77 +0,0 @@ -package net.corda.plugins - -import org.apache.commons.io.FileUtils -import org.apache.commons.io.IOUtils -import org.assertj.core.api.Assertions.* -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.rules.TemporaryFolder -import java.io.File -import java.io.IOException -import java.nio.charset.StandardCharsets -import java.nio.file.Files -import org.gradle.testkit.runner.GradleRunner -import org.gradle.testkit.runner.TaskOutcome - -class CordformTest { - @Rule - @JvmField - val testProjectDir = TemporaryFolder() - private var buildFile: File? = null - - private companion object { - val cordaFinanceJarName = "corda-finance-3.0-SNAPSHOT" - val localCordappJarName = "locally-built-cordapp" - val notaryNodeName = "Notary Service" - } - - @Before - fun setup() { - buildFile = testProjectDir.newFile("build.gradle") - } - - @Test - fun `a node with cordapp dependency`() { - val runner = getStandardGradleRunnerFor("DeploySingleNodeWithCordapp.gradle") - - val result = runner.build() - - assertThat(result.task(":deployNodes")!!.outcome).isEqualTo(TaskOutcome.SUCCESS) - assertThat(getNodeCordappJar(notaryNodeName, cordaFinanceJarName)).exists() - } - - @Test - fun `deploy a node with cordapp config`() { - val runner = getStandardGradleRunnerFor("DeploySingleNodeWithCordappConfig.gradle") - - val result = runner.build() - - assertThat(result.task(":deployNodes")!!.outcome).isEqualTo(TaskOutcome.SUCCESS) - assertThat(getNodeCordappJar(notaryNodeName, cordaFinanceJarName)).exists() - assertThat(getNodeCordappConfig(notaryNodeName, cordaFinanceJarName)).exists() - } - - @Test - fun `deploy the locally built cordapp with cordapp config`() { - val runner = getStandardGradleRunnerFor("DeploySingleNodeWithLocallyBuildCordappAndConfig.gradle") - - val result = runner.build() - - assertThat(result.task(":deployNodes")!!.outcome).isEqualTo(TaskOutcome.SUCCESS) - assertThat(getNodeCordappJar(notaryNodeName, localCordappJarName)).exists() - assertThat(getNodeCordappConfig(notaryNodeName, localCordappJarName)).exists() - } - - private fun getStandardGradleRunnerFor(buildFileResourceName: String): GradleRunner { - createBuildFile(buildFileResourceName) - return GradleRunner.create() - .withProjectDir(testProjectDir.root) - .withArguments("deployNodes", "-s") - .withPluginClasspath() - } - - private fun createBuildFile(buildFileResourceName: String) = IOUtils.copy(javaClass.getResourceAsStream(buildFileResourceName), buildFile!!.outputStream()) - private fun getNodeCordappJar(nodeName: String, cordappJarName: String) = File(testProjectDir.root, "build/nodes/$nodeName/cordapps/$cordappJarName.jar") - private fun getNodeCordappConfig(nodeName: String, cordappJarName: String) = File(testProjectDir.root, "build/nodes/$nodeName/cordapps/$cordappJarName.conf") -} \ No newline at end of file diff --git a/gradle-plugins/cordformation/src/test/resources/net/corda/plugins/DeploySingleNodeWithCordapp.gradle b/gradle-plugins/cordformation/src/test/resources/net/corda/plugins/DeploySingleNodeWithCordapp.gradle deleted file mode 100644 index 10eca84d66..0000000000 --- a/gradle-plugins/cordformation/src/test/resources/net/corda/plugins/DeploySingleNodeWithCordapp.gradle +++ /dev/null @@ -1,33 +0,0 @@ -buildscript { - ext { - corda_group = 'net.corda' - corda_release_version = '3.0-SNAPSHOT' // TODO: Set to 3.0.0 when Corda 3 is released - jolokia_version = '1.3.7' - } -} - -plugins { - id 'java' - id 'net.corda.plugins.cordformation' -} - -repositories { - mavenCentral() - maven { url 'https://ci-artifactory.corda.r3cev.com/artifactory/corda-dev' } -} - -dependencies { - runtime "$corda_group:corda:$corda_release_version" - runtime "$corda_group:corda-node-api:$corda_release_version" - cordapp "$corda_group:corda-finance:$corda_release_version" -} - -task deployNodes(type: net.corda.plugins.Cordform) { - node { - name 'O=Notary Service,L=Zurich,C=CH' - notary = [validating : true] - p2pPort 10002 - rpcPort 10003 - cordapps = ["$corda_group:corda-finance:$corda_release_version"] - } -} \ No newline at end of file diff --git a/gradle-plugins/cordformation/src/test/resources/net/corda/plugins/DeploySingleNodeWithCordappConfig.gradle b/gradle-plugins/cordformation/src/test/resources/net/corda/plugins/DeploySingleNodeWithCordappConfig.gradle deleted file mode 100644 index 4bb6642752..0000000000 --- a/gradle-plugins/cordformation/src/test/resources/net/corda/plugins/DeploySingleNodeWithCordappConfig.gradle +++ /dev/null @@ -1,35 +0,0 @@ -buildscript { - ext { - corda_group = 'net.corda' - corda_release_version = '3.0-SNAPSHOT' // TODO: Set to 3.0.0 when Corda 3 is released - jolokia_version = '1.3.7' - } -} - -plugins { - id 'java' - id 'net.corda.plugins.cordformation' -} - -repositories { - mavenCentral() - maven { url 'https://ci-artifactory.corda.r3cev.com/artifactory/corda-dev' } -} - -dependencies { - runtime "$corda_group:corda:$corda_release_version" - runtime "$corda_group:corda-node-api:$corda_release_version" - cordapp "$corda_group:corda-finance:$corda_release_version" -} - -task deployNodes(type: net.corda.plugins.Cordform) { - node { - name 'O=Notary Service,L=Zurich,C=CH' - notary = [validating : true] - p2pPort 10002 - rpcPort 10003 - cordapp "$corda_group:corda-finance:$corda_release_version", { - config "a=b" - } - } -} \ No newline at end of file diff --git a/gradle-plugins/cordformation/src/test/resources/net/corda/plugins/DeploySingleNodeWithLocallyBuildCordappAndConfig.gradle b/gradle-plugins/cordformation/src/test/resources/net/corda/plugins/DeploySingleNodeWithLocallyBuildCordappAndConfig.gradle deleted file mode 100644 index 6e0447764f..0000000000 --- a/gradle-plugins/cordformation/src/test/resources/net/corda/plugins/DeploySingleNodeWithLocallyBuildCordappAndConfig.gradle +++ /dev/null @@ -1,39 +0,0 @@ -buildscript { - ext { - corda_group = 'net.corda' - corda_release_version = '3.0-SNAPSHOT' // TODO: Set to 3.0.0 when Corda 3 is released - jolokia_version = '1.3.7' - } -} - -plugins { - id 'java' - id 'net.corda.plugins.cordformation' -} - -repositories { - mavenCentral() - maven { url 'https://ci-artifactory.corda.r3cev.com/artifactory/corda-dev' } -} - -dependencies { - runtime "$corda_group:corda:$corda_release_version" - runtime "$corda_group:corda-node-api:$corda_release_version" - cordapp "$corda_group:corda-finance:$corda_release_version" -} - -jar { - baseName 'locally-built-cordapp' -} - -task deployNodes(type: net.corda.plugins.Cordform, dependsOn: [jar]) { - node { - name 'O=Notary Service,L=Zurich,C=CH' - notary = [validating : true] - p2pPort 10002 - rpcPort 10003 - cordapp { - config "a=b" - } - } -} \ No newline at end of file From fa34a16f27c6347a81aed3a61d74ee94c309eefd Mon Sep 17 00:00:00 2001 From: Maksymilian Pawlak <120831+m4ksio@users.noreply.github.com> Date: Thu, 15 Feb 2018 12:05:10 +0000 Subject: [PATCH 27/50] IRS Docker nomenclature fix (#2531) * We check properties, so why I named them variables? --- .../src/system-test/kotlin/IRSDemoDockerTest.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/samples/irs-demo/src/system-test/kotlin/IRSDemoDockerTest.kt b/samples/irs-demo/src/system-test/kotlin/IRSDemoDockerTest.kt index 034d4ff2d4..b40e829c9f 100644 --- a/samples/irs-demo/src/system-test/kotlin/IRSDemoDockerTest.kt +++ b/samples/irs-demo/src/system-test/kotlin/IRSDemoDockerTest.kt @@ -19,16 +19,16 @@ import kotlin.test.assertTrue class IRSDemoDockerTest { companion object { - private fun ensureSystemVariable(variable: String) { - if (System.getProperty(variable) == null) { - throw IllegalStateException("System variable $variable not set. Please refer to README file for proper setup instructions.") + private fun ensureProperty(property: String) { + if (System.getProperty(property) == null) { + throw IllegalStateException("System property $property not set. Please refer to README file for proper setup instructions.") } } init { - ensureSystemVariable("CORDAPP_DOCKER_COMPOSE") - ensureSystemVariable("WEB_DOCKER_COMPOSE") - ensureSystemVariable("phantomjs.binary.path") + ensureProperty("CORDAPP_DOCKER_COMPOSE") + ensureProperty("WEB_DOCKER_COMPOSE") + ensureProperty("phantomjs.binary.path") } @ClassRule From ae63de34ad34edcff363abd7f4c08cc9de031d29 Mon Sep 17 00:00:00 2001 From: Maksymilian Pawlak <120831+m4ksio@users.noreply.github.com> Date: Thu, 15 Feb 2018 12:22:31 +0000 Subject: [PATCH 28/50] Run H2 fix test in parallel (#2504) * Parallel execution for reduced test time --- .../corda/node/internal/AbstractNodeTests.kt | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/node/src/test/kotlin/net/corda/node/internal/AbstractNodeTests.kt b/node/src/test/kotlin/net/corda/node/internal/AbstractNodeTests.kt index 5dc2f28844..c7494b6f2d 100644 --- a/node/src/test/kotlin/net/corda/node/internal/AbstractNodeTests.kt +++ b/node/src/test/kotlin/net/corda/node/internal/AbstractNodeTests.kt @@ -42,12 +42,22 @@ class AbstractNodeTests { @Test fun `H2 fix is applied`() { - repeat(if (relaxedThoroughness) 1 else 100) { - // Two "nodes" seems to be the magic number to reproduce the problem: - val urls = (0 until 2).map { freshURL() } + val pool = Executors.newFixedThreadPool(5) + val runs = if (relaxedThoroughness) 1 else 100 + (0 until runs).map { + // Four "nodes" seems to be the magic number to reproduce the problem on CI: + val urls = (0 until 4).map { freshURL() } // Haven't been able to reproduce in a warm JVM: - assertEquals(0, startJavaProcess(urls).waitFor()) - } + pool.fork { + assertEquals(0, startJavaProcess(urls).waitFor()) + } + }.transpose().getOrThrow() + pool.shutdown() + // The above will always run all processes, even if the very first fails + // In theory this can be handled better, but + // a) we expect to run all the runs, that's how the test passes + // b) it would require relatively complex handling (futures+threads), which is not worth it + // because of a) } } From 308d70c0d1c43125c4ff302d4a210306c6540a31 Mon Sep 17 00:00:00 2001 From: Anthony Keenan Date: Thu, 15 Feb 2018 13:21:20 +0000 Subject: [PATCH 29/50] CORDA-939 One last internal exposure (#2537) * One last internal exposure * Fix api-current.txt * Reformat class --- .ci/api-current.txt | 7 ++++--- .../corda/testing/services/MockAttachmentStorage.kt | 10 ++++------ 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/.ci/api-current.txt b/.ci/api-current.txt index b4c94505e9..18a44b7a84 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -1369,6 +1369,10 @@ public final class net.corda.core.flows.NotaryFlow extends java.lang.Object public (net.corda.core.transactions.SignedTransaction, net.corda.core.utilities.ProgressTracker) @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public List call() @org.jetbrains.annotations.NotNull public net.corda.core.utilities.ProgressTracker getProgressTracker() + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull protected final net.corda.core.utilities.UntrustworthyData notarise(net.corda.core.identity.Party) + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull protected net.corda.core.utilities.UntrustworthyData sendAndReceiveNonValidating(net.corda.core.identity.Party, net.corda.core.flows.FlowSession) + @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull protected net.corda.core.utilities.UntrustworthyData sendAndReceiveValidating(net.corda.core.flows.FlowSession) + @org.jetbrains.annotations.NotNull protected final List validateResponse(net.corda.core.utilities.UntrustworthyData, net.corda.core.identity.Party) public static final net.corda.core.flows.NotaryFlow$Client$Companion Companion ## public static final class net.corda.core.flows.NotaryFlow$Client$Companion extends java.lang.Object @@ -4652,6 +4656,3 @@ public final class net.corda.testing.services.MockAttachmentStorage extends net. public static final class net.corda.testing.services.MockAttachmentStorage$Companion extends java.lang.Object public final byte[] getBytes(java.io.InputStream) ## -public static final class net.corda.testing.services.MockAttachmentStorage$openAttachment$1 extends net.corda.core.internal.AbstractAttachment - @org.jetbrains.annotations.NotNull public net.corda.core.crypto.SecureHash getId() -## diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockAttachmentStorage.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockAttachmentStorage.kt index 8cf1a9ccf2..f827efb39d 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockAttachmentStorage.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/services/MockAttachmentStorage.kt @@ -43,11 +43,11 @@ class MockAttachmentStorage : AttachmentStorage, SingletonSerializeAsToken() { val files = HashMap() + private class MockAttachment(dataLoader: () -> ByteArray, override val id: SecureHash) : AbstractAttachment(dataLoader) + override fun openAttachment(id: SecureHash): Attachment? { val f = files[id] ?: return null - return object : AbstractAttachment({ f }) { - override val id = id - } + return MockAttachment({ f }, id) } override fun queryAttachments(criteria: AttachmentQueryCriteria, sorting: AttachmentSort?): List { @@ -64,10 +64,8 @@ class MockAttachmentStorage : AttachmentStorage, SingletonSerializeAsToken() { override fun importOrGetAttachment(jar: InputStream): AttachmentId { try { return importAttachment(jar) - } - catch (faee: java.nio.file.FileAlreadyExistsException) { + } catch (faee: java.nio.file.FileAlreadyExistsException) { return AttachmentId.parse(faee.message!!) } } - } From 2864ce1384196bf995eecabacfc1da4f469bd295 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Thu, 15 Feb 2018 14:06:41 +0000 Subject: [PATCH 30/50] ENT-1519: Ensure NodeInfo always has at least one address by checking in the c'tor (#2538) Further, the look up of the node's own node-info from its database has been tightened to ensure there isn't more than one. Also fixed some brittle code which was assuming exactly one address rather than at least one. --- .../kotlin/net/corda/core/node/NodeInfo.kt | 4 +- .../core/node/services/NetworkMapCache.kt | 23 ++++++----- docs/source/changelog.rst | 19 ++++++--- .../internal/bridging/AMQPBridgeManager.kt | 5 +-- .../network/PersistentNetworkMapCacheTest.kt | 41 +++++++++++++++++++ .../net/corda/node/internal/AbstractNode.kt | 40 +++++++++--------- .../kotlin/net/corda/node/internal/Node.kt | 2 +- .../network/PersistentNetworkMapCache.kt | 15 +++++-- .../net/corda/testing/node/MockServices.kt | 3 +- .../node/internal/InternalMockNetwork.kt | 4 +- 10 files changed, 112 insertions(+), 44 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt b/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt index 1977b601d9..d24fa67e7a 100644 --- a/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt +++ b/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt @@ -26,7 +26,9 @@ data class NodeInfo(val addresses: List, ) { // TODO We currently don't support multi-IP/multi-identity nodes, we only left slots in the data structures. init { - require(legalIdentitiesAndCerts.isNotEmpty()) { "Node should have at least one legal identity" } + require(addresses.isNotEmpty()) { "Node must have at least one address" } + require(legalIdentitiesAndCerts.isNotEmpty()) { "Node must have at least one legal identity" } + require(platformVersion > 0) { "Platform version must be at least 1" } } @Transient private var _legalIdentities: List? = null diff --git a/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt b/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt index 69ab1a7307..395e5cbccb 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt @@ -67,12 +67,22 @@ interface NetworkMapCacheBase { fun track(): DataFeed, NetworkMapCache.MapChange> /** - * Look up the node info for a legal name. - * Notice that when there are more than one node for a given name (in case of distributed services) first service node - * found will be returned. + * Return a [NodeInfo] which has the given legal name for one of its identities, or null if no such node is found. + * + * @throws IllegalArgumentException If more than one matching node is found, in the case of a distributed service identity + * (such as with a notary cluster). For such a scenerio use [getNodesByLegalName] instead. */ fun getNodeByLegalName(name: CordaX500Name): NodeInfo? + /** + * Return a list of [NodeInfo]s which have the given legal name for one of their identities, or an empty list if no + * such nodes are found. + * + * Normally there is at most one node for a legal name, but for distributed service identities (such as with a notary + * cluster) there can be multiple nodes sharing the same identity. + */ + fun getNodesByLegalName(name: CordaX500Name): List + /** Look up the node info for a host and port. */ fun getNodeByAddress(address: NetworkHostAndPort): NodeInfo? @@ -100,13 +110,6 @@ interface NetworkMapCacheBase { */ fun getNodesByLegalIdentityKey(identityKey: PublicKey): List - /** - * Look up the node information entries for a legal name. - * Note that normally there will be only one node for a legal name, but for clusters of nodes or distributed services there - * can be multiple nodes. - */ - fun getNodesByLegalName(name: CordaX500Name): List - /** Returns information about the party, which may be a specific node or a service */ fun getPartyInfo(party: Party): PartyInfo? diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 8ddbc09382..3f6079c63e 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -7,12 +7,12 @@ from the previous milestone release. UNRELEASED ---------- -* Per CorDapp configuration is now exposed. ``CordappContext`` now exposes a ``CordappConfig`` object that is populated -at CorDapp context creation time from a file source during runtime. +* Added ``NetworkMapCache.getNodesByLegalName`` for querying nodes belonging to a distributed service such as a notary cluster + where they all share a common identity. ``NetworkMapCache.getNodeByLegalName`` has been tightened to throw if more than + one node with the legal name is found. -* Provided experimental support for specifying your own webserver to be used instead of the default development webserver in ``Cordform`` using the ``webserverJar`` argument - -* The test utils in ``Expect.kt``, ``SerializationTestHelpers.kt``, ``TestConstants.kt`` and ``TestUtils.kt`` have moved from the ``net.corda.testing`` package to the ``net.corda.testing.core`` package, and ``FlowStackSnapshot.kt`` has moved to the ``net.corda.testing.services`` package. Moving items out of the ``net.corda.testing.*`` package will help make it clearer which parts of the api are stable. The bash script ``tools\scripts\update-test-packages.sh`` can be used to smooth the upgrade process for existing projects. +* Per CorDapp configuration is now exposed. ``CordappContext`` now exposes a ``CordappConfig`` object that is populated + at CorDapp context creation time from a file source during runtime. * Introduced Flow Draining mode, in which a node continues executing existing flows, but does not start new. This is to support graceful node shutdown/restarts. In particular, when this mode is on, new flows through RPC will be rejected, scheduled flows will be ignored, and initial session messages will not be consumed. @@ -195,9 +195,18 @@ at CorDapp context creation time from a file source during runtime. * Marked ``stateMachine`` on ``FlowLogic`` as ``CordaInternal`` to make clear that is it not part of the public api and is only for internal use +* Provided experimental support for specifying your own webserver to be used instead of the default development + webserver in ``Cordform`` using the ``webserverJar`` argument + * Created new ``StartedMockNode`` and ``UnstartedMockNode`` classes which are wrappers around our MockNode implementation that expose relevant methods for testing without exposing internals, create these using a ``MockNetwork``. +* The test utils in ``Expect.kt``, ``SerializationTestHelpers.kt``, ``TestConstants.kt`` and ``TestUtils.kt`` have moved + from the ``net.corda.testing`` package to the ``net.corda.testing.core`` package, and ``FlowStackSnapshot.kt`` has moved to the + ``net.corda.testing.services`` package. Moving items out of the ``net.corda.testing.*`` package will help make it clearer which + parts of the api are stable. The bash script ``tools\scripts\update-test-packages.sh`` can be used to smooth the upgrade + process for existing projects. + .. _changelog_v1: Release 1.0 diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/AMQPBridgeManager.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/AMQPBridgeManager.kt index 523369b370..07e4b5e0f5 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/AMQPBridgeManager.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/AMQPBridgeManager.kt @@ -155,9 +155,8 @@ class AMQPBridgeManager(val config: NodeSSLConfiguration, val p2pAddress: Networ } } - private fun gatherAddresses(node: NodeInfo): Sequence { - val address = node.addresses.single() - return node.legalIdentitiesAndCerts.map { ArtemisMessagingComponent.NodeAddress(it.party.owningKey, address) }.asSequence() + private fun gatherAddresses(node: NodeInfo): List { + return node.legalIdentitiesAndCerts.map { ArtemisMessagingComponent.NodeAddress(it.party.owningKey, node.addresses[0]) } } override fun deployBridge(queueName: String, target: NetworkHostAndPort, legalNames: Set) { diff --git a/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt index 607852e7a8..8f7790b0e2 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/network/PersistentNetworkMapCacheTest.kt @@ -9,6 +9,8 @@ import net.corda.node.internal.Node import net.corda.node.internal.StartedNode import net.corda.testing.core.* import net.corda.testing.node.internal.NodeBasedTest +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.junit.Before import org.junit.Test import kotlin.test.assertEquals @@ -35,6 +37,45 @@ class PersistentNetworkMapCacheTest : NodeBasedTest() { } } + @Test + fun `unknown legal name`() { + val alice = startNodesWithPort(listOf(ALICE))[0] + val netMapCache = alice.services.networkMapCache + alice.database.transaction { + assertThat(netMapCache.getNodesByLegalName(DUMMY_NOTARY_NAME)).isEmpty() + assertThat(netMapCache.getNodeByLegalName(DUMMY_NOTARY_NAME)).isNull() + assertThat(netMapCache.getPeerByLegalName(DUMMY_NOTARY_NAME)).isNull() + assertThat(netMapCache.getPeerCertificateByLegalName(DUMMY_NOTARY_NAME)).isNull() + } + } + + @Test + fun `nodes in distributed service`() { + val alice = startNodesWithPort(listOf(ALICE))[0] + val netMapCache = alice.services.networkMapCache + + val distServiceNodeInfos = alice.database.transaction { + val distributedIdentity = TestIdentity(DUMMY_NOTARY_NAME).identity + (1..2).map { + val nodeInfo = NodeInfo( + addresses = listOf(NetworkHostAndPort("localhost", 1000 + it)), + legalIdentitiesAndCerts = listOf(TestIdentity.fresh("Org-$it").identity, distributedIdentity), + platformVersion = 3, + serial = 1 + ) + netMapCache.addNode(nodeInfo) + nodeInfo + } + } + + alice.database.transaction { + assertThat(netMapCache.getNodesByLegalName(DUMMY_NOTARY_NAME)).containsOnlyElementsOf(distServiceNodeInfos) + assertThatExceptionOfType(IllegalArgumentException::class.java) + .isThrownBy { netMapCache.getNodeByLegalName(DUMMY_NOTARY_NAME) } + .withMessageContaining(DUMMY_NOTARY_NAME.toString()) + } + } + @Test fun `get nodes by owning key and by name`() { val alice = startNodesWithPort(listOf(ALICE))[0] diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 92d58d863d..d97db49099 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -178,13 +178,13 @@ abstract class AbstractNode(val configuration: NodeConfiguration, // a code smell. val persistentNetworkMapCache = PersistentNetworkMapCache(database, notaries = emptyList()) persistentNetworkMapCache.start() - val (keyPairs, info) = initNodeInfo(persistentNetworkMapCache, identity, identityKeyPair) - val signedNodeInfo = info.sign { publicKey, serialised -> + val (keyPairs, nodeInfo) = initNodeInfo(persistentNetworkMapCache, identity, identityKeyPair) + val signedNodeInfo = nodeInfo.sign { publicKey, serialised -> val privateKey = keyPairs.single { it.public == publicKey }.private privateKey.sign(serialised.bytes) } NodeInfoWatcher.saveToFile(configuration.baseDirectory, signedNodeInfo) - info + nodeInfo } } @@ -203,11 +203,11 @@ abstract class AbstractNode(val configuration: NodeConfiguration, // Do all of this in a database transaction so anything that might need a connection has one. val (startedImpl, schedulerService) = initialiseDatabasePersistence(schemaService, identityService) { database -> val networkMapCache = NetworkMapCacheImpl(PersistentNetworkMapCache(database, GlobalProperties.networkParameters.notaries).start(), identityService) - val (keyPairs, info) = initNodeInfo(networkMapCache, identity, identityKeyPair) - identityService.loadIdentities(info.legalIdentitiesAndCerts) + val (keyPairs, nodeInfo) = initNodeInfo(networkMapCache, identity, identityKeyPair) + identityService.loadIdentities(nodeInfo.legalIdentitiesAndCerts) val transactionStorage = makeTransactionStorage(database, configuration.transactionCacheSizeBytes) val nodeProperties = NodePropertiesPersistentStore(StubbedNodeUniqueIdProvider::value, database) - val nodeServices = makeServices(keyPairs, schemaService, transactionStorage, database, info, identityService, networkMapCache, nodeProperties) + val nodeServices = makeServices(keyPairs, schemaService, transactionStorage, database, nodeInfo, identityService, networkMapCache, nodeProperties) val notaryService = makeNotaryService(nodeServices, database) val smm = makeStateMachineManager(database) val flowLogicRefFactory = FlowLogicRefFactoryImpl(cordappLoader.appClassLoader) @@ -239,7 +239,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, registerCordappFlows(smm) _services.rpcFlows += cordappLoader.cordapps.flatMap { it.rpcFlows } startShell(rpcOps) - Pair(StartedNodeImpl(this, _services, info, checkpointStorage, smm, attachments, network, database, rpcOps, flowStarter, notaryService), schedulerService) + Pair(StartedNodeImpl(this, _services, nodeInfo, checkpointStorage, smm, attachments, network, database, rpcOps, flowStarter, notaryService), schedulerService) } networkMapUpdater = NetworkMapUpdater(services.networkMapCache, NodeInfoWatcher(configuration.baseDirectory, getRxIoScheduler(), Duration.ofMillis(configuration.additionalNodeInfoPollingFrequencyMsec)), @@ -297,20 +297,22 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } } - var info = NodeInfo( + val nodeInfoWithBlankSerial = NodeInfo( myAddresses(), setOf(identity, myNotaryIdentity).filterNotNull(), versionInfo.platformVersion, - platformClock.instant().toEpochMilli() + serial = 0 ) - // Check if we have already stored a version of 'our own' NodeInfo, this is to avoid regenerating it with - // a different timestamp. - networkMapCache.getNodesByLegalName(configuration.myLegalName).firstOrNull()?.let { - if (info.copy(serial = it.serial) == it) { - info = it - } + + val nodeInfoFromDb = networkMapCache.getNodeByLegalName(identity.name) + + val nodeInfo = if (nodeInfoWithBlankSerial == nodeInfoFromDb?.copy(serial = 0)) { + // The node info hasn't changed. We use the one from the database to preserve the serial. + nodeInfoFromDb + } else { + nodeInfoWithBlankSerial.copy(serial = platformClock.millis()) } - return Pair(keyPairs, info) + return Pair(keyPairs, nodeInfo) } protected abstract fun myAddresses(): List @@ -534,7 +536,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, schemaService: SchemaService, transactionStorage: WritableTransactionStorage, database: CordaPersistence, - info: NodeInfo, + nodeInfo: NodeInfo, identityService: IdentityServiceInternal, networkMapCache: NetworkMapCacheInternal, nodeProperties: NodePropertiesStore): MutableList { @@ -551,10 +553,10 @@ abstract class AbstractNode(val configuration: NodeConfiguration, MonitoringService(metrics), cordappProvider, database, - info, + nodeInfo, networkMapCache, nodeProperties) - network = makeMessagingService(database, info, nodeProperties) + network = makeMessagingService(database, nodeInfo, nodeProperties) val tokenizableServices = mutableListOf(attachments, network, services.vaultService, services.keyManagementService, services.identityService, platformClock, services.auditService, services.monitoringService, services.networkMapCache, services.schemaService, diff --git a/node/src/main/kotlin/net/corda/node/internal/Node.kt b/node/src/main/kotlin/net/corda/node/internal/Node.kt index 8762a1bf0e..d191af1403 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -157,7 +157,7 @@ open class Node(configuration: NodeConfiguration, val serverAddress = configuration.messagingServerAddress ?: makeLocalMessageBroker() val rpcServerAddresses = if (configuration.rpcOptions.standAloneBroker) BrokerAddresses(configuration.rpcOptions.address!!, configuration.rpcOptions.adminAddress) else startLocalRpcBroker() - val advertisedAddress = info.addresses.single() + val advertisedAddress = info.addresses[0] bridgeControlListener = BridgeControlListener(configuration, serverAddress, networkParameters.maxMessageSize) printBasicNodeInfo("Incoming connection address", advertisedAddress.toString()) diff --git a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt index 4c646b8f46..19fb9fd875 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapCache.kt @@ -7,12 +7,11 @@ import net.corda.core.identity.AbstractParty import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate -import net.corda.core.node.NotaryInfo import net.corda.core.internal.bufferUntilSubscribed import net.corda.core.internal.concurrent.openFuture -import net.corda.node.internal.schemas.NodeInfoSchemaV1 import net.corda.core.messaging.DataFeed import net.corda.core.node.NodeInfo +import net.corda.core.node.NotaryInfo import net.corda.core.node.services.IdentityService import net.corda.core.node.services.NetworkMapCache.MapChange import net.corda.core.node.services.PartyInfo @@ -21,6 +20,7 @@ import net.corda.core.serialization.serialize import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.contextLogger import net.corda.core.utilities.loggerFor +import net.corda.node.internal.schemas.NodeInfoSchemaV1 import net.corda.node.services.api.NetworkMapCacheBaseInternal import net.corda.node.services.api.NetworkMapCacheInternal import net.corda.node.utilities.NonInvalidatingCache @@ -153,8 +153,17 @@ open class PersistentNetworkMapCache( return null } - override fun getNodeByLegalName(name: CordaX500Name): NodeInfo? = getNodesByLegalName(name).firstOrNull() + override fun getNodeByLegalName(name: CordaX500Name): NodeInfo? { + val nodeInfos = getNodesByLegalName(name) + return when (nodeInfos.size) { + 0 -> null + 1 -> nodeInfos[0] + else -> throw IllegalArgumentException("More than one node found with legal name $name") + } + } + override fun getNodesByLegalName(name: CordaX500Name): List = database.transaction { queryByLegalName(session, name) } + override fun getNodesByLegalIdentityKey(identityKey: PublicKey): List = nodesByKeyCache[identityKey] private val nodesByKeyCache = NonInvalidatingCache>(1024, 8, { key -> database.transaction { queryByIdentityKey(session, key) } }) diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt index feeef6ab8b..f993b38298 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -16,6 +16,7 @@ import net.corda.core.node.services.* import net.corda.core.serialization.SerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.VersionInfo import net.corda.node.internal.configureDatabase import net.corda.node.internal.cordapp.CordappLoader @@ -206,7 +207,7 @@ open class MockServices private constructor( override val clock: Clock get() = Clock.systemUTC() override val myInfo: NodeInfo get() { - return NodeInfo(emptyList(), listOf(initialIdentity.identity), 1, serial = 1L) + return NodeInfo(listOf(NetworkHostAndPort("mock.node.services", 10000)), listOf(initialIdentity.identity), 1, serial = 1L) } override val transactionVerifierService: TransactionVerifierService get() = InMemoryTransactionVerifierService(2) private val mockCordappProvider: MockCordappProvider = MockCordappProvider(cordappLoader, attachments) diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt index 05646d3f20..e19d571f1d 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt @@ -311,7 +311,9 @@ open class InternalMockNetwork(private val cordappPackages: List, override fun makeTransactionVerifierService() = InMemoryTransactionVerifierService(1) - override fun myAddresses(): List = emptyList() + // NodeInfo requires a non-empty addresses list and so we give it a dummy value for mock nodes. + // The non-empty addresses check is important to have and so we tolerate the ugliness here. + override fun myAddresses(): List = listOf(NetworkHostAndPort("mock.node", 1000)) // Allow unit tests to modify the serialization whitelist list before the node start, // so they don't have to ServiceLoad test whitelists into all unit tests. From ed0cf91946430906880bf2587372025ba86a5bf6 Mon Sep 17 00:00:00 2001 From: igor nitto Date: Thu, 15 Feb 2018 17:10:07 +0000 Subject: [PATCH 31/50] Start Jolokia agents if configured without modifying JVM options [CORDA-1042] (#2541) * Jolokia agents are loaded dynamically if configured * Renamed exportJmxTo (never used) to jmxMonitoringHttpPort and take it from config * Updated documentation and tests --- .idea/compiler.xml | 3 +- build.gradle | 4 +- docs/source/corda-configuration-file.rst | 4 +- docs/source/node-administration.rst | 2 +- node/build.gradle | 4 +- .../net/corda/node/amqp/AMQPBridgeTest.kt | 2 +- .../net/corda/node/amqp/ProtonWrapperTests.kt | 2 +- .../net/corda/node/internal/AbstractNode.kt | 20 ++++++++ .../kotlin/net/corda/node/internal/Node.kt | 4 +- .../node/services/config/NodeConfiguration.kt | 4 +- .../messaging/ArtemisMessagingServer.kt | 2 +- .../corda/node/utilities/JVMAgentRegistry.kt | 50 +++++++++++++++++++ .../node/utilities/NodeBuildProperties.kt | 27 ++++++++++ node/src/main/resources/build.properties | 5 ++ .../messaging/ArtemisMessagingTest.kt | 2 +- .../net/corda/testing/driver/DriverTests.kt | 5 +- .../node/internal/InternalMockNetwork.kt | 2 +- 17 files changed, 125 insertions(+), 17 deletions(-) create mode 100644 node/src/main/kotlin/net/corda/node/utilities/JVMAgentRegistry.kt create mode 100644 node/src/main/kotlin/net/corda/node/utilities/NodeBuildProperties.kt create mode 100644 node/src/main/resources/build.properties diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 64293946a1..afbe5b7416 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -39,6 +39,7 @@ + @@ -165,4 +166,4 @@ - + \ No newline at end of file diff --git a/build.gradle b/build.gradle index f022e0e095..46e3956b41 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,7 @@ buildscript { // For sharing constants between builds Properties constants = new Properties() file("$projectDir/constants.properties").withInputStream { constants.load(it) } + file("${project(':node').projectDir}/src/main/resources/build.properties").withInputStream { constants.load(it) } // Our version: bump this on release. ext.corda_release_version = "3.0-SNAPSHOT" @@ -40,7 +41,7 @@ buildscript { ext.jackson_version = '2.9.3' ext.jetty_version = '9.4.7.v20170914' ext.jersey_version = '2.25' - ext.jolokia_version = '1.3.7' + ext.jolokia_version = constants.getProperty("jolokiaAgentVersion") ext.assertj_version = '3.8.0' ext.slf4j_version = '1.7.25' ext.log4j_version = '2.9.1' @@ -71,6 +72,7 @@ buildscript { ext.docker_compose_rule_version = '0.33.0' ext.selenium_version = '3.8.1' ext.ghostdriver_version = '2.1.0' + ext.eaagentloader_version = '1.0.3' // Update 121 is required for ObjectInputFilter and at time of writing 131 was latest: ext.java8_minUpdateVersion = '131' diff --git a/docs/source/corda-configuration-file.rst b/docs/source/corda-configuration-file.rst index 9dd048a719..3c41f75d5b 100644 --- a/docs/source/corda-configuration-file.rst +++ b/docs/source/corda-configuration-file.rst @@ -185,8 +185,8 @@ path to the node's base directory. :port: The port to start SSH server on -:exportJMXTo: If set to ``http``, will enable JMX metrics reporting via the Jolokia HTTP/JSON agent. - Default Jolokia access url is http://127.0.0.1:7005/jolokia/ +:jmxMonitoringHttpPort: If set, will enable JMX metrics reporting via the Jolokia HTTP/JSON agent on the corresponding port. + Default Jolokia access url is http://127.0.0.1:port/jolokia/ :transactionCacheSizeMegaBytes: Optionally specify how much memory should be used for caching of ledger transactions in memory. Otherwise defaults to 8MB plus 5% of all heap memory above 300MB. diff --git a/docs/source/node-administration.rst b/docs/source/node-administration.rst index 2fadfccef0..4132ac9385 100644 --- a/docs/source/node-administration.rst +++ b/docs/source/node-administration.rst @@ -106,7 +106,7 @@ Here are a few ways to build dashboards and extract monitoring data for a node: It can bridge any data input to any output using their plugin system, for example, Telegraf can be configured to collect data from Jolokia and write to DataDog web api. -The Node configuration parameter `exportJMXTo` should be set to ``http`` to ensure a Jolokia agent is instrumented with +The Node configuration parameter `jmxMonitoringHttpPort` has to be present in order to ensure a Jolokia agent is instrumented with the JVM run-time. The following JMX statistics are exported: diff --git a/node/build.gradle b/node/build.gradle index 2aeb7878d3..50130933af 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -172,6 +172,9 @@ dependencies { // Jsh: Testing SSH server integrationTestCompile group: 'com.jcraft', name: 'jsch', version: '0.1.54' + // AgentLoader: dynamic loading of JVM agents + compile group: 'com.ea.agentloader', name: 'ea-agent-loader', version: "${eaagentloader_version}" + // Jetty dependencies for NetworkMapClient test. // Web stuff: for HTTP[S] servlets testCompile "org.eclipse.jetty:jetty-servlet:${jetty_version}" @@ -183,7 +186,6 @@ dependencies { testCompile "org.glassfish.jersey.containers:jersey-container-servlet-core:${jersey_version}" testCompile "org.glassfish.jersey.containers:jersey-container-jetty-http:${jersey_version}" - // Jolokia JVM monitoring agent runtime "org.jolokia:jolokia-jvm:${jolokia_version}:agent" } diff --git a/node/src/integration-test/kotlin/net/corda/node/amqp/AMQPBridgeTest.kt b/node/src/integration-test/kotlin/net/corda/node/amqp/AMQPBridgeTest.kt index f13e73de06..39c0a8880f 100644 --- a/node/src/integration-test/kotlin/net/corda/node/amqp/AMQPBridgeTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/amqp/AMQPBridgeTest.kt @@ -138,7 +138,7 @@ class AMQPBridgeTest { doReturn("trustpass").whenever(it).trustStorePassword doReturn("cordacadevpass").whenever(it).keyStorePassword doReturn(artemisAddress).whenever(it).p2pAddress - doReturn("").whenever(it).exportJMXto + doReturn(null).whenever(it).jmxMonitoringHttpPort doReturn(emptyList()).whenever(it).certificateChainCheckPolicies } artemisConfig.configureWithDevSSLCertificate() diff --git a/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt b/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt index 64d7c09990..8262f0b30b 100644 --- a/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/amqp/ProtonWrapperTests.kt @@ -222,7 +222,7 @@ class ProtonWrapperTests { doReturn("trustpass").whenever(it).trustStorePassword doReturn("cordacadevpass").whenever(it).keyStorePassword doReturn(NetworkHostAndPort("0.0.0.0", artemisPort)).whenever(it).p2pAddress - doReturn("").whenever(it).exportJMXto + doReturn(null).whenever(it).jmxMonitoringHttpPort doReturn(emptyList()).whenever(it).certificateChainCheckPolicies } artemisConfig.configureWithDevSSLCertificate() diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index d97db49099..af380101f6 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -59,6 +59,8 @@ import net.corda.node.services.vault.NodeVaultService import net.corda.node.services.vault.VaultSoftLockManager import net.corda.node.shell.InteractiveShell import net.corda.node.utilities.AffinityExecutor +import net.corda.node.utilities.NodeBuildProperties +import net.corda.node.utilities.JVMAgentRegistry import net.corda.nodeapi.internal.DevIdentityGenerator import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.persistence.CordaPersistence @@ -73,6 +75,7 @@ import rx.Observable import rx.Scheduler import java.io.IOException import java.lang.reflect.InvocationTargetException +import java.nio.file.Paths import java.security.KeyPair import java.security.KeyStoreException import java.security.PublicKey @@ -192,6 +195,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, check(started == null) { "Node has already been started" } log.info("Node starting up ...") initCertificate() + initialiseJVMAgents() val schemaService = NodeSchemaService(cordappLoader.cordappSchemas, configuration.notary != null) val (identity, identityKeyPair) = obtainIdentity(notaryConfig = null) val identityService = makeIdentityService(identity.certificate) @@ -749,6 +753,22 @@ abstract class AbstractNode(val configuration: NodeConfiguration, return NodeVaultService(platformClock, keyManagementService, stateLoader, hibernateConfig) } + /** Load configured JVM agents */ + private fun initialiseJVMAgents() { + configuration.jmxMonitoringHttpPort?.let { port -> + requireNotNull(NodeBuildProperties.JOLOKIA_AGENT_VERSION) { + "'jolokiaAgentVersion' missing from build properties" + } + log.info("Starting Jolokia agent on HTTP port: $port") + val libDir = Paths.get(configuration.baseDirectory.toString(), "drivers") + val jarFilePath = JVMAgentRegistry.resolveAgentJar( + "jolokia-jvm-${NodeBuildProperties.JOLOKIA_AGENT_VERSION}-agent.jar", libDir) ?: + throw Error("Unable to locate agent jar file") + log.info("Agent jar file: $jarFilePath") + JVMAgentRegistry.attach("jolokia", "port=$port", jarFilePath) + } + } + private inner class ServiceHubInternalImpl( override val identityService: IdentityService, // Place the long term identity key in the KMS. Eventually, this is likely going to be separated again because diff --git a/node/src/main/kotlin/net/corda/node/internal/Node.kt b/node/src/main/kotlin/net/corda/node/internal/Node.kt index d191af1403..a774e81b79 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -192,9 +192,9 @@ open class Node(configuration: NodeConfiguration, val rpcBrokerDirectory: Path = baseDirectory / "brokers" / "rpc" with(rpcOptions) { rpcBroker = if (useSsl) { - ArtemisRpcBroker.withSsl(this.address!!, sslConfig, securityManager, certificateChainCheckPolicies, networkParameters.maxMessageSize, exportJMXto.isNotEmpty(), rpcBrokerDirectory) + ArtemisRpcBroker.withSsl(this.address!!, sslConfig, securityManager, certificateChainCheckPolicies, networkParameters.maxMessageSize, jmxMonitoringHttpPort != null, rpcBrokerDirectory) } else { - ArtemisRpcBroker.withoutSsl(this.address!!, adminAddress!!, sslConfig, securityManager, certificateChainCheckPolicies, networkParameters.maxMessageSize, exportJMXto.isNotEmpty(), rpcBrokerDirectory) + ArtemisRpcBroker.withoutSsl(this.address!!, adminAddress!!, sslConfig, securityManager, certificateChainCheckPolicies, networkParameters.maxMessageSize, jmxMonitoringHttpPort != null, rpcBrokerDirectory) } } return rpcBroker!!.addresses diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt index 450bab72a4..bdc2ec265f 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt @@ -25,7 +25,7 @@ val Int.MB: Long get() = this * 1024L * 1024L interface NodeConfiguration : NodeSSLConfiguration { val myLegalName: CordaX500Name val emailAddress: String - val exportJMXto: String + val jmxMonitoringHttpPort: Int? val dataSourceProperties: Properties val rpcUsers: List val security: SecurityConfiguration? @@ -118,6 +118,7 @@ data class NodeConfigurationImpl( /** This is not retrieved from the config file but rather from a command line argument. */ override val baseDirectory: Path, override val myLegalName: CordaX500Name, + override val jmxMonitoringHttpPort: Int? = null, override val emailAddress: String, override val keyStorePassword: String, override val trustStorePassword: String, @@ -184,7 +185,6 @@ data class NodeConfigurationImpl( return errors } - override val exportJMXto: String get() = "http" override val transactionCacheSizeBytes: Long get() = transactionCacheSizeMegaBytes?.MB ?: super.transactionCacheSizeBytes override val attachmentContentCacheSizeBytes: Long diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt index f6837b80c1..ab07903a1e 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt @@ -144,7 +144,7 @@ class ArtemisMessagingServer(private val config: NodeConfiguration, managementNotificationAddress = SimpleString(NOTIFICATIONS_ADDRESS) // JMX enablement - if (config.exportJMXto.isNotEmpty()) { + if (config.jmxMonitoringHttpPort != null) { isJMXManagementEnabled = true isJMXUseBrokerName = true } diff --git a/node/src/main/kotlin/net/corda/node/utilities/JVMAgentRegistry.kt b/node/src/main/kotlin/net/corda/node/utilities/JVMAgentRegistry.kt new file mode 100644 index 0000000000..54850dee10 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/utilities/JVMAgentRegistry.kt @@ -0,0 +1,50 @@ +package net.corda.node.utilities + +import com.ea.agentloader.AgentLoader +import net.corda.core.internal.exists +import net.corda.core.internal.isRegularFile +import java.net.URLClassLoader +import java.nio.file.Path +import java.nio.file.Paths +import java.util.concurrent.ConcurrentHashMap + +/** + * Helper class for loading JVM agents dynamically + */ +object JVMAgentRegistry { + + /** + * Names and options of loaded agents + */ + val loadedAgents = ConcurrentHashMap() + + /** + * Load and attach agent located at given [jar], unless [loadedAgents] + * indicate that one of its instance has been already loaded. + */ + fun attach(agentName: String, options: String, jar: Path) { + loadedAgents.computeIfAbsent(agentName.toLowerCase()) { + AgentLoader.loadAgent(jar.toString(), options) + options + } + } + + /** + * Attempt finding location of jar for given agent by first searching into + * "drivers" directory of [nodeBaseDirectory] and then falling back to + * classpath. Returns null if no match is found. + */ + fun resolveAgentJar(jarFileName: String, driversDir: Path): Path? { + require(jarFileName.endsWith(".jar")) { "jarFileName does not have .jar suffix" } + + val path = Paths.get(driversDir.toString(), jarFileName) + return if (path.exists() && path.isRegularFile()) { + path + } else { + (this::class.java.classLoader as? URLClassLoader) + ?.urLs + ?.map { Paths.get(it.path) } + ?.firstOrNull { it.fileName.toString() == jarFileName } + } + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/utilities/NodeBuildProperties.kt b/node/src/main/kotlin/net/corda/node/utilities/NodeBuildProperties.kt new file mode 100644 index 0000000000..5d7d96cf56 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/utilities/NodeBuildProperties.kt @@ -0,0 +1,27 @@ +package net.corda.node.utilities + +import java.util.* + +/** + * Expose properties defined in top-level 'constants.properties' file. + */ +object NodeBuildProperties { + + // Note: initialization order is important + private val data by lazy { + Properties().apply { + NodeBuildProperties::class.java.getResourceAsStream("/build.properties") + ?.let { load(it) } + } + } + + /** + * Jolokia dependency version + */ + val JOLOKIA_AGENT_VERSION = get("jolokiaAgentVersion") + + /** + * Get property value by name + */ + fun get(key: String): String? = data.getProperty(key) +} \ No newline at end of file diff --git a/node/src/main/resources/build.properties b/node/src/main/resources/build.properties new file mode 100644 index 0000000000..72577915e7 --- /dev/null +++ b/node/src/main/resources/build.properties @@ -0,0 +1,5 @@ +# Build constants exported as resource file to make them visible in Node program +# Note: sadly, due to present limitation of IntelliJ-IDEA in processing resource files, these constants cannot be +# imported from top-level 'constants.properties' file + +jolokiaAgentVersion=1.3.7 \ No newline at end of file diff --git a/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTest.kt b/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTest.kt index 2f6dfc23a1..d9e9f5512e 100644 --- a/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTest.kt @@ -67,7 +67,7 @@ class ArtemisMessagingTest { doReturn("trustpass").whenever(it).trustStorePassword doReturn("cordacadevpass").whenever(it).keyStorePassword doReturn(NetworkHostAndPort("0.0.0.0", serverPort)).whenever(it).p2pAddress - doReturn("").whenever(it).exportJMXto + doReturn(null).whenever(it).jmxMonitoringHttpPort doReturn(emptyList()).whenever(it).certificateChainCheckPolicies doReturn(5).whenever(it).messageRedeliveryDelaySeconds } diff --git a/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt index 3a08b300c9..39ba0cdece 100644 --- a/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt +++ b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt @@ -91,10 +91,11 @@ class DriverTests { @Test fun `monitoring mode enables jolokia exporting of JMX metrics via HTTP JSON`() { - driver(DriverParameters(jmxPolicy = JmxPolicy(true))) { + driver(DriverParameters(startNodesInProcess = false)) { // start another node so we gain access to node JMX metrics - startNode(providedName = DUMMY_REGULATOR_NAME).getOrThrow() val webAddress = NetworkHostAndPort("localhost", 7006) + startNode(providedName = DUMMY_REGULATOR_NAME, + customOverrides = mapOf("jmxMonitoringHttpPort" to webAddress.port)).getOrThrow() // request access to some JMX metrics via Jolokia HTTP/JSON val api = HttpApi.fromHostAndPort(webAddress, "/jolokia/") val versionAsJson = api.getJson("/jolokia/version/") diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt index e19d571f1d..b00f4a9db7 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt @@ -469,7 +469,7 @@ private fun mockNodeConfiguration(): NodeConfiguration { doReturn(null).whenever(it).notary doReturn(DatabaseConfig()).whenever(it).database doReturn("").whenever(it).emailAddress - doReturn("").whenever(it).exportJMXto + doReturn(null).whenever(it).jmxMonitoringHttpPort doReturn(true).whenever(it).devMode doReturn(null).whenever(it).compatibilityZoneURL doReturn(emptyList()).whenever(it).certificateChainCheckPolicies From c2485858f50c3391bfe6fdb14533c523e6cf5048 Mon Sep 17 00:00:00 2001 From: Anthony Keenan Date: Fri, 16 Feb 2018 10:22:41 +0000 Subject: [PATCH 32/50] CORDA-1011 Add powershell script to update testing package imports (#2528) * Add powershell script to update testing package imports * Make sure script works with java files * Address review comments --- docs/source/changelog.rst | 6 +++--- tools/scripts/upgrade-test-packages.ps1 | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 tools/scripts/upgrade-test-packages.ps1 diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 3f6079c63e..51b08d6924 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -203,9 +203,9 @@ UNRELEASED * The test utils in ``Expect.kt``, ``SerializationTestHelpers.kt``, ``TestConstants.kt`` and ``TestUtils.kt`` have moved from the ``net.corda.testing`` package to the ``net.corda.testing.core`` package, and ``FlowStackSnapshot.kt`` has moved to the - ``net.corda.testing.services`` package. Moving items out of the ``net.corda.testing.*`` package will help make it clearer which - parts of the api are stable. The bash script ``tools\scripts\update-test-packages.sh`` can be used to smooth the upgrade - process for existing projects. + ``net.corda.testing.services`` package. Moving existing classes out of the ``net.corda.testing.*`` package + will help make it clearer which parts of the api are stable. Scripts have been provided to smooth the upgrade + process for existing projects in the ``tools\scripts`` directory of the Corda repo. .. _changelog_v1: diff --git a/tools/scripts/upgrade-test-packages.ps1 b/tools/scripts/upgrade-test-packages.ps1 new file mode 100644 index 0000000000..79028809a2 --- /dev/null +++ b/tools/scripts/upgrade-test-packages.ps1 @@ -0,0 +1,4 @@ +Get-ChildItem $args[0] -Include *.kt, *.java -Recurse | +Foreach-Object { + (Get-Content $_.FullName) -replace 'net.corda.testing.(\*|generateStateRef|freeLocalHostAndPort|getFreeLocalPorts|getTestPartyAndCertificate|TestIdentity|chooseIdentity|singleIdentity|TEST_TX_TIME|DUMMY_NOTARY_NAME|DUMMY_BANK_A_NAME|DUMMY_BANK_B_NAME|DUMMY_BANK_C_NAME|BOC_NAME|ALICE_NAME|BOB_NAME|CHARLIE_NAME|DEV_INTERMEDIATE_CA|DEV_ROOT_CA|dummyCommand|DummyCommandData|MAX_MESSAGE_SIZE|SerializationEnvironmentRule|setGlobalSerialization|expect|sequence|parallel|replicate|genericExpectEvents)', 'net.corda.testing.core.$1' -replace 'net.corda.testing.FlowStackSnapshotFactoryImpl', 'net.corda.testing.services.FlowStackSnapshotFactoryImpl' -join "`r`n" | Set-Content -NoNewline -Force $_.FullName +} \ No newline at end of file From 307e2988c9be32e77aafa89b720efdc83b662429 Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Fri, 16 Feb 2018 12:14:31 +0000 Subject: [PATCH 33/50] DOCS: update AMQP serialization (#2543) * DOCS: update AMQP serialization * Update serialization.rst --- docs/source/serialization.rst | 47 +++++++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/docs/source/serialization.rst b/docs/source/serialization.rst index eed77ecdd6..de984b9b18 100644 --- a/docs/source/serialization.rst +++ b/docs/source/serialization.rst @@ -396,6 +396,51 @@ Providing a public getter, as per the following example, is acceptable: } } +Mismatched Class Properties / Constructor Parameters +```````````````````````````````````````````````````` + +Consider an example where you wish to ensure that a property of class whose type is some form of container is always sorted using some specific criteria yet you wish to maintain the immutability of the class. + +This could be codified as follows: + +.. container:: codeset + + .. sourcecode:: kotlin + + @CordaSerializable + class ConfirmRequest(statesToConsume: List, val transactionId: SecureHash) { + companion object { + private val stateRefComparator = compareBy({ it.txhash }, { it.index }) + } + + private val states = statesToConsume.sortedWith(stateRefComparator) + } + +The intention in the example is to always ensure that the states are stored in a specific order regardless of the ordering +of the list used to initialise instances of the class. This is achieved by using the first constructor parameter as the +basis for a private member. However, because that member is not mentioned in the constructor (whose parameters determine +what is serializable as discussed above) it would not be serialized. In addition, as there is no provided mechanism to retrieve +a value for ``statesToConsume`` we would fail to build a serializer for this Class. + +In this case a secondary constructor annotated with ``@ConstructorForDeserialization`` would not be a valid solution as the +two signatures would be the same. Best practice is thus to provide a getter for the constructor parameter which explicitly +associates it with the actual member variable. + +.. container:: codeset + + .. sourcecode:: kotlin + + @CordaSerializable + class ConfirmRequest(statesToConsume: List, val transactionId: SecureHash) { + companion object { + private val stateRefComparator = compareBy({ it.txhash }, { it.index }) + } + + private val states = statesToConsume.sortedWith(stateRefComparator) + + //Explicit "getter" for a property identified from the constructor parameters + fun getStatesToConsume() = states + } Enums ````` @@ -451,5 +496,3 @@ and a version of the current state of the class instantiated. More detail can be found in :doc:`serialization-default-evolution`. - - From 458db7cb24c3c5007dba75c60b1f21a7b76350f0 Mon Sep 17 00:00:00 2001 From: igor nitto Date: Fri, 16 Feb 2018 12:17:46 +0000 Subject: [PATCH 34/50] Cleanup build (#2551) * Remove compile-time dependencies on jolokia-war from webserver * Stop exporting jolokia version to all projects, it is only used by node --- build.gradle | 2 -- node/build.gradle | 8 ++++++++ webserver/build.gradle | 1 - 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 46e3956b41..49eae46bda 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,6 @@ buildscript { // For sharing constants between builds Properties constants = new Properties() file("$projectDir/constants.properties").withInputStream { constants.load(it) } - file("${project(':node').projectDir}/src/main/resources/build.properties").withInputStream { constants.load(it) } // Our version: bump this on release. ext.corda_release_version = "3.0-SNAPSHOT" @@ -41,7 +40,6 @@ buildscript { ext.jackson_version = '2.9.3' ext.jetty_version = '9.4.7.v20170914' ext.jersey_version = '2.25' - ext.jolokia_version = constants.getProperty("jolokiaAgentVersion") ext.assertj_version = '3.8.0' ext.slf4j_version = '1.7.25' ext.log4j_version = '2.9.1' diff --git a/node/build.gradle b/node/build.gradle index 50130933af..8b06b452c4 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -9,6 +9,14 @@ apply plugin: 'com.jfrog.artifactory' description 'Corda node modules' +// Import private compile time constants +buildscript { + def properties = new Properties() + file("$projectDir/src/main/resources/build.properties").withInputStream { properties.load(it) } + + ext.jolokia_version = properties.getProperty('jolokiaAgentVersion') +} + //noinspection GroovyAssignabilityCheck configurations { compile { diff --git a/webserver/build.gradle b/webserver/build.gradle index e0f66a360c..3715cfaf67 100644 --- a/webserver/build.gradle +++ b/webserver/build.gradle @@ -38,7 +38,6 @@ dependencies { compile "org.eclipse.jetty:jetty-servlet:$jetty_version" compile "org.eclipse.jetty:jetty-webapp:$jetty_version" compile "javax.servlet:javax.servlet-api:3.1.0" - compile "org.jolokia:jolokia-war:$jolokia_version" compile "commons-fileupload:commons-fileupload:$fileupload_version" // Log4J: logging framework (with SLF4J bindings) From fe7c129ae7f89ea5830efd0d7dc0116bba4ba069 Mon Sep 17 00:00:00 2001 From: Konstantinos Chalkias Date: Fri, 16 Feb 2018 12:32:53 +0000 Subject: [PATCH 35/50] CORDA-1038 Update verifySignaturesExcept in api-transactions.rst (#2546) --- docs/source/api-transactions.rst | 18 ++++++++++++++++++ .../java/net/corda/docs/FlowCookbookJava.java | 11 ++++++++++- .../main/kotlin/net/corda/docs/FlowCookbook.kt | 9 ++++++++- 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/docs/source/api-transactions.rst b/docs/source/api-transactions.rst index 0c02f4b565..888aed09e7 100644 --- a/docs/source/api-transactions.rst +++ b/docs/source/api-transactions.rst @@ -615,6 +615,24 @@ which the signatures are allowed to be missing: :end-before: DOCEND 36 :dedent: 16 +There is also an overload of ``SignedTransaction.verifySignaturesExcept``, which takes a ``Collection`` of the +public keys for which the signatures are allowed to be missing: + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 54 + :end-before: DOCEND 54 + :dedent: 8 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 54 + :end-before: DOCEND 54 + :dedent: 16 + + If the transaction is missing any signatures without the corresponding public keys being passed in, a ``SignaturesMissingException`` is thrown. diff --git a/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java b/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java index 9a6f6d6e5f..bf2c58b858 100644 --- a/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java +++ b/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java @@ -540,12 +540,21 @@ public class FlowCookbookJava { // DOCEND 35 // If the transaction is only partially signed, we have to pass in - // a list of the public keys corresponding to the missing + // a vararg of the public keys corresponding to the missing // signatures, explicitly telling the system not to check them. // DOCSTART 36 onceSignedTx.verifySignaturesExcept(counterpartyPubKey); // DOCEND 36 + // There is also an overload of ``verifySignaturesExcept`` which accepts + // a ``Collection`` of the public keys corresponding to the missing + // signatures. In the example below, we could also use + // ``Arrays.asList(counterpartyPubKey)`` instead of + // ``Collections.singletonList(counterpartyPubKey)``. + // DOCSTART 54 + onceSignedTx.verifySignaturesExcept(Collections.singletonList(counterpartyPubKey)); + // DOCEND 54 + // We can also choose to only check the signatures that are // present. BE VERY CAREFUL - this function provides no guarantees // that the signatures are correct, or that none are missing. diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt index f5380582de..e0179281da 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt @@ -522,12 +522,19 @@ class InitiatorFlow(val arg1: Boolean, val arg2: Int, private val counterparty: // DOCEND 35 // If the transaction is only partially signed, we have to pass in - // a list of the public keys corresponding to the missing + // a vararg of the public keys corresponding to the missing // signatures, explicitly telling the system not to check them. // DOCSTART 36 onceSignedTx.verifySignaturesExcept(counterpartyPubKey) // DOCEND 36 + // There is also an overload of ``verifySignaturesExcept`` which accepts + // a ``Collection`` of the public keys corresponding to the missing + // signatures. + // DOCSTART 54 + onceSignedTx.verifySignaturesExcept(listOf(counterpartyPubKey)) + // DOCEND 54 + // We can also choose to only check the signatures that are // present. BE VERY CAREFUL - this function provides no guarantees // that the signatures are correct, or that none are missing. From 9711ea88c005990f006c732c37e3cf7b757faf07 Mon Sep 17 00:00:00 2001 From: Michele Sollecito Date: Fri, 16 Feb 2018 13:16:26 +0000 Subject: [PATCH 36/50] Fixed Demobench. (#2548) --- .../net/corda/demobench/model/NodeConfig.kt | 16 +++++++++++++++- .../net/corda/demobench/model/NodeController.kt | 1 + .../kotlin/net/corda/demobench/model/NodeData.kt | 2 ++ .../net/corda/demobench/views/NodeTabView.kt | 1 + .../net/corda/demobench/model/NodeConfigTest.kt | 4 ++++ .../corda/demobench/model/NodeControllerTest.kt | 2 ++ 6 files changed, 25 insertions(+), 1 deletion(-) diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt index ae5b6deeb1..b251955d5c 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt @@ -1,6 +1,9 @@ package net.corda.demobench.model +import com.typesafe.config.Config +import com.typesafe.config.ConfigFactory.empty import com.typesafe.config.ConfigRenderOptions +import com.typesafe.config.ConfigValueFactory import net.corda.core.identity.CordaX500Name import net.corda.core.internal.copyToDirectory import net.corda.core.internal.createDirectories @@ -19,6 +22,7 @@ data class NodeConfig( val myLegalName: CordaX500Name, val p2pAddress: NetworkHostAndPort, val rpcAddress: NetworkHostAndPort, + val rpcAdminAddress: NetworkHostAndPort, /** This is not used by the node but by the webserver which looks at node.conf. */ val webAddress: NetworkHostAndPort, val notary: NotaryService?, @@ -38,7 +42,17 @@ data class NodeConfig( @Suppress("unused") private val useTestClock = true - fun toText(): String = toConfig().root().render(renderOptions) + private fun asConfig(): Config { + + val config = toConfig() + val rpcSettings = empty() + .withValue("address", ConfigValueFactory.fromAnyRef(rpcAddress.toString())) + .withValue("adminAddress", ConfigValueFactory.fromAnyRef(rpcAdminAddress.toString())) + .root() + return config.withoutPath("rpcAddress").withoutPath("rpcAdminAddress").withValue("rpcSettings", rpcSettings) + } + + fun toText(): String = asConfig().root().render(renderOptions) } /** diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt index 1c53fdd82f..bae600a2e0 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt @@ -75,6 +75,7 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { ), p2pAddress = nodeData.p2pPort.toLocalAddress(), rpcAddress = nodeData.rpcPort.toLocalAddress(), + rpcAdminAddress = nodeData.rpcAdminPort.toLocalAddress(), webAddress = nodeData.webPort.toLocalAddress(), notary = notary, h2port = nodeData.h2Port.value, diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeData.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeData.kt index 7abdd09775..b8af55e5ff 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeData.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeData.kt @@ -35,6 +35,7 @@ class NodeData { val nearestCity = SimpleObjectProperty(CityDatabase["London"]!!) val p2pPort = SimpleIntegerProperty() val rpcPort = SimpleIntegerProperty() + val rpcAdminPort = SimpleIntegerProperty() val webPort = SimpleIntegerProperty() val h2Port = SimpleIntegerProperty() val extraServices = SimpleListProperty(observableArrayList()) @@ -45,6 +46,7 @@ class NodeDataModel : ItemViewModel(NodeData()) { val nearestCity = bind { item?.nearestCity } val p2pPort = bind { item?.p2pPort } val rpcPort = bind { item?.rpcPort } + val rpcAdminPort = bind { item?.rpcAdminPort } val webPort = bind { item?.webPort } val h2Port = bind { item?.h2Port } } diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTabView.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTabView.kt index 5fb32aa9e6..8cded0186d 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTabView.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTabView.kt @@ -188,6 +188,7 @@ class NodeTabView : Fragment() { model.p2pPort.value = nodeController.nextPort model.rpcPort.value = nodeController.nextPort + model.rpcAdminPort.value = nodeController.nextPort model.webPort.value = nodeController.nextPort model.h2Port.value = nodeController.nextPort diff --git a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt index 1c82a08654..cbe6f54a18 100644 --- a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt +++ b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt @@ -28,6 +28,7 @@ class NodeConfigTest { legalName = myLegalName, p2pPort = 10001, rpcPort = 40002, + rpcAdminPort = 40003, webPort = 20001, h2port = 30001, notary = NotaryService(validating = false), @@ -55,6 +56,7 @@ class NodeConfigTest { legalName = myLegalName, p2pPort = 10001, rpcPort = 40002, + rpcAdminPort = 40003, webPort = 20001, h2port = 30001, notary = NotaryService(validating = false), @@ -77,6 +79,7 @@ class NodeConfigTest { legalName: CordaX500Name = CordaX500Name(organisation = "Unknown", locality = "Nowhere", country = "GB"), p2pPort: Int = -1, rpcPort: Int = -1, + rpcAdminPort: Int = -1, webPort: Int = -1, h2port: Int = -1, notary: NotaryService?, @@ -86,6 +89,7 @@ class NodeConfigTest { myLegalName = legalName, p2pAddress = localPort(p2pPort), rpcAddress = localPort(rpcPort), + rpcAdminAddress = localPort(rpcAdminPort), webAddress = localPort(webPort), h2port = h2port, notary = notary, diff --git a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeControllerTest.kt b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeControllerTest.kt index 18db310a5b..e6b5454144 100644 --- a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeControllerTest.kt +++ b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeControllerTest.kt @@ -150,6 +150,7 @@ class NodeControllerTest { organisation: String = "Unknown", p2pPort: Int = 0, rpcPort: Int = 0, + rpcAdminPort: Int = 0, webPort: Int = 0, h2port: Int = 0, notary: NotaryService? = null, @@ -163,6 +164,7 @@ class NodeControllerTest { ), p2pAddress = localPort(p2pPort), rpcAddress = localPort(rpcPort), + rpcAdminAddress = localPort(rpcAdminPort), webAddress = localPort(webPort), h2port = h2port, notary = notary, From cb7a0229a8c876b5799825f01ce98515e397c5de Mon Sep 17 00:00:00 2001 From: Matthew Nesbit Date: Fri, 16 Feb 2018 16:03:39 +0000 Subject: [PATCH 37/50] Fix a hang in AMQP protocol code that occurs when pausing in debugger causes protocol timeout, but wasn't driving event procesing to actuially kill the socket. (#2557) --- .../internal/protonwrapper/engine/ConnectionStateMachine.kt | 3 +++ .../nodeapi/internal/protonwrapper/engine/EventProcessor.kt | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/engine/ConnectionStateMachine.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/engine/ConnectionStateMachine.kt index 1ab83fcc4e..473c28876c 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/engine/ConnectionStateMachine.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/engine/ConnectionStateMachine.kt @@ -168,12 +168,14 @@ internal class ConnectionStateMachine(serverMode: Boolean, val transport = event.transport log.debug { "Transport Head Closed $transport" } transport.close_tail() + onTransportInternal(transport) } override fun onTransportTailClosed(event: Event) { val transport = event.transport log.debug { "Transport Tail Closed $transport" } transport.close_head() + onTransportInternal(transport) } override fun onTransportClosed(event: Event) { @@ -195,6 +197,7 @@ internal class ConnectionStateMachine(serverMode: Boolean, } else { log.info("Error (no description returned).") } + onTransportInternal(transport) } override fun onTransport(event: Event) { diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/engine/EventProcessor.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/engine/EventProcessor.kt index 14c21b97f2..c02c6f2541 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/engine/EventProcessor.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/engine/EventProcessor.kt @@ -79,7 +79,10 @@ internal class EventProcessor(channel: Channel, if ((connection.localState != EndpointState.CLOSED) && !connection.transport.isClosed) { val now = System.currentTimeMillis() val tickDelay = Math.max(0L, connection.transport.tick(now) - now) - executor.schedule({ tick(connection) }, tickDelay, TimeUnit.MILLISECONDS) + executor.schedule({ + tick(connection) + processEvents() + }, tickDelay, TimeUnit.MILLISECONDS) } } catch (ex: Exception) { connection.transport.close() From fee89c044f9167777f8d60617686f7a47e5e11d8 Mon Sep 17 00:00:00 2001 From: Matthew Nesbit Date: Fri, 16 Feb 2018 16:13:05 +0000 Subject: [PATCH 38/50] Enhance the pluggability of the bridging and messaging code, so that more complex HA and out of process bridges can be written. (#2558) --- .../internal/ArtemisMessagingClient.kt | 16 ++++++++++---- .../internal/bridging/AMQPBridgeManager.kt | 11 ++++++---- .../bridging/BridgeControlListener.kt | 20 ++++++++++++------ .../protonwrapper/netty/AMQPChannelHandler.kt | 12 +++++------ .../protonwrapper/netty/AMQPServer.kt | 21 +++++++++++++++---- .../kotlin/net/corda/node/internal/Node.kt | 2 +- 6 files changed, 57 insertions(+), 25 deletions(-) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingClient.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingClient.kt index 99b3ab8e38..4bbdf42d88 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingClient.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ArtemisMessagingClient.kt @@ -13,17 +13,25 @@ import org.apache.activemq.artemis.api.core.client.ClientProducer import org.apache.activemq.artemis.api.core.client.ClientSession import org.apache.activemq.artemis.api.core.client.ClientSessionFactory -class ArtemisMessagingClient(private val config: SSLConfiguration, private val serverAddress: NetworkHostAndPort, private val maxMessageSize: Int) { +interface ArtemisSessionProvider { + fun start(): ArtemisMessagingClient.Started + fun stop() + val started: ArtemisMessagingClient.Started? +} + +class ArtemisMessagingClient(private val config: SSLConfiguration, + private val serverAddress: NetworkHostAndPort, + private val maxMessageSize: Int) : ArtemisSessionProvider { companion object { private val log = loggerFor() } class Started(val sessionFactory: ClientSessionFactory, val session: ClientSession, val producer: ClientProducer) - var started: Started? = null + override var started: Started? = null private set - fun start(): Started = synchronized(this) { + override fun start(): Started = synchronized(this) { check(started == null) { "start can't be called twice" } log.info("Connecting to message broker: $serverAddress") // TODO Add broker CN to config for host verification in case the embedded broker isn't used @@ -48,7 +56,7 @@ class ArtemisMessagingClient(private val config: SSLConfiguration, private val s return Started(sessionFactory, session, producer).also { started = it } } - fun stop() = synchronized(this) { + override fun stop() = synchronized(this) { started?.run { producer.close() // Ensure any trailing messages are committed to the journal diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/AMQPBridgeManager.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/AMQPBridgeManager.kt index 07e4b5e0f5..41f5a7d72b 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/AMQPBridgeManager.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/AMQPBridgeManager.kt @@ -12,6 +12,7 @@ import net.corda.nodeapi.internal.ArtemisMessagingComponent import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.NODE_USER import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER import net.corda.nodeapi.internal.ArtemisMessagingComponent.RemoteInboxAddress.Companion.translateLocalQueueToInboxAddress +import net.corda.nodeapi.internal.ArtemisSessionProvider import net.corda.nodeapi.internal.bridging.AMQPBridgeManager.AMQPBridge.Companion.getBridgeName import net.corda.nodeapi.internal.config.NodeSSLConfiguration import net.corda.nodeapi.internal.protonwrapper.messages.MessageStatus @@ -35,7 +36,7 @@ import kotlin.concurrent.withLock * The Netty thread pool used by the AMQPBridges is also shared and managed by the AMQPBridgeManager. */ @VisibleForTesting -class AMQPBridgeManager(val config: NodeSSLConfiguration, val p2pAddress: NetworkHostAndPort, val maxMessageSize: Int) : BridgeManager { +class AMQPBridgeManager(config: NodeSSLConfiguration, val artemisMessageClientFactory: () -> ArtemisSessionProvider) : BridgeManager { private val lock = ReentrantLock() private val bridgeNameToBridgeMap = mutableMapOf() @@ -43,7 +44,9 @@ class AMQPBridgeManager(val config: NodeSSLConfiguration, val p2pAddress: Networ private val keyStore = config.loadSslKeyStore().internal private val keyStorePrivateKeyPassword: String = config.keyStorePassword private val trustStore = config.loadTrustStore().internal - private var artemis: ArtemisMessagingClient? = null + private var artemis: ArtemisSessionProvider? = null + + constructor(config: NodeSSLConfiguration, p2pAddress: NetworkHostAndPort, maxMessageSize: Int) : this(config, { ArtemisMessagingClient(config, p2pAddress, maxMessageSize) }) companion object { private const val NUM_BRIDGE_THREADS = 0 // Default sized pool @@ -64,7 +67,7 @@ class AMQPBridgeManager(val config: NodeSSLConfiguration, val p2pAddress: Networ keyStorePrivateKeyPassword: String, trustStore: KeyStore, sharedEventGroup: EventLoopGroup, - private val artemis: ArtemisMessagingClient) { + private val artemis: ArtemisSessionProvider) { companion object { fun getBridgeName(queueName: String, hostAndPort: NetworkHostAndPort): String = "$queueName -> $hostAndPort" } @@ -190,7 +193,7 @@ class AMQPBridgeManager(val config: NodeSSLConfiguration, val p2pAddress: Networ override fun start() { sharedEventLoopGroup = NioEventLoopGroup(NUM_BRIDGE_THREADS) - val artemis = ArtemisMessagingClient(config, p2pAddress, maxMessageSize) + val artemis = artemisMessageClientFactory() this.artemis = artemis artemis.start() } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/BridgeControlListener.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/BridgeControlListener.kt index 570f28cf48..2d0d53a19d 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/BridgeControlListener.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/bridging/BridgeControlListener.kt @@ -10,6 +10,7 @@ import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.BRIDGE_CON import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.BRIDGE_NOTIFY import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_PREFIX import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEERS_PREFIX +import net.corda.nodeapi.internal.ArtemisSessionProvider import net.corda.nodeapi.internal.config.NodeSSLConfiguration import org.apache.activemq.artemis.api.core.RoutingType import org.apache.activemq.artemis.api.core.SimpleString @@ -18,14 +19,17 @@ import org.apache.activemq.artemis.api.core.client.ClientMessage import java.util.* class BridgeControlListener(val config: NodeSSLConfiguration, - val p2pAddress: NetworkHostAndPort, - val maxMessageSize: Int) : AutoCloseable { + val artemisMessageClientFactory: () -> ArtemisSessionProvider) : AutoCloseable { private val bridgeId: String = UUID.randomUUID().toString() - private val bridgeManager: BridgeManager = AMQPBridgeManager(config, p2pAddress, maxMessageSize) + private val bridgeManager: BridgeManager = AMQPBridgeManager(config, artemisMessageClientFactory) private val validInboundQueues = mutableSetOf() - private var artemis: ArtemisMessagingClient? = null + private var artemis: ArtemisSessionProvider? = null private var controlConsumer: ClientConsumer? = null + constructor(config: NodeSSLConfiguration, + p2pAddress: NetworkHostAndPort, + maxMessageSize: Int) : this(config, { ArtemisMessagingClient(config, p2pAddress, maxMessageSize) }) + companion object { private val log = contextLogger() } @@ -33,7 +37,7 @@ class BridgeControlListener(val config: NodeSSLConfiguration, fun start() { stop() bridgeManager.start() - val artemis = ArtemisMessagingClient(config, p2pAddress, maxMessageSize) + val artemis = artemisMessageClientFactory() this.artemis = artemis artemis.start() val artemisClient = artemis.started!! @@ -56,6 +60,7 @@ class BridgeControlListener(val config: NodeSSLConfiguration, } fun stop() { + validInboundQueues.clear() controlConsumer?.close() controlConsumer = null artemis?.stop() @@ -65,6 +70,10 @@ class BridgeControlListener(val config: NodeSSLConfiguration, override fun close() = stop() + fun validateReceiveTopic(topic: String): Boolean { + return topic in validInboundQueues + } + private fun validateInboxQueueName(queueName: String): Boolean { return queueName.startsWith(P2P_PREFIX) && artemis!!.started!!.session.queueQuery(SimpleString(queueName)).isExists } @@ -90,7 +99,6 @@ class BridgeControlListener(val config: NodeSSLConfiguration, for (outQueue in controlMessage.sendQueues) { bridgeManager.deployBridge(outQueue.queueName, outQueue.targets.first(), outQueue.legalNames.toSet()) } - // TODO For now we just record the inboxes, but we don't use the information, but eventually out of process bridges will use this for validating inbound messages. validInboundQueues.addAll(controlMessage.inboxQueues) } is BridgeControl.BridgeToNodeSnapshotRequest -> { diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPChannelHandler.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPChannelHandler.kt index 4054b7c7fd..25855df558 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPChannelHandler.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPChannelHandler.kt @@ -38,8 +38,8 @@ internal class AMQPChannelHandler(private val serverMode: Boolean, private val onReceive: (ReceivedMessage) -> Unit) : ChannelDuplexHandler() { private val log = LoggerFactory.getLogger(allowedRemoteLegalNames?.firstOrNull()?.toString() ?: "AMQPChannelHandler") private lateinit var remoteAddress: InetSocketAddress - private lateinit var localCert: X509Certificate - private lateinit var remoteCert: X509Certificate + private var localCert: X509Certificate? = null + private var remoteCert: X509Certificate? = null private var eventProcessor: EventProcessor? = null override fun channelActive(ctx: ChannelHandlerContext) { @@ -51,7 +51,7 @@ internal class AMQPChannelHandler(private val serverMode: Boolean, private fun createAMQPEngine(ctx: ChannelHandlerContext) { val ch = ctx.channel() - eventProcessor = EventProcessor(ch, serverMode, localCert.subjectX500Principal.toString(), remoteCert.subjectX500Principal.toString(), userName, password) + eventProcessor = EventProcessor(ch, serverMode, localCert!!.subjectX500Principal.toString(), remoteCert!!.subjectX500Principal.toString(), userName, password) val connection = eventProcessor!!.connection val transport = connection.transport as ProtonJTransport if (trace) { @@ -72,7 +72,7 @@ internal class AMQPChannelHandler(private val serverMode: Boolean, override fun channelInactive(ctx: ChannelHandlerContext) { val ch = ctx.channel() log.info("Closed client connection ${ch.id()} from $remoteAddress to ${ch.localAddress()}") - onClose(Pair(ch as SocketChannel, ConnectionChange(remoteAddress, null, false))) + onClose(Pair(ch as SocketChannel, ConnectionChange(remoteAddress, remoteCert, false))) eventProcessor?.close() ctx.fireChannelInactive() } @@ -84,7 +84,7 @@ internal class AMQPChannelHandler(private val serverMode: Boolean, localCert = sslHandler.engine().session.localCertificates[0].x509 remoteCert = sslHandler.engine().session.peerCertificates[0].x509 try { - val remoteX500Name = CordaX500Name.build(remoteCert.subjectX500Principal) + val remoteX500Name = CordaX500Name.build(remoteCert!!.subjectX500Principal) require(allowedRemoteLegalNames == null || remoteX500Name in allowedRemoteLegalNames) log.info("handshake completed subject: $remoteX500Name") } catch (ex: IllegalArgumentException) { @@ -124,7 +124,7 @@ internal class AMQPChannelHandler(private val serverMode: Boolean, require(inetAddress == remoteAddress) { "Message for incorrect endpoint" } - require(CordaX500Name.parse(msg.destinationLegalName) == CordaX500Name.build(remoteCert.subjectX500Principal)) { + require(CordaX500Name.parse(msg.destinationLegalName) == CordaX500Name.build(remoteCert!!.subjectX500Principal)) { "Message for incorrect legal identity" } log.debug { "channel write ${msg.applicationProperties["_AMQ_DUPL_ID"]}" } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPServer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPServer.kt index 588c7fb8a0..9c98767d0e 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPServer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/protonwrapper/netty/AMQPServer.kt @@ -38,7 +38,7 @@ class AMQPServer(val hostName: String, private val userName: String?, private val password: String?, private val keyStore: KeyStore, - private val keyStorePrivateKeyPassword: String, + private val keyStorePrivateKeyPassword: CharArray, private val trustStore: KeyStore, private val trace: Boolean = false) : AutoCloseable { @@ -59,15 +59,21 @@ class AMQPServer(val hostName: String, private var serverChannel: Channel? = null private val clientChannels = ConcurrentHashMap() - init { - } + constructor(hostName: String, + port: Int, + userName: String?, + password: String?, + keyStore: KeyStore, + keyStorePrivateKeyPassword: String, + trustStore: KeyStore, + trace: Boolean = false) : this(hostName, port, userName, password, keyStore, keyStorePrivateKeyPassword.toCharArray(), trustStore, trace) private class ServerChannelInitializer(val parent: AMQPServer) : ChannelInitializer() { private val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()) private val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) init { - keyManagerFactory.init(parent.keyStore, parent.keyStorePrivateKeyPassword.toCharArray()) + keyManagerFactory.init(parent.keyStore, parent.keyStorePrivateKeyPassword) trustManagerFactory.init(parent.trustStore) } @@ -169,6 +175,13 @@ class AMQPServer(val hostName: String, } } + fun dropConnection(connectionRemoteHost: InetSocketAddress) { + val channel = clientChannels[connectionRemoteHost] + if (channel != null) { + channel.close() + } + } + fun complete(delivery: Delivery, target: InetSocketAddress) { val channel = clientChannels[target] channel?.apply { diff --git a/node/src/main/kotlin/net/corda/node/internal/Node.kt b/node/src/main/kotlin/net/corda/node/internal/Node.kt index a774e81b79..395577f49e 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -210,7 +210,7 @@ open class Node(configuration: NodeConfiguration, } override fun myAddresses(): List { - return listOf(configuration.messagingServerAddress ?: getAdvertisedAddress()) + return listOf(getAdvertisedAddress()) } private fun getAdvertisedAddress(): NetworkHostAndPort { From 5b93abdc57603e6675a4b437caf753e35a1d965c Mon Sep 17 00:00:00 2001 From: Andrius Dagys Date: Fri, 16 Feb 2018 16:14:06 +0000 Subject: [PATCH 39/50] CORDA-1010: Send a request signature in addition to a transaction to the notary (#2527) CORDA-1010: Notary flow - clients now send a signature over a notarisation request in addition to the transaction. This will be logged by the notary to be able to prove that a particular party has requested the consumption of a particular state. --- .ci/api-current.txt | 17 ++- .../corda/core/flows/NotarisationRequest.kt | 101 ++++++++++++++++++ .../kotlin/net/corda/core/flows/NotaryFlow.kt | 59 ++++++---- .../corda/core/flows/SendTransactionFlow.kt | 2 +- .../net/corda/core/internal/InternalUtils.kt | 20 ++++ .../corda/core/node/services/NotaryService.kt | 2 +- .../core/transactions/BaseTransactions.kt | 2 + .../BFTNonValidatingNotaryService.kt | 44 +++++--- .../node/services/transactions/BFTSMaRt.kt | 19 ++-- .../transactions/NonValidatingNotaryFlow.kt | 42 +++++--- .../transactions/ValidatingNotaryFlow.kt | 32 +++++- .../transactions/NotaryServiceTests.kt | 87 ++++++++++++++- .../corda/notarydemo/MyCustomNotaryService.kt | 54 +++++++--- 13 files changed, 387 insertions(+), 94 deletions(-) create mode 100644 core/src/main/kotlin/net/corda/core/flows/NotarisationRequest.kt diff --git a/.ci/api-current.txt b/.ci/api-current.txt index 18a44b7a84..de26e31da6 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -1128,7 +1128,8 @@ public final class net.corda.core.flows.ContractUpgradeFlow extends java.lang.Ob public (net.corda.core.contracts.StateAndRef, Class) @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull protected net.corda.core.flows.AbstractStateReplacementFlow$UpgradeTx assembleTx() ## -public abstract class net.corda.core.flows.DataVendingFlow extends net.corda.core.flows.FlowLogic +public class net.corda.core.flows.DataVendingFlow extends net.corda.core.flows.FlowLogic + public (net.corda.core.flows.FlowSession, Object) @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.Nullable public Void call() @org.jetbrains.annotations.NotNull public final net.corda.core.flows.FlowSession getOtherSideSession() @org.jetbrains.annotations.NotNull public final Object getPayload() @@ -1322,11 +1323,11 @@ public @interface net.corda.core.flows.InitiatingFlow @org.jetbrains.annotations.NotNull public String toString() ## @net.corda.core.serialization.CordaSerializable public static final class net.corda.core.flows.NotaryError$General extends net.corda.core.flows.NotaryError - public (String) - @org.jetbrains.annotations.NotNull public final String component1() - @org.jetbrains.annotations.NotNull public final net.corda.core.flows.NotaryError$General copy(String) + public (Throwable) + @org.jetbrains.annotations.NotNull public final Throwable component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.flows.NotaryError$General copy(Throwable) public boolean equals(Object) - @org.jetbrains.annotations.NotNull public final String getCause() + @org.jetbrains.annotations.NotNull public final Throwable getCause() public int hashCode() @org.jetbrains.annotations.NotNull public String toString() ## @@ -1370,8 +1371,6 @@ public final class net.corda.core.flows.NotaryFlow extends java.lang.Object @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public List call() @org.jetbrains.annotations.NotNull public net.corda.core.utilities.ProgressTracker getProgressTracker() @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull protected final net.corda.core.utilities.UntrustworthyData notarise(net.corda.core.identity.Party) - @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull protected net.corda.core.utilities.UntrustworthyData sendAndReceiveNonValidating(net.corda.core.identity.Party, net.corda.core.flows.FlowSession) - @co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull protected net.corda.core.utilities.UntrustworthyData sendAndReceiveValidating(net.corda.core.flows.FlowSession) @org.jetbrains.annotations.NotNull protected final List validateResponse(net.corda.core.utilities.UntrustworthyData, net.corda.core.identity.Party) public static final net.corda.core.flows.NotaryFlow$Client$Companion Companion ## @@ -2923,7 +2922,7 @@ public static final class net.corda.core.serialization.SingletonSerializationTok @org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getId() @org.jetbrains.annotations.NotNull public final String getReason() ## -@net.corda.core.DoNotImplement public abstract class net.corda.core.transactions.CoreTransaction extends net.corda.core.transactions.BaseTransaction +@net.corda.core.serialization.CordaSerializable @net.corda.core.DoNotImplement public abstract class net.corda.core.transactions.CoreTransaction extends net.corda.core.transactions.BaseTransaction public () @org.jetbrains.annotations.NotNull public abstract List getInputs() ## @@ -3165,7 +3164,7 @@ public class net.corda.core.transactions.TransactionBuilder extends java.lang.Ob public abstract void verifyRequiredSignatures() public abstract void verifySignaturesExcept(Collection) ## -@net.corda.core.DoNotImplement public abstract class net.corda.core.transactions.TraversableTransaction extends net.corda.core.transactions.CoreTransaction +@net.corda.core.serialization.CordaSerializable @net.corda.core.DoNotImplement public abstract class net.corda.core.transactions.TraversableTransaction extends net.corda.core.transactions.CoreTransaction public (List) @org.jetbrains.annotations.NotNull public final List getAttachments() @org.jetbrains.annotations.NotNull public final List getAvailableComponentGroups() diff --git a/core/src/main/kotlin/net/corda/core/flows/NotarisationRequest.kt b/core/src/main/kotlin/net/corda/core/flows/NotarisationRequest.kt new file mode 100644 index 0000000000..166d38dc68 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/flows/NotarisationRequest.kt @@ -0,0 +1,101 @@ +package net.corda.core.flows + +import net.corda.core.contracts.StateRef +import net.corda.core.crypto.DigitalSignature +import net.corda.core.crypto.SecureHash +import net.corda.core.identity.Party +import net.corda.core.serialization.CordaSerializable +import net.corda.core.serialization.serialize +import net.corda.core.transactions.CoreTransaction +import net.corda.core.transactions.SignedTransaction +import java.security.InvalidKeyException +import java.security.SignatureException + +/** + * A notarisation request specifies a list of states to consume and the id of the consuming transaction. Its primary + * purpose is for notarisation traceability – a signature over the notarisation request, [NotarisationRequestSignature], + * allows a notary to prove that a certain party requested the consumption of a particular state. + * + * While the signature must be retained, the notarisation request does not need to be transferred or stored anywhere - it + * can be built from a [SignedTransaction] or a [CoreTransaction]. The notary can recompute it from the committed states index. + * + * In case there is a need to prove that a party spent a particular state, the notary will: + * 1) Locate the consuming transaction id in the index, along with all other states consumed in the same transaction. + * 2) Build a [NotarisationRequest]. + * 3) Locate the [NotarisationRequestSignature] for the transaction id. The signature will contain the signing public key. + * 4) Demonstrate the signature verifies against the serialized request. The provided states are always sorted internally, + * to ensure the serialization does not get affected by the order. + */ +@CordaSerializable +class NotarisationRequest(statesToConsume: List, val transactionId: SecureHash) { + companion object { + /** Sorts in ascending order first by transaction hash, then by output index. */ + private val stateRefComparator = compareBy({ it.txhash }, { it.index }) + } + + private val _statesToConsumeSorted = statesToConsume.sortedWith(stateRefComparator) + + /** States this request specifies to be consumed. Sorted to ensure the serialized form does not get affected by the state order. */ + val statesToConsume: List get() = _statesToConsumeSorted // Getter required for AMQP serialization + + /** Verifies the signature against this notarisation request. Checks that the signature is issued by the right party. */ + fun verifySignature(requestSignature: NotarisationRequestSignature, intendedSigner: Party) { + val signature = requestSignature.digitalSignature + if (intendedSigner.owningKey != signature.by) { + val errorMessage = "Expected a signature by ${intendedSigner.owningKey}, but received by ${signature.by}}" + throw NotaryException(NotaryError.RequestSignatureInvalid(IllegalArgumentException(errorMessage))) + } + // TODO: if requestSignature was generated over an old version of NotarisationRequest, we need to be able to + // reserialize it in that version to get the exact same bytes. Modify the serialization logic once that's + // available. + val expectedSignedBytes = this.serialize().bytes + verifyCorrectBytesSigned(signature, expectedSignedBytes) + } + + private fun verifyCorrectBytesSigned(signature: DigitalSignature.WithKey, bytes: ByteArray) { + try { + signature.verify(bytes) + } catch (e: Exception) { + when (e) { + is InvalidKeyException, is SignatureException -> { + val error = NotaryError.RequestSignatureInvalid(e) + throw NotaryException(error) + } + else -> throw e + } + } + } +} + +/** + * A wrapper around a digital signature used for notarisation requests. + * + * The [platformVersion] is required so the notary can verify the signature against the right version of serialized + * bytes of the [NotarisationRequest]. Otherwise, the request may be rejected. + */ +@CordaSerializable +data class NotarisationRequestSignature(val digitalSignature: DigitalSignature.WithKey, val platformVersion: Int) + +/** + * Container for the transaction and notarisation request signature. + * This is the payload that gets sent by a client to a notary service for committing the input states of the [transaction]. + */ +@CordaSerializable +data class NotarisationPayload(val transaction: Any, val requestSignature: NotarisationRequestSignature) { + init { + require(transaction is SignedTransaction || transaction is CoreTransaction) { + "Unsupported transaction type in the notarisation payload: ${transaction.javaClass.simpleName}" + } + } + + /** + * A helper for automatically casting the underlying [transaction] payload to a [SignedTransaction]. + * Should only be used by validating notaries. + */ + val signedTransaction get() = transaction as SignedTransaction + /** + * A helper for automatically casting the underlying [transaction] payload to a [CoreTransaction]. + * Should only be used by non-validating notaries. + */ + val coreTransaction get() = transaction as CoreTransaction +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt b/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt index 8e38471384..9c2cbee976 100644 --- a/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt @@ -9,10 +9,12 @@ import net.corda.core.crypto.TransactionSignature import net.corda.core.crypto.keys import net.corda.core.identity.Party import net.corda.core.internal.FetchDataFlow +import net.corda.core.internal.generateSignature import net.corda.core.node.services.NotaryService import net.corda.core.node.services.TrustedAuthorityNotaryService import net.corda.core.node.services.UniquenessProvider import net.corda.core.serialization.CordaSerializable +import net.corda.core.transactions.CoreTransaction import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.UntrustworthyData @@ -73,15 +75,17 @@ class NotaryFlow { return notaryParty } + /** Notarises the transaction with the [notaryParty], obtains the notary's signature(s). */ @Throws(NotaryException::class) @Suspendable protected fun notarise(notaryParty: Party): UntrustworthyData> { return try { val session = initiateFlow(notaryParty) + val requestSignature = NotarisationRequest(stx.inputs, stx.id).generateSignature(serviceHub) if (serviceHub.networkMapCache.isValidatingNotary(notaryParty)) { - sendAndReceiveValidating(session) + sendAndReceiveValidating(session, requestSignature) } else { - sendAndReceiveNonValidating(notaryParty, session) + sendAndReceiveNonValidating(notaryParty, session, requestSignature) } } catch (e: NotaryException) { if (e.error is NotaryError.Conflict) { @@ -92,21 +96,23 @@ class NotaryFlow { } @Suspendable - protected open fun sendAndReceiveValidating(session: FlowSession): UntrustworthyData> { - subFlow(SendTransactionWithRetry(session, stx)) + private fun sendAndReceiveValidating(session: FlowSession, signature: NotarisationRequestSignature): UntrustworthyData> { + val payload = NotarisationPayload(stx, signature) + subFlow(NotarySendTransactionFlow(session, payload)) return session.receive() } @Suspendable - protected open fun sendAndReceiveNonValidating(notaryParty: Party, session: FlowSession): UntrustworthyData> { - val tx: Any = if (stx.isNotaryChangeTransaction()) { + private fun sendAndReceiveNonValidating(notaryParty: Party, session: FlowSession, signature: NotarisationRequestSignature): UntrustworthyData> { + val tx: CoreTransaction = if (stx.isNotaryChangeTransaction()) { stx.notaryChangeTx // Notary change transactions do not support filtering } else { stx.buildFilteredTransaction(Predicate { it is StateRef || it is TimeWindow || it == notaryParty }) } - return session.sendAndReceiveWithRetry(tx) + return session.sendAndReceiveWithRetry(NotarisationPayload(tx, signature)) } + /** Checks that the notary's signature(s) is/are valid. */ protected fun validateResponse(response: UntrustworthyData>, notaryParty: Party): List { return response.unwrap { signatures -> signatures.forEach { validateSignature(it, stx.id, notaryParty) } @@ -118,16 +124,16 @@ class NotaryFlow { check(sig.by in notaryParty.owningKey.keys) { "Invalid signer for the notary result" } sig.verify(txId) } - } - /** - * The [SendTransactionWithRetry] flow is equivalent to [SendTransactionFlow] but using [sendAndReceiveWithRetry] - * instead of [sendAndReceive], [SendTransactionWithRetry] is intended to be use by the notary client only. - */ - private class SendTransactionWithRetry(otherSideSession: FlowSession, stx: SignedTransaction) : SendTransactionFlow(otherSideSession, stx) { - @Suspendable - override fun sendPayloadAndReceiveDataRequest(otherSideSession: FlowSession, payload: Any): UntrustworthyData { - return otherSideSession.sendAndReceiveWithRetry(payload) + /** + * The [NotarySendTransactionFlow] flow is similar to [SendTransactionFlow], but uses [NotarisationPayload] as the + * initial message, and retries message delivery. + */ + private class NotarySendTransactionFlow(otherSide: FlowSession, payload: NotarisationPayload) : DataVendingFlow(otherSide, payload) { + @Suspendable + override fun sendPayloadAndReceiveDataRequest(otherSideSession: FlowSession, payload: Any): UntrustworthyData { + return otherSideSession.sendAndReceiveWithRetry(payload) + } } } @@ -186,10 +192,16 @@ class NotaryFlow { */ data class TransactionParts(val id: SecureHash, val inputs: List, val timestamp: TimeWindow?, val notary: Party?) +/** + * Exception thrown by the notary service if any issues are encountered while trying to commit a transaction. The + * underlying [error] specifies the cause of failure. + */ class NotaryException(val error: NotaryError) : FlowException("Unable to notarise: $error") +/** Specifies the cause for notarisation request failure. */ @CordaSerializable sealed class NotaryError { + /** Occurs when one or more input states of transaction with [txId] have already been consumed by another transaction. */ data class Conflict(val txId: SecureHash, val conflict: SignedData) : NotaryError() { override fun toString() = "One or more input states for transaction $txId have been used in another transaction" } @@ -199,18 +211,27 @@ sealed class NotaryError { override fun toString() = "Current time $currentTime is outside the time bounds specified by the transaction: $txTimeWindow" companion object { - @JvmField @Deprecated("Here only for binary compatibility purposes, do not use.") + @JvmField + @Deprecated("Here only for binary compatibility purposes, do not use.") val INSTANCE = TimeWindowInvalid(Instant.EPOCH, TimeWindow.fromOnly(Instant.EPOCH)) } } + /** Occurs when the provided transaction fails to verify. */ data class TransactionInvalid(val cause: Throwable) : NotaryError() { override fun toString() = cause.toString() } + /** Occurs when the transaction sent for notarisation is assigned to a different notary identity. */ object WrongNotary : NotaryError() - data class General(val cause: String): NotaryError() { - override fun toString() = cause + /** Occurs when the notarisation request signature does not verify for the provided transaction. */ + data class RequestSignatureInvalid(val cause: Throwable) : NotaryError() { + override fun toString() = "Request signature invalid: $cause" + } + + /** Occurs when the notary service encounters an unexpected issue or becomes temporarily unavailable. */ + data class General(val cause: Throwable) : NotaryError() { + override fun toString() = cause.toString() } } diff --git a/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt b/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt index 9352d3e178..16f402c486 100644 --- a/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt @@ -28,7 +28,7 @@ open class SendTransactionFlow(otherSide: FlowSession, stx: SignedTransaction) : */ open class SendStateAndRefFlow(otherSideSession: FlowSession, stateAndRefs: List>) : DataVendingFlow(otherSideSession, stateAndRefs) -sealed class DataVendingFlow(val otherSideSession: FlowSession, val payload: Any) : FlowLogic() { +open class DataVendingFlow(val otherSideSession: FlowSession, val payload: Any) : FlowLogic() { @Suspendable protected open fun sendPayloadAndReceiveDataRequest(otherSideSession: FlowSession, payload: Any) = otherSideSession.sendAndReceive(payload) diff --git a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt index f47e62e6d8..8a17ec5966 100644 --- a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt @@ -7,7 +7,11 @@ import net.corda.core.cordapp.CordappConfig import net.corda.core.cordapp.CordappContext import net.corda.core.cordapp.CordappProvider import net.corda.core.crypto.* +import net.corda.core.flows.NotarisationRequest +import net.corda.core.flows.NotarisationRequestSignature +import net.corda.core.flows.NotaryFlow import net.corda.core.identity.CordaX500Name +import net.corda.core.node.ServiceHub import net.corda.core.node.ServicesForResolution import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializedBytes @@ -381,4 +385,20 @@ fun ByteBuffer.copyBytes() = ByteArray(remaining()).also { get(it) } fun createCordappContext(cordapp: Cordapp, attachmentId: SecureHash?, classLoader: ClassLoader, config: CordappConfig): CordappContext { return CordappContext(cordapp, attachmentId, classLoader, config) +} +/** Verifies that the correct notarisation request was signed by the counterparty. */ +fun NotaryFlow.Service.validateRequest(request: NotarisationRequest, signature: NotarisationRequestSignature) { + val requestingParty = otherSideSession.counterparty + request.verifySignature(signature, requestingParty) + // TODO: persist the signature for traceability. Do we need to persist the request as well? +} + +/** Creates a signature over the notarisation request using the legal identity key. */ +fun NotarisationRequest.generateSignature(serviceHub: ServiceHub): NotarisationRequestSignature { + val serializedRequest = this.serialize().bytes + val signature = with(serviceHub) { + val myLegalIdentity = myInfo.legalIdentitiesAndCerts.first().owningKey + keyManagementService.sign(serializedRequest, myLegalIdentity) + } + return NotarisationRequestSignature(signature, serviceHub.myInfo.platformVersion) } \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt b/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt index bc77fe9683..68badaa4e7 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/NotaryService.kt @@ -94,7 +94,7 @@ abstract class TrustedAuthorityNotaryService : NotaryService() { } } catch (e: Exception) { log.error("Internal error", e) - throw NotaryException(NotaryError.General("Service unavailable, please try again later")) + throw NotaryException(NotaryError.General(Exception("Service unavailable, please try again later"))) } } diff --git a/core/src/main/kotlin/net/corda/core/transactions/BaseTransactions.kt b/core/src/main/kotlin/net/corda/core/transactions/BaseTransactions.kt index 2d82c4850b..10031d795e 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/BaseTransactions.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/BaseTransactions.kt @@ -3,12 +3,14 @@ package net.corda.core.transactions import net.corda.core.contracts.ContractState import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateRef +import net.corda.core.serialization.CordaSerializable /** * A transaction with the minimal amount of information required to compute the unique transaction [id], and * resolve a [FullTransaction]. This type of transaction, wrapped in [SignedTransaction], gets transferred across the * wire and recorded to storage. */ +@CordaSerializable abstract class CoreTransaction : BaseTransaction() { /** The inputs of this transaction, containing state references only **/ abstract override val inputs: List diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt index 979e2d2ef0..a04ce94d6f 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt @@ -12,13 +12,19 @@ import net.corda.core.flows.NotaryError import net.corda.core.flows.NotaryException import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party +import net.corda.core.flows.NotarisationPayload +import net.corda.core.flows.NotarisationRequest import net.corda.core.node.services.NotaryService import net.corda.core.node.services.UniquenessProvider import net.corda.core.schemas.PersistentStateRef import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize +import net.corda.core.transactions.CoreTransaction import net.corda.core.transactions.FilteredTransaction -import net.corda.core.utilities.* +import net.corda.core.utilities.contextLogger +import net.corda.core.utilities.debug +import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.unwrap import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.config.BFTSMaRtConfiguration import net.corda.node.utilities.AppendOnlyPersistentMap @@ -67,25 +73,25 @@ class BFTNonValidatingNotaryService( replicaHolder.getOrThrow() // It's enough to wait for the ServiceReplica constructor to return. } - fun commitTransaction(tx: Any, otherSide: Party) = client.commitTransaction(tx, otherSide) + fun commitTransaction(payload: NotarisationPayload, otherSide: Party) = client.commitTransaction(payload, otherSide) override fun createServiceFlow(otherPartySession: FlowSession): FlowLogic = ServiceFlow(otherPartySession, this) private class ServiceFlow(val otherSideSession: FlowSession, val service: BFTNonValidatingNotaryService) : FlowLogic() { @Suspendable override fun call(): Void? { - val stx = otherSideSession.receive().unwrap { it } - val signatures = commit(stx) + val payload = otherSideSession.receive().unwrap { it } + val signatures = commit(payload) otherSideSession.send(signatures) return null } - private fun commit(stx: FilteredTransaction): List { - val response = service.commitTransaction(stx, otherSideSession.counterparty) + private fun commit(payload: NotarisationPayload): List { + val response = service.commitTransaction(payload, otherSideSession.counterparty) when (response) { is BFTSMaRt.ClusterResponse.Error -> throw NotaryException(response.error) is BFTSMaRt.ClusterResponse.Signatures -> { - log.debug("All input states of transaction ${stx.id} have been committed") + log.debug("All input states of transaction ${payload.coreTransaction.id} have been committed") return response.txSignatures } } @@ -132,28 +138,34 @@ class BFTNonValidatingNotaryService( notaryIdentityKey: PublicKey) : BFTSMaRt.Replica(config, replicaId, createMap, services, notaryIdentityKey) { override fun executeCommand(command: ByteArray): ByteArray { - val request = command.deserialize() - val ftx = request.tx as FilteredTransaction - val response = verifyAndCommitTx(ftx, request.callerIdentity) + val commitRequest = command.deserialize() + verifyRequest(commitRequest) + val response = verifyAndCommitTx(commitRequest.payload.coreTransaction, commitRequest.callerIdentity) return response.serialize().bytes } - fun verifyAndCommitTx(ftx: FilteredTransaction, callerIdentity: Party): BFTSMaRt.ReplicaResponse { + private fun verifyAndCommitTx(transaction: CoreTransaction, callerIdentity: Party): BFTSMaRt.ReplicaResponse { return try { - val id = ftx.id - val inputs = ftx.inputs - val notary = ftx.notary - NotaryService.validateTimeWindow(services.clock, ftx.timeWindow) + val id = transaction.id + val inputs = transaction.inputs + val notary = transaction.notary + if (transaction is FilteredTransaction) NotaryService.validateTimeWindow(services.clock, transaction.timeWindow) if (notary !in services.myInfo.legalIdentities) throw NotaryException(NotaryError.WrongNotary) commitInputStates(inputs, id, callerIdentity) log.debug { "Inputs committed successfully, signing $id" } - BFTSMaRt.ReplicaResponse.Signature(sign(ftx)) + BFTSMaRt.ReplicaResponse.Signature(sign(id)) } catch (e: NotaryException) { log.debug { "Error processing transaction: ${e.error}" } BFTSMaRt.ReplicaResponse.Error(e.error) } } + private fun verifyRequest(commitRequest: BFTSMaRt.CommitRequest) { + val transaction = commitRequest.payload.coreTransaction + val notarisationRequest = NotarisationRequest(transaction.inputs, transaction.id) + notarisationRequest.verifySignature(commitRequest.payload.requestSignature, commitRequest.callerIdentity) + // TODO: persist the signature for traceability. + } } override fun start() { diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRt.kt b/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRt.kt index 84ca283b48..899ecde450 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRt.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRt.kt @@ -17,6 +17,7 @@ import net.corda.core.crypto.* import net.corda.core.flows.NotaryError import net.corda.core.flows.NotaryException import net.corda.core.identity.Party +import net.corda.core.flows.NotarisationPayload import net.corda.core.internal.declaredField import net.corda.core.internal.toTypedArray import net.corda.core.node.services.UniquenessProvider @@ -25,8 +26,6 @@ import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize -import net.corda.core.transactions.FilteredTransaction -import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.contextLogger import net.corda.core.utilities.debug import net.corda.node.services.api.ServiceHubInternal @@ -52,7 +51,7 @@ import java.util.* object BFTSMaRt { /** Sent from [Client] to [Replica]. */ @CordaSerializable - data class CommitRequest(val tx: Any, val callerIdentity: Party) + data class CommitRequest(val payload: NotarisationPayload, val callerIdentity: Party) /** Sent from [Replica] to [Client]. */ @CordaSerializable @@ -101,13 +100,12 @@ object BFTSMaRt { * Sends a transaction commit request to the BFT cluster. The [proxy] will deliver the request to every * replica, and block until a sufficient number of replies are received. */ - fun commitTransaction(transaction: Any, otherSide: Party): ClusterResponse { - require(transaction is FilteredTransaction || transaction is SignedTransaction) { "Unsupported transaction type: ${transaction.javaClass.name}" } + fun commitTransaction(payload: NotarisationPayload, otherSide: Party): ClusterResponse { awaitClientConnectionToCluster() cluster.waitUntilAllReplicasHaveInitialized() - val requestBytes = CommitRequest(transaction, otherSide).serialize().bytes + val requestBytes = CommitRequest(payload, otherSide).serialize().bytes val responseBytes = proxy.invokeOrdered(requestBytes) - return responseBytes.deserialize() + return responseBytes.deserialize() } /** A comparator to check if replies from two replicas are the same. */ @@ -242,12 +240,15 @@ object BFTSMaRt { } } + /** Generates a signature over an arbitrary array of bytes. */ protected fun sign(bytes: ByteArray): DigitalSignature.WithKey { return services.database.transaction { services.keyManagementService.sign(bytes, notaryIdentityKey) } } - protected fun sign(filteredTransaction: FilteredTransaction): TransactionSignature { - return services.database.transaction { services.createSignature(filteredTransaction, notaryIdentityKey) } + /** Generates a transaction signature over the specified transaction [txId]. */ + protected fun sign(txId: SecureHash): TransactionSignature { + val signableData = SignableData(txId, SignatureMetadata(services.myInfo.platformVersion, Crypto.findSignatureScheme(notaryIdentityKey).schemeNumberID)) + return services.keyManagementService.sign(signableData, notaryIdentityKey) } // TODO: diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt b/node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt index a12d565d52..5eaaa71c52 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt @@ -1,11 +1,15 @@ package net.corda.node.services.transactions import co.paralleluniverse.fibers.Suspendable -import net.corda.core.flows.FlowSession import net.corda.core.contracts.ComponentGroupEnum +import net.corda.core.flows.FlowSession import net.corda.core.flows.NotaryFlow import net.corda.core.flows.TransactionParts +import net.corda.core.flows.NotarisationPayload +import net.corda.core.flows.NotarisationRequest +import net.corda.core.internal.validateRequest import net.corda.core.node.services.TrustedAuthorityNotaryService +import net.corda.core.transactions.CoreTransaction import net.corda.core.transactions.FilteredTransaction import net.corda.core.transactions.NotaryChangeWireTransaction import net.corda.core.utilities.unwrap @@ -21,22 +25,30 @@ class NonValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAut */ @Suspendable override fun receiveAndVerifyTx(): TransactionParts { - val parts = otherSideSession.receive().unwrap { - when (it) { - is FilteredTransaction -> { - it.verify() - it.checkAllComponentsVisible(ComponentGroupEnum.INPUTS_GROUP) - it.checkAllComponentsVisible(ComponentGroupEnum.TIMEWINDOW_GROUP) - val notary = it.notary - TransactionParts(it.id, it.inputs, it.timeWindow, notary) - } - is NotaryChangeWireTransaction -> TransactionParts(it.id, it.inputs, null, it.notary) - else -> { - throw IllegalArgumentException("Received unexpected transaction type: ${it::class.java.simpleName}," + - "expected either ${FilteredTransaction::class.java.simpleName} or ${NotaryChangeWireTransaction::class.java.simpleName}") + return otherSideSession.receive().unwrap { payload -> + val transaction = payload.coreTransaction + val request = NotarisationRequest(transaction.inputs, transaction.id) + validateRequest(request, payload.requestSignature) + extractParts(transaction) + } + } + + private fun extractParts(tx: CoreTransaction): TransactionParts { + return when (tx) { + is FilteredTransaction -> { + tx.apply { + verify() + checkAllComponentsVisible(ComponentGroupEnum.INPUTS_GROUP) + checkAllComponentsVisible(ComponentGroupEnum.TIMEWINDOW_GROUP) } + val notary = tx.notary + TransactionParts(tx.id, tx.inputs, tx.timeWindow, notary) + } + is NotaryChangeWireTransaction -> TransactionParts(tx.id, tx.inputs, null, tx.notary) + else -> { + throw IllegalArgumentException("Received unexpected transaction type: ${tx::class.java.simpleName}," + + "expected either ${FilteredTransaction::class.java.simpleName} or ${NotaryChangeWireTransaction::class.java.simpleName}") } } - return parts } } \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt index 8ce7ba6365..907428429c 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt @@ -4,8 +4,14 @@ import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.TimeWindow import net.corda.core.contracts.TransactionVerificationException import net.corda.core.flows.* +import net.corda.core.flows.NotarisationPayload +import net.corda.core.flows.NotarisationRequest +import net.corda.core.internal.ResolveTransactionsFlow +import net.corda.core.internal.validateRequest import net.corda.core.node.services.TrustedAuthorityNotaryService +import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionWithSignatures +import net.corda.core.utilities.unwrap import java.security.SignatureException /** @@ -22,15 +28,15 @@ class ValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAuthor @Suspendable override fun receiveAndVerifyTx(): TransactionParts { try { - val stx = subFlow(ReceiveTransactionFlow(otherSideSession, checkSufficientSignatures = false)) + val stx = receiveTransaction() val notary = stx.notary checkNotary(notary) val timeWindow: TimeWindow? = if (stx.isNotaryChangeTransaction()) null else stx.tx.timeWindow - val transactionWithSignatures = stx.resolveTransactionWithSignatures(serviceHub) - checkSignatures(transactionWithSignatures) + resolveAndContractVerify(stx) + verifySignatures(stx) return TransactionParts(stx.id, stx.inputs, timeWindow, notary!!) } catch (e: Exception) { throw when (e) { @@ -41,6 +47,26 @@ class ValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAuthor } } + @Suspendable + private fun receiveTransaction(): SignedTransaction { + return otherSideSession.receive().unwrap { + val stx = it.signedTransaction + validateRequest(NotarisationRequest(stx.inputs, stx.id), it.requestSignature) + stx + } + } + + @Suspendable + private fun resolveAndContractVerify(stx: SignedTransaction) { + subFlow(ResolveTransactionsFlow(stx, otherSideSession)) + stx.verify(serviceHub, false) + } + + private fun verifySignatures(stx: SignedTransaction) { + val transactionWithSignatures = stx.resolveTransactionWithSignatures(serviceHub) + checkSignatures(transactionWithSignatures) + } + private fun checkSignatures(tx: TransactionWithSignatures) { try { tx.verifySignaturesExcept(service.notaryIdentityKey) diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt index 5cf3319fbb..d37c5a3259 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt @@ -3,24 +3,35 @@ package net.corda.node.services.transactions import net.corda.core.concurrent.CordaFuture import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateRef +import net.corda.core.crypto.Crypto +import net.corda.core.crypto.SecureHash import net.corda.core.crypto.TransactionSignature +import net.corda.core.crypto.sign import net.corda.core.flows.NotaryError import net.corda.core.flows.NotaryException import net.corda.core.flows.NotaryFlow import net.corda.core.identity.Party +import net.corda.core.flows.NotarisationPayload +import net.corda.core.flows.NotarisationRequest +import net.corda.core.flows.NotarisationRequestSignature +import net.corda.core.internal.generateSignature +import net.corda.core.messaging.MessageRecipients import net.corda.core.node.ServiceHub +import net.corda.core.serialization.deserialize +import net.corda.core.serialization.serialize import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.seconds import net.corda.node.services.api.StartedNodeServices -import net.corda.testing.core.ALICE_NAME +import net.corda.node.services.messaging.Message +import net.corda.node.services.statemachine.InitialSessionMessage import net.corda.testing.contracts.DummyContract +import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.dummyCommand -import net.corda.testing.node.MockNetwork -import net.corda.testing.node.MockNodeParameters import net.corda.testing.core.singleIdentity -import net.corda.testing.node.startFlow +import net.corda.testing.node.* import org.assertj.core.api.Assertions.assertThat import org.junit.After import org.junit.Before @@ -34,6 +45,7 @@ import kotlin.test.assertTrue class NotaryServiceTests { private lateinit var mockNet: MockNetwork private lateinit var notaryServices: StartedNodeServices + private lateinit var aliceNode: StartedMockNode private lateinit var aliceServices: StartedNodeServices private lateinit var notary: Party private lateinit var alice: Party @@ -41,7 +53,8 @@ class NotaryServiceTests { @Before fun setup() { mockNet = MockNetwork(cordappPackages = listOf("net.corda.testing.contracts")) - aliceServices = mockNet.createNode(MockNodeParameters(legalName = ALICE_NAME)).services + aliceNode = mockNet.createNode(MockNodeParameters(legalName = ALICE_NAME)) + aliceServices = aliceNode.services notaryServices = mockNet.defaultNotaryNode.services //TODO get rid of that notary = mockNet.defaultNotaryIdentity alice = aliceServices.myInfo.singleIdentity() @@ -159,6 +172,70 @@ class NotaryServiceTests { notaryError.conflict.verified() } + @Test + fun `should reject when notarisation request not signed by the requesting party`() { + runNotarisationAndInterceptClientPayload { originalPayload -> + val transaction = originalPayload.signedTransaction + val randomKeyPair = Crypto.generateKeyPair() + val bytesToSign = NotarisationRequest(transaction.inputs, transaction.id).serialize().bytes + val modifiedSignature = NotarisationRequestSignature(randomKeyPair.sign(bytesToSign), aliceServices.myInfo.platformVersion) + originalPayload.copy(requestSignature = modifiedSignature) + } + } + + @Test + fun `should reject when incorrect notarisation request signed - inputs don't match`() { + runNotarisationAndInterceptClientPayload { originalPayload -> + val transaction = originalPayload.signedTransaction + val wrongInputs = listOf(StateRef(SecureHash.randomSHA256(), 0)) + val request = NotarisationRequest(wrongInputs, transaction.id) + val modifiedSignature = request.generateSignature(aliceServices) + originalPayload.copy(requestSignature = modifiedSignature) + } + } + + @Test + fun `should reject when incorrect notarisation request signed - transaction id doesn't match`() { + runNotarisationAndInterceptClientPayload { originalPayload -> + val transaction = originalPayload.signedTransaction + val wrongTransactionId = SecureHash.randomSHA256() + val request = NotarisationRequest(transaction.inputs, wrongTransactionId) + val modifiedSignature = request.generateSignature(aliceServices) + originalPayload.copy(requestSignature = modifiedSignature) + } + } + + private fun runNotarisationAndInterceptClientPayload(payloadModifier: (NotarisationPayload) -> NotarisationPayload) { + aliceNode.setMessagingServiceSpy(object : MessagingServiceSpy(aliceNode.network) { + override fun send(message: Message, target: MessageRecipients, retryId: Long?, sequenceKey: Any, additionalHeaders: Map) { + val messageData = message.data.deserialize() as? InitialSessionMessage + val payload = messageData?.firstPayload!!.deserialize() + + if (payload is NotarisationPayload) { + val alteredPayload = payloadModifier(payload) + val alteredMessageData = messageData.copy(firstPayload = alteredPayload.serialize()) + val alteredMessage = InMemoryMessagingNetwork.InMemoryMessage(message.topic, OpaqueBytes(alteredMessageData.serialize().bytes), message.uniqueMessageId) + messagingService.send(alteredMessage, target, retryId) + + } else { + messagingService.send(message, target, retryId) + } + } + }) + + val stx = run { + val inputState = issueState(aliceServices, alice) + val tx = TransactionBuilder(notary) + .addInputState(inputState) + .addCommand(dummyCommand(alice.owningKey)) + aliceServices.signInitialTransaction(tx) + } + + val future = runNotaryClient(stx) + val ex = assertFailsWith(NotaryException::class) { future.getOrThrow() } + assertThat(ex.error).isInstanceOf(NotaryError.RequestSignatureInvalid::class.java) + } + private fun runNotaryClient(stx: SignedTransaction): CordaFuture> { val flow = NotaryFlow.Client(stx) val future = aliceServices.startFlow(flow) diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt index 1dd9cda94b..e79c54e5bf 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt @@ -4,12 +4,16 @@ import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.TimeWindow import net.corda.core.contracts.TransactionVerificationException import net.corda.core.flows.* +import net.corda.core.flows.NotarisationPayload +import net.corda.core.flows.NotarisationRequest +import net.corda.core.internal.ResolveTransactionsFlow +import net.corda.core.internal.validateRequest import net.corda.core.node.AppServiceHub import net.corda.core.node.services.CordaService -import net.corda.core.node.services.TimeWindowChecker import net.corda.core.node.services.TrustedAuthorityNotaryService -import net.corda.core.transactions.LedgerTransaction +import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionWithSignatures +import net.corda.core.utilities.unwrap import net.corda.node.services.transactions.PersistentUniquenessProvider import java.security.PublicKey import java.security.SignatureException @@ -17,7 +21,8 @@ import java.security.SignatureException /** * A custom notary service should provide a constructor that accepts two parameters of types [AppServiceHub] and [PublicKey]. * - * Note that at present only a single-node notary service can be customised. + * Note that the support for custom notaries is still experimental – at present only a single-node notary service can be customised. + * The notary-related APIs might change in the future. */ // START 1 @CordaService @@ -41,19 +46,15 @@ class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidating @Suspendable override fun receiveAndVerifyTx(): TransactionParts { try { - val stx = subFlow(ReceiveTransactionFlow(otherSideSession, checkSufficientSignatures = false)) + val stx = receiveTransaction() val notary = stx.notary checkNotary(notary) - var timeWindow: TimeWindow? = null - val transactionWithSignatures = if (stx.isNotaryChangeTransaction()) { - stx.resolveNotaryChangeTransaction(serviceHub) - } else { - val wtx = stx.tx - customVerify(wtx.toLedgerTransaction(serviceHub)) - timeWindow = wtx.timeWindow - stx - } - checkSignatures(transactionWithSignatures) + val timeWindow: TimeWindow? = if (stx.isNotaryChangeTransaction()) + null + else + stx.tx.timeWindow + resolveAndContractVerify(stx) + verifySignatures(stx) return TransactionParts(stx.id, stx.inputs, timeWindow, notary!!) } catch (e: Exception) { throw when (e) { @@ -64,8 +65,25 @@ class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidating } } - private fun customVerify(transaction: LedgerTransaction) { - // Add custom verification logic + @Suspendable + private fun receiveTransaction(): SignedTransaction { + return otherSideSession.receive().unwrap { + val stx = it.signedTransaction + validateRequest(NotarisationRequest(stx.inputs, stx.id), it.requestSignature) + stx + } + } + + @Suspendable + private fun resolveAndContractVerify(stx: SignedTransaction) { + subFlow(ResolveTransactionsFlow(stx, otherSideSession)) + stx.verify(serviceHub, false) + customVerify(stx) + } + + private fun verifySignatures(stx: SignedTransaction) { + val transactionWithSignatures = stx.resolveTransactionWithSignatures(serviceHub) + checkSignatures(transactionWithSignatures) } private fun checkSignatures(tx: TransactionWithSignatures) { @@ -75,5 +93,9 @@ class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidating throw NotaryException(NotaryError.TransactionInvalid(e)) } } + + private fun customVerify(stx: SignedTransaction) { + // Add custom verification logic + } } // END 2 From 003e14ce57dfde6af6a26c42f92d5ffa0baf4f9a Mon Sep 17 00:00:00 2001 From: Clinton Alexander Date: Fri, 16 Feb 2018 11:49:51 +0000 Subject: [PATCH 40/50] Added a helpful error log when validating keystores. --- node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index af380101f6..daf5873530 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -589,6 +589,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, log.warn("Certificate key store found but key store password does not match configuration.") false } catch (e: IOException) { + log.error("IO exception while trying to validate keystore", e) false } require(containCorrectKeys) { From 7e13fe6148e8f9b8d44d005665a2fbfed2ca2148 Mon Sep 17 00:00:00 2001 From: Clinton Alexander Date: Fri, 16 Feb 2018 15:22:07 +0000 Subject: [PATCH 41/50] Updating to gradle plugins 4.0.0 --- constants.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/constants.properties b/constants.properties index f246bd4e90..49ea962adb 100644 --- a/constants.properties +++ b/constants.properties @@ -1,4 +1,4 @@ -gradlePluginsVersion=3.0.7 +gradlePluginsVersion=4.0.0 kotlinVersion=1.2.20 platformVersion=2 guavaVersion=21.0 From b8fa44d721ef4ffbe422db2fd1cc2f26d0f3b586 Mon Sep 17 00:00:00 2001 From: Matthew Nesbit Date: Fri, 16 Feb 2018 18:17:15 +0000 Subject: [PATCH 42/50] The IRS demo has a clash between web server ports and the regulator node on port 10010. This bumps the ports up by 1 to fix the issue. (#2562) --- samples/irs-demo/cordapp/build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/samples/irs-demo/cordapp/build.gradle b/samples/irs-demo/cordapp/build.gradle index 00cf16c846..5ee75f4ee2 100644 --- a/samples/irs-demo/cordapp/build.gradle +++ b/samples/irs-demo/cordapp/build.gradle @@ -95,10 +95,10 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { } node { name "O=Regulator,L=Moscow,C=RU" - p2pPort 10010 + p2pPort 10011 rpcSettings { - port 10009 - adminPort 10029 + port 10012 + adminPort 10032 } cordapps = ["${project.group}:finance:$corda_release_version"] cordapps = ["${project(":finance").group}:finance:$corda_release_version"] From 71c94f8a9d9595daf22e9e5f6ad152c4fc5b750d Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Mon, 19 Feb 2018 12:23:59 +0000 Subject: [PATCH 43/50] Reversing the introduction of GlobalProperties (by CORDA-961), and added NetworkParameters to ServicesForResolution instead. (#2563) Also, replaced the maxTransactionSize parameter of the driver and MockNetwork with NetworkParameters. --- .ci/api-current.txt | 24 ++--- .../corda/core/internal/GlobalProperties.kt | 14 --- .../kotlin/net/corda/core/node/ServiceHub.kt | 3 + .../core/transactions/WireTransaction.kt | 25 ++++-- docs/source/changelog.rst | 8 +- .../node/services/AttachmentLoadingTests.kt | 20 +++-- .../statemachine/LargeTransactionsTest.kt | 16 ++-- .../net/corda/node/internal/AbstractNode.kt | 35 +++++--- .../kotlin/net/corda/node/internal/Node.kt | 42 ++++++--- .../services/network/NetworkMapClientTest.kt | 2 +- .../services/network/NetworkMapUpdaterTest.kt | 4 +- .../network/NetworkParametersReaderTest.kt | 2 +- .../transactions/MaxTransactionSizeTests.kt | 5 +- .../node/services/vault/VaultQueryTests.kt | 2 +- .../node/services/vault/VaultWithCashTest.kt | 7 +- .../kotlin/net/corda/testing/driver/Driver.kt | 87 ++++++++++--------- .../net/corda/testing/node/MockNetwork.kt | 21 +++-- .../net/corda/testing/node/MockServices.kt | 32 +++++-- .../net/corda/testing/node/NodeTestUtils.kt | 3 - .../testing/node/internal/DriverDSLImpl.kt | 15 ++-- .../node/internal/InternalMockNetwork.kt | 31 +++---- .../testing/node/internal/NodeBasedTest.kt | 2 +- .../corda/testing/node/internal/RPCDriver.kt | 8 +- .../net/corda/smoketesting/NodeProcess.kt | 8 +- .../common/internal/ParametersUtilities.kt | 4 +- .../net/corda/verifier/GeneratedLedger.kt | 41 ++++++--- .../net/corda/verifier/VerifierDriver.kt | 7 +- 27 files changed, 281 insertions(+), 187 deletions(-) delete mode 100644 core/src/main/kotlin/net/corda/core/internal/GlobalProperties.kt diff --git a/.ci/api-current.txt b/.ci/api-current.txt index de26e31da6..de990eb1e3 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -3186,7 +3186,7 @@ public class net.corda.core.transactions.TransactionBuilder extends java.lang.Ob @org.jetbrains.annotations.NotNull public final net.corda.core.contracts.PrivacySalt getPrivacySalt() @org.jetbrains.annotations.NotNull public final Set getRequiredSigningKeys() public int hashCode() - @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.LedgerTransaction toLedgerTransaction(kotlin.jvm.functions.Function1, kotlin.jvm.functions.Function1, kotlin.jvm.functions.Function1, kotlin.jvm.functions.Function1) + @kotlin.Deprecated @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.LedgerTransaction toLedgerTransaction(kotlin.jvm.functions.Function1, kotlin.jvm.functions.Function1, kotlin.jvm.functions.Function1, kotlin.jvm.functions.Function1) @org.jetbrains.annotations.NotNull public final net.corda.core.transactions.LedgerTransaction toLedgerTransaction(net.corda.core.node.ServicesForResolution) @org.jetbrains.annotations.NotNull public String toString() public static final net.corda.core.transactions.WireTransaction$Companion Companion @@ -3656,12 +3656,12 @@ public final class net.corda.testing.driver.Driver extends java.lang.Object ## public final class net.corda.testing.driver.DriverParameters extends java.lang.Object public () - public (boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, Map, boolean, boolean, boolean, boolean, List, List, net.corda.testing.driver.JmxPolicy, int) + public (boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, Map, boolean, boolean, boolean, boolean, List, List, net.corda.testing.driver.JmxPolicy, net.corda.core.node.NetworkParameters) public final boolean component1() @org.jetbrains.annotations.NotNull public final List component10() @org.jetbrains.annotations.NotNull public final List component11() @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.JmxPolicy component12() - public final int component13() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.NetworkParameters component13() @org.jetbrains.annotations.NotNull public final java.nio.file.Path component2() @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.PortAllocation component3() @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.PortAllocation component4() @@ -3670,14 +3670,14 @@ public final class net.corda.testing.driver.DriverParameters extends java.lang.O public final boolean component7() public final boolean component8() public final boolean component9() - @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters copy(boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, Map, boolean, boolean, boolean, boolean, List, List, net.corda.testing.driver.JmxPolicy, int) + @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters copy(boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, Map, boolean, boolean, boolean, boolean, List, List, net.corda.testing.driver.JmxPolicy, net.corda.core.node.NetworkParameters) public boolean equals(Object) @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.PortAllocation getDebugPortAllocation() @org.jetbrains.annotations.NotNull public final java.nio.file.Path getDriverDirectory() @org.jetbrains.annotations.NotNull public final List getExtraCordappPackagesToScan() public final boolean getInitialiseSerialization() @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.JmxPolicy getJmxPolicy() - public final int getMaxTransactionSize() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.NetworkParameters getNetworkParameters() @org.jetbrains.annotations.NotNull public final List getNotarySpecs() @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.PortAllocation getPortAllocation() public final boolean getStartNodesInProcess() @@ -3911,7 +3911,7 @@ public final class net.corda.testing.node.MockKeyManagementService extends net.c public class net.corda.testing.node.MockNetwork extends java.lang.Object public (List) public (List, net.corda.testing.node.MockNetworkParameters) - public (List, net.corda.testing.node.MockNetworkParameters, boolean, boolean, net.corda.testing.node.InMemoryMessagingNetwork$ServicePeerAllocationStrategy, boolean, List, int) + public (List, net.corda.testing.node.MockNetworkParameters, boolean, boolean, net.corda.testing.node.InMemoryMessagingNetwork$ServicePeerAllocationStrategy, boolean, List, net.corda.core.node.NetworkParameters) @org.jetbrains.annotations.NotNull public final java.nio.file.Path baseDirectory(int) @org.jetbrains.annotations.NotNull public final net.corda.testing.node.StartedMockNode createNode() @org.jetbrains.annotations.NotNull public final net.corda.testing.node.StartedMockNode createNode(net.corda.core.identity.CordaX500Name) @@ -3933,7 +3933,7 @@ public class net.corda.testing.node.MockNetwork extends java.lang.Object @org.jetbrains.annotations.NotNull public final net.corda.testing.node.StartedMockNode getDefaultNotaryNode() @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNetworkParameters getDefaultParameters() public final boolean getInitialiseSerialization() - public final int getMaxTransactionSize() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.NetworkParameters getNetworkParameters() public final boolean getNetworkSendManuallyPumped() public final int getNextNodeId() @org.jetbrains.annotations.NotNull public final List getNotaryNodes() @@ -3960,24 +3960,24 @@ public final class net.corda.testing.node.MockNetworkNotarySpec extends java.lan ## public final class net.corda.testing.node.MockNetworkParameters extends java.lang.Object public () - public (boolean, boolean, net.corda.testing.node.InMemoryMessagingNetwork$ServicePeerAllocationStrategy, boolean, List, int) + public (boolean, boolean, net.corda.testing.node.InMemoryMessagingNetwork$ServicePeerAllocationStrategy, boolean, List, net.corda.core.node.NetworkParameters) public final boolean component1() public final boolean component2() @org.jetbrains.annotations.NotNull public final net.corda.testing.node.InMemoryMessagingNetwork$ServicePeerAllocationStrategy component3() public final boolean component4() @org.jetbrains.annotations.NotNull public final List component5() - public final int component6() - @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNetworkParameters copy(boolean, boolean, net.corda.testing.node.InMemoryMessagingNetwork$ServicePeerAllocationStrategy, boolean, List, int) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.NetworkParameters component6() + @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNetworkParameters copy(boolean, boolean, net.corda.testing.node.InMemoryMessagingNetwork$ServicePeerAllocationStrategy, boolean, List, net.corda.core.node.NetworkParameters) public boolean equals(Object) public final boolean getInitialiseSerialization() - public final int getMaxTransactionSize() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.NetworkParameters getNetworkParameters() public final boolean getNetworkSendManuallyPumped() @org.jetbrains.annotations.NotNull public final List getNotarySpecs() @org.jetbrains.annotations.NotNull public final net.corda.testing.node.InMemoryMessagingNetwork$ServicePeerAllocationStrategy getServicePeerAllocationStrategy() public final boolean getThreadPerNode() public int hashCode() @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNetworkParameters setInitialiseSerialization(boolean) - @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNetworkParameters setMaxTransactionSize(int) + @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNetworkParameters setNetworkParameters(net.corda.core.node.NetworkParameters) @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNetworkParameters setNetworkSendManuallyPumped(boolean) @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNetworkParameters setNotarySpecs(List) @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNetworkParameters setServicePeerAllocationStrategy(net.corda.testing.node.InMemoryMessagingNetwork$ServicePeerAllocationStrategy) diff --git a/core/src/main/kotlin/net/corda/core/internal/GlobalProperties.kt b/core/src/main/kotlin/net/corda/core/internal/GlobalProperties.kt deleted file mode 100644 index bedac0269a..0000000000 --- a/core/src/main/kotlin/net/corda/core/internal/GlobalProperties.kt +++ /dev/null @@ -1,14 +0,0 @@ -package net.corda.core.internal - -import net.corda.core.node.NetworkParameters - -// TODO: This will cause problems when we run tests in parallel, make each node have its own properties. -object GlobalProperties { - private var _networkParameters: NetworkParameters? = null - - var networkParameters: NetworkParameters - get() = checkNotNull(_networkParameters) { "Property 'networkParameters' has not been initialised." } - set(value) { - _networkParameters = value - } -} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt index 2c31eb96f1..0d2f36ff43 100644 --- a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt +++ b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt @@ -61,6 +61,9 @@ interface ServicesForResolution : StateLoader { /** Provides access to anything relating to cordapps including contract attachment resolution and app context */ val cordappProvider: CordappProvider + + /** Returns the network parameters the node is operating under. */ + val networkParameters: NetworkParameters } /** diff --git a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt index d973152262..8e397b9051 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt @@ -5,7 +5,6 @@ import net.corda.core.contracts.ComponentGroupEnum.* import net.corda.core.crypto.* import net.corda.core.identity.Party import net.corda.core.internal.Emoji -import net.corda.core.internal.GlobalProperties import net.corda.core.node.ServicesForResolution import net.corda.core.node.services.AttachmentId import net.corda.core.serialization.CordaSerializable @@ -85,11 +84,12 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr */ @Throws(AttachmentResolutionException::class, TransactionResolutionException::class) fun toLedgerTransaction(services: ServicesForResolution): LedgerTransaction { - return toLedgerTransaction( + return toLedgerTransactionInternal( resolveIdentity = { services.identityService.partyFromKey(it) }, resolveAttachment = { services.attachments.openAttachment(it) }, resolveStateRef = { services.loadState(it) }, - resolveContractAttachment = { services.cordappProvider.getContractAttachmentID(it.contract) } + resolveContractAttachment = { services.cordappProvider.getContractAttachmentID(it.contract) }, + maxTransactionSize = services.networkParameters.maxTransactionSize ) } @@ -100,12 +100,23 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr * @throws AttachmentResolutionException if a required attachment was not found using [resolveAttachment]. * @throws TransactionResolutionException if an input was not found not using [resolveStateRef]. */ + @Deprecated("Use toLedgerTransaction(ServicesForTransaction) instead") @Throws(AttachmentResolutionException::class, TransactionResolutionException::class) fun toLedgerTransaction( resolveIdentity: (PublicKey) -> Party?, resolveAttachment: (SecureHash) -> Attachment?, resolveStateRef: (StateRef) -> TransactionState<*>?, resolveContractAttachment: (TransactionState) -> AttachmentId? + ): LedgerTransaction { + return toLedgerTransactionInternal(resolveIdentity, resolveAttachment, resolveStateRef, resolveContractAttachment, 10485760) + } + + private fun toLedgerTransactionInternal( + resolveIdentity: (PublicKey) -> Party?, + resolveAttachment: (SecureHash) -> Attachment?, + resolveStateRef: (StateRef) -> TransactionState<*>?, + resolveContractAttachment: (TransactionState) -> AttachmentId?, + maxTransactionSize: Int ): LedgerTransaction { // Look up public keys to authenticated identities. This is just a stub placeholder and will all change in future. val authenticatedArgs = commands.map { @@ -120,15 +131,15 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr // Order of attachments is important since contracts may refer to indexes so only append automatic attachments val attachments = (attachments.map { resolveAttachment(it) ?: throw AttachmentResolutionException(it) } + contractAttachments).distinct() val ltx = LedgerTransaction(resolvedInputs, outputs, authenticatedArgs, attachments, id, notary, timeWindow, privacySalt) - checkTransactionSize(ltx) + checkTransactionSize(ltx, maxTransactionSize) return ltx } - private fun checkTransactionSize(ltx: LedgerTransaction) { - var remainingTransactionSize = GlobalProperties.networkParameters.maxTransactionSize + private fun checkTransactionSize(ltx: LedgerTransaction, maxTransactionSize: Int) { + var remainingTransactionSize = maxTransactionSize fun minus(size: Int) { - require(remainingTransactionSize > size) { "Transaction exceeded network's maximum transaction size limit : ${GlobalProperties.networkParameters.maxTransactionSize} bytes." } + require(remainingTransactionSize > size) { "Transaction exceeded network's maximum transaction size limit : $maxTransactionSize bytes." } remainingTransactionSize -= size } diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 51b08d6924..ffe65a63bb 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -76,7 +76,10 @@ UNRELEASED :doc:`corda-configuration-file` for more details. * Introducing the concept of network parameters which are a set of constants which all nodes on a network must agree on - to correctly interop. + to correctly interop. These can be retrieved from ``ServiceHub.networkParameters``. + + * One of these parameters, ``maxTransactionSize``, limits the size of a transaction, including its attachments, so that + all nodes have sufficient memory to validate transactions. * The set of valid notaries has been moved to the network parameters. Notaries are no longer identified by the CN in their X500 name. @@ -89,9 +92,6 @@ UNRELEASED * Moved ``NodeInfoSchema`` to internal package as the node info's database schema is not part of the public API. This was needed to allow changes to the schema. - * Introduced max transaction size limit on transactions. The max transaction size parameter is set by the compatibility zone - operator. The parameter is distributed to Corda nodes by network map service as part of the ``NetworkParameters``. - * Support for external user credentials data source and password encryption [CORDA-827]. * Exporting additional JMX metrics (artemis, hibernate statistics) and loading Jolokia agent at JVM startup when using diff --git a/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt index 01582516b5..b419fdefcc 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/AttachmentLoadingTests.kt @@ -4,6 +4,9 @@ import com.nhaarman.mockito_kotlin.doReturn import com.nhaarman.mockito_kotlin.whenever import net.corda.core.contracts.Contract import net.corda.core.contracts.PartyAndReference +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.TransactionState +import net.corda.core.cordapp.CordappProvider import net.corda.core.flows.FlowLogic import net.corda.core.flows.UnexpectedFlowEndException import net.corda.core.identity.CordaX500Name @@ -12,7 +15,9 @@ import net.corda.core.internal.concurrent.transpose import net.corda.core.internal.createDirectories import net.corda.core.internal.div import net.corda.core.internal.toLedgerTransaction +import net.corda.core.node.NetworkParameters import net.corda.core.node.ServicesForResolution +import net.corda.core.node.services.AttachmentStorage import net.corda.core.node.services.IdentityService import net.corda.core.serialization.SerializationFactory import net.corda.core.transactions.TransactionBuilder @@ -20,6 +25,7 @@ import net.corda.core.utilities.contextLogger import net.corda.core.utilities.getOrThrow import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.internal.cordapp.CordappProviderImpl +import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.DUMMY_BANK_A_NAME import net.corda.testing.core.DUMMY_NOTARY_NAME import net.corda.testing.core.SerializationEnvironmentRule @@ -79,12 +85,14 @@ class AttachmentLoadingTests { } } - private val services = rigorousMock().also { - doReturn(attachments).whenever(it).attachments - doReturn(provider).whenever(it).cordappProvider - doReturn(rigorousMock().also { - doReturn(null).whenever(it).partyFromKey(DUMMY_BANK_A.owningKey) - }).whenever(it).identityService + private val services = object : ServicesForResolution { + override fun loadState(stateRef: StateRef): TransactionState<*> = throw NotImplementedError() + override val identityService = rigorousMock().apply { + doReturn(null).whenever(this).partyFromKey(DUMMY_BANK_A.owningKey) + } + override val attachments: AttachmentStorage get() = this@AttachmentLoadingTests.attachments + override val cordappProvider: CordappProvider get() = this@AttachmentLoadingTests.provider + override val networkParameters: NetworkParameters = testNetworkParameters() } @Test diff --git a/node/src/integration-test/kotlin/net/corda/node/services/statemachine/LargeTransactionsTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/statemachine/LargeTransactionsTest.kt index cd5f572d50..aa007be6d7 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/statemachine/LargeTransactionsTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/statemachine/LargeTransactionsTest.kt @@ -9,6 +9,8 @@ import net.corda.core.internal.concurrent.transpose import net.corda.core.messaging.startFlow import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.getOrThrow +import net.corda.node.services.config.MB +import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyState import net.corda.testing.core.* @@ -68,11 +70,15 @@ class LargeTransactionsTest { fun checkCanSendLargeTransactions() { // These 4 attachments yield a transaction that's got >10mb attached, so it'd push us over the Artemis // max message size. - val bigFile1 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024 * 3, 0) - val bigFile2 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024 * 3, 1) - val bigFile3 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024 * 3, 2) - val bigFile4 = InputStreamAndHash.createInMemoryTestZip(1024 * 1024 * 3, 3) - driver(DriverParameters(startNodesInProcess = true, extraCordappPackagesToScan = listOf("net.corda.testing.contracts"), portAllocation = PortAllocation.RandomFree)) { + val bigFile1 = InputStreamAndHash.createInMemoryTestZip(3.MB.toInt(), 0) + val bigFile2 = InputStreamAndHash.createInMemoryTestZip(3.MB.toInt(), 1) + val bigFile3 = InputStreamAndHash.createInMemoryTestZip(3.MB.toInt(), 2) + val bigFile4 = InputStreamAndHash.createInMemoryTestZip(3.MB.toInt(), 3) + driver(DriverParameters( + startNodesInProcess = true, + extraCordappPackagesToScan = listOf("net.corda.testing.contracts"), + networkParameters = testNetworkParameters(maxTransactionSize = 13.MB.toInt()), + portAllocation = PortAllocation.RandomFree)) { val rpcUser = User("admin", "admin", setOf("ALL")) val (alice, _) = listOf(ALICE_NAME, BOB_NAME).map { startNode(providedName = it, rpcUsers = listOf(rpcUser)) }.transpose().getOrThrow() CordaRPCClient(alice.rpcAddress).use(rpcUser.username, rpcUser.password) { diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index daf5873530..fbf823fba2 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -14,7 +14,6 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.FlowStateMachine -import net.corda.core.internal.GlobalProperties import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.concurrent.map import net.corda.core.internal.concurrent.openFuture @@ -59,8 +58,8 @@ import net.corda.node.services.vault.NodeVaultService import net.corda.node.services.vault.VaultSoftLockManager import net.corda.node.shell.InteractiveShell import net.corda.node.utilities.AffinityExecutor -import net.corda.node.utilities.NodeBuildProperties import net.corda.node.utilities.JVMAgentRegistry +import net.corda.node.utilities.NodeBuildProperties import net.corda.nodeapi.internal.DevIdentityGenerator import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.persistence.CordaPersistence @@ -200,18 +199,27 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val (identity, identityKeyPair) = obtainIdentity(notaryConfig = null) val identityService = makeIdentityService(identity.certificate) networkMapClient = configuration.compatibilityZoneURL?.let { NetworkMapClient(it, identityService.trustRoot) } - GlobalProperties.networkParameters = NetworkParametersReader(identityService.trustRoot, networkMapClient, configuration.baseDirectory).networkParameters - check(GlobalProperties.networkParameters.minimumPlatformVersion <= versionInfo.platformVersion) { + val networkParameters = NetworkParametersReader(identityService.trustRoot, networkMapClient, configuration.baseDirectory).networkParameters + check(networkParameters.minimumPlatformVersion <= versionInfo.platformVersion) { "Node's platform version is lower than network's required minimumPlatformVersion" } // Do all of this in a database transaction so anything that might need a connection has one. val (startedImpl, schedulerService) = initialiseDatabasePersistence(schemaService, identityService) { database -> - val networkMapCache = NetworkMapCacheImpl(PersistentNetworkMapCache(database, GlobalProperties.networkParameters.notaries).start(), identityService) + val networkMapCache = NetworkMapCacheImpl(PersistentNetworkMapCache(database, networkParameters.notaries).start(), identityService) val (keyPairs, nodeInfo) = initNodeInfo(networkMapCache, identity, identityKeyPair) identityService.loadIdentities(nodeInfo.legalIdentitiesAndCerts) val transactionStorage = makeTransactionStorage(database, configuration.transactionCacheSizeBytes) val nodeProperties = NodePropertiesPersistentStore(StubbedNodeUniqueIdProvider::value, database) - val nodeServices = makeServices(keyPairs, schemaService, transactionStorage, database, nodeInfo, identityService, networkMapCache, nodeProperties) + val nodeServices = makeServices( + keyPairs, + schemaService, + transactionStorage, + database, + nodeInfo, + identityService, + networkMapCache, + nodeProperties, + networkParameters) val notaryService = makeNotaryService(nodeServices, database) val smm = makeStateMachineManager(database) val flowLogicRefFactory = FlowLogicRefFactoryImpl(cordappLoader.appClassLoader) @@ -248,7 +256,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, networkMapUpdater = NetworkMapUpdater(services.networkMapCache, NodeInfoWatcher(configuration.baseDirectory, getRxIoScheduler(), Duration.ofMillis(configuration.additionalNodeInfoPollingFrequencyMsec)), networkMapClient, - GlobalProperties.networkParameters.serialize().hash, + networkParameters.serialize().hash, configuration.baseDirectory) runOnStop += networkMapUpdater::close @@ -543,7 +551,8 @@ abstract class AbstractNode(val configuration: NodeConfiguration, nodeInfo: NodeInfo, identityService: IdentityServiceInternal, networkMapCache: NetworkMapCacheInternal, - nodeProperties: NodePropertiesStore): MutableList { + nodeProperties: NodePropertiesStore, + networkParameters: NetworkParameters): MutableList { checkpointStorage = DBCheckpointStorage() val metrics = MetricRegistry() attachments = NodeAttachmentService(metrics, configuration.attachmentContentCacheSizeBytes, configuration.attachmentCacheBound) @@ -559,8 +568,9 @@ abstract class AbstractNode(val configuration: NodeConfiguration, database, nodeInfo, networkMapCache, - nodeProperties) - network = makeMessagingService(database, nodeInfo, nodeProperties) + nodeProperties, + networkParameters) + network = makeMessagingService(database, nodeInfo, nodeProperties, networkParameters) val tokenizableServices = mutableListOf(attachments, network, services.vaultService, services.keyManagementService, services.identityService, platformClock, services.auditService, services.monitoringService, services.networkMapCache, services.schemaService, @@ -695,7 +705,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, _started = null } - protected abstract fun makeMessagingService(database: CordaPersistence, info: NodeInfo, nodeProperties: NodePropertiesStore): MessagingService + protected abstract fun makeMessagingService(database: CordaPersistence, info: NodeInfo, nodeProperties: NodePropertiesStore, networkParameters: NetworkParameters): MessagingService protected abstract fun startMessagingService(rpcOps: RPCOps) private fun obtainIdentity(notaryConfig: NotaryConfig?): Pair { @@ -783,7 +793,8 @@ abstract class AbstractNode(val configuration: NodeConfiguration, override val database: CordaPersistence, override val myInfo: NodeInfo, override val networkMapCache: NetworkMapCacheInternal, - override val nodeProperties: NodePropertiesStore + override val nodeProperties: NodePropertiesStore, + override val networkParameters: NetworkParameters ) : SingletonSerializeAsToken(), ServiceHubInternal, StateLoader by validatedTransactions { override val rpcFlows = ArrayList>>() override val stateMachineRecordedTransactionMapping = DBTransactionMappingStorage() diff --git a/node/src/main/kotlin/net/corda/node/internal/Node.kt b/node/src/main/kotlin/net/corda/node/internal/Node.kt index 395577f49e..d15d749d05 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -2,12 +2,12 @@ package net.corda.node.internal import com.codahale.metrics.JmxReporter import net.corda.core.concurrent.CordaFuture -import net.corda.core.internal.GlobalProperties.networkParameters import net.corda.core.internal.concurrent.openFuture import net.corda.core.internal.concurrent.thenMatch import net.corda.core.internal.div import net.corda.core.internal.uncheckedCast import net.corda.core.messaging.RPCOps +import net.corda.core.node.NetworkParameters import net.corda.core.node.NodeInfo import net.corda.core.node.ServiceHub import net.corda.core.node.services.IdentityService @@ -147,7 +147,10 @@ open class Node(configuration: NodeConfiguration, private var shutdownHook: ShutdownHook? = null - override fun makeMessagingService(database: CordaPersistence, info: NodeInfo, nodeProperties: NodePropertiesStore): MessagingService { + override fun makeMessagingService(database: CordaPersistence, + info: NodeInfo, + nodeProperties: NodePropertiesStore, + networkParameters: NetworkParameters): MessagingService { // Construct security manager reading users data either from the 'security' config section // if present or from rpcUsers list if the former is missing from config. val securityManagerConfig = configuration.security?.authService ?: @@ -155,8 +158,12 @@ open class Node(configuration: NodeConfiguration, securityManager = RPCSecurityManagerImpl(securityManagerConfig) - val serverAddress = configuration.messagingServerAddress ?: makeLocalMessageBroker() - val rpcServerAddresses = if (configuration.rpcOptions.standAloneBroker) BrokerAddresses(configuration.rpcOptions.address!!, configuration.rpcOptions.adminAddress) else startLocalRpcBroker() + val serverAddress = configuration.messagingServerAddress ?: makeLocalMessageBroker(networkParameters) + val rpcServerAddresses = if (configuration.rpcOptions.standAloneBroker) { + BrokerAddresses(configuration.rpcOptions.address!!, configuration.rpcOptions.adminAddress) + } else { + startLocalRpcBroker(networkParameters) + } val advertisedAddress = info.addresses[0] bridgeControlListener = BridgeControlListener(configuration, serverAddress, networkParameters.maxMessageSize) @@ -185,16 +192,31 @@ open class Node(configuration: NodeConfiguration, drainingModeWasChangedEvents = nodeProperties.flowsDrainingMode.values) } - private fun startLocalRpcBroker(): BrokerAddresses? { + private fun startLocalRpcBroker(networkParameters: NetworkParameters): BrokerAddresses? { with(configuration) { return rpcOptions.address?.let { require(rpcOptions.address != null) { "RPC address needs to be specified for local RPC broker." } val rpcBrokerDirectory: Path = baseDirectory / "brokers" / "rpc" with(rpcOptions) { rpcBroker = if (useSsl) { - ArtemisRpcBroker.withSsl(this.address!!, sslConfig, securityManager, certificateChainCheckPolicies, networkParameters.maxMessageSize, jmxMonitoringHttpPort != null, rpcBrokerDirectory) + ArtemisRpcBroker.withSsl( + this.address!!, + sslConfig, + securityManager, + certificateChainCheckPolicies, + networkParameters.maxMessageSize, + jmxMonitoringHttpPort != null, + rpcBrokerDirectory) } else { - ArtemisRpcBroker.withoutSsl(this.address!!, adminAddress!!, sslConfig, securityManager, certificateChainCheckPolicies, networkParameters.maxMessageSize, jmxMonitoringHttpPort != null, rpcBrokerDirectory) + ArtemisRpcBroker.withoutSsl( + this.address!!, + adminAddress!!, + sslConfig, + securityManager, + certificateChainCheckPolicies, + networkParameters.maxMessageSize, + jmxMonitoringHttpPort != null, + rpcBrokerDirectory) } } return rpcBroker!!.addresses @@ -202,16 +224,14 @@ open class Node(configuration: NodeConfiguration, } } - private fun makeLocalMessageBroker(): NetworkHostAndPort { + private fun makeLocalMessageBroker(networkParameters: NetworkParameters): NetworkHostAndPort { with(configuration) { messageBroker = ArtemisMessagingServer(this, p2pAddress.port, networkParameters.maxMessageSize) return NetworkHostAndPort("localhost", p2pAddress.port) } } - override fun myAddresses(): List { - return listOf(getAdvertisedAddress()) - } + override fun myAddresses(): List = listOf(getAdvertisedAddress()) private fun getAdvertisedAddress(): NetworkHostAndPort { return with(configuration) { diff --git a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapClientTest.kt b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapClientTest.kt index 283ae2c71a..6582196e27 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapClientTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapClientTest.kt @@ -98,7 +98,7 @@ class NetworkMapClientTest { @Test fun `handle parameters update`() { - val nextParameters = testNetworkParameters(emptyList(), epoch = 2) + val nextParameters = testNetworkParameters(epoch = 2) val originalNetworkParameterHash = server.networkParameters.serialize().hash val nextNetworkParameterHash = nextParameters.serialize().hash val description = "Test parameters" diff --git a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt index a00bf44a35..fd0214955d 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/NetworkMapUpdaterTest.kt @@ -201,7 +201,7 @@ class NetworkMapUpdaterTest { val snapshot = paramsFeed.snapshot val updates = paramsFeed.updates.bufferUntilSubscribed() assertEquals(null, snapshot) - val newParameters = testNetworkParameters(emptyList(), epoch = 2) + val newParameters = testNetworkParameters(epoch = 2) val updateDeadline = Instant.now().plus(1, ChronoUnit.DAYS) scheduleParametersUpdate(newParameters, "Test update", updateDeadline) updater.subscribeToNetworkMap() @@ -219,7 +219,7 @@ class NetworkMapUpdaterTest { @Test fun `ack network parameters update`() { - val newParameters = testNetworkParameters(emptyList(), epoch = 314) + val newParameters = testNetworkParameters(epoch = 314) scheduleParametersUpdate(newParameters, "Test update", Instant.MIN) updater.subscribeToNetworkMap() // TODO: Remove sleep in unit test. diff --git a/node/src/test/kotlin/net/corda/node/services/network/NetworkParametersReaderTest.kt b/node/src/test/kotlin/net/corda/node/services/network/NetworkParametersReaderTest.kt index abd8d33fd1..86fba98581 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/NetworkParametersReaderTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/NetworkParametersReaderTest.kt @@ -50,7 +50,7 @@ class NetworkParametersReaderTest { fun `read correct set of parameters from file`() { val fs = Jimfs.newFileSystem(Configuration.unix()) val baseDirectory = fs.getPath("/node").createDirectories() - val oldParameters = testNetworkParameters(emptyList(), epoch = 1) + val oldParameters = testNetworkParameters(epoch = 1) NetworkParametersCopier(oldParameters).install(baseDirectory) NetworkParametersCopier(server.networkParameters, update = true).install(baseDirectory) // Parameters update file. val parameters = NetworkParametersReader(DEV_ROOT_CA.certificate, networkMapClient, baseDirectory).networkParameters diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/MaxTransactionSizeTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/MaxTransactionSizeTests.kt index 7b6a077b8d..1b041bcde8 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/MaxTransactionSizeTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/MaxTransactionSizeTests.kt @@ -8,6 +8,7 @@ import net.corda.core.internal.InputStreamAndHash import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.getOrThrow import net.corda.node.services.api.StartedNodeServices +import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyState import net.corda.testing.core.ALICE_NAME @@ -34,7 +35,9 @@ class MaxTransactionSizeTests { @Before fun setup() { - mockNet = MockNetwork(listOf("net.corda.testing.contracts", "net.corda.node.services.transactions"), maxTransactionSize = 3_000_000) + mockNet = MockNetwork( + listOf("net.corda.testing.contracts", "net.corda.node.services.transactions"), + networkParameters = testNetworkParameters(maxTransactionSize = 3_000_000)) val aliceNode = mockNet.createNode(MockNodeParameters(legalName = ALICE_NAME)) val bobNode = mockNet.createNode(MockNodeParameters(legalName = BOB_NAME)) notaryServices = mockNet.defaultNotaryNode.services diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt index c75bf3f447..04e0b34c37 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt @@ -110,7 +110,7 @@ class VaultQueryTests { cordappPackages, makeTestIdentityService(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, dummyCashIssuer.identity, dummyNotary.identity), megaCorp, - DUMMY_NOTARY_KEY) + moreKeys = DUMMY_NOTARY_KEY) database = databaseAndServices.first services = databaseAndServices.second vaultFiller = VaultFiller(services, dummyNotary) diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt index 1dcb7cb984..e7e3a50ea2 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt @@ -1,6 +1,9 @@ package net.corda.node.services.vault -import net.corda.core.contracts.* +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.InsufficientBalanceException +import net.corda.core.contracts.LinearState +import net.corda.core.contracts.UniqueIdentifier import net.corda.core.crypto.generateKeyPair import net.corda.core.identity.AnonymousParty import net.corda.core.identity.CordaX500Name @@ -74,7 +77,7 @@ class VaultWithCashTest { cordappPackages, makeTestIdentityService(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, dummyCashIssuer.identity, dummyNotary.identity), TestIdentity(MEGA_CORP.name, servicesKey), - dummyNotary.keyPair) + moreKeys = dummyNotary.keyPair) database = databaseAndServices.first services = databaseAndServices.second vaultFiller = VaultFiller(services, dummyNotary) diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt index bf06b7cb08..03ce962d2a 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt @@ -8,12 +8,13 @@ import net.corda.core.flows.FlowLogic import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.messaging.CordaRPCOps +import net.corda.core.node.NetworkParameters import net.corda.core.node.NodeInfo import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.internal.Node import net.corda.node.services.api.StartedNodeServices import net.corda.node.services.config.VerifierType -import net.corda.nodeapi.internal.persistence.CordaPersistence +import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.DUMMY_NOTARY_NAME import net.corda.testing.node.NotarySpec import net.corda.testing.node.User @@ -25,7 +26,6 @@ import java.net.InetSocketAddress import java.net.ServerSocket import java.nio.file.Path import java.nio.file.Paths -import java.sql.Connection import java.util.concurrent.atomic.AtomicInteger /** @@ -109,6 +109,12 @@ data class NodeParameters( fun setMaximumHeapSize(maximumHeapSize: String): NodeParameters = copy(maximumHeapSize = maximumHeapSize) } +/** + * @property startJmxHttpServer Indicates whether the spawned nodes should start with a Jolokia JMX agent to enable remote + * JMX monitoring using HTTP/JSON + * @property jmxHttpServerPortAllocation The port allocation strategy to use for remote Jolokia/JMX monitoring over HTTP. + * Defaults to incremental. + */ data class JmxPolicy(val startJmxHttpServer: Boolean = false, val jmxHttpServerPortAllocation: PortAllocation? = if (startJmxHttpServer) PortAllocation.Incremental(7005) else null) @@ -128,30 +134,10 @@ data class JmxPolicy(val startJmxHttpServer: Boolean = false, * * @param defaultParameters The default parameters for the driver. Allows the driver to be configured in builder style * when called from Java code. - * @param isDebug Indicates whether the spawned nodes should start in jdwt debug mode and have debug level logging. - * @param driverDirectory The base directory node directories go into, defaults to "build//". The node - * directories themselves are "//", where legalName defaults to "-" - * and may be specified in [DriverDSL.startNode]. - * @param portAllocation The port allocation strategy to use for the messaging and the web server addresses. Defaults to incremental. - * @param debugPortAllocation The port allocation strategy to use for jvm debugging. Defaults to incremental. - * @param systemProperties A Map of extra system properties which will be given to each new node. Defaults to empty. - * @param useTestClock If true the test clock will be used in Node. - * @param startNodesInProcess Provides the default behaviour of whether new nodes should start inside this process or - * not. Note that this may be overridden in [DriverDSL.startNode]. - * @param waitForAllNodesToFinish If true, the nodes will not shut down automatically after executing the code in the driver DSL block. - * It will wait for them to be shut down externally instead. - * @param notarySpecs The notaries advertised for this network. These nodes will be started automatically and will be - * available from [DriverDSL.notaryHandles]. Defaults to a simple validating notary. - * @param jmxPolicy Used to specify whether to expose JMX metrics via Jolokia HHTP/JSON. Defines two attributes: - * startJmxHttpServer: indicates whether the spawned nodes should start with a Jolokia JMX agent to enable remote JMX monitoring using HTTP/JSON. - * jmxHttpServerPortAllocation: the port allocation strategy to use for remote Jolokia/JMX monitoring over HTTP. Defaults to incremental. - * @param dsl The dsl itself. + * @property dsl The dsl itself. * @return The value returned in the [dsl] closure. */ -fun driver( - defaultParameters: DriverParameters = DriverParameters(), - dsl: DriverDSL.() -> A -): A { +fun driver(defaultParameters: DriverParameters = DriverParameters(), dsl: DriverDSL.() -> A): A { return genericDriver( driverDsl = DriverDSLImpl( portAllocation = defaultParameters.portAllocation, @@ -166,7 +152,7 @@ fun driver( extraCordappPackagesToScan = defaultParameters.extraCordappPackagesToScan, jmxPolicy = defaultParameters.jmxPolicy, compatibilityZone = null, - maxTransactionSize = defaultParameters.maxTransactionSize + networkParameters = defaultParameters.networkParameters ), coerce = { it }, dsl = dsl, @@ -174,7 +160,27 @@ fun driver( ) } -/** Helper builder for configuring a [driver] from Java. */ +/** + * Builder for configuring a [driver]. + * @property isDebug Indicates whether the spawned nodes should start in jdwt debug mode and have debug level logging. + * @property driverDirectory The base directory node directories go into, defaults to "build//". The node + * directories themselves are "//", where legalName defaults to "-" + * and may be specified in [DriverDSL.startNode]. + * @property portAllocation The port allocation strategy to use for the messaging and the web server addresses. Defaults + * to incremental. + * @property debugPortAllocation The port allocation strategy to use for jvm debugging. Defaults to incremental. + * @property systemProperties A Map of extra system properties which will be given to each new node. Defaults to empty. + * @property useTestClock If true the test clock will be used in Node. + * @property startNodesInProcess Provides the default behaviour of whether new nodes should start inside this process or + * not. Note that this may be overridden in [DriverDSL.startNode]. + * @property waitForAllNodesToFinish If true, the nodes will not shut down automatically after executing the code in the + * driver DSL block. It will wait for them to be shut down externally instead. + * @property notarySpecs The notaries advertised for this network. These nodes will be started automatically and will be + * available from [DriverDSL.notaryHandles]. Defaults to a simple validating notary. + * @property jmxPolicy Used to specify whether to expose JMX metrics via Jolokia HHTP/JSON. + * @property networkParameters The network parmeters to be used by all the nodes. [NetworkParameters.notaries] must be + * empty as notaries are defined by [notarySpecs]. + */ @Suppress("unused") data class DriverParameters( val isDebug: Boolean = false, @@ -189,18 +195,19 @@ data class DriverParameters( val notarySpecs: List = listOf(NotarySpec(DUMMY_NOTARY_NAME)), val extraCordappPackagesToScan: List = emptyList(), val jmxPolicy: JmxPolicy = JmxPolicy(), - val maxTransactionSize: Int = Int.MAX_VALUE + val networkParameters: NetworkParameters = testNetworkParameters() ) { - fun setIsDebug(isDebug: Boolean) = copy(isDebug = isDebug) - fun setDriverDirectory(driverDirectory: Path) = copy(driverDirectory = driverDirectory) - fun setPortAllocation(portAllocation: PortAllocation) = copy(portAllocation = portAllocation) - fun setDebugPortAllocation(debugPortAllocation: PortAllocation) = copy(debugPortAllocation = debugPortAllocation) - fun setSystemProperties(systemProperties: Map) = copy(systemProperties = systemProperties) - fun setUseTestClock(useTestClock: Boolean) = copy(useTestClock = useTestClock) - fun setInitialiseSerialization(initialiseSerialization: Boolean) = copy(initialiseSerialization = initialiseSerialization) - fun setStartNodesInProcess(startNodesInProcess: Boolean) = copy(startNodesInProcess = startNodesInProcess) - fun setWaitForAllNodesToFinish(waitForAllNodesToFinish: Boolean) = copy(waitForAllNodesToFinish = waitForAllNodesToFinish) - fun setNotarySpecs(notarySpecs: List) = copy(notarySpecs = notarySpecs) - fun setExtraCordappPackagesToScan(extraCordappPackagesToScan: List) = copy(extraCordappPackagesToScan = extraCordappPackagesToScan) - fun setJmxPolicy(jmxPolicy: JmxPolicy) = copy(jmxPolicy = jmxPolicy) -} \ No newline at end of file + fun setIsDebug(isDebug: Boolean): DriverParameters = copy(isDebug = isDebug) + fun setDriverDirectory(driverDirectory: Path): DriverParameters = copy(driverDirectory = driverDirectory) + fun setPortAllocation(portAllocation: PortAllocation): DriverParameters = copy(portAllocation = portAllocation) + fun setDebugPortAllocation(debugPortAllocation: PortAllocation): DriverParameters = copy(debugPortAllocation = debugPortAllocation) + fun setSystemProperties(systemProperties: Map): DriverParameters = copy(systemProperties = systemProperties) + fun setUseTestClock(useTestClock: Boolean): DriverParameters = copy(useTestClock = useTestClock) + fun setInitialiseSerialization(initialiseSerialization: Boolean): DriverParameters = copy(initialiseSerialization = initialiseSerialization) + fun setStartNodesInProcess(startNodesInProcess: Boolean): DriverParameters = copy(startNodesInProcess = startNodesInProcess) + fun setWaitForAllNodesToFinish(waitForAllNodesToFinish: Boolean): DriverParameters = copy(waitForAllNodesToFinish = waitForAllNodesToFinish) + fun setNotarySpecs(notarySpecs: List): DriverParameters = copy(notarySpecs = notarySpecs) + fun setExtraCordappPackagesToScan(extraCordappPackagesToScan: List): DriverParameters = copy(extraCordappPackagesToScan = extraCordappPackagesToScan) + fun setJmxPolicy(jmxPolicy: JmxPolicy): DriverParameters = copy(jmxPolicy = jmxPolicy) + fun setNetworkParameters(networkParameters: NetworkParameters): DriverParameters = copy(networkParameters = networkParameters) +} diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetwork.kt index ad951d99c1..b275c01ba6 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetwork.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetwork.kt @@ -6,15 +6,14 @@ import net.corda.core.crypto.random63BitValue import net.corda.core.flows.FlowLogic import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party +import net.corda.core.node.NetworkParameters import net.corda.core.node.NodeInfo import net.corda.node.VersionInfo import net.corda.node.internal.StartedNode import net.corda.node.services.api.StartedNodeServices import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.messaging.MessagingService -import net.corda.nodeapi.internal.persistence.CordaPersistence -import net.corda.nodeapi.internal.persistence.DatabaseTransaction -import net.corda.nodeapi.internal.persistence.TransactionIsolationLevel +import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.DUMMY_NOTARY_NAME import net.corda.testing.node.internal.InternalMockNetwork import net.corda.testing.node.internal.setMessagingServiceSpy @@ -53,13 +52,13 @@ data class MockNetworkParameters( val servicePeerAllocationStrategy: InMemoryMessagingNetwork.ServicePeerAllocationStrategy = InMemoryMessagingNetwork.ServicePeerAllocationStrategy.Random(), val initialiseSerialization: Boolean = true, val notarySpecs: List = listOf(MockNetworkNotarySpec(DUMMY_NOTARY_NAME)), - val maxTransactionSize: Int = Int.MAX_VALUE) { + val networkParameters: NetworkParameters = testNetworkParameters()) { fun setNetworkSendManuallyPumped(networkSendManuallyPumped: Boolean): MockNetworkParameters = copy(networkSendManuallyPumped = networkSendManuallyPumped) fun setThreadPerNode(threadPerNode: Boolean): MockNetworkParameters = copy(threadPerNode = threadPerNode) fun setServicePeerAllocationStrategy(servicePeerAllocationStrategy: InMemoryMessagingNetwork.ServicePeerAllocationStrategy): MockNetworkParameters = copy(servicePeerAllocationStrategy = servicePeerAllocationStrategy) fun setInitialiseSerialization(initialiseSerialization: Boolean): MockNetworkParameters = copy(initialiseSerialization = initialiseSerialization) fun setNotarySpecs(notarySpecs: List): MockNetworkParameters = copy(notarySpecs = notarySpecs) - fun setMaxTransactionSize(maxTransactionSize: Int): MockNetworkParameters = copy(maxTransactionSize = maxTransactionSize) + fun setNetworkParameters(networkParameters: NetworkParameters): MockNetworkParameters = copy(networkParameters = networkParameters) } /** Represents a node configuration for injection via [MockNetworkParameters] **/ @@ -143,11 +142,19 @@ open class MockNetwork( val servicePeerAllocationStrategy: InMemoryMessagingNetwork.ServicePeerAllocationStrategy = defaultParameters.servicePeerAllocationStrategy, val initialiseSerialization: Boolean = defaultParameters.initialiseSerialization, val notarySpecs: List = defaultParameters.notarySpecs, - val maxTransactionSize: Int = defaultParameters.maxTransactionSize) { + val networkParameters: NetworkParameters = defaultParameters.networkParameters) { @JvmOverloads constructor(cordappPackages: List, parameters: MockNetworkParameters = MockNetworkParameters()) : this(cordappPackages, defaultParameters = parameters) - private val internalMockNetwork: InternalMockNetwork = InternalMockNetwork(cordappPackages, defaultParameters, networkSendManuallyPumped, threadPerNode, servicePeerAllocationStrategy, initialiseSerialization, notarySpecs, maxTransactionSize) + private val internalMockNetwork = InternalMockNetwork( + cordappPackages, + defaultParameters, + networkSendManuallyPumped, + threadPerNode, + servicePeerAllocationStrategy, + initialiseSerialization, + notarySpecs, + networkParameters) val defaultNotaryNode get() : StartedMockNode = StartedMockNode.create(internalMockNetwork.defaultNotaryNode) val defaultNotaryIdentity get() : Party = internalMockNetwork.defaultNotaryIdentity val notaryNodes get() : List = internalMockNetwork.notaryNodes.map { StartedMockNode.create(it) } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt index f993b38298..3dd0e0f8be 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -7,7 +7,6 @@ import net.corda.core.crypto.* import net.corda.core.flows.FlowLogic import net.corda.core.identity.CordaX500Name import net.corda.core.identity.PartyAndCertificate -import net.corda.core.internal.GlobalProperties import net.corda.core.messaging.DataFeed import net.corda.core.messaging.FlowHandle import net.corda.core.messaging.FlowProgressHandle @@ -33,10 +32,11 @@ import net.corda.node.services.vault.NodeVaultService import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.HibernateConfiguration +import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.TestIdentity import net.corda.testing.internal.DEV_ROOT_CA -import net.corda.testing.services.MockAttachmentStorage import net.corda.testing.internal.MockCordappProvider +import net.corda.testing.services.MockAttachmentStorage import org.bouncycastle.operator.ContentSigner import rx.Observable import rx.subjects.PublishSubject @@ -62,6 +62,7 @@ open class MockServices private constructor( cordappLoader: CordappLoader, override val validatedTransactions: WritableTransactionStorage, override val identityService: IdentityService, + override val networkParameters: NetworkParameters, private val initialIdentity: TestIdentity, private val moreKeys: Array ) : ServiceHub, StateLoader by validatedTransactions { @@ -95,16 +96,18 @@ open class MockServices private constructor( * @return a pair where the first element is the instance of [CordaPersistence] and the second is [MockServices]. */ @JvmStatic + @JvmOverloads fun makeTestDatabaseAndMockServices(cordappPackages: List, identityService: IdentityService, initialIdentity: TestIdentity, + networkParameters: NetworkParameters = testNetworkParameters(), vararg moreKeys: KeyPair): Pair { val cordappLoader = CordappLoader.createWithTestPackages(cordappPackages) val dataSourceProps = makeTestDataSourceProperties() val schemaService = NodeSchemaService(cordappLoader.cordappSchemas) val database = configureDatabase(dataSourceProps, DatabaseConfig(), identityService, schemaService) val mockService = database.transaction { - object : MockServices(cordappLoader, identityService, initialIdentity, moreKeys) { + object : MockServices(cordappLoader, identityService, networkParameters, initialIdentity, moreKeys) { override val vaultService: VaultServiceInternal = makeVaultService(database.hibernateConfig, schemaService) override fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable) { @@ -128,30 +131,43 @@ open class MockServices private constructor( } } - private constructor(cordappLoader: CordappLoader, identityService: IdentityService, + private constructor(cordappLoader: CordappLoader, identityService: IdentityService, networkParameters: NetworkParameters, initialIdentity: TestIdentity, moreKeys: Array) - : this(cordappLoader, MockTransactionStorage(), identityService, initialIdentity, moreKeys) + : this(cordappLoader, MockTransactionStorage(), identityService, networkParameters, initialIdentity, moreKeys) /** * Create a mock [ServiceHub] that looks for app code in the given package names, uses the provided identity service * (you can get one from [makeTestIdentityService]) and represents the given identity. */ @JvmOverloads - constructor(cordappPackages: List, initialIdentity: TestIdentity, identityService: IdentityService = makeTestIdentityService(), vararg moreKeys: KeyPair) : this(CordappLoader.createWithTestPackages(cordappPackages), identityService, initialIdentity, moreKeys) + constructor(cordappPackages: List, + initialIdentity: TestIdentity, + identityService: IdentityService = makeTestIdentityService(), + vararg moreKeys: KeyPair) : + this(CordappLoader.createWithTestPackages(cordappPackages), identityService, testNetworkParameters(), initialIdentity, moreKeys) + + constructor(cordappPackages: List, + initialIdentity: TestIdentity, + identityService: IdentityService, + networkParameters: NetworkParameters, + vararg moreKeys: KeyPair) : + this(CordappLoader.createWithTestPackages(cordappPackages), identityService, networkParameters, initialIdentity, moreKeys) /** * Create a mock [ServiceHub] that looks for app code in the given package names, uses the provided identity service * (you can get one from [makeTestIdentityService]) and represents the given identity. */ @JvmOverloads - constructor(cordappPackages: List, initialIdentityName: CordaX500Name, identityService: IdentityService = makeTestIdentityService(), key: KeyPair, vararg moreKeys: KeyPair) : this(cordappPackages, TestIdentity(initialIdentityName, key), identityService, *moreKeys) + constructor(cordappPackages: List, initialIdentityName: CordaX500Name, identityService: IdentityService = makeTestIdentityService(), key: KeyPair, vararg moreKeys: KeyPair) : + this(cordappPackages, TestIdentity(initialIdentityName, key), identityService, *moreKeys) /** * Create a mock [ServiceHub] that can't load CorDapp code, which uses the provided identity service * (you can get one from [makeTestIdentityService]) and which represents the given identity. */ @JvmOverloads - constructor(cordappPackages: List, initialIdentityName: CordaX500Name, identityService: IdentityService = makeTestIdentityService()) : this(cordappPackages, TestIdentity(initialIdentityName), identityService) + constructor(cordappPackages: List, initialIdentityName: CordaX500Name, identityService: IdentityService = makeTestIdentityService()) : + this(cordappPackages, TestIdentity(initialIdentityName), identityService) /** * Create a mock [ServiceHub] that can't load CorDapp code, and which uses a default service identity. diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/NodeTestUtils.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/NodeTestUtils.kt index 008a0c2e81..dcf47f96b3 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/NodeTestUtils.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/NodeTestUtils.kt @@ -9,13 +9,11 @@ import net.corda.core.context.InvocationContext import net.corda.core.flows.FlowLogic import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party -import net.corda.core.internal.GlobalProperties import net.corda.core.node.ServiceHub import net.corda.core.serialization.internal.effectiveSerializationEnv import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.getOrThrow import net.corda.node.services.api.StartedNodeServices -import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.core.TestIdentity import net.corda.testing.core.chooseIdentity @@ -36,7 +34,6 @@ fun ServiceHub.ledger( false } return LedgerDSL(TestLedgerDSLInterpreter(this), notary).apply { - GlobalProperties.networkParameters = testNetworkParameters(emptyList()) if (serializationExists) { script() } else { diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt index f5978eded5..e7cc449d84 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt @@ -15,6 +15,7 @@ import net.corda.core.identity.CordaX500Name import net.corda.core.internal.* import net.corda.core.internal.concurrent.* import net.corda.core.messaging.CordaRPCOps +import net.corda.core.node.NetworkParameters import net.corda.core.node.NotaryInfo import net.corda.core.node.services.NetworkMapCache import net.corda.core.serialization.deserialize @@ -39,15 +40,14 @@ import net.corda.nodeapi.internal.crypto.X509KeyStore import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.network.NetworkParametersCopier import net.corda.nodeapi.internal.network.NodeInfoFilesCopier -import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import net.corda.testing.core.DUMMY_BANK_A_NAME -import net.corda.testing.internal.setGlobalSerialization import net.corda.testing.driver.* import net.corda.testing.driver.internal.InProcessImpl import net.corda.testing.driver.internal.NodeHandleInternal import net.corda.testing.driver.internal.OutOfProcessImpl +import net.corda.testing.internal.setGlobalSerialization import net.corda.testing.node.ClusterSpec import net.corda.testing.node.MockServices.Companion.MOCK_VERSION_INFO import net.corda.testing.node.NotarySpec @@ -92,7 +92,7 @@ class DriverDSLImpl( val jmxPolicy: JmxPolicy, val notarySpecs: List, val compatibilityZone: CompatibilityZoneParams?, - val maxTransactionSize: Int + val networkParameters: NetworkParameters ) : InternalDriverDSL { private var _executorService: ScheduledExecutorService? = null val executorService get() = _executorService!! @@ -387,6 +387,7 @@ class DriverDSLImpl( if (startNodesInProcess) { Schedulers.reset() } + require(networkParameters.notaries.isEmpty()) { "Define notaries using notarySpecs" } _executorService = Executors.newScheduledThreadPool(2, ThreadFactoryBuilder().setNameFormat("driver-pool-thread-%d").build()) _shutdownManager = ShutdownManager(executorService) val notaryInfosFuture = if (compatibilityZone == null) { @@ -704,7 +705,7 @@ class DriverDSLImpl( * The local version of the network map, which is a bunch of classes that copy the relevant files to the node directories. */ private inner class LocalNetworkMap(notaryInfos: List) { - val networkParametersCopier = NetworkParametersCopier(testNetworkParameters(notaryInfos, maxTransactionSize = maxTransactionSize)) + val networkParametersCopier = NetworkParametersCopier(networkParameters.copy(notaries = notaryInfos)) // TODO: this object will copy NodeInfo files from started nodes to other nodes additional-node-infos/ // This uses the FileSystem and adds a delay (~5 seconds) given by the time we wait before polling the file system. // Investigate whether we can avoid that. @@ -966,7 +967,7 @@ fun genericDriver( jmxPolicy = defaultParameters.jmxPolicy, notarySpecs = defaultParameters.notarySpecs, compatibilityZone = null, - maxTransactionSize = defaultParameters.maxTransactionSize + networkParameters = defaultParameters.networkParameters ) ) val shutdownHook = addShutdownHook(driverDsl::shutdown) @@ -1008,7 +1009,7 @@ fun internalDriver( notarySpecs: List = DriverParameters().notarySpecs, extraCordappPackagesToScan: List = DriverParameters().extraCordappPackagesToScan, jmxPolicy: JmxPolicy = DriverParameters().jmxPolicy, - maxTransactionSize: Int = DriverParameters().maxTransactionSize, + networkParameters: NetworkParameters = DriverParameters().networkParameters, compatibilityZone: CompatibilityZoneParams? = null, dsl: DriverDSLImpl.() -> A ): A { @@ -1026,7 +1027,7 @@ fun internalDriver( extraCordappPackagesToScan = extraCordappPackagesToScan, jmxPolicy = jmxPolicy, compatibilityZone = compatibilityZone, - maxTransactionSize = maxTransactionSize + networkParameters = networkParameters ), coerce = { it }, dsl = dsl, diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt index b00f4a9db7..d6574593b9 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt @@ -18,6 +18,7 @@ import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.MessageRecipients import net.corda.core.messaging.RPCOps import net.corda.core.messaging.SingleMessageRecipient +import net.corda.core.node.NetworkParameters import net.corda.core.node.NodeInfo import net.corda.core.node.NotaryInfo import net.corda.core.node.services.IdentityService @@ -32,11 +33,7 @@ import net.corda.node.internal.StartedNode import net.corda.node.internal.cordapp.CordappLoader import net.corda.node.services.api.NodePropertiesStore import net.corda.node.services.api.SchemaService -import net.corda.node.services.config.BFTSMaRtConfiguration -import net.corda.node.services.config.CertChainPolicyConfig -import net.corda.node.services.config.NodeConfiguration -import net.corda.node.services.config.NotaryConfig -import net.corda.node.services.config.VerifierType +import net.corda.node.services.config.* import net.corda.node.services.keys.E2ETestKeyManagementService import net.corda.node.services.messaging.MessagingService import net.corda.node.services.transactions.BFTNonValidatingNotaryService @@ -51,16 +48,11 @@ import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.internal.rigorousMock +import net.corda.testing.internal.setGlobalSerialization import net.corda.testing.internal.testThreadFactory -import net.corda.testing.node.InMemoryMessagingNetwork -import net.corda.testing.node.MessagingServiceSpy -import net.corda.testing.node.MockNetworkNotarySpec -import net.corda.testing.node.MockNetworkParameters -import net.corda.testing.node.MockNodeParameters +import net.corda.testing.node.* import net.corda.testing.node.MockServices.Companion.MOCK_VERSION_INFO import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties -import net.corda.testing.internal.setGlobalSerialization -import net.corda.testing.node.TestClock import org.apache.activemq.artemis.utils.ReusableLatch import org.apache.sshd.common.util.security.SecurityUtils import rx.internal.schedulers.CachedThreadScheduler @@ -107,13 +99,14 @@ open class InternalMockNetwork(private val cordappPackages: List, servicePeerAllocationStrategy: InMemoryMessagingNetwork.ServicePeerAllocationStrategy = defaultParameters.servicePeerAllocationStrategy, initialiseSerialization: Boolean = defaultParameters.initialiseSerialization, val notarySpecs: List = defaultParameters.notarySpecs, - maxTransactionSize: Int = Int.MAX_VALUE, + networkParameters: NetworkParameters = testNetworkParameters(), val defaultFactory: (MockNodeArgs) -> MockNode = InternalMockNetwork::MockNode) { init { // Apache SSHD for whatever reason registers a SFTP FileSystemProvider - which gets loaded by JimFS. // This SFTP support loads BouncyCastle, which we want to avoid. // Please see https://issues.apache.org/jira/browse/SSHD-736 - it's easier then to create our own fork of SSHD SecurityUtils.setAPrioriDisabledProvider("BC", true) // XXX: Why isn't this static? + require(networkParameters.notaries.isEmpty()) { "Define notaries using notarySpecs" } } var nextNodeId = 0 @@ -123,7 +116,7 @@ open class InternalMockNetwork(private val cordappPackages: List, val messagingNetwork = InMemoryMessagingNetwork.create(networkSendManuallyPumped, servicePeerAllocationStrategy, busyLatch) // A unique identifier for this network to segregate databases with the same nodeID but different networks. private val networkId = random63BitValue() - private val networkParameters: NetworkParametersCopier + private val networkParametersCopier: NetworkParametersCopier private val _nodes = mutableListOf() private val serializationEnv = try { setGlobalSerialization(initialiseSerialization) @@ -199,7 +192,7 @@ open class InternalMockNetwork(private val cordappPackages: List, filesystem.getPath("/nodes").createDirectory() val notaryInfos = generateNotaryIdentities() // The network parameters must be serialised before starting any of the nodes - networkParameters = NetworkParametersCopier(testNetworkParameters(notaryInfos, maxTransactionSize = maxTransactionSize)) + networkParametersCopier = NetworkParametersCopier(networkParameters.copy(notaries = notaryInfos)) @Suppress("LeakingThis") notaryNodes = createNotaries() } catch (t: Throwable) { @@ -251,7 +244,7 @@ open class InternalMockNetwork(private val cordappPackages: List, override val started: StartedNode? get() = uncheckedCast(super.started) override fun start(): StartedNode { - mockNet.networkParameters.install(configuration.baseDirectory) + mockNet.networkParametersCopier.install(configuration.baseDirectory) val started: StartedNode = uncheckedCast(super.start()) advertiseNodeToNetwork(started) return started @@ -269,7 +262,7 @@ open class InternalMockNetwork(private val cordappPackages: List, // We only need to override the messaging service here, as currently everything that hits disk does so // through the java.nio API which we are already mocking via Jimfs. - override fun makeMessagingService(database: CordaPersistence, info: NodeInfo, nodeProperties: NodePropertiesStore): MessagingService { + override fun makeMessagingService(database: CordaPersistence, info: NodeInfo, nodeProperties: NodePropertiesStore, networkParameters: NetworkParameters): MessagingService { require(id >= 0) { "Node ID must be zero or positive, was passed: " + id } return mockNet.messagingNetwork.createNodeWithID( !mockNet.threadPerNode, @@ -317,9 +310,9 @@ open class InternalMockNetwork(private val cordappPackages: List, // Allow unit tests to modify the serialization whitelist list before the node start, // so they don't have to ServiceLoad test whitelists into all unit tests. - val testSerializationWhitelists by lazy { super.serializationWhitelists.toMutableList() } + private val _serializationWhitelists by lazy { super.serializationWhitelists.toMutableList() } override val serializationWhitelists: List - get() = testSerializationWhitelists + get() = _serializationWhitelists private var dbCloser: (() -> Any?)? = null override fun initialiseDatabasePersistence(schemaService: SchemaService, identityService: IdentityService, insideTransaction: (CordaPersistence) -> T): T { return super.initialiseDatabasePersistence(schemaService, identityService) { database -> diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt index 560a52a865..dd8237450b 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt @@ -53,7 +53,7 @@ abstract class NodeBasedTest(private val cordappPackages: List = emptyLi @Before fun init() { - defaultNetworkParameters = NetworkParametersCopier(testNetworkParameters(emptyList())) + defaultNetworkParameters = NetworkParametersCopier(testNetworkParameters()) } /** diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt index e2770f4b80..040f42057d 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/RPCDriver.kt @@ -15,6 +15,7 @@ import net.corda.core.internal.concurrent.map import net.corda.core.internal.div import net.corda.core.internal.uncheckedCast import net.corda.core.messaging.RPCOps +import net.corda.core.node.NetworkParameters import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.internal.security.RPCSecurityManagerImpl import net.corda.node.services.messaging.RPCServer @@ -23,6 +24,7 @@ import net.corda.nodeapi.ArtemisTcpTransport import net.corda.nodeapi.ConnectionDirection import net.corda.nodeapi.RPCApi import net.corda.nodeapi.internal.serialization.KRYO_RPC_CLIENT_CONTEXT +import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.core.MAX_MESSAGE_SIZE import net.corda.testing.driver.JmxPolicy import net.corda.testing.driver.PortAllocation @@ -106,7 +108,7 @@ fun rpcDriver( notarySpecs: List = emptyList(), externalTrace: Trace? = null, jmxPolicy: JmxPolicy = JmxPolicy(), - maxTransactionSize: Int = Int.MAX_VALUE, + networkParameters: NetworkParameters = testNetworkParameters(), dsl: RPCDriverDSL.() -> A ): A { return genericDriver( @@ -124,7 +126,7 @@ fun rpcDriver( notarySpecs = notarySpecs, jmxPolicy = jmxPolicy, compatibilityZone = null, - maxTransactionSize = maxTransactionSize + networkParameters = networkParameters ), externalTrace ), coerce = { it }, @@ -157,7 +159,7 @@ data class RPCDriverDSL( private val driverDSL: DriverDSLImpl, private val externalTrace: Trace? ) : InternalDriverDSL by driverDSL { private companion object { - val notificationAddress = "notifications" + const val notificationAddress = "notifications" private fun ConfigurationImpl.configureCommonSettings(maxFileSize: Int, maxBufferedBytesPerClient: Long) { managementNotificationAddress = SimpleString(notificationAddress) diff --git a/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt b/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt index 38ebfc9d2b..11ec0e7bb7 100644 --- a/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt +++ b/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt @@ -9,8 +9,8 @@ import net.corda.core.internal.div import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.contextLogger import net.corda.nodeapi.internal.network.NetworkParametersCopier -import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.common.internal.asContextEnv +import net.corda.testing.common.internal.testNetworkParameters import java.net.URL import java.nio.file.Path import java.nio.file.Paths @@ -51,8 +51,8 @@ class NodeProcess( // TODO All use of this factory have duplicate code which is either bundling the calling module or a 3rd party module // as a CorDapp for the nodes. class Factory( - val buildDirectory: Path = Paths.get("build"), - val cordaJarUrl: URL? = this::class.java.getResource("/corda.jar") + private val buildDirectory: Path = Paths.get("build"), + private val cordaJarUrl: URL? = this::class.java.getResource("/corda.jar") ) { val cordaJar: Path by lazy { require(cordaJarUrl != null, { "corda.jar could not be found in classpath" }) @@ -66,7 +66,7 @@ class NodeProcess( KryoClientSerializationScheme.createSerializationEnv().asContextEnv { // There are no notaries in the network parameters for smoke test nodes. If this is required then we would // need to introduce the concept of a "network" which predefines the notaries, like the driver and MockNetwork - NetworkParametersCopier(testNetworkParameters(emptyList())) + NetworkParametersCopier(testNetworkParameters()) } } diff --git a/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/ParametersUtilities.kt b/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/ParametersUtilities.kt index ea089257c2..322e9e90b7 100644 --- a/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/ParametersUtilities.kt +++ b/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/ParametersUtilities.kt @@ -5,12 +5,12 @@ import net.corda.core.node.NotaryInfo import java.time.Instant fun testNetworkParameters( - notaries: List, + notaries: List = emptyList(), minimumPlatformVersion: Int = 1, modifiedTime: Instant = Instant.now(), maxMessageSize: Int = 10485760, // TODO: Make this configurable and consistence across driver, bootstrapper, demobench and NetworkMapServer - maxTransactionSize: Int = Int.MAX_VALUE, + maxTransactionSize: Int = maxMessageSize, epoch: Int = 1 ): NetworkParameters { return NetworkParameters( diff --git a/verifier/src/integration-test/kotlin/net/corda/verifier/GeneratedLedger.kt b/verifier/src/integration-test/kotlin/net/corda/verifier/GeneratedLedger.kt index afc019ed9a..a07bc0c1ea 100644 --- a/verifier/src/integration-test/kotlin/net/corda/verifier/GeneratedLedger.kt +++ b/verifier/src/integration-test/kotlin/net/corda/verifier/GeneratedLedger.kt @@ -1,17 +1,27 @@ package net.corda.verifier +import com.nhaarman.mockito_kotlin.any +import com.nhaarman.mockito_kotlin.doAnswer +import com.nhaarman.mockito_kotlin.whenever import net.corda.client.mock.Generator import net.corda.core.contracts.* +import net.corda.core.cordapp.CordappProvider import net.corda.core.crypto.SecureHash import net.corda.core.crypto.entropyToKeyPair import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party +import net.corda.core.node.NetworkParameters +import net.corda.core.node.ServicesForResolution +import net.corda.core.node.services.AttachmentStorage +import net.corda.core.node.services.IdentityService import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.WireTransaction import net.corda.nodeapi.internal.serialization.GeneratedAttachment +import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.contracts.DummyContract +import net.corda.testing.internal.rigorousMock import org.apache.commons.lang.ArrayUtils.EMPTY_BYTE_ARRAY import java.math.BigInteger import java.security.PublicKey @@ -29,25 +39,36 @@ data class GeneratedLedger( val attachments: Set, val identities: Set ) { - val hashTransactionMap: Map by lazy { transactions.associateBy(WireTransaction::id) } - val attachmentMap: Map by lazy { attachments.associateBy(Attachment::id) } - val identityMap: Map by lazy { identities.associateBy(Party::owningKey) } - val contractAttachmentMap: Map by lazy { + private val hashTransactionMap: Map by lazy { transactions.associateBy(WireTransaction::id) } + private val attachmentMap: Map by lazy { attachments.associateBy(Attachment::id) } + private val identityMap: Map by lazy { identities.associateBy(Party::owningKey) } + private val contractAttachmentMap: Map by lazy { attachments.mapNotNull { it as? ContractAttachment }.associateBy { it.contract } } + private val services = object : ServicesForResolution { + override fun loadState(stateRef: StateRef): TransactionState<*> { + return hashTransactionMap[stateRef.txhash]?.outputs?.get(stateRef.index) ?: throw TransactionResolutionException(stateRef.txhash) + } + override val identityService = rigorousMock().apply { + doAnswer { identityMap[it.arguments[0]] }.whenever(this).partyFromKey(any()) + } + override val attachments = rigorousMock().apply { + doAnswer { attachmentMap[it.arguments[0]] }.whenever(this).openAttachment(any()) + } + override val cordappProvider = rigorousMock().apply { + doAnswer { contractAttachmentMap[it.arguments[0]]?.id }.whenever(this).getContractAttachmentID(any()) + } + override val networkParameters = testNetworkParameters() + } + companion object { val empty = GeneratedLedger(emptyList(), emptyMap(), emptySet(), emptySet()) val contractAttachment = ContractAttachment(GeneratedAttachment(EMPTY_BYTE_ARRAY), DummyContract.PROGRAM_ID) } fun resolveWireTransaction(transaction: WireTransaction): LedgerTransaction { - return transaction.toLedgerTransaction( - resolveIdentity = { identityMap[it] }, - resolveAttachment = { attachmentMap[it] }, - resolveStateRef = { hashTransactionMap[it.txhash]?.outputs?.get(it.index) }, - resolveContractAttachment = { contractAttachmentMap[it.contract]?.id } - ) + return transaction.toLedgerTransaction(services) } val attachmentsGenerator: Generator> by lazy { diff --git a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt index 50c1b8d7e1..d9c6fcef06 100644 --- a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt +++ b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt @@ -5,13 +5,13 @@ import com.typesafe.config.ConfigFactory import net.corda.core.concurrent.CordaFuture import net.corda.core.crypto.random63BitValue import net.corda.core.identity.CordaX500Name -import net.corda.core.internal.GlobalProperties import net.corda.core.internal.concurrent.OpenFuture import net.corda.core.internal.concurrent.doneFuture import net.corda.core.internal.concurrent.fork import net.corda.core.internal.concurrent.openFuture import net.corda.core.internal.createDirectories import net.corda.core.internal.div +import net.corda.core.node.NetworkParameters import net.corda.core.serialization.internal.nodeSerializationEnv import net.corda.core.transactions.LedgerTransaction import net.corda.core.utilities.NetworkHostAndPort @@ -64,7 +64,7 @@ fun verifierDriver( extraCordappPackagesToScan: List = emptyList(), notarySpecs: List = emptyList(), jmxPolicy: JmxPolicy = JmxPolicy(), - maxTransactionSize: Int = Int.MAX_VALUE, + networkParameters: NetworkParameters = testNetworkParameters(), dsl: VerifierDriverDSL.() -> A ) = genericDriver( driverDsl = VerifierDriverDSL( @@ -81,7 +81,7 @@ fun verifierDriver( notarySpecs = notarySpecs, jmxPolicy = jmxPolicy, compatibilityZone = null, - maxTransactionSize = maxTransactionSize + networkParameters = networkParameters ) ), coerce = { it }, @@ -167,7 +167,6 @@ data class VerifierDriverDSL(private val driverDSL: DriverDSLImpl) : InternalDri /** Starts a lightweight verification requestor that implements the Node's Verifier API */ fun startVerificationRequestor(name: CordaX500Name): CordaFuture { val hostAndPort = driverDSL.portAllocation.nextHostAndPort() - GlobalProperties.networkParameters = testNetworkParameters(emptyList(), maxTransactionSize = driverDSL.maxTransactionSize) return driverDSL.executorService.fork { startVerificationRequestorInternal(name, hostAndPort) } From 65681e8e9d55164a46fe796cf8fc0d62790ed8fb Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Mon, 19 Feb 2018 14:22:20 +0000 Subject: [PATCH 44/50] plugins folder renamed to cordapps --- docs/source/deploying-a-node.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/source/deploying-a-node.rst b/docs/source/deploying-a-node.rst index 6b17b9dd5d..6918cc743d 100644 --- a/docs/source/deploying-a-node.rst +++ b/docs/source/deploying-a-node.rst @@ -30,8 +30,8 @@ handling, and ensures the Corda service is run at boot. 4. (Optional) Download the `Corda webserver jar `_ (under ``/VERSION_NUMBER/corda-VERSION_NUMBER.jar``) and place it in ``/opt/corda`` -5. Create a directory called ``plugins`` in ``/opt/corda`` and save your CorDapp jar file to it. Alternatively, download one of - our `sample CorDapps `_ to the ``plugins`` directory +5. Create a directory called ``cordapps`` in ``/opt/corda`` and save your CorDapp jar file to it. Alternatively, download one of + our `sample CorDapps `_ to the ``cordapps`` directory 6. Save the below as ``/opt/corda/node.conf``. See :doc:`corda-configuration-file` for a description of these options @@ -199,8 +199,8 @@ at boot, and means the Corda service stays running with no users connected to th mkdir C:\Corda wget http://jcenter.bintray.com/net/corda/corda/VERSION_NUMBER/corda-VERSION_NUMBER.jar -OutFile C:\Corda\corda.jar -2. Create a directory called ``plugins`` in ``/opt/corda`` and save your CorDapp jar file to it. Alternatively, - download one of our `sample CorDapps `_ to the ``plugins`` directory +2. Create a directory called ``cordapps`` in ``/opt/corda`` and save your CorDapp jar file to it. Alternatively, + download one of our `sample CorDapps `_ to the ``cordapps`` directory 3. Save the below as ``C:\Corda\node.conf``. See :doc:`corda-configuration-file` for a description of these options @@ -282,4 +282,4 @@ You can verify Corda is running by connecting to your RPC port from another host ``telnet your-hostname.example.com 10002`` If you receive the message "Escape character is ^]", Corda is running and accessible. Press Ctrl-] and Ctrl-D to exit -telnet. \ No newline at end of file +telnet. From 7f1bfac8b092519a53b1f059b73c7a9b62c13d35 Mon Sep 17 00:00:00 2001 From: Andras Slemmer Date: Mon, 19 Feb 2018 14:53:28 +0000 Subject: [PATCH 45/50] Write better test for dupes --- .../net/corda/client/rpc/RPCStabilityTests.kt | 101 ++++++++++++------ .../main/kotlin/net/corda/nodeapi/RPCApi.kt | 4 +- .../node/services/messaging/RPCServer.kt | 62 ++++++----- 3 files changed, 101 insertions(+), 66 deletions(-) diff --git a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt index de74d563cb..0ac85510f9 100644 --- a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt +++ b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt @@ -13,13 +13,12 @@ import net.corda.core.utilities.* import net.corda.node.services.messaging.RPCServerConfiguration import net.corda.nodeapi.RPCApi import net.corda.testing.core.SerializationEnvironmentRule -import net.corda.testing.internal.* +import net.corda.testing.internal.testThreadFactory import net.corda.testing.node.internal.* import org.apache.activemq.artemis.api.core.SimpleString import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue -import org.junit.Ignore import org.junit.Rule import org.junit.Test import rx.Observable @@ -31,6 +30,7 @@ import java.util.concurrent.Executors import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.atomic.AtomicLong class RPCStabilityTests { @Rule @@ -352,44 +352,77 @@ class RPCStabilityTests { } } - interface StreamOps : RPCOps { - fun stream(streamInterval: Duration): Observable - } - class StreamOpsImpl : StreamOps { - override val protocolVersion = 0 - override fun stream(streamInterval: Duration): Observable { - return Observable.interval(streamInterval.toNanos(), TimeUnit.NANOSECONDS) + @Test + fun `deduplication in the server`() { + rpcDriver { + val server = startRpcServer(ops = SlowConsumerRPCOpsImpl()).getOrThrow() + + // Construct an RPC client session manually + val myQueue = "${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.test.${random63BitValue()}" + val session = startArtemisSession(server.broker.hostAndPort!!) + session.createTemporaryQueue(myQueue, myQueue) + val consumer = session.createConsumer(myQueue, null, -1, -1, false) + val replies = ArrayList() + consumer.setMessageHandler { + replies.add(it) + it.acknowledge() + } + + val producer = session.createProducer(RPCApi.RPC_SERVER_QUEUE_NAME) + session.start() + + pollUntilClientNumber(server, 1) + + val message = session.createMessage(false) + val request = RPCApi.ClientToServer.RpcRequest( + clientAddress = SimpleString(myQueue), + methodName = DummyOps::protocolVersion.name, + serialisedArguments = emptyList().serialize(context = SerializationDefaults.RPC_SERVER_CONTEXT), + replyId = Trace.InvocationId.newInstance(), + sessionId = Trace.SessionId.newInstance() + ) + request.writeToClientMessage(message) + message.putLongProperty(RPCApi.DEDUPLICATION_SEQUENCE_NUMBER_FIELD_NAME, 0) + producer.send(message) + // duplicate the message + producer.send(message) + + pollUntilTrue("Number of replies is 1") { + replies.size == 1 + }.getOrThrow() } } - @Ignore("This is flaky as sometimes artemis delivers out of order messages after the kick") + @Test - fun `deduplication on the client side`() { + fun `deduplication in the client`() { rpcDriver { - val server = startRpcServer(ops = StreamOpsImpl()).getOrThrow() - val proxy = startRpcClient( - server.broker.hostAndPort!!, - configuration = RPCClientConfiguration.default.copy( - connectionRetryInterval = 1.days // switch off failover - ) - ).getOrThrow() - // Find the internal address of the client - val clientAddress = server.broker.serverControl.addressNames.find { it.startsWith(RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX) } - val events = ArrayList() - // Start streaming an incrementing value 2000 times per second from the server. - val subscription = proxy.stream(streamInterval = Duration.ofNanos(500_000)).subscribe { - events.add(it) - } - // These sleeps are *fine*, the invariant should hold regardless of any delays - Thread.sleep(50) - // Kick the client. This seems to trigger redelivery of (presumably non-acked) messages. - server.broker.serverControl.closeConsumerConnectionsForAddress(clientAddress) - Thread.sleep(50) - subscription.unsubscribe() - for (i in 0 until events.size) { - require(events[i] == i.toLong()) { - "Events not incremental, possible duplicate, ${events[i]} != ${i.toLong()}\nExpected: ${(0..i).toList()}\nGot : $events\n" + val broker = startRpcBroker().getOrThrow() + + // Construct an RPC server session manually + val session = startArtemisSession(broker.hostAndPort!!) + val consumer = session.createConsumer(RPCApi.RPC_SERVER_QUEUE_NAME) + val producer = session.createProducer() + val dedupeId = AtomicLong(0) + consumer.setMessageHandler { + it.acknowledge() + val request = RPCApi.ClientToServer.fromClientMessage(it) + when (request) { + is RPCApi.ClientToServer.RpcRequest -> { + val reply = RPCApi.ServerToClient.RpcReply(request.replyId, Try.Success(0), "server") + val message = session.createMessage(false) + reply.writeToClientMessage(SerializationDefaults.RPC_SERVER_CONTEXT, message) + message.putLongProperty(RPCApi.DEDUPLICATION_SEQUENCE_NUMBER_FIELD_NAME, dedupeId.getAndIncrement()) + producer.send(request.clientAddress, message) + // duplicate the reply + producer.send(request.clientAddress, message) + } + is RPCApi.ClientToServer.ObservablesClosed -> { + } } } + session.start() + + startRpcClient(broker.hostAndPort!!).getOrThrow() } } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/RPCApi.kt b/node-api/src/main/kotlin/net/corda/nodeapi/RPCApi.kt index 338bac1bfd..9392c176c7 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/RPCApi.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/RPCApi.kt @@ -75,7 +75,6 @@ object RPCApi { const val DEDUPLICATION_SEQUENCE_NUMBER_FIELD_NAME = "deduplication-sequence-number" - val RPC_CLIENT_BINDING_REMOVAL_FILTER_EXPRESSION = "${ManagementHelper.HDR_NOTIFICATION_TYPE} = '${CoreNotificationType.BINDING_REMOVED.name}' AND " + "${ManagementHelper.HDR_ROUTING_NAME} LIKE '$RPC_CLIENT_QUEUE_NAME_PREFIX.%'" @@ -181,12 +180,11 @@ object RPCApi { abstract fun writeToClientMessage(context: SerializationContext, message: ClientMessage) + /** The identity used to identify the deduplication ID sequence. This should be unique per server JVM run */ abstract val deduplicationIdentity: String /** * Reply in response to an [ClientToServer.RpcRequest]. - * @property deduplicationSequenceNumber a sequence number strictly incrementing with each message. Use this for - * duplicate detection on the client. */ data class RpcReply( val id: InvocationId, diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt b/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt index 58788db721..bc47992147 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt @@ -299,40 +299,44 @@ class RPCServer( lifeCycle.requireState(State.STARTED) val clientToServer = RPCApi.ClientToServer.fromClientMessage(artemisMessage) log.debug { "-> RPC -> $clientToServer" } - when (clientToServer) { - is RPCApi.ClientToServer.RpcRequest -> { - val deduplicationSequenceNumber = artemisMessage.getLongProperty(RPCApi.DEDUPLICATION_SEQUENCE_NUMBER_FIELD_NAME) - if (deduplicationChecker.checkDuplicateMessageId( - identity = clientToServer.clientAddress, - sequenceNumber = deduplicationSequenceNumber - )) { - log.info("Message duplication detected, discarding message") - return - } - val arguments = Try.on { - clientToServer.serialisedArguments.deserialize>(context = RPC_SERVER_CONTEXT) - } - val context = artemisMessage.context(clientToServer.sessionId) - context.invocation.pushToLoggingContext() - when (arguments) { - is Try.Success -> { - rpcExecutor!!.submit { - val result = invokeRpc(context, clientToServer.methodName, arguments.value) - sendReply(clientToServer.replyId, clientToServer.clientAddress, result) + try { + when (clientToServer) { + is RPCApi.ClientToServer.RpcRequest -> { + val deduplicationSequenceNumber = artemisMessage.getLongProperty(RPCApi.DEDUPLICATION_SEQUENCE_NUMBER_FIELD_NAME) + if (deduplicationChecker.checkDuplicateMessageId( + identity = clientToServer.clientAddress, + sequenceNumber = deduplicationSequenceNumber + )) { + log.info("Message duplication detected, discarding message") + return + } + val arguments = Try.on { + clientToServer.serialisedArguments.deserialize>(context = RPC_SERVER_CONTEXT) + } + val context = artemisMessage.context(clientToServer.sessionId) + context.invocation.pushToLoggingContext() + when (arguments) { + is Try.Success -> { + log.info("SUBMITTING") + rpcExecutor!!.submit { + val result = invokeRpc(context, clientToServer.methodName, arguments.value) + sendReply(clientToServer.replyId, clientToServer.clientAddress, result) + } + } + is Try.Failure -> { + // We failed to deserialise the arguments, route back the error + log.warn("Inbound RPC failed", arguments.exception) + sendReply(clientToServer.replyId, clientToServer.clientAddress, arguments) } } - is Try.Failure -> { - // We failed to deserialise the arguments, route back the error - log.warn("Inbound RPC failed", arguments.exception) - sendReply(clientToServer.replyId, clientToServer.clientAddress, arguments) - } + } + is RPCApi.ClientToServer.ObservablesClosed -> { + observableMap.invalidateAll(clientToServer.ids) } } - is RPCApi.ClientToServer.ObservablesClosed -> { - observableMap.invalidateAll(clientToServer.ids) - } + } finally { + artemisMessage.acknowledge() } - artemisMessage.acknowledge() } private fun invokeRpc(context: RpcAuthContext, methodName: String, arguments: List): Try { From 32bcf0a06c59c87f12836d6e4843ec4f08924062 Mon Sep 17 00:00:00 2001 From: Andras Slemmer Date: Mon, 19 Feb 2018 15:16:12 +0000 Subject: [PATCH 46/50] Address more comments --- .../net/corda/nodeapi/internal/DeduplicationChecker.kt | 1 + .../net/corda/node/services/messaging/RPCServer.kt | 9 +++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/DeduplicationChecker.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/DeduplicationChecker.kt index 2fc69bbd1e..b35b8922a3 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/DeduplicationChecker.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/DeduplicationChecker.kt @@ -22,6 +22,7 @@ class DeduplicationChecker(cacheExpiry: Duration) { /** * @param identity the identity that generates the sequence numbers. * @param sequenceNumber the sequence number to check. + * @return true if the message is unique, false if it's a duplicate. */ fun checkDuplicateMessageId(identity: Any, sequenceNumber: Long): Boolean { return watermarkCache[identity].getAndUpdate { maxOf(sequenceNumber, it) } >= sequenceNumber diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt b/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt index bc47992147..103eb36476 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt @@ -412,10 +412,11 @@ class RPCServer( return Pair(Actor(Id(validatedUser), securityManager.id, targetLegalIdentity), securityManager.buildSubject(validatedUser)) } - // We construct an observable context on each RPC request. If subsequently a nested Observable is - // encountered this same context is propagated by the instrumented KryoPool. This way all - // observations rooted in a single RPC will be muxed correctly. Note that the context construction - // itself is quite cheap. + /* + * We construct an observable context on each RPC request. If subsequently a nested Observable is encountered this + * same context is propagated by the instrumented KryoPool. This way all observations rooted in a single RPC will be + * muxed correctly. Note that the context construction itself is quite cheap. + */ inner class ObservableContext( val observableMap: ObservableSubscriptionMap, val clientAddressToObservables: SetMultimap, From 4f0c6928319363b89cc5a717eda14862ecb698b9 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Fri, 16 Feb 2018 17:21:23 +0100 Subject: [PATCH 47/50] Minor: reduce the size of the MockNetwork API a bit, add some better documentation --- .../confidential/IdentitySyncFlowTests.kt | 5 +- .../confidential/SwapIdentitiesFlowTests.kt | 1 - .../net/corda/core/flows/AttachmentTests.kt | 1 - .../AttachmentSerializationTest.kt | 1 - .../node/messaging/TwoPartyTradeFlowTests.kt | 4 +- .../ScheduledFlowsDrainingModeTest.kt | 2 +- .../services/events/ScheduledFlowTests.kt | 2 +- .../statemachine/FlowFrameworkTests.kt | 4 +- .../transactions/MaxTransactionSizeTests.kt | 4 +- .../vault/VaultSoftLockManagerTest.kt | 1 - .../net/corda/netmap/simulation/Simulation.kt | 4 +- .../testing/node/InMemoryMessagingNetwork.kt | 3 +- .../net/corda/testing/node/MockNetwork.kt | 83 ++++++++++++------- .../node/internal/InternalMockNetwork.kt | 19 +---- .../node/MockNodeFactoryInJavaTest.java | 2 +- .../node/internal/InternalMockNetworkTests.kt | 2 +- 16 files changed, 67 insertions(+), 71 deletions(-) diff --git a/confidential-identities/src/test/kotlin/net/corda/confidential/IdentitySyncFlowTests.kt b/confidential-identities/src/test/kotlin/net/corda/confidential/IdentitySyncFlowTests.kt index 2b6863a2ba..84b95c31bc 100644 --- a/confidential-identities/src/test/kotlin/net/corda/confidential/IdentitySyncFlowTests.kt +++ b/confidential-identities/src/test/kotlin/net/corda/confidential/IdentitySyncFlowTests.kt @@ -18,7 +18,6 @@ import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import net.corda.testing.core.CHARLIE_NAME import net.corda.testing.core.singleIdentity -import net.corda.testing.node.MockNetwork import net.corda.testing.node.internal.InternalMockNetwork import net.corda.testing.node.startFlow import org.junit.After @@ -35,9 +34,9 @@ class IdentitySyncFlowTests { fun before() { // We run this in parallel threads to help catch any race conditions that may exist. mockNet = InternalMockNetwork( + cordappPackages = listOf("net.corda.finance.contracts.asset"), networkSendManuallyPumped = false, - threadPerNode = true, - cordappPackages = listOf("net.corda.finance.contracts.asset") + threadPerNode = true ) } diff --git a/confidential-identities/src/test/kotlin/net/corda/confidential/SwapIdentitiesFlowTests.kt b/confidential-identities/src/test/kotlin/net/corda/confidential/SwapIdentitiesFlowTests.kt index 1f3cb3b529..111b8e4ac6 100644 --- a/confidential-identities/src/test/kotlin/net/corda/confidential/SwapIdentitiesFlowTests.kt +++ b/confidential-identities/src/test/kotlin/net/corda/confidential/SwapIdentitiesFlowTests.kt @@ -3,7 +3,6 @@ package net.corda.confidential import net.corda.core.identity.* import net.corda.core.utilities.getOrThrow import net.corda.testing.core.* -import net.corda.testing.node.MockNetwork import net.corda.testing.node.internal.InternalMockNetwork import org.junit.Before import net.corda.testing.node.startFlow diff --git a/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt b/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt index 6c31273511..608d7c894e 100644 --- a/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt @@ -13,7 +13,6 @@ import net.corda.node.services.persistence.NodeAttachmentService import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import net.corda.testing.core.singleIdentity -import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNodeParameters import net.corda.testing.node.internal.InternalMockNetwork import net.corda.testing.node.startFlow diff --git a/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt b/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt index 8551845849..88acc9a8c2 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt @@ -18,7 +18,6 @@ import net.corda.node.services.persistence.NodeAttachmentService import net.corda.nodeapi.internal.persistence.currentDBSession import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME -import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNodeParameters import net.corda.testing.core.singleIdentity import net.corda.testing.node.internal.InternalMockNetwork diff --git a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt index 6ffcdcd158..5395c0453d 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt @@ -103,7 +103,7 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { // We run this in parallel threads to help catch any race conditions that may exist. The other tests // we run in the unit test thread exclusively to speed things up, ensure deterministic results and // allow interruption half way through. - mockNet = InternalMockNetwork(threadPerNode = true, cordappPackages = cordappPackages) + mockNet = InternalMockNetwork(cordappPackages = cordappPackages, threadPerNode = true) val ledgerIdentityService = rigorousMock() MockServices(cordappPackages, MEGA_CORP.name, ledgerIdentityService).ledger(DUMMY_NOTARY) { val notaryNode = mockNet.defaultNotaryNode @@ -155,7 +155,7 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) { @Test(expected = InsufficientBalanceException::class) fun `trade cash for commercial paper fails using soft locking`() { - mockNet = InternalMockNetwork(threadPerNode = true, cordappPackages = cordappPackages) + mockNet = InternalMockNetwork(cordappPackages = cordappPackages, threadPerNode = true) val ledgerIdentityService = rigorousMock() MockServices(cordappPackages, MEGA_CORP.name, ledgerIdentityService).ledger(DUMMY_NOTARY) { val notaryNode = mockNet.defaultNotaryNode diff --git a/node/src/test/kotlin/net/corda/node/modes/draining/ScheduledFlowsDrainingModeTest.kt b/node/src/test/kotlin/net/corda/node/modes/draining/ScheduledFlowsDrainingModeTest.kt index 9e85b92863..eed40b383d 100644 --- a/node/src/test/kotlin/net/corda/node/modes/draining/ScheduledFlowsDrainingModeTest.kt +++ b/node/src/test/kotlin/net/corda/node/modes/draining/ScheduledFlowsDrainingModeTest.kt @@ -52,7 +52,7 @@ class ScheduledFlowsDrainingModeTest { @Before fun setup() { - mockNet = InternalMockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.testing.contracts")) + mockNet = InternalMockNetwork(cordappPackages = listOf("net.corda.testing.contracts"), threadPerNode = true) aliceNode = mockNet.createNode(MockNodeParameters(legalName = ALICE_NAME)) bobNode = mockNet.createNode(MockNodeParameters(legalName = BOB_NAME)) notary = mockNet.defaultNotaryIdentity diff --git a/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt b/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt index cc1559cf5b..96a3b63a87 100644 --- a/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt @@ -103,7 +103,7 @@ class ScheduledFlowTests { @Before fun setup() { - mockNet = InternalMockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.testing.contracts")) + mockNet = InternalMockNetwork(cordappPackages = listOf("net.corda.testing.contracts"), threadPerNode = true) aliceNode = mockNet.createNode(MockNodeParameters(legalName = ALICE_NAME)) bobNode = mockNet.createNode(MockNodeParameters(legalName = BOB_NAME)) notary = mockNet.defaultNotaryIdentity diff --git a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt index da80ff4b2e..9797fca399 100644 --- a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt @@ -77,8 +77,8 @@ class FlowFrameworkTests { @Before fun start() { mockNet = InternalMockNetwork( - servicePeerAllocationStrategy = RoundRobin(), - cordappPackages = listOf("net.corda.finance.contracts", "net.corda.testing.contracts") + cordappPackages = listOf("net.corda.finance.contracts", "net.corda.testing.contracts"), + servicePeerAllocationStrategy = RoundRobin() ) aliceNode = mockNet.createNode(MockNodeParameters(legalName = ALICE_NAME)) bobNode = mockNet.createNode(MockNodeParameters(legalName = BOB_NAME)) diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/MaxTransactionSizeTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/MaxTransactionSizeTests.kt index 1b041bcde8..6cb8cdf04e 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/MaxTransactionSizeTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/MaxTransactionSizeTests.kt @@ -35,9 +35,7 @@ class MaxTransactionSizeTests { @Before fun setup() { - mockNet = MockNetwork( - listOf("net.corda.testing.contracts", "net.corda.node.services.transactions"), - networkParameters = testNetworkParameters(maxTransactionSize = 3_000_000)) + mockNet = MockNetwork(listOf("net.corda.testing.contracts", "net.corda.node.services.transactions"), networkParameters = testNetworkParameters(maxTransactionSize = 3_000_000)) val aliceNode = mockNet.createNode(MockNodeParameters(legalName = ALICE_NAME)) val bobNode = mockNet.createNode(MockNodeParameters(legalName = BOB_NAME)) notaryServices = mockNet.defaultNotaryNode.services diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultSoftLockManagerTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultSoftLockManagerTest.kt index 02e5e1c1dd..c3b423cfe1 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultSoftLockManagerTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultSoftLockManagerTest.kt @@ -27,7 +27,6 @@ import net.corda.node.internal.InitiatedFlowFactory import net.corda.node.services.api.VaultServiceInternal import net.corda.nodeapi.internal.persistence.HibernateConfiguration import net.corda.testing.core.chooseIdentity -import net.corda.testing.node.MockNetwork import net.corda.testing.internal.rigorousMock import net.corda.testing.node.MockNodeParameters import net.corda.testing.node.internal.InternalMockNetwork diff --git a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt index 66ba4e7cbd..4b0a828e9f 100644 --- a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt +++ b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt @@ -71,9 +71,9 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, } val mockNet = InternalMockNetwork( + cordappPackages = listOf("net.corda.finance.contract", "net.corda.irs"), networkSendManuallyPumped = networkSendManuallyPumped, - threadPerNode = runAsync, - cordappPackages = listOf("net.corda.finance.contract", "net.corda.irs")) + threadPerNode = runAsync) // TODO: Regulatory nodes don't actually exist properly, this is a last minute demo request. // So we just fire a message at a node that doesn't know how to handle it, and it'll ignore it. // But that's fine for visualisation purposes. diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt index e2aa643797..70613b3253 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt @@ -192,7 +192,8 @@ class InMemoryMessagingNetwork private constructor( } /** - * Mock service loadbalancing + * How traffic is allocated in the case where multiple nodes share a single identity, which happens for notaries + * in a cluster. You don't normally ever need to change this: it is mostly useful for testing notary implementations. */ @DoNotImplement sealed class ServicePeerAllocationStrategy { diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetwork.kt index b275c01ba6..46f2cedb04 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetwork.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetwork.kt @@ -38,35 +38,48 @@ data class MockNodeParameters( val entropyRoot: BigInteger = BigInteger.valueOf(random63BitValue()), val configOverrides: (NodeConfiguration) -> Any? = {}, val version: VersionInfo = MockServices.MOCK_VERSION_INFO) { - fun setForcedID(forcedID: Int?): MockNodeParameters = copy(forcedID = forcedID) - fun setLegalName(legalName: CordaX500Name?): MockNodeParameters = copy(legalName = legalName) - fun setEntropyRoot(entropyRoot: BigInteger): MockNodeParameters = copy(entropyRoot = entropyRoot) - fun setConfigOverrides(configOverrides: (NodeConfiguration) -> Any?): MockNodeParameters = copy(configOverrides = configOverrides) + fun withForcedID(forcedID: Int?): MockNodeParameters = copy(forcedID = forcedID) + fun withLegalName(legalName: CordaX500Name?): MockNodeParameters = copy(legalName = legalName) + fun withEntropyRoot(entropyRoot: BigInteger): MockNodeParameters = copy(entropyRoot = entropyRoot) + fun withConfigOverrides(configOverrides: (NodeConfiguration) -> Any?): MockNodeParameters = copy(configOverrides = configOverrides) } -/** Helper builder for configuring a [InternalMockNetwork] from Java. */ +/** + * Immutable builder for configuring a [MockNetwork]. Kotlin users can also use named parameters to the constructor + * of [MockNetwork], which is more convenient. + * + * @property networkSendManuallyPumped If true then messages will not be routed from sender to receiver until you use + * the [MockNetwork.runNetwork] method. This is useful for writing single-threaded unit test code that can examine the + * state of the mock network before and after a message is sent, without races and without the receiving node immediately + * sending a response. The default is false, so you must call runNetwork. + * @property threadPerNode If true then each node will be run in its own thread. This can result in race conditions in + * your code if not carefully written, but is more realistic and may help if you have flows in your app that do long + * blocking operations. The default is false. + * @property servicePeerAllocationStrategy How messages are load balanced in the case where a single compound identity + * is used by multiple nodes. You rarely if ever need to change that, it's primarily of interest to people testing + * notary code. + * @property notarySpecs The notaries to use in the mock network. By default you get one mock notary and that is usually sufficient. + */ @Suppress("unused") data class MockNetworkParameters( val networkSendManuallyPumped: Boolean = false, val threadPerNode: Boolean = false, val servicePeerAllocationStrategy: InMemoryMessagingNetwork.ServicePeerAllocationStrategy = InMemoryMessagingNetwork.ServicePeerAllocationStrategy.Random(), - val initialiseSerialization: Boolean = true, val notarySpecs: List = listOf(MockNetworkNotarySpec(DUMMY_NOTARY_NAME)), val networkParameters: NetworkParameters = testNetworkParameters()) { - fun setNetworkSendManuallyPumped(networkSendManuallyPumped: Boolean): MockNetworkParameters = copy(networkSendManuallyPumped = networkSendManuallyPumped) - fun setThreadPerNode(threadPerNode: Boolean): MockNetworkParameters = copy(threadPerNode = threadPerNode) - fun setServicePeerAllocationStrategy(servicePeerAllocationStrategy: InMemoryMessagingNetwork.ServicePeerAllocationStrategy): MockNetworkParameters = copy(servicePeerAllocationStrategy = servicePeerAllocationStrategy) - fun setInitialiseSerialization(initialiseSerialization: Boolean): MockNetworkParameters = copy(initialiseSerialization = initialiseSerialization) - fun setNotarySpecs(notarySpecs: List): MockNetworkParameters = copy(notarySpecs = notarySpecs) - fun setNetworkParameters(networkParameters: NetworkParameters): MockNetworkParameters = copy(networkParameters = networkParameters) + fun withNetworkParameters(networkParameters: NetworkParameters): MockNetworkParameters = copy(networkParameters = networkParameters) + fun withNetworkSendManuallyPumped(networkSendManuallyPumped: Boolean): MockNetworkParameters = copy(networkSendManuallyPumped = networkSendManuallyPumped) + fun withThreadPerNode(threadPerNode: Boolean): MockNetworkParameters = copy(threadPerNode = threadPerNode) + fun withServicePeerAllocationStrategy(servicePeerAllocationStrategy: InMemoryMessagingNetwork.ServicePeerAllocationStrategy): MockNetworkParameters = copy(servicePeerAllocationStrategy = servicePeerAllocationStrategy) + fun withNotarySpecs(notarySpecs: List): MockNetworkParameters = copy(notarySpecs = notarySpecs) } -/** Represents a node configuration for injection via [MockNetworkParameters] **/ +/** Represents a node configuration for injection via [MockNetworkParameters]. */ data class MockNetworkNotarySpec(val name: CordaX500Name, val validating: Boolean = true) { constructor(name: CordaX500Name) : this(name, validating = true) } -/** A class that represents an unstarted mock node for testing. **/ +/** A class that represents an unstarted mock node for testing. */ class UnstartedMockNode private constructor(private val node: InternalMockNetwork.MockNode) { companion object { internal fun create(node: InternalMockNetwork.MockNode): UnstartedMockNode { @@ -79,7 +92,7 @@ class UnstartedMockNode private constructor(private val node: InternalMockNetwor fun start() = StartedMockNode.create(node.start()) } -/** A class that represents a started mock node for testing. **/ +/** A class that represents a started mock node for testing. */ class StartedMockNode private constructor(private val node: StartedNode) { companion object { internal fun create(node: StartedNode): StartedMockNode { @@ -123,9 +136,14 @@ class StartedMockNode private constructor(private val node: StartedNode, val defaultParameters: MockNetworkParameters = MockNetworkParameters(), val networkSendManuallyPumped: Boolean = defaultParameters.networkSendManuallyPumped, val threadPerNode: Boolean = defaultParameters.threadPerNode, val servicePeerAllocationStrategy: InMemoryMessagingNetwork.ServicePeerAllocationStrategy = defaultParameters.servicePeerAllocationStrategy, - val initialiseSerialization: Boolean = defaultParameters.initialiseSerialization, val notarySpecs: List = defaultParameters.notarySpecs, val networkParameters: NetworkParameters = defaultParameters.networkParameters) { @JvmOverloads constructor(cordappPackages: List, parameters: MockNetworkParameters = MockNetworkParameters()) : this(cordappPackages, defaultParameters = parameters) - private val internalMockNetwork = InternalMockNetwork( - cordappPackages, - defaultParameters, - networkSendManuallyPumped, - threadPerNode, - servicePeerAllocationStrategy, - initialiseSerialization, - notarySpecs, - networkParameters) - val defaultNotaryNode get() : StartedMockNode = StartedMockNode.create(internalMockNetwork.defaultNotaryNode) - val defaultNotaryIdentity get() : Party = internalMockNetwork.defaultNotaryIdentity - val notaryNodes get() : List = internalMockNetwork.notaryNodes.map { StartedMockNode.create(it) } - val nextNodeId get() : Int = internalMockNetwork.nextNodeId + private val internalMockNetwork: InternalMockNetwork = InternalMockNetwork(cordappPackages, defaultParameters, networkSendManuallyPumped, threadPerNode, servicePeerAllocationStrategy, notarySpecs) + + /** Which node will be used as the primary notary during transaction builds. */ + val defaultNotaryNode get(): StartedMockNode = StartedMockNode.create(internalMockNetwork.defaultNotaryNode) + /** The [Party] of the [defaultNotaryNode] */ + val defaultNotaryIdentity get(): Party = internalMockNetwork.defaultNotaryIdentity + /** A list of all notary nodes in the network that have been started. */ + val notaryNodes get(): List = internalMockNetwork.notaryNodes.map { StartedMockNode.create(it) } + /** In a mock network, nodes have an incrementing integer ID. Real networks do not have this. Returns the next ID that will be used. */ + val nextNodeId get(): Int = internalMockNetwork.nextNodeId /** Create a started node with the given identity. **/ fun createPartyNode(legalName: CordaX500Name? = null): StartedMockNode = StartedMockNode.create(internalMockNetwork.createPartyNode(legalName)) @@ -166,7 +181,9 @@ open class MockNetwork( /** Create a started node with the given parameters. **/ fun createNode(parameters: MockNodeParameters = MockNodeParameters()): StartedMockNode = StartedMockNode.create(internalMockNetwork.createNode(parameters)) - /** Create a started node with the given parameters. + /** + * Create a started node with the given parameters. + * * @param legalName the node's legal name. * @param forcedID a unique identifier for the node. * @param entropyRoot the initial entropy value to use when generating keys. Defaults to an (insecure) random value, @@ -187,7 +204,9 @@ open class MockNetwork( /** Create an unstarted node with the given parameters. **/ fun createUnstartedNode(parameters: MockNodeParameters = MockNodeParameters()): UnstartedMockNode = UnstartedMockNode.create(internalMockNetwork.createUnstartedNode(parameters)) - /** Create an unstarted node with the given parameters. + /** + * Create an unstarted node with the given parameters. + * * @param legalName the node's legal name. * @param forcedID a unique identifier for the node. * @param entropyRoot the initial entropy value to use when generating keys. Defaults to an (insecure) random value, diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt index d6574593b9..4c42d319ce 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt @@ -76,28 +76,11 @@ data class MockNodeArgs( val version: VersionInfo = MOCK_VERSION_INFO ) -/** - * A mock node brings up a suite of in-memory services in a fast manner suitable for unit testing. - * Components that do IO are either swapped out for mocks, or pointed to a [Jimfs] in memory filesystem or an in - * memory H2 database instance. - * - * Mock network nodes require manual pumping by default: they will not run asynchronous. This means that - * for message exchanges to take place (and associated handlers to run), you must call the [runNetwork] - * method. - * - * You can get a printout of every message sent by using code like: - * - * LogHelper.setLevel("+messages") - * - * By default a single notary node is automatically started, which forms part of the network parameters for all the nodes. - * This node is available by calling [defaultNotaryNode]. - */ open class InternalMockNetwork(private val cordappPackages: List, defaultParameters: MockNetworkParameters = MockNetworkParameters(), val networkSendManuallyPumped: Boolean = defaultParameters.networkSendManuallyPumped, val threadPerNode: Boolean = defaultParameters.threadPerNode, servicePeerAllocationStrategy: InMemoryMessagingNetwork.ServicePeerAllocationStrategy = defaultParameters.servicePeerAllocationStrategy, - initialiseSerialization: Boolean = defaultParameters.initialiseSerialization, val notarySpecs: List = defaultParameters.notarySpecs, networkParameters: NetworkParameters = testNetworkParameters(), val defaultFactory: (MockNodeArgs) -> MockNode = InternalMockNetwork::MockNode) { @@ -119,7 +102,7 @@ open class InternalMockNetwork(private val cordappPackages: List, private val networkParametersCopier: NetworkParametersCopier private val _nodes = mutableListOf() private val serializationEnv = try { - setGlobalSerialization(initialiseSerialization) + setGlobalSerialization(true) } catch (e: IllegalStateException) { throw IllegalStateException("Using more than one InternalMockNetwork simultaneously is not supported.", e) } diff --git a/testing/node-driver/src/test/java/net/corda/testing/node/MockNodeFactoryInJavaTest.java b/testing/node-driver/src/test/java/net/corda/testing/node/MockNodeFactoryInJavaTest.java index fbf6bb01f4..739e5dcf27 100644 --- a/testing/node-driver/src/test/java/net/corda/testing/node/MockNodeFactoryInJavaTest.java +++ b/testing/node-driver/src/test/java/net/corda/testing/node/MockNodeFactoryInJavaTest.java @@ -13,7 +13,7 @@ public class MockNodeFactoryInJavaTest { private static void factoryIsEasyToPassInUsingJava() { //noinspection Convert2MethodRef new MockNetwork(emptyList()); - new MockNetwork(emptyList(), new MockNetworkParameters().setInitialiseSerialization(false)); + new MockNetwork(emptyList(), new MockNetworkParameters().withThreadPerNode(true)); //noinspection Convert2MethodRef new MockNetwork(emptyList()).createNode(new MockNodeParameters()); } diff --git a/testing/node-driver/src/test/kotlin/net/corda/testing/node/internal/InternalMockNetworkTests.kt b/testing/node-driver/src/test/kotlin/net/corda/testing/node/internal/InternalMockNetworkTests.kt index 478585af17..fe2780977a 100644 --- a/testing/node-driver/src/test/kotlin/net/corda/testing/node/internal/InternalMockNetworkTests.kt +++ b/testing/node-driver/src/test/kotlin/net/corda/testing/node/internal/InternalMockNetworkTests.kt @@ -9,7 +9,7 @@ class InternalMockNetworkTests { fun `does not leak serialization env if init fails`() { val e = Exception("didn't work") assertThatThrownBy { - object : InternalMockNetwork(emptyList(), initialiseSerialization = true) { + object : InternalMockNetwork(emptyList()) { override fun createNotaries() = throw e } }.isSameAs(e) From c704ff63701757bd5e8d004dabdaf8dfceab3cfa Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Mon, 19 Feb 2018 14:55:13 +0100 Subject: [PATCH 48/50] Fix a build issue caused by a bad auto-import. --- .../corda/node/internal/cordapp/CordappConfigFileProvider.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappConfigFileProvider.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappConfigFileProvider.kt index ce30f5f0f6..921b67f98a 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappConfigFileProvider.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappConfigFileProvider.kt @@ -4,7 +4,6 @@ import com.typesafe.config.Config import com.typesafe.config.ConfigFactory import net.corda.core.internal.cordapp.CordappConfigProvider import net.corda.core.utilities.loggerFor -import sun.plugin.dom.exception.InvalidStateException import java.io.File class CordappConfigFileProvider(val configDir: File = DEFAULT_CORDAPP_CONFIG_DIR) : CordappConfigProvider { @@ -22,7 +21,7 @@ class CordappConfigFileProvider(val configDir: File = DEFAULT_CORDAPP_CONFIG_DIR val configFile = File(configDir, name + CONFIG_EXT) return if (configFile.exists()) { if (configFile.isDirectory) { - throw InvalidStateException("ile at ${configFile.absolutePath} is a directory, expected a config file") + throw IllegalStateException("File at ${configFile.absolutePath} is a directory, expected a config file") } else { logger.info("Found config for cordapp $name in ${configFile.absolutePath}") ConfigFactory.parseFile(configFile) From ad959c7af1eab7ecb393aba736cdcf617ace1346 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Mon, 19 Feb 2018 18:11:20 +0000 Subject: [PATCH 49/50] Changed the wither methods in DriverParameters and NodeParameters to begin with "with" and not "set" (#2570) --- .ci/api-current.txt | 68 +++++++++---------- .../kotlin/net/corda/testing/driver/Driver.kt | 38 +++++------ .../net/corda/testing/node/MockNetwork.kt | 5 +- 3 files changed, 54 insertions(+), 57 deletions(-) diff --git a/.ci/api-current.txt b/.ci/api-current.txt index de990eb1e3..672d74ef83 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -3675,7 +3675,6 @@ public final class net.corda.testing.driver.DriverParameters extends java.lang.O @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.PortAllocation getDebugPortAllocation() @org.jetbrains.annotations.NotNull public final java.nio.file.Path getDriverDirectory() @org.jetbrains.annotations.NotNull public final List getExtraCordappPackagesToScan() - public final boolean getInitialiseSerialization() @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.JmxPolicy getJmxPolicy() @org.jetbrains.annotations.NotNull public final net.corda.core.node.NetworkParameters getNetworkParameters() @org.jetbrains.annotations.NotNull public final List getNotarySpecs() @@ -3686,19 +3685,18 @@ public final class net.corda.testing.driver.DriverParameters extends java.lang.O public final boolean getWaitForAllNodesToFinish() public int hashCode() public final boolean isDebug() - @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters setDebugPortAllocation(net.corda.testing.driver.PortAllocation) - @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters setDriverDirectory(java.nio.file.Path) - @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters setExtraCordappPackagesToScan(List) - @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters setInitialiseSerialization(boolean) - @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters setIsDebug(boolean) - @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters setJmxPolicy(net.corda.testing.driver.JmxPolicy) - @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters setNotarySpecs(List) - @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters setPortAllocation(net.corda.testing.driver.PortAllocation) - @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters setStartNodesInProcess(boolean) - @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters setSystemProperties(Map) - @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters setUseTestClock(boolean) - @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters setWaitForAllNodesToFinish(boolean) public String toString() + @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters withDebugPortAllocation(net.corda.testing.driver.PortAllocation) + @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters withDriverDirectory(java.nio.file.Path) + @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters withExtraCordappPackagesToScan(List) + @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters withIsDebug(boolean) + @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters withJmxPolicy(net.corda.testing.driver.JmxPolicy) + @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters withNotarySpecs(List) + @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters withPortAllocation(net.corda.testing.driver.PortAllocation) + @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters withStartNodesInProcess(boolean) + @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters withSystemProperties(Map) + @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters withUseTestClock(boolean) + @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters withWaitForAllNodesToFinish(boolean) ## @net.corda.core.DoNotImplement public interface net.corda.testing.driver.InProcess extends net.corda.testing.driver.NodeHandle @org.jetbrains.annotations.NotNull public abstract net.corda.node.services.api.StartedNodeServices getServices() @@ -3743,13 +3741,13 @@ public final class net.corda.testing.driver.NodeParameters extends java.lang.Obj @org.jetbrains.annotations.Nullable public final Boolean getStartInSameProcess() @org.jetbrains.annotations.NotNull public final net.corda.node.services.config.VerifierType getVerifierType() public int hashCode() - @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.NodeParameters setCustomerOverrides(Map) - @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.NodeParameters setMaximumHeapSize(String) - @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.NodeParameters setProvidedName(net.corda.core.identity.CordaX500Name) - @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.NodeParameters setRpcUsers(List) - @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.NodeParameters setStartInSameProcess(Boolean) - @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.NodeParameters setVerifierType(net.corda.node.services.config.VerifierType) public String toString() + @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.NodeParameters withCustomerOverrides(Map) + @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.NodeParameters withMaximumHeapSize(String) + @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.NodeParameters withProvidedName(net.corda.core.identity.CordaX500Name) + @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.NodeParameters withRpcUsers(List) + @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.NodeParameters withStartInSameProcess(Boolean) + @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.NodeParameters withVerifierType(net.corda.node.services.config.VerifierType) ## public final class net.corda.testing.driver.NotaryHandle extends java.lang.Object public (net.corda.core.identity.Party, boolean, net.corda.core.concurrent.CordaFuture) @@ -3911,7 +3909,7 @@ public final class net.corda.testing.node.MockKeyManagementService extends net.c public class net.corda.testing.node.MockNetwork extends java.lang.Object public (List) public (List, net.corda.testing.node.MockNetworkParameters) - public (List, net.corda.testing.node.MockNetworkParameters, boolean, boolean, net.corda.testing.node.InMemoryMessagingNetwork$ServicePeerAllocationStrategy, boolean, List, net.corda.core.node.NetworkParameters) + public (List, net.corda.testing.node.MockNetworkParameters, boolean, boolean, net.corda.testing.node.InMemoryMessagingNetwork$ServicePeerAllocationStrategy, List, net.corda.core.node.NetworkParameters) @org.jetbrains.annotations.NotNull public final java.nio.file.Path baseDirectory(int) @org.jetbrains.annotations.NotNull public final net.corda.testing.node.StartedMockNode createNode() @org.jetbrains.annotations.NotNull public final net.corda.testing.node.StartedMockNode createNode(net.corda.core.identity.CordaX500Name) @@ -3932,7 +3930,6 @@ public class net.corda.testing.node.MockNetwork extends java.lang.Object @org.jetbrains.annotations.NotNull public final net.corda.core.identity.Party getDefaultNotaryIdentity() @org.jetbrains.annotations.NotNull public final net.corda.testing.node.StartedMockNode getDefaultNotaryNode() @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNetworkParameters getDefaultParameters() - public final boolean getInitialiseSerialization() @org.jetbrains.annotations.NotNull public final net.corda.core.node.NetworkParameters getNetworkParameters() public final boolean getNetworkSendManuallyPumped() public final int getNextNodeId() @@ -3960,29 +3957,26 @@ public final class net.corda.testing.node.MockNetworkNotarySpec extends java.lan ## public final class net.corda.testing.node.MockNetworkParameters extends java.lang.Object public () - public (boolean, boolean, net.corda.testing.node.InMemoryMessagingNetwork$ServicePeerAllocationStrategy, boolean, List, net.corda.core.node.NetworkParameters) + public (boolean, boolean, net.corda.testing.node.InMemoryMessagingNetwork$ServicePeerAllocationStrategy, List, net.corda.core.node.NetworkParameters) public final boolean component1() public final boolean component2() @org.jetbrains.annotations.NotNull public final net.corda.testing.node.InMemoryMessagingNetwork$ServicePeerAllocationStrategy component3() - public final boolean component4() - @org.jetbrains.annotations.NotNull public final List component5() - @org.jetbrains.annotations.NotNull public final net.corda.core.node.NetworkParameters component6() - @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNetworkParameters copy(boolean, boolean, net.corda.testing.node.InMemoryMessagingNetwork$ServicePeerAllocationStrategy, boolean, List, net.corda.core.node.NetworkParameters) + @org.jetbrains.annotations.NotNull public final List component4() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.NetworkParameters component5() + @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNetworkParameters copy(boolean, boolean, net.corda.testing.node.InMemoryMessagingNetwork$ServicePeerAllocationStrategy, List, net.corda.core.node.NetworkParameters) public boolean equals(Object) - public final boolean getInitialiseSerialization() @org.jetbrains.annotations.NotNull public final net.corda.core.node.NetworkParameters getNetworkParameters() public final boolean getNetworkSendManuallyPumped() @org.jetbrains.annotations.NotNull public final List getNotarySpecs() @org.jetbrains.annotations.NotNull public final net.corda.testing.node.InMemoryMessagingNetwork$ServicePeerAllocationStrategy getServicePeerAllocationStrategy() public final boolean getThreadPerNode() public int hashCode() - @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNetworkParameters setInitialiseSerialization(boolean) - @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNetworkParameters setNetworkParameters(net.corda.core.node.NetworkParameters) - @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNetworkParameters setNetworkSendManuallyPumped(boolean) - @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNetworkParameters setNotarySpecs(List) - @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNetworkParameters setServicePeerAllocationStrategy(net.corda.testing.node.InMemoryMessagingNetwork$ServicePeerAllocationStrategy) - @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNetworkParameters setThreadPerNode(boolean) public String toString() + @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNetworkParameters withNetworkParameters(net.corda.core.node.NetworkParameters) + @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNetworkParameters withNetworkSendManuallyPumped(boolean) + @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNetworkParameters withNotarySpecs(List) + @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNetworkParameters withServicePeerAllocationStrategy(net.corda.testing.node.InMemoryMessagingNetwork$ServicePeerAllocationStrategy) + @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNetworkParameters withThreadPerNode(boolean) ## public final class net.corda.testing.node.MockNodeParameters extends java.lang.Object public () @@ -4000,11 +3994,11 @@ public final class net.corda.testing.node.MockNodeParameters extends java.lang.O @org.jetbrains.annotations.Nullable public final net.corda.core.identity.CordaX500Name getLegalName() @org.jetbrains.annotations.NotNull public final net.corda.node.VersionInfo getVersion() public int hashCode() - @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNodeParameters setConfigOverrides(kotlin.jvm.functions.Function1) - @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNodeParameters setEntropyRoot(java.math.BigInteger) - @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNodeParameters setForcedID(Integer) - @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNodeParameters setLegalName(net.corda.core.identity.CordaX500Name) public String toString() + @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNodeParameters withConfigOverrides(kotlin.jvm.functions.Function1) + @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNodeParameters withEntropyRoot(java.math.BigInteger) + @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNodeParameters withForcedID(Integer) + @org.jetbrains.annotations.NotNull public final net.corda.testing.node.MockNodeParameters withLegalName(net.corda.core.identity.CordaX500Name) ## public class net.corda.testing.node.MockServices extends java.lang.Object implements net.corda.core.node.StateLoader, net.corda.core.node.ServiceHub public () diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt index 03ce962d2a..1083dd0674 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt @@ -101,12 +101,12 @@ data class NodeParameters( val startInSameProcess: Boolean? = null, val maximumHeapSize: String = "200m" ) { - fun setProvidedName(providedName: CordaX500Name?): NodeParameters = copy(providedName = providedName) - fun setRpcUsers(rpcUsers: List): NodeParameters = copy(rpcUsers = rpcUsers) - fun setVerifierType(verifierType: VerifierType): NodeParameters = copy(verifierType = verifierType) - fun setCustomerOverrides(customOverrides: Map): NodeParameters = copy(customOverrides = customOverrides) - fun setStartInSameProcess(startInSameProcess: Boolean?): NodeParameters = copy(startInSameProcess = startInSameProcess) - fun setMaximumHeapSize(maximumHeapSize: String): NodeParameters = copy(maximumHeapSize = maximumHeapSize) + fun withProvidedName(providedName: CordaX500Name?): NodeParameters = copy(providedName = providedName) + fun withRpcUsers(rpcUsers: List): NodeParameters = copy(rpcUsers = rpcUsers) + fun withVerifierType(verifierType: VerifierType): NodeParameters = copy(verifierType = verifierType) + fun withCustomerOverrides(customOverrides: Map): NodeParameters = copy(customOverrides = customOverrides) + fun withStartInSameProcess(startInSameProcess: Boolean?): NodeParameters = copy(startInSameProcess = startInSameProcess) + fun withMaximumHeapSize(maximumHeapSize: String): NodeParameters = copy(maximumHeapSize = maximumHeapSize) } /** @@ -197,17 +197,17 @@ data class DriverParameters( val jmxPolicy: JmxPolicy = JmxPolicy(), val networkParameters: NetworkParameters = testNetworkParameters() ) { - fun setIsDebug(isDebug: Boolean): DriverParameters = copy(isDebug = isDebug) - fun setDriverDirectory(driverDirectory: Path): DriverParameters = copy(driverDirectory = driverDirectory) - fun setPortAllocation(portAllocation: PortAllocation): DriverParameters = copy(portAllocation = portAllocation) - fun setDebugPortAllocation(debugPortAllocation: PortAllocation): DriverParameters = copy(debugPortAllocation = debugPortAllocation) - fun setSystemProperties(systemProperties: Map): DriverParameters = copy(systemProperties = systemProperties) - fun setUseTestClock(useTestClock: Boolean): DriverParameters = copy(useTestClock = useTestClock) - fun setInitialiseSerialization(initialiseSerialization: Boolean): DriverParameters = copy(initialiseSerialization = initialiseSerialization) - fun setStartNodesInProcess(startNodesInProcess: Boolean): DriverParameters = copy(startNodesInProcess = startNodesInProcess) - fun setWaitForAllNodesToFinish(waitForAllNodesToFinish: Boolean): DriverParameters = copy(waitForAllNodesToFinish = waitForAllNodesToFinish) - fun setNotarySpecs(notarySpecs: List): DriverParameters = copy(notarySpecs = notarySpecs) - fun setExtraCordappPackagesToScan(extraCordappPackagesToScan: List): DriverParameters = copy(extraCordappPackagesToScan = extraCordappPackagesToScan) - fun setJmxPolicy(jmxPolicy: JmxPolicy): DriverParameters = copy(jmxPolicy = jmxPolicy) - fun setNetworkParameters(networkParameters: NetworkParameters): DriverParameters = copy(networkParameters = networkParameters) + fun withIsDebug(isDebug: Boolean): DriverParameters = copy(isDebug = isDebug) + fun withDriverDirectory(driverDirectory: Path): DriverParameters = copy(driverDirectory = driverDirectory) + fun withPortAllocation(portAllocation: PortAllocation): DriverParameters = copy(portAllocation = portAllocation) + fun withDebugPortAllocation(debugPortAllocation: PortAllocation): DriverParameters = copy(debugPortAllocation = debugPortAllocation) + fun withSystemProperties(systemProperties: Map): DriverParameters = copy(systemProperties = systemProperties) + fun withUseTestClock(useTestClock: Boolean): DriverParameters = copy(useTestClock = useTestClock) + fun withInitialiseSerialization(initialiseSerialization: Boolean): DriverParameters = copy(initialiseSerialization = initialiseSerialization) + fun withStartNodesInProcess(startNodesInProcess: Boolean): DriverParameters = copy(startNodesInProcess = startNodesInProcess) + fun withWaitForAllNodesToFinish(waitForAllNodesToFinish: Boolean): DriverParameters = copy(waitForAllNodesToFinish = waitForAllNodesToFinish) + fun withNotarySpecs(notarySpecs: List): DriverParameters = copy(notarySpecs = notarySpecs) + fun withExtraCordappPackagesToScan(extraCordappPackagesToScan: List): DriverParameters = copy(extraCordappPackagesToScan = extraCordappPackagesToScan) + fun withJmxPolicy(jmxPolicy: JmxPolicy): DriverParameters = copy(jmxPolicy = jmxPolicy) + fun withNetworkParameters(networkParameters: NetworkParameters): DriverParameters = copy(networkParameters = networkParameters) } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetwork.kt index 46f2cedb04..23d2f41fd2 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetwork.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockNetwork.kt @@ -59,6 +59,8 @@ data class MockNodeParameters( * is used by multiple nodes. You rarely if ever need to change that, it's primarily of interest to people testing * notary code. * @property notarySpecs The notaries to use in the mock network. By default you get one mock notary and that is usually sufficient. + * @property networkParameters The network parameters to be used by all the nodes. [NetworkParameters.notaries] must be + * empty as notaries are defined by [notarySpecs]. */ @Suppress("unused") data class MockNetworkParameters( @@ -66,7 +68,8 @@ data class MockNetworkParameters( val threadPerNode: Boolean = false, val servicePeerAllocationStrategy: InMemoryMessagingNetwork.ServicePeerAllocationStrategy = InMemoryMessagingNetwork.ServicePeerAllocationStrategy.Random(), val notarySpecs: List = listOf(MockNetworkNotarySpec(DUMMY_NOTARY_NAME)), - val networkParameters: NetworkParameters = testNetworkParameters()) { + val networkParameters: NetworkParameters = testNetworkParameters() +) { fun withNetworkParameters(networkParameters: NetworkParameters): MockNetworkParameters = copy(networkParameters = networkParameters) fun withNetworkSendManuallyPumped(networkSendManuallyPumped: Boolean): MockNetworkParameters = copy(networkSendManuallyPumped = networkSendManuallyPumped) fun withThreadPerNode(threadPerNode: Boolean): MockNetworkParameters = copy(threadPerNode = threadPerNode) From 979aef1308aaa9c77ac41190d6b986401c00c655 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Tue, 20 Feb 2018 12:57:59 +0000 Subject: [PATCH 50/50] Removed DriverParameters.initialiseSerialization (#2572) --- .ci/api-current.txt | 11 +++++------ .../main/kotlin/net/corda/testing/driver/Driver.kt | 4 +--- .../net/corda/testing/node/internal/DriverDSLImpl.kt | 4 ++-- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/.ci/api-current.txt b/.ci/api-current.txt index 672d74ef83..95f69464fb 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -3656,12 +3656,11 @@ public final class net.corda.testing.driver.Driver extends java.lang.Object ## public final class net.corda.testing.driver.DriverParameters extends java.lang.Object public () - public (boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, Map, boolean, boolean, boolean, boolean, List, List, net.corda.testing.driver.JmxPolicy, net.corda.core.node.NetworkParameters) + public (boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, Map, boolean, boolean, boolean, List, List, net.corda.testing.driver.JmxPolicy, net.corda.core.node.NetworkParameters) public final boolean component1() @org.jetbrains.annotations.NotNull public final List component10() - @org.jetbrains.annotations.NotNull public final List component11() - @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.JmxPolicy component12() - @org.jetbrains.annotations.NotNull public final net.corda.core.node.NetworkParameters component13() + @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.JmxPolicy component11() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.NetworkParameters component12() @org.jetbrains.annotations.NotNull public final java.nio.file.Path component2() @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.PortAllocation component3() @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.PortAllocation component4() @@ -3669,8 +3668,8 @@ public final class net.corda.testing.driver.DriverParameters extends java.lang.O public final boolean component6() public final boolean component7() public final boolean component8() - public final boolean component9() - @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters copy(boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, Map, boolean, boolean, boolean, boolean, List, List, net.corda.testing.driver.JmxPolicy, net.corda.core.node.NetworkParameters) + @org.jetbrains.annotations.NotNull public final List component9() + @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.DriverParameters copy(boolean, java.nio.file.Path, net.corda.testing.driver.PortAllocation, net.corda.testing.driver.PortAllocation, Map, boolean, boolean, boolean, List, List, net.corda.testing.driver.JmxPolicy, net.corda.core.node.NetworkParameters) public boolean equals(Object) @org.jetbrains.annotations.NotNull public final net.corda.testing.driver.PortAllocation getDebugPortAllocation() @org.jetbrains.annotations.NotNull public final java.nio.file.Path getDriverDirectory() diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt index 1083dd0674..cf9d5f216d 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt @@ -156,7 +156,7 @@ fun driver(defaultParameters: DriverParameters = DriverParameters(), dsl: Dr ), coerce = { it }, dsl = dsl, - initialiseSerialization = defaultParameters.initialiseSerialization + initialiseSerialization = true ) } @@ -189,7 +189,6 @@ data class DriverParameters( val debugPortAllocation: PortAllocation = PortAllocation.Incremental(5005), val systemProperties: Map = emptyMap(), val useTestClock: Boolean = false, - val initialiseSerialization: Boolean = true, val startNodesInProcess: Boolean = false, val waitForAllNodesToFinish: Boolean = false, val notarySpecs: List = listOf(NotarySpec(DUMMY_NOTARY_NAME)), @@ -203,7 +202,6 @@ data class DriverParameters( fun withDebugPortAllocation(debugPortAllocation: PortAllocation): DriverParameters = copy(debugPortAllocation = debugPortAllocation) fun withSystemProperties(systemProperties: Map): DriverParameters = copy(systemProperties = systemProperties) fun withUseTestClock(useTestClock: Boolean): DriverParameters = copy(useTestClock = useTestClock) - fun withInitialiseSerialization(initialiseSerialization: Boolean): DriverParameters = copy(initialiseSerialization = initialiseSerialization) fun withStartNodesInProcess(startNodesInProcess: Boolean): DriverParameters = copy(startNodesInProcess = startNodesInProcess) fun withWaitForAllNodesToFinish(waitForAllNodesToFinish: Boolean): DriverParameters = copy(waitForAllNodesToFinish = waitForAllNodesToFinish) fun withNotarySpecs(notarySpecs: List): DriverParameters = copy(notarySpecs = notarySpecs) diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt index e7cc449d84..846a9eea2f 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt @@ -952,7 +952,7 @@ fun genericDriver( driverDslWrapper: (DriverDSLImpl) -> D, coerce: (D) -> DI, dsl: DI.() -> A ): A { - val serializationEnv = setGlobalSerialization(defaultParameters.initialiseSerialization) + val serializationEnv = setGlobalSerialization(true) val driverDsl = driverDslWrapper( DriverDSLImpl( portAllocation = defaultParameters.portAllocation, @@ -1003,7 +1003,7 @@ fun internalDriver( debugPortAllocation: PortAllocation = DriverParameters().debugPortAllocation, systemProperties: Map = DriverParameters().systemProperties, useTestClock: Boolean = DriverParameters().useTestClock, - initialiseSerialization: Boolean = DriverParameters().initialiseSerialization, + initialiseSerialization: Boolean = true, startNodesInProcess: Boolean = DriverParameters().startNodesInProcess, waitForAllNodesToFinish: Boolean = DriverParameters().waitForAllNodesToFinish, notarySpecs: List = DriverParameters().notarySpecs,